Kotlin 프로젝트에서 JPA 설정을 간결하고 Kotlin 친화적으로 만드는 방법

Jayson·2025년 5월 30일
0
post-thumbnail

Kotlin과 JPA 사용 시 만나는 문제점

Kotlin으로 Spring Data JPA 등을 사용하다 보면 두 가지 큰 제약을 바로 만나게 됩니다. 첫째, Kotlin 클래스는 기본적으로 final이라서 JPA가 프록시(proxy)를 만들기 어렵습니다. Hibernate 구현체 기준으로 JPA 엔티티 클래스와 그 메소드, 필드는 final이면 안 되는 것이 권장됩니다. 실제 Hibernate 문서를 보면 "엔티티 클래스는 final이면 안 되며, 엔티티 클래스의 메서드나 필드도 final이면 안 된다. 기술적으로 Hibernate가 final 클래스를 처리할 수는 있으나, 그렇게 하면 지연 로딩(lazy-loading)을 위한 프록시 생성을 할 수 없다"고 합니다. 요약하면 엔티티 클래스와 그 멤버가 모두 open(열려있어야) 해야 지연 로딩 같은 JPA 기능을 제대로 활용할 수 있습니다. Kotlin에서는 클래스와 멤버가 기본적으로 final이므로, 별도의 조치를 하지 않으면 이러한 프록시 기반의 기능이 제한됩니다.

두번째 문제는 Kotlin 클래스에는 자바처럼 기본 생성자(no-arg constructor)가 없다는 점입니다. JPA 표준 명세상 엔티티 클래스에는 public 또는 protected 기본 생성자가 필요합니다. Hibernate를 사용하면 기본 생성자가 없을 때 엔티티를 조회하는 시점에 InstantiationException: No default constructor for entity 예외가 발생합니다. 실제로 엔티티 매니저로 find를 호출하면, JPA가 리플렉션으로 객체를 인스턴스화하려다 기본 생성자가 없어서 실패하게 됩니다. Kotlin에서는 일반적으로 주 생성자(primary constructor)에 필요한 프로퍼티를 정의하는데, 이 경우 파라미터가 하나라도 있으면 컴파일러가 기본 생성자를 만들어 주지 않습니다. 따라서 엔티티를 Kotlin으로 정의하면 별도로 기본 생성자를 작성하거나 모든 파라미터에 기본값을 지정해줘야 하는 번거로움이 생깁니다.

요약하면, Kotlin과 JPA를 함께 사용할 때:

  • JPA 프록시 생성을 위해 엔티티 클래스 및 그 필드를 open 해야 한다.
  • JPA 엔티티의 인스턴스 생성을 위해 기본 생성자를 제공해야 한다.

이를 수동으로 처리하면 엔티티 클래스마다 open 키워드를 일일이 붙이고, 불필요한 기본 생성자나 기본값을 작성하는 보일러플레이트 코드가 늘어납니다. 다행히 Kotlin에서는 이러한 문제를 해결하기 위한 전용 컴파일러 플러그인들을 제공하여 JPA 관련 설정을 더욱 Kotlin 친화적으로 만들 수 있습니다.

kotlin-allopen 플러그인: final 클래스 문제 해결

앞서 언급한 final 클래스 문제를 해결해주는 것이 바로 kotlin-allopen 컴파일러 플러그인입니다. 이 플러그인은 지정한 애노테이션이 붙은 클래스에 대해서 컴파일 시 자동으로 클래스와 그 멤버에 open 키워드를 적용합니다. Kotlin 공식 문서에도 "Kotlin의 클래스와 멤버는 기본적으로 final인데, Spring AOP와 같은 프레임워크에서는 클래스를 open으로 만들어야 하는 요구사항이 있다. all-open 플러그인은 특정 애노테이션이 붙은 클래스와 그 멤버를 명시적인 open 키워드 없이도 열어준다"고 설명되어 있습니다

JPA 환경에서는 엔티티에 @Entity 등을 붙이는데, 이 플러그인을 사용하면 @Entity 클래스는 컴파일된 바이트코드에서 자동으로 open 클래스로 변경되고, 모든 프로퍼티도 open이 됩니다. 즉, 코드 상으로는 open을 전혀 적어주지 않아도 JPA/Hibernate가 프록시를 만들 수 있는 형태로 바뀝니다. Lazy Loading(지연 로딩)을 사용하는 경우에도, 엔티티 클래스와 필드가 open으로 열려 있어야 Hibernate가 지연 로딩 프록시 객체로 대체할 수 있습니다. kotlin-allopen 플러그인을 적용하면 개발자가 일일이 open을 명시하지 않아도 되므로 보일러플레이트가 줄어듭니다.

