Kotlin과 Spring을 이용해 개발하다보면 자연스럽게 Spring Data JPA를 이용하는 경우가 많습니다. JPA를 사용하면서 가장 먼저 마주하게 되는 것이 엔티티 클래스를 정의하는 것입니다. 엔티티 클래스를 정의함에 있어서 Kotlin의 특성과 JPA를 정확하게 알지 못해 경험했던 문제를 공유하고 이에 대한 정확한 지식과 저의 생각을 공유하고자 합니다.
우선 문제를 이해하기 위해 kotlin properties
와 Access Strategy
에 대해 간단히 알아보겠습니다.
코틀린 공식 문서의 properties 에는 다음과 같이 되어 있습니다.
Properties in Kotlin classes can be declared either as mutable, using the
var
keyword, or as read-only, using theval
keyword.
간단하게 변경 가능한 프로퍼티는 var
키워드를, 읽기 전용(불변) 프로퍼티는 val
키워드를 이용해 선언할 수 있다는 것입니다.
The full syntax of a read-only property declaration differs from a mutable one in two ways: it starts with
val
instead ofvar
and does not allow a setter
또한 읽기 전용(불변) 프로퍼티는 var
대신 val
로 시작(선언)한다는 것과 setter
가 허용되지 않는 다는 것이 차이점이라고 합니다. 여기서 핵심적인 내용은 val
키워드는 setter
를 허용하지 않는 다는 점입니다.
Spring Data JPA의 구현체로 주로 사용되는 Hibernate의 User Guide의 Access strateegies를 보면 2가지 전략을 소개하고 있습니다.
As a Jakarta Persistence provider, Hibernate can introspect both the entity attributes (instance fields) or the accessors (instance properties). By default, the placement of the
@Id
annotation gives the default access strategy.
When using property-based access, Hibernate uses the accessors for both reading and writing the entity state.
When using field-based access, adding other entity-level methods is much more flexible because Hibernate won’t consider those part of the persistence state.
프로퍼티 기반의 접근 방식은 엔티티의 상태에 대한 읽기와 쓰기에서 모두 접근자(메서드)를 이용한다고 되어 있습니다. 한편 필드 기반의 접근 방식은 특별한 접근에 대한 설명이 되어 있지 않습니다. 또한 Id
어노테이션에 의해서 기본 접근 전략이 결정된다고 합니다.
@Entity
class Sample(
@Id
val id: String = UUID.randomUUID().toString(),
@get:Column(length = 32, nullable = false, updatable = false)
val immutableField: String,
)
이 엔티티 클래스의 문제점은 Access Strategy
의 혼용하고 있다는 것입니다. @Id
에 의해서 현재는 필드 기반 접근 방식을 기반 접근 방식으로 설정하였습니다. 하지만 immutableField
에 대해서 프로퍼티 기반의 접근 방식을 사용하고 있습니다. 이럴 경우 실제로 @Column
에 대한 정보는 전혀 이용하지 않게 됩니다. 실제로 jpa의 ddl-auto 옵션을 통해 table 생성 ddl를 자동 생성하도록 하면 아래와 같이 기본 값으로 처리된 것을 확인할 수 있습니다.
// hibernate의 auto-ddl
Hibernate:
create table sample (
id varchar(255) not null,
immutable_field varchar(255),
primary key (id)
) engine=InnoDB
이 문제는 일반적으로 기본 접근 전략과 동일하도록 @get:을 지워주워 기본 전략과 동일하도록 맞춰주거나 @get:Access(AccessType.PROPERTY)
을 이용하여 기본 접근 전략을 Overriding하여 해결할 수 있습니다.
하지만 예시의 코드에서는 기본 전략을 Overriding을 할 수 없습니다. 그 이유는 바로 val
로 선언된 불변 필드에 대해서는 Property-based access
사용할 수 없기 때문입니다. 프로퍼티 베이스 접근방법은 게터 와 세터 메서드를 이용해 필드에 접근하게 되는 데 val
로 선언된 불변 프로퍼티는 별도의 세터가 생성되지 않기 때문입니다. 실제로 아래와 같이 코드를 수정하게 되면 다음 에러 메세지를 보시게 됩니다.
@Entity
class Sample(
@Id
val id: String = UUID.randomUUID().toString(),
@get:Access(AccessType.PROPERTY)
@get:Column(length = 32, nullable = false, updatable = false)
val immutableField: String,
)
// org.hibernate.PropertyNotFoundException: Could not locate setter method for property
이 문제는 val
키워드가 아닌 var
키워드로 변경하거나 val
키워드의 경우 접근 전략을 필드 접근 전략으로 설정해주어야 합니다.
개인적으로 엔티티 클래스를 자유롭게 정의하기 위해서는 필드 접근 전략이 좋다고 생각합니다. 적절하게 불변 객체의 변경을 제어할 수 있는 val
키워드를 사용할 수 있기 때문입니다.
하지만 코틀린에서는 언어 차원에서 필드가 아닌 프로퍼티로 정의하고 사용하도록 하고 있어 필드에 대한 접근 전략이 언어 레벨에서 문제가 없는 지 조금 더 확인이 필요할 것 같습니다.