관리 메뉴

피터의 개발이야기

[kotlin] Springboot - JPA 의존성 주입 본문

Programming/Kotlin

[kotlin] Springboot - JPA 의존성 주입

기록하는 백앤드개발자 2024. 5. 23. 10:10
반응형

ㅁ 들어가며

ㅇ spring boot tutorial를 참조하여 나만의 확장 프로그램 만들기를 구현해 보았습니다.

 

ㅁ Gradle 수정

plugins {
  ...
  kotlin("plugin.allopen") version "1.9.22"
}

allOpen {
  annotation("jakarta.persistence.Entity")
  annotation("jakarta.persistence.Embeddable")
  annotation("jakarta.persistence.MappedSuperclass")
}

ㅇ 코틀린에서 JPA를 사용하기 위해서는 allopen 플러그인을 사용해야한다.
ㅇ allopen 플로그인과 JPA를 사용하기 위한 추가 annotation을 설정하였다.

ㅇ 수정파일: build.gradle.kts

 

ㅁEntity 생성

@Entity
class Article(
    var title: String,
    var headline: String,
    var content: String,
    @ManyToOne var author: User,
    var slug: String = title.toSlug(),
    var addedAt: LocalDateTime = LocalDateTime.now(),
    @Id @GeneratedValue var id: Long? = null)

@Entity
class User(
    var login: String,
    var firstname: String,
    var lastname: String,
    var description: String? = null,
    @Id @GeneratedValue var id: Long? = null)

ㅇ 속성과 생성자 매개변수를 동시에 선언할 수 있는 Kotlin 기본 생성자 간결 구문을 사용하여 모델을 생성한다.

ㅇ 파일명: Entities.kt

ㅇ 생성자의 매개변수 String.toSlug()에 기본 인수를 제공하기 위해 확장을 사용하고 있다 .

ㅇ 기본값이 있는 선택적 매개변수는 위치 인수 사용 시 생략이 가능하도록 마지막 위치에 정의된다.

   ( Kotlin은 명명된 인수도 지원합니다 ).

ㅇ Kotlin에서는 동일한 파일에 간결한 클래스 선언을 그룹화하는 것이 일반적이다.
ㅇ 여기서는 JPA가 불변 클래스 또는 클래스에 의해 자동으로 생성된 메소드와 작동하도록 설계되지 않았기 때문에 속성이 있는 data클래스를 사용하지 않는다.

ㅇ 다른 Spring Data 플레이버를 사용하는 경우 대부분은 이러한 구성을 지원하도록 설계되었으므로 Spring Data MongoDB, Spring Data JDBC 등을 사용할 때와 같은 클래스를 사용해야 한다.

 

ㅁ  Spring Data JPA 저장소

interface ArticleRepository : CrudRepository<Article, Long> {
  fun findBySlug(slug: String): Article?
  fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}

interface UserRepository : CrudRepository<User, Long> {
  fun findByLogin(login: String): User?
}

ㅇ Repositories.kt

 

ㅁ  JPA 테스트를 작성

@DataJpaTest
class RepositoriesTests @Autowired constructor(
    val entityManager: TestEntityManager,
    val userRepository: UserRepository,
    val articleRepository: ArticleRepository) {

  @Test
  fun `When findByIdOrNull then return Article`() {
    val johnDoe = User("johnDoe", "John", "Doe")
    entityManager.persist(johnDoe)
    val article = Article("Lorem", "Lorem", "dolor sit amet", johnDoe)
    entityManager.persist(article)
    entityManager.flush()
    val found = articleRepository.findByIdOrNull(article.id!!)
    assertThat(found).isEqualTo(article)
  }

  @Test
  fun `When findByLogin then return User`() {
    val johnDoe = User("johnDoe", "John", "Doe")
    entityManager.persist(johnDoe)
    entityManager.flush()
    val user = userRepository.findByLogin(johnDoe.login)
    assertThat(user).isEqualTo(johnDoe)
  }
}

ㅇ RepositoriesTests.kt

 

ㅁ 블러그 템플릿 수정

ㅇ "블로그" Mustache 템플릿을 업데이트한다.

{{> header}}

<h1>{{title}}</h1>

<div class="articles">

  {{#articles}}
    <section>
      <header class="article-header">
        <h2 class="article-title"><a href="/article/{{slug}}">{{title}}</a></h2>
        <div class="article-meta">By  <strong>{{author.firstname}}</strong>, on <strong>{{addedAt}}</strong></div>
      </header>
      <div class="article-description">
        {{headline}}
      </div>
    </section>
  {{/articles}}
</div>

{{> footer}}

 blog.mustache

 

{{> header}}

<section class="article">
  <header class="article-header">
    <h1 class="article-title">{{article.title}}</h1>
    <p class="article-meta">By  <strong>{{article.author.firstname}}</strong>, on <strong>{{article.addedAt}}</strong></p>
  </header>

  <div class="article-description">
    {{article.headline}}

    {{article.content}}
  </div>
</section>

{{> footer}}

ㅇ 기사 템플릿 수정

ㅇ article.mustache

ㅁ HtmlController

@Controller
class HtmlController(private val repository: ArticleRepository) {

  @GetMapping("/")
  fun blog(model: Model): String {
    model["title"] = "Blog"
    model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
    return "blog"
  }

  @GetMapping("/article/{slug}")
  fun article(@PathVariable slug: String, model: Model): String {
    val article = repository
        .findBySlug(slug)
        ?.render()
        ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")
    model["title"] = article.title
    model["article"] = article
    return "article"
  }

  fun Article.render() = RenderedArticle(
      slug,
      title,
      headline,
      content,
      author,
      addedAt.format()
  )

  data class RenderedArticle(
      val slug: String,
      val title: String,
      val headline: String,
      val content: String,
      val author: User,
      val addedAt: String)

}

ㅇ 형식화된 날짜로 블로그 및 기사 페이지를 렌더링하기 위해 HtmlControlle를 업데이트한다.

 

ㅁ 초기 데이터 생성

@Configuration
class BlogConfiguration {

  @Bean
  fun databaseInitializer(userRepository: UserRepository,
              articleRepository: ArticleRepository) = ApplicationRunner {

    val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
    articleRepository.save(Article(
        title = "Lorem",
        headline = "Lorem",
        content = "dolor sit amet",
        author = johnDoe
    ))
    articleRepository.save(Article(
        title = "Ipsum",
        headline = "Ipsum",
        content = "dolor sit amet",
        author = johnDoe
    ))
  }
}

ㅇ 새 클래스에 데이터를 주입하기 위해 BlogConfiguration.kt 생성한다.

ㅇ Configuration은 빈 생성 주기 시 한번만 실행된다.

 

ㅁ TestCase 수정

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {

  @BeforeAll
  fun setup() {
    println(">> Setup")
  }

  @Test
  fun `Assert blog page title, content and status code`() {
    println(">> Assert blog page title, content and status code")
    val entity = restTemplate.getForEntity<String>("/")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains("<h1>Blog</h1>", "Lorem")
  }

  @Test
  fun `Assert article page title, content and status code`() {
    println(">> Assert article page title, content and status code")
    val title = "Lorem"
    val entity = restTemplate.getForEntity<String>("/article/${title.toSlug()}")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains(title, "Lorem", "dolor sit amet")
  }

  @AfterAll
  fun teardown() {
    println(">> Tear down")
  }

}

ㅇ API의 변경에 따라 새로운 테스트 케이스를 IntegrationTests에 작성한다.

 

ㅁ Test

ㅇ localhost:8080 초기화면에서 blog 리스트가 출력되고, 아티클 클릭 시 상세 화면이 출력된다.

반응형
Comments