Gradle Kotlin DSL 설정: build.gradle.kts 파일에 Kotlin All-Open 플러그인을 추가하고 JPA 관련 애노테이션을 지정하면 됩니다. 예를 들면 다음과 같습니다:

plugins {
    kotlin("plugin.allopen") version "1.9.25"
    // ... (기존 kotlin("jvm") 등 플러그인들)
}
 
allOpen {
    annotation("jakarta.persistence.Entity")
    annotation("jakarta.persistence.Embeddable")
    annotation("jakarta.persistence.MappedSuperclass")
}

위와 같이 설정하면 컴파일러가 @Entity, @Embeddable, @MappedSuperclass 애노테이션이 붙은 클래스는 자동으로 open으로 취급합니다. 만약 Spring Boot를 사용 중이라면 기본적으로 kotlin("plugin.spring") 플러그인이 포함되어 있어 Spring 관련 (@Component, @Transactional 등) 클래스는 자동으로 open 처리됩니다. 그러나 @Entity는 kotlin-spring 플러그인의 대상이 아니므로 별도로 all-open 설정을 해주어야 합니다. Spring 공식 가이드에서도 "Lazy fetch가 제대로 동작하려면 엔티티를 open으로 만들어야 하며, 이를 위해 Kotlin allopen 플러그인을 사용할 것"을 권장하고 있습니다.

kotlin-noarg 플러그인: 엔티티의 기본 생성자 자동 생성

다음으로 기본 생성자 문제를 해결해주는 것이 kotlin-noarg 컴파일러 플러그인입니다. 이 플러그인은 지정한 애노테이션이 붙은 클래스에 대해 매개변수가 없는 생성자(기본 생성자)를 컴파일 시에 추가로 생성해줍니다. 생성된 생성자는 바이트코드 상에서만 존재하는 synthetic 생성자이며, 직접 호출할 수는 없지만 리플렉션으로는 호출 가능하다는 특징이 있습니다. 이러한 synthetic 기본 생성자를 통해 JPA가 Kotlin 엔티티를 문제없이 인스턴스화할 수 있게 됩니다.

간단히 말해, kotlin-noarg를 사용하면 엔티티 클래스에 개발자가 일일이 기본 생성자를 작성하지 않아도 컴파일러가 알아서 JPA가 호출할 생성자를 추가해줍니다. 이를 적용하지 않은 경우 Kotlin 코드에서는 JPA 요구사항을 맞추기 위해 아래와 같은 방식을 썼을 것입니다:

  • 직접 기본 생성자 정의: 엔티티 클래스 내부에 constructor() { ... }를 정의 (필드를 초기화하거나 비워둠).
  • 모든 필드에 기본값 지정: 주 생성자의 모든 파라미터에 기본값을 넣어 자동으로 기본 생성자가 생기도록 함. 예를 들어 class Member(var name: String = "")처럼 모든 프로퍼티에 = "" 또는 = null을 지정.

이러한 수작업은 번거롭기도 하고, 불필요한 초기값("" 등)을 남겨야 하는 단점이 있습니다. kotlin-noarg 플러그인을 쓰면 이런 작업 없이도 컴파일 시점에 JPA용 기본 생성자가 자동 생성되므로, Kotlin스러운 간결한 문법을 유지할 수 있습니다.

Gradle 설정: Gradle Kotlin DSL에서 no-arg 플러그인을 적용하고 JPA 관련 애노테이션을 지정하면 됩니다. 예를 들어 수동으로 설정한다면:

plugins {
    kotlin("plugin.noarg") version "1.9.25"
}
 
noArg {
    annotation("jakarta.persistence.Entity")
    annotation("jakarta.persistence.Embeddable")
    annotation("jakarta.persistence.MappedSuperclass")
    // invokeInitializers 옵션: 필요하다면 true 설정 (기본 생성자가 필드 초기화 블록도 호출하게 함)
    
}

위 설정은 @Entity 등으로 표시된 클래스에 대해 매개변수 없는 생성자를 자동 추가해줍니다. 이렇게 하면 Intellij 등의 검사에서 사라지는 메시지를 볼 수 있습니다. 실제로 kotlin-noarg 적용 후에는 "Class 'Member' should have [public, protected] no-arg constructor" 같은 오류가 더 이상 나타나지 않았다는 보고도 있습니다.

kotlin-jpa 플러그인: JPA 전용 설정의 간소화

JetBrains에서는 JPA 사용 시 위의 noarg 설정을 더 편하게 할 수 있도록 kotlin-jpa 플러그인을 제공합니다. kotlin-jpa는 사실상 kotlin-noarg 플러그인의 JPA 특화 래퍼입니다. 이 플러그인을 적용하면 별도의 noArg { ... } 블록을 작성하지 않아도 자동으로 @Entity, @Embeddable, @MappedSuperclass를 no-arg 대상으로 지정해줍니다. 즉, kotlin-jpa는 위에서 수동으로 한 noarg 설정을 한 번에 해결해주는 편의성 플러그인입니다.

