Kotlin에서 JPA 설정하기 총정리

Kotlin 환경에서 JPA를 사용하려면 Java와 다르게 추가 설정이 필요하고 염두에 두어야 할 주의사항이 있습니다.

  1. All-open 플러그인 설정
  2. No-arg 플러그인 설정
  3. val vs var, final vs open
  4. equals & hashCode
  5. data class 써도 되는지
  6. Sparing 사용하지 않는 경우 Gradle에서 Entity 스캔 경로 설정 트릭

All-open 플러그인 설정

JPA의 엔티티에 @~ToMany 어노테이션을 사용할 경우 기본으로 설정되는 lazy loading 기능은 사용자가 만든 클래스를 상속한 프록시 객체를 이용해 구현합니다. 문제는 Kotlin에서 클래스는 기본적으로 final이기 때문에 lazy loading 기능을 사용하기 위해 일일이 open을 붙여줘야 합니다. 이 문제를 해결하기 위해 자동으로 지정한 클래스들을 open으로 설정할 수 있는 All-open 플러그인을 사용하면 됩니다. [1]

설정 코드는 아래와 같습니다. (Kotlin 환경을 가정하므로 Gradle도 Groovy가 아닌 Kotlin DSL로 작성했습니다.)

참고로 공식문서를 보면 kotlin-spring 플러그인이 따로 있는데 이는 All-open 플러그인과 동일하나 추가로 Spring의 @Component, @Async, @Transactional, @Cacheable, @SpringBootTest 어노테이션이 붙은 클래스를 open으로 만듭니다.

No-arg 플러그인 설정

Hibernate는 엔티티 클래스의 기본 생성자(no-arg constructor)를 사용하기 때문에 마찬가지로 이를 일일이 생성해주어야 합니다. 이 문제는 No-arg 플러그인으로 해결할 수 있습니다. [2]

참고로 kotlin-jpa 플러그인을 설정할 경우 JPA의 @Entity, @Embedabble, MappedSuperclass 어노테이션이 붙은 클래스의 no-arg 생성자를 자동으로 생성합니다. 따라서 No-arg 플러그인 대신 kotlin-jpa 플러그인을 사용하시길 추천드립니다.

val vs var, final vs open

allOpen 플러그인을 사용하셨다면 자동으로 @Entity를 붙인 클래스가 open이 되고 그 클래스에 속한 프로퍼티와 메소드 모두 open이 됩니다. 만약 프록시를 통한 지연 로딩 기능을 사용하고 싶다면 프로퍼티에 final을 붙여서는 안됩니다. 대신 메소드는 붙여도 상관없습니다.

val, var 논의는 이 글에서 적다 너무 길어져서 따로 새 글로 분리했습니다. 결론만 정리하면 Hibernate의 경우 val을 써도 괜찮아보이지만 공식 문서를 따르면 backing field가 final이면 안되기 때문에 var을 쓰는 게 맞습니다. 대안으로 varprotected set을 붙여 함께 사용할 수 있습니다. [3]

더 자세한 내용은 제가 쓴 Kotlin JPA의 Entity에 val 필드를 사용해도 될까?를 참고해주세요!

equals & hashCode

equalshashCode에 대해서 Hibernate 유저 가이드에 이런 말이 나옵니다. “As you can see the question of equals/hashCode is not trivial, nor is there a one-size-fits-all solution” [4] 즉, 어떤 상황이든 적용할 수 있는 완벽한 해답이 없고 상황에 따라 적절한 구현이 필요합니다. 공식문서를 보면 기본 equalshashCode를 사용할 때 발생할 수 있는 문제를 보여줍니다. 그 외 구현체별 발생할 수 있는 문제도 참고하시면 좋습니다. 문서 막바지에 equals 판별 시 오브젝트 identity(동일성)와 자연 키(비즈니스 키, 비즈니스 관점에서 객체를 고유하게 식별하는데 사용되는 값. 예: 주민등록번호, 이메일) 동등성을 함께 확인하는 방법을 제시합니다.

결론적으로 문서를 읽어보시고 비즈니스 요구사항에 따라 적절한 equalshashCode 구현을 선택해야 합니다. (기회가 된다면 이에 대해 자세한 글을 써보겠습니다.)

data class 사용해도 될까?

data class는 대부분 사용하지 말라는 의견이 많습니다. [5][6][7] 주로 equals, hashCode 문제와 연관지어 근거를 듭니다. 위에서 언급했듯 비즈니스 상황에 따라 적절한 equals, hashCode를 구현해야 하는데 data class를 사용할 때 자동으로 생성하는 equals, hashCode로 인해 문제가 발생할 수 있다는 의견입니다.

또한 data class는 기본적으로 final이고 이를 open으로 바꾸지 못합니다. 다만 All-open 플러그인을 쓰면 data classopen으로 사용할 수 있어 이 문제는 자연히 해결됩니다.

마지막으로 data class가 자동으로 생성하는 equals, hashCode, toString을 사용하다 의도치 않게 지연 로딩이 발생해 성능상 손해가 발생할 수 있다는 의견이 있습니다. 우리가 통제할 수 있게 직접 메소드를 만들어 필요한 변수만 로딩해야 한다는 주장인데 이 역시 override로 해결할 수 있긴 합니다.

