본문 바로가기
kotlin

[kotlin-tip] kotlin 에서 jpa entity 작성하기

by 권성호 2023. 1. 12.

사실이 아니라 공부한 내용과 생각을 정리한 글입니다. 언제든 가르침을 주신다면 감사하겠습니다.

 

kotlin 은 간결하고 안전한 언어이다.

간결한 코드 작성을 위해 필드와 게터, 세터를 한 번에 표현할 수 있는 프로퍼티라는 개념을 도입했고 생성자 작성 시 프로퍼티까지 한 번에 작성할 수 있는 문법도 지원한다. 또한 data class를 사용하면 기존 자바에서 관용적으로 작성하던 여러 부가 메서드를 자동으로 생성한다.

안전한 코드 작성을 위해 불변 타입을 선호하고, null 허용 타입과 null 불가 타입을 분리했으며 null 허용 타입의 경우 null check를 효과적으로 수행할 수 있는 여러 도구를 제공한다.

이밖에도 kotlin에서 간결하고 안전한 코드 작성을 위해 지원하는 기능은 많고, 이를 언어 차원에서 지원받을 수 있다는 것은 충분히 매력적인 것 같다.

 

하지만 jpa entity 를 작성하게 되면 jpa 사상과 kotlin 사상이 충돌하는 지점을 발견하게 되고 kotlin 이 은총알은 아니다는 것을 알게 된다.

jpa entity 를 작성하면서 느낀 충돌 지점과 내가 생각한 내용들을 정리하려 한다.


1. id 작성하기: null 허용? 디폴트 값?

jpa 가 지원하는 id 생성전략을 사용하면 엔티티가 영속화되기 전까지 id 값은 정해지지 않는다. 이 정해지지 않는 상태를 어떻게 표현해야 할까?

JPARepository의 구현체의 save 함수를 살펴보면, 정해지지 않은 상태를 null, 혹은 0으로 표현하는 것을 알 수 있다.

//SimpleJpaRepository.java
@Transactional
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null.");
    if (this.entityInformation.isNew(entity)) {
        this.em.persist(entity);
        return entity;
    } else {
        return this.em.merge(entity);
    }
}

//AbstractEntityInformation.java
public boolean isNew(T entity) {

    ID id = getId(entity);
    Class<ID> idType = getIdType();

    if (!idType.isPrimitive()) {
        return id == null;
    }

    if (id instanceof Number) {
        return ((Number) id).longValue() == 0L;
    }

    throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}

따라서 id를 0으로 초기화하여 kotlin의 null-safety 한 타입의 장점을 취할 수 도 있을 것이고, 의도적으로 null로 초기화하여 명확하게 값이 없음을 명시해 줄 수도 있다.

아직 두 방법 중에 어느 게 맞는지 확신을 못하겠다. 조금 더 써봐야 할 것 같다...


2. constructor 작성하기: 디폴트 생성자? 기본이 final

jpa는 엔티티를 생성할 때 디폴트 생성자 호출 후 리플렉션을 통해 각 필드에 값을 넣어준다. 따라서, jpa 엔티티를 작성할 때 디폴트 생성자는 꼭 포함되어야 한다.

kotlin으로 클래스를 작성하면 주 생성자를 통해  한번에 생성자와 프로퍼티를 선언할 수 있다. 이는 코틀린이 지향하는 간결함을 대표하는 기능이지만, jpa 엔티티를 이렇게 작성하면 디폴트 생성자는 또 별도로 작성해줘야 한다. 즉, 부가 코드를 계속 작성하게 되고, 이는 kotlin 이 지향하는 간결함을 낮추는 요인이다.

kotlin 에선 jpa 엔티티 작성 시 발생하는 이러한 불협화음을 낮추기 위해 kotlin-jpa plugin이라는 별도의 플러그인을 지원한다.

kotlin-jpa plugin 은 기본 생성자를 만들어주는 no-arg 플러그인을 래핑 한 플러그인이다.
@Entity, @MappedSuperClass, @Embeddable 어노테이션이 있는 클래스의 default constructor를 자동으로 생성해 준다.

 

kotlin 은 기본적으로 final이다. java 진영에서 무분별하게 상속이 사용되면서 클래스 간 강결합으로 인해 발생한 수많은 시행착오를 통해 내린 결정인 것 같다.

kotlin의 이러한 특성은 jpa entity 연관관계 메핑 시 lazy-loading 전략을 사용하게 되면 문제가 된다. 

lazy-loading 은 실제 객체가 아닌 그 객체를 상속받은 프록시 객체로 동작하게 되는데, final 이면 상속이 불가하여 프락시객체를 생성할 수 없기 때문이다.   이를 방지하기 위해 kotlin에서 all-open 플러그인을 지원한다.


3. getter, setter 작성하기: private setter, public getter

jpa 엔티티는 가장 보호받아야 할 객체로 간주되는 경우가 대부분이다. 따라서 java의 경우, jpa 엔티티의 필드에 대해 setter를 닫고 getter 만 열어두는 것은 흔하게 사용되던 패턴이다.  엔티티의 상태를 변경할 때, 관용적으로 사용되는 setter 보단 상태 변경에 대한 의미를 명시적으로 내포하고 있는 메서드를 사용하는 것이 조금 더 엔티티를 보호할 수 있는 방법이라는 것이다.

그런데, kotlin 은 getter와 setter 가 언어 차원에서 하나로 묶여있다. 즉, 어떤 프로퍼티가 var 라면 이는 getter와 setter 가 둘다 있음을 의미하고 val 라면 getter 만 있음을 의미한다. private 인지 public인지 지정을 getter와 setter 단위가 아니라 var과 val 단위로 할 수 있다.

물론, 별도의 커스텀 getter, setter 선언을 통해 접근 제한자를 지정할 수 있지만 이는 kotlin의 간결함을 저해시키는 요인이라고 생각된다.

개인적으로 private setter, public getter를 내포하는 하나의 키워드를 kotlin에서 지원한다면 더욱 간결한 코르를 작성할 수 있지 않을까 생각한다.

이런 고민은 나뿐만 아니라 많은 사람들이 하고 있는 것 같다.(https://discuss.kotlinlang.org/t/private-setter-for-var-in-primary-constructor/3640)


4. equals, hashcode, toString 작성하기: data class? 직접 override?

클래스를 작성하다보면 필수적으로 작성하게 되는 메서드들이 있다. 대표적으로 equals, hashcode, toString 이 그것이다. 

java에서 이러한 보일러플래이트 성격의 코드 작성을 줄이기 위해 롬복이 등장했다면, kotlin 에는 data class 가 존재한다.

data class 는 언어 차원에서 간결성을 지원하지만 그만큼 제약사항이 많다.

data class 는 기본 생성자에 정의한 Property 들이 copy equals, hashCode, toString함수들에 활용된다. jpa 엔티티는 equals, hashCode 는 id 기반으로 동작해야 하는데, data class 의 특성과는 맞지 않은 것을 알 수 있다.

 


5. 연관관계 작성하기: lateinit? nullable?

s

 


JVM 생태계에서 아직 수많은 라이브러리가 Java를 기반으로 설계되어 있고 이 생태계가 점차 kotlin으로 이동하게 된다면 위에서 언급한 간극들을 보완할 수 있는 좀 더 효과적인 해결책들이 많이 등장할 것이라고 기대한다.

 

참고자료

댓글