우선 Kotlin과 JPA에 관해서 이야기하기 전에 JPA의 Entity가 갖는 요구사항에 관해 이야기해보자.
- 엔티티 클래스는 매개변수가 없는 public 또는 protected 생성자를 가져야 한다.
- 지연 로딩을 사용하려면 엔티티 클래스는 final이 아니어야 한다.
기본적으로 코틀린에서의 클래스는 코틀린의 문법상 프로퍼티를 선언하면서 자동으로 생성되는 기본 생성자를 사용하는 경우가 많다. 그리고 추가적인 생성자는 반드시 기본 생성자를 상속해야하는 코틀린의 문법상 특징으로 인해 코틀린에서는 매개변수가 없는 생성자를 작성하는 것이 굉장히 번거롭다.
이를 위해서 코틀린에서는 Gradle 또는 Maven에서 추가적인 플러그인을 지정하는 것으로 @Entity, @Embeddable, @MappedSuperClass 어노테이션이 붙은 모든 클래스에 자동으로 매개변수가 없는 생성자를 만들 수 있다.
아래 코드를 추가한 후 리로드를 하면, 엔티티 클래스를 자바 코드로 디컴파일 했을 때 매개변수가 없는 생성자가 추가되어 있는 것을 볼 수 있다. 이 플러그인은 Spring Initializr를 통해 스프링부트 프로젝트를 생성할 때 Spring Data JPA를 포함하였다면 기본적으로 추가되어 있다.
plugins {
...
kotlin("plugin.jpa") version "{Kotlin버전}"
...
}
이렇게 JPA 플러그인을 통해 자동으로 생성된 매개변수가 없는 생성자는 public 생성자이긴 하지만 자바나 코틀린 코드에서 직접 호출하는 것은 불가능하고, 오직 리플렉션을 통해서만 호출할 수 있기 때문에 간단하게 외부 접근을 허용하지 않는 생성자를 만들 수 있다.
코틀린의 모든 클래스는 기본적으로 final 클래스이다. 엔티티 클래스가 final 클래스라고 해도 JPA를 사용하는 것 자체는 문제가 없지만 지연 로딩을 제대로 사용할 수 없다는 단점이 있다.
하지만 엔티티 클래스마다 모두 open 키워드를 붙여주는 것은 매우 번거로운 작업이다. 다행히 위의 매개변수가 없는 생성자의 경우와 마찬가지로 이 경우도 추가적인 플러그인을 통해 자동으로 모든 클래스를 open 해줄 수 있다.
다음과 같이 build.gradle.kts 에 해당 코드를 추가해주면 된다.
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
코틀린에서는 Lombok의 toString(), equals(), hashCode(), copy()를 기본적으로 포함하고 구조 분해를 사용할 수 있는 data class를 제공한다. 하이버네이트 사용자 가이드를 따르면 equals()와 hashCode()는 특별한 상황이 아니라면 구현하지 않는 것이 좋다고 한다. 또한, 이 특별한 상황에서도 ID 값만을 이용하여 구현해야 한다고 언급한다. 하지만 매번 구현하기는 번거로우니 종종 data class로 엔티티를 만드는 경우가 있는데, 두 가지 이유로 인해 data class는 JPA의 엔티티로 사용하기에는 적합하지 않다.
첫번째로는 data class가 기본적으로 포함하는 메서드가 StackOverflow를 발생시킬 수 있다는 점이다. 양방향 연관관계를 같는 두 엔티티 사이에서 자동으로 작성된 toString()이나 equals() 등의 메서드를 사용하면 서로가 상대방의 메서드를 무한히 호출하는 순환참조가 발생하여 StackOverflow를 발생시킬 수 있다.
두 번째 이유는 data class가 기본적으로 open 될 수 없다는 점이다.(위의 Gradle에서 All-open 플러그인을 사용하면 가능하지만...)
- JPA의 엔티티는 매개변수가 없는 private이 아닌 생성자를 가져야 한다. -> JPA 플러그인을 추가하는 것으로 해결
- 지연 로딩을 사용하기 위해서는 엔티티 클래스가 open 되어야 한다. -> All-open 플러그인 설정을 통해 편리하게 해결 가능
- JPA 엔티티를 data class로 만드는 것은 적합하지 않다.