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")
}