
Kotlin과 JPA를 함께 사용할 때 주의해야 할 점 몇 가지를 정리해보고자 한다.
//아직은 찍먹 수준이라 공부하면서 추 후 내용을 더 추가해보고자 한다.
아래 예제 코드를 보면, 생성자 안의 var 프로퍼티가 있어 setter를 사용할 순 있으나, setter 대신 updateName()를 구현했다. 캡슐화를 위한 관행을 생각한다면 Java에서와 마찬가지로 setter를 외부에서 바로 호출하는 것 보다, 적절한 이름의 함수로 사용하는 것이 훨씬 좋기 때문이다.
@Entity
@Table(name = "users")
class User(
var name: String,
val age: Int?,
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true)
val userLoanHistories: MutableList<UserLoanHistory> = mutableListOf(),
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
) {
init {
if (name.isBlank()) {
throw IllegalArgumentException("이름은 비어 있을 수 없습니다")
}
}
fun updateName(name: String) {
this.name = name
}
fun loanBook(book: Book) {
this.userLoanHistories.add(UserLoanHistory(this, book.name, false))
}
fun returnBook(bookName: String) {
this.userLoanHistories.first { history -> history.bookName == bookName }.doReturn()
}
}
@Entity
@Table(name = "users")
class User(
private var _name: String,
//...
) {
val name: String
get() = this._name
//...
}
_name 프로퍼티를 만들고, 읽기 전용으로 추가 프로퍼티인 name을 만드는 방식이다.
@Entity
@Table(name = "users")
class User(
name: String, // 프로퍼티가 아닌, 생성자 인자로만 name을 받는다
//...
) {
val name = name
private set
//...
}
User의 생성자에서 name을 프로퍼티가 아닌, 생성자 인자로만 받고 이 name을 변경가능한 프로퍼티로 넣어주되, name 프로퍼티에 private setter를 달아두는 것이다.
위 두 가지 방법은 프로퍼티가 많아지면 번거롭다는 단점이 있다.
User 클래스 주생성자 안에 있는 userLoanHistories와 id는 꼭 주생성자 안에 있을 필요는 없다.
@Entity
@Table(name = "users")
class User(
var name: String,
val age: Int?,
) {
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true)
val userLoanHistories: MutableList<UserLoanHistory> = mutableListOf(),
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
fun updateName(name: String) {
this.name = name
}
fun loanBook(book: Book) {
this.userLoanHistories.add(UserLoanHistory(this, book.name, false))
}
fun returnBook(bookName: String) {
this.userLoanHistories.first { history -> history.bookName == bookName }.doReturn()
}
}
생성자냐 클래스 body안이냐는 큰 상관은 없다. 다음 두 가지 사항을 고려하여 적절하게 대응하는게 옳은 것 같다.
JPA Entity는 data class를 피하는 것이 좋다.
kotlin의 data class는 equals(), hashCode(), toString() 등의 함수를 자동으로 만들어준다. 연관관계의 상황에선 이 세 가지 함수들은 문제가 될 수 있는 경우가 존재한다.

위 그림 처럼, 1:N 연관관계를 맺고 있는 상황을 가정하면, User 쪽에 equals()가 호출된다면, User는 자신과 관계를 맺고 있는 UserLoanHistory의 equals()를 호출하게 되고, 다시 UserLoanHistory는 자신과 관계를 맺고 있는 User의 equals()를 호출하게 된다.
kotlin은 기본적으로 Class, 함수 모두 final이다. 이는 상속과 오버라이드가 막혀있다.
JPA를 사용할 때 Proxy Lazy Fetching을 완전히 사용하려면 클래스가 상속이 가능해야 한다. @OneToMany에 있어선 Lazy Fetching이 동작하지만, @ManyToOne에 대해선 Lazy Fetching 옵션을 명시적으로 주더라도 동작하지 않는다고 한다.
all-open 기능을 통해 @Entity 클래스들은 decompile 했을 때도 class가 열려 있게 처리해줘야 한다. 이를 위해 build.gradle에 다음 내용들을 추가하자.
plugins {
id 'org.jetbrains.kotlin.plugin.allopen' version '1.6.21'
}
// plugins, dependencies와 같은 level (build.gradle 최상단에 추가)
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
annotation("javax.persistence.Embeddable")
}