정리하면 data class는 편리하지만 JPA 엔티티 클래스로 쓰기에는 의도치 않은 문제가 발생할 수 없기 때문에 일반 클래스 사용을 권장하는 편입니다.

Spring 사용하지 않고 Gradle에서 JPA를 설정하는 경우 Entity 스캔 경로 설정 트릭

사실 이 글을 쓰게된 계기입니다. Spring을 사용하지 않고 Gradle에서 JPA를 사용하는 경우 @Entity 어노테이션을 붙여도 엔티티 클래스를 스캔하지 못하는 문제가 발생합니다. 해당 문제는 최근 JPA에 대해 학습하기 위해 자바 ORM 표준 JPA 프로그래밍 책을 실습하던 중 발견한 문제인데요. 다행히 저와 같은 문제를 겪으신 분이 자바 ORM 표준 JPA 프로그래밍 인프런 강의 Q&A 게시판에 해결 방법을 알려주셨습니다. [8][9]

참고로 사이트의 내용은 Java를 기준으로 설명하기 때문에 Kotlin 환경으로 바꿔서 설명하겠습니다. Gradle의 기본 설정에서 영속성 컨텍스트 설정 파일인 src/main/resources/META-INF/persistence.xml은 빌드 후 build/resources/main/META-INF/persistence.xml에 들어갑니다. 이 때 빌드된 다른 클래스 파일은 build/classes/kotlin/main/<package>/ 경로에 위치합니다. Hibernate는 빌드된 META-INF/persistence.xml의 위치를 기준으로 클래스를 스캔하기 때문에 build/resources/main/ 을 스캔하고 엔티티 클래스를 찾을 수 없다는 에러가 발생합니다.

따라서 persistence.xml 파일의 빌드 후 위치를 바꾸기 위해 다음과 같은 설정이 필요합니다.

이제 빌드 후 build/classes/kotlin/main/META-INF/persistence.xml에 위치하여 정상적으로 엔티티 클래스를 스캔할 수 있습니다.

문제는 이 방법은 일종의 트릭으로 리소스 파일의 빌드 위치를 임의로 변경하기 때문에 다른 문제가 발생할 수 있습니다. 따라서 엔티티 클래스를 스캔할 다른 방법이 필요한데 persistence.xml 파일에서 직접 엔티티 클래스 경로를 설정할 수 있습니다. 그 외 직접 어노테이션 기반 엔티티 클래스 스캔 코드를 짜는 것도 방법이 될 수 있지만 역시 가장 편리한 방법은 Spring을 함께 사용하는 방법입니다. Spring의 강력한 의존 관계 설정 기능을 통해 자동으로 @Entity 어노테이션이 붙은 클래스를 찾고 영속성 설정을 완료할 수 있습니다.

정리

Kotlin에서 JPA를 사용하기 위해서는 JPA에서 엔티티 클래스가 지켜야 하는 규약으로 인해 설정해야 할 것들이 많습니다. 규약에 대해 궁금하신 분은 더 자세히 탐구한 제 글을 보셔도 도움이 되겠습니다.

글의 목차 순서대로 allOpen, noArgs 플러그인 사용, 주의 해서 val 사용, 필요한 경우 적절한 equals, hashCode 구현, data class 대신 일반 클래스 사용하기를 지키면 큰 문제 없이 코틀린에서 JPA를 사용할 수 있습니다.

코틀린을 쓸 때 자바 라이브러리를 모두 사용할 수 있다고 광고합니다. 실제로 호환성 문제는 그다지 없지만 막상 사용하다보면 불편한 점을 마주칠 때가 많습니다. 이 글을 읽으시는 분들도 코틀린 환경에서 자바 라이브러리를 많이 사용하실 겁니다. 사용하시면서 겪은 불편한 점과 해결 과정을 공유해주신다면 저를 포함해 많은 코틀린 개발자 분들에게 큰 도움이 될 것입니다. 감사합니다!

References

  1. https://kotlinlang.org/docs/all-open-plugin.html
  2. https://kotlinlang.org/docs/no-arg-plugin.html
  3. https://kotlinlang.org/docs/properties.html#getters-and-setters
  4. https://docs.jboss.org/hibernate/orm/6.4/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode
  5. https://kotlinexpertise.com/hibernate-with-kotlin-spring-boot/
  6. https://jpa-buddy.com/blog/best-practices-and-common-pitfalls/
  7. https://wslog.dev/kotlin-jpa-entity
  8. https://www.inflearn.com/questions/17098/unknown-entity-%EC%98%A4%EB%A5%98?commentId=214093#214093
  9. https://discuss.gradle.org/t/jpa-entity-classes-are-not-discovered-automatically-with-gradle/11339/5
  10. https://blog.sapzil.org/2017/11/02/kotlin-jpa-pitfalls/
  11. https://blog.sapzil.org/2018/08/26/kotlin-jpa-pitfalls-embeddable/#idclass%EC%99%80-implicitnamingstrategy
comments powered by Disqus

Related