Gradle 설정: 사용법은 간단합니다. build.gradle.kts의 플러그인 섹션에 다음과 같이 추가하기만 하면 됩니다:

plugins {
    kotlin("plugin.jpa") version "1.9.25"
}

이렇게 kotlin-jpa를 적용하는 것만으로, JPA를 위한 모든 기본 생성자 문제가 해결됩니다. Kotlin 공식 가이드와 Spring Boot 가이드에서도 Kotlin에서 JPA를 사용할 때 이 플러그인을 활성화할 것을 추천하고 있습니다.

단, 앞서 설명했듯이 kotlin-jpa 플러그인은 기본 생성자(no-arg) 문제만 해결해주고, 엔티티 클래스를 open으로 만들어주지는 않습니다. 클래스 open 처리는 kotlin-allopen이나 kotlin-spring 플러그인이 맡는 역할입니다. Spring Boot 초기 생성된 프로젝트를 보면 기본으로 kotlin-jpa와 kotlin-spring 두 개가 포함되는데, 이 경우 JPA 엔티티에 대해서는 기본 생성자는 자동 생성되지만 클래스가 open으로 열리지는 않습니다. 그 결과 Kotlin 코드가 아무 변경 없이도 동작은 하지만, 앞서 말한 Lazy 로딩과 같은 기능은 제대로 작동하지 않을 수 있습니다 (프록시 객체가 만들어지지 않아 즉시 로딩되는 등). 따라서 kotlin-jpa를 사용할 때는 반드시 앞 절에서 언급한 all-open 설정도 함께 적용해야 JPA를 100% 활용할 수 있습니다.

권장 플러그인 조합과 마무리

요약하면, Kotlin 1.9.25 + Spring Boot 3.x 환경에서 JPA와 Kotlin을 자연스럽게 통합하려면 다음과 같은 플러그인 조합과 설정을 권장합니다:

  • kotlin("plugin.jpa") 플러그인 – JPA 관련 엔티티에 대해 기본 생성자를 자동 추가 (no-arg 설정). JPA 사용 시 꼭 활성화하여 Kotlin의 non-null 프로퍼티를 엔티티에 사용할 수 있도록 합니다.

  • kotlin("plugin.allopen") 플러그인 (또는 Spring Boot의 kotlin("plugin.spring") + 수동 allOpen 설정) – 엔티티 클래스 및 필드를 자동으로 open 처리. build.gradle.kts의 allOpen { ... } 블록에 JPA 애노테이션(@Entity, @Embeddable, @MappedSuperclass)을 지정하여 Lazy 로딩 시 프록시 객체가 활용될 수 있게 합니다.

  • (선택) kotlin("plugin.noarg") 플러그인 – 만약 kotlin-jpa를 사용하지 않는 특별한 경우에 직접 noarg 대상을 지정해야 할 때 사용합니다. 일반적인 Spring Boot 프로젝트에서는 kotlin-jpa로 충분합니다.

위와 같은 설정을 통해 엔티티 클래스 작성 시 따로 open이나 기본 생성자를 신경 쓰지 않아도 됩니다. 예를 들어 Lazy 로딩이 필요한 연관 관계도 그냥 평소처럼 @ManyToOne(fetch = LAZY)를 선언하면, 컴파일된 클래스에서는 해당 필드와 getter가 open으로 열려 있어 Hibernate가 적절한 시점에 프록시를 초기화해줄 것입니다. 반대로 이런 설정 없이 final로 두면 Lazy 설정이 무색하게 즉시 두 번의 쿼리가 나가거나, 성능상 이슈가 생길 수 있다는 점을 기억해야 합니다.

마지막으로, Kotlin 지원이 잘 갖춰진 최신 Spring Data JPA 환경에서는 위 플러그인들의 도움으로 Kotlin의 null 안전성과 data class 형태를 최대한 활용하면서 JPA를 사용할 수 있습니다. 불필요한 보일러플레이트를 제거하고, 엔티티 설계를 Kotlin스럽게 표현해보세요. 이러한 설정 조합은 이미 많은 Kotlin 백엔드 프로젝트에서 표준처럼 쓰이고 있으며, Kotlin 공식 문서와 Spring 가이드에서도 권장되는 방식입니다. Kotlin 친화적인 JPA 환경을 구축하여 더 깨끗하고 유지보수 쉬운 백엔드 코드를 작성하시기 바랍니다.

참고

profile
Small Big Cycle

0개의 댓글