kotlin data class로 entity구현하기
Kotlin의 data class는 일단 equals(), copy(), toString() 등 기본적인 메서드들을 만들어줘서 너무 편하다. 기본적으로 immutable인 점도 좋다. 하지만 도메인 모델의 풍성한 로직을 표현하는데 적합할까? 그건 추후 더 복잡한 비즈니스 로직을 구현할 때 확인할 예정이다.
일단 기본적인 기능 구현 시 궁금했던 점은
- data class의 모든 필드를 val로 정의하였을 때 더티체킹이 잘 작동할지?
- id default값을 기본 0으로 생성해주는데 save 시 새로 생성해야 한다고 판단할 수 있을지? 필드값도 주입 안받으면 기본 0으로 생성되니까 문제는 없어야 맞긴 한데..
- 기본 equals()는 구조적인 동일성을 판단하는데 Entity의 equals와 호환이 될지
였다. 그래서 Kotest로 테스트 해봤다.
코틀린 환경에서 Spring Data JPA의 save()를 테스트 하기 위해 mocking이 아닌 실제 프레임워크 테스트가 필요했다.
Junit에서와 마찬가지로 @SpringBootTest
애너테이션을 통해 스프링 빈 주입이 가능했다.
Kotest
다양한 테스트 스타일을 지원한다. 원하는 스타일의 Spec을 상속받아 클래스를 생성하여 사용한다.
특히 BDD 스타일인 BehaviorSpec이 흥미로웠다. 주석으로 처리되는 given, when, then 같은 테스트 원칙들을 DSL로 코드화했다는 점이 매우 코틀린스럽다. JUnit과 같은 애너테이션 스타일도 지원하지만 굳이 쓸 필요는 없어보인다.
아래는 BehaviorSpec을 상속받아 만든 테스트 예시이다. 클래스 생성자 파라미터로 코드블록을 넣으면 init 블럭에서 해당 코드블록이 실행된다.
class ProposeGenerator {
fun generateProposal(title: String): FundingProposal =
FundingProposal(title = title)
}
class ProPoseGeneratorTest: BehaviorSpec({
given("제안서 생성기가 주어졌을 때") {
val generator = ProposeGenerator()
`when`("제안서를 생성한다면") {
val proposal = generator.generateProposal("제안서 제목")
then("제안서는 생성된다") {
proposal.title shouldBe "제안서 제목"
}
}
}
})]
뭔가 다른 처리가 필요하면 클래스 바디로 init 블록을 따로 뺄 수도 있다. Spring Data JPA가 어떻게 작동하는지 알아보기 위해서는 @Autowired
를 통한 의존성 주입이 필요했기에 아래와 같이 작성하였다. FunSpec도 사용해봤다.
@SpringBootTest
class ProposeUpdateTest(@Autowired private val repository: ProposalRepository): FunSpec() {
// @Autowired //이런식으로는 사용할 수 없다
// private val repository: ProposalRepository
@Autowired
val service = ProposeService(repository)
init {
context("신청된 제안서가 있는 상황에서") {
val proposal = service.createFundingProposal("제안서 제목")
println("proposal: $proposal")
test("제안서를 수정한다면") {
val newTitle = "새로운 제안서 제목"
val updatedProposal = service.updateFundingProposal(proposal.id, newTitle)
println("updatedProposal: $updatedProposal")
updatedProposal!!.id shouldBe proposal.id
}
}
}
}
테스트 결과
- immutable인 data class와 그 안의 val 속성들을 조작이 아닌 깊은 복사(=copy())했을 때 더티체킹은 작동하지 않는다. 더티체킹은 객체의 속성 단위에서 체크할 뿐 객체 reference는 작동하지 않는다고 한다. save()를 통해 persist해줘야 한다. 비즈니스로직과 프레임워크를 분리하기 위해 더티체킹을 일부러 사용하지 않기도 한다고 한다.
- 0도 null이라고 인식한다고 한다. 기본타입에서는 default값이 있으니까 당연히 되어야 하는게 맞다.
- 이건 더티체킹과 엮여서 좀 헷갈렸던 부분. EntityManager로부터 관리되지 않는 Entity들은 일단 논외. hashcode를 더 공부해야할듯.
프레임워크가 어떻게 동작하는지 보기 위해서 어쩔수 없이 SpringBootTest를 했지만 매번 리부팅되는 걸 보면서 속이 너무나도 답답했다.
mocking은 내일하자.