본문 바로가기
  • 개발하는 곰돌이
Development/JPA

[Kotlin/JPA] 코틀린과 JPA를 함께 사용할 때 추가적으로 설정해야 하는 것들과 Data class

by 개발하는 곰돌이 2023. 3. 29.

목차

    개요

    JPA는 자바 진영에서 DB와 연동하기 위해 사용하는 기술 중 하나이다. 자바와 100% 호환되는 코틀린의 특성상 JPA 역시 사용할 수 있긴 하지만 기본적으로 자바에 맞춰진 기술이다 보니 코틀린과 JPA를 함께 사용할 때 문제없이 사용하기 위해선 추가적으로 설정해야 할 것들이 있다는 것을 알게 되었다. 이 글에서는 코틀린과 JPA를 함께 사용할 때 추가적으로 설정해야 하는 것들과 그 외의 주의사항에 대해 정리한다.

     

    JPA의 구현체는 가장 많이 사용되는 하이버네이트를 따른다.

    JPA의 Entity가 갖는 요구사항

    우선 왜 이러한 설정을 사용해야 하는지, 또 왜 이런 사항을 주의해야 하는지를 이해하기 위해선 JPA의 엔티티가 갖는 요구사항을 알 필요가 있었다. 하이버네이트 사용자 가이드에서 이야기하는 엔티티가 갖는 요구사항은 여러 가지가 있지만 이 글에서 다룰 내용과 관련 있는 요구사항은 다음과 같다.

    • 엔티티 클래스는 매개변수가 없는 public 또는 protected 생성자를 가져야 한다.
    • 지연 로딩을 사용하려면 엔티티 클래스는 final이 아니어야 한다.

    엔티티 클래스는 매개변수가 없는 public 또는 protected 생성자를 가져야 한다.

    기본적으로 코틀린에서의 클래스는 코틀린의 문법상 프로퍼티를 선언하면서 자동으로 생성되는 기본 생성자를 사용하는 경우가 많다. 그리고 추가적인 생성자는 반드시 기본 생성자를 상속해야하는 코틀린의 문법상 특징으로 인해 코틀린에서는 매개변수가 없는 생성자를 작성하는 것이 굉장히 번거롭다.

     

    이를 위해 코틀린에서는 Gradle 또는 Maven에서 추가적인 플러그인을 지정하는 것으로 @Entity, @Embeddable, @MappedSuperclass 어노테이션이 붙은 모든 클래스에 자동으로 매개변수가 없는 생성자를 만들 수 있다.

     

    빌드 환경에 따라 build.gradle, build.gradle.kts, pom.xml에 아래 코드를 추가한 후 리로드를 하면, 엔티티 클래스를 자바 코드로 디컴파일 했을 때 매개변수가 없는 생성자가 추가되어 있는 것을 볼 수 있다. 이 플러그인은 Spring Initializr를 통해 스프링부트 프로젝트를 생성할 때 Spring Data JPA를 포함하였다면 기본적으로 추가되어 있다.

     

    Gradle(Groovy)

    plugins {
        ...
        id 'org.jetbrains.kotlin.plugin.jpa' version '{Kotlin버전}'
        ...
    }

     

    Gradle(Kotlin)

    plugins {
        ...
        kotlin("plugin.jpa") version "{Kotlin버전}"
        ...
    }

     

    Maven

    <compilerPlugins>
        <plugin>jpa</plugin>
    </compilerPlugins>

     

    좌 : 플러그인 추가 전 / 우 : 플러그인 추가 후의 코틀린의 Member 클래스를 Java로 변경한 코드

     

    이렇게 jpa 플러그인을 통해 자동으로 생성된 매개변수가 없는 생성자는 public 생성자이긴 하지만 자바나 코틀린 코드에서 직접 호출하는 것은 불가능하고, 오직 리플렉션을 통해서만 호출할 수 있기 때문에 간단하게 외부 접근을 허용하지 않는 생성자를 만들 수 있다.

    지연 로딩을 사용하려면 엔티티 클래스는 final이 아니어야 한다.

    코틀린의 모든 클래스는 기본적으로 final 클래스이다. 엔티티 클래스가 final 클래스라고 해도 JPA를 사용하는 것 자체는 문제가 없지만 지연 로딩을 제대로 사용할 수 없다는 단점이 있다.

     

    final 클래스라고 하더라도 완전히 지연 로딩을 사용할 수 없는 것은 아니다. @OneToMany의 경우에는 final 클래스라도 지연 로딩을 사용할 수 있다. 하지만 @OneToMany를 사용하는 경우는 거의 없거나 굳이 있다고 하더라도 양방향 연관 관계를 위해서이다. 대부분의 경우에는 @ManyToOne을 사용하게 되는데, 이 경우에는 엔티티 클래스가 final 클래스라면 지연 로딩이 작동하지 않는다. 따라서, 엔티티 클래스들을 open해줄 필요가 있다.

     

    하지만 엔티티 클래스마다 모두 open 키워드를 붙여주는 것은 굉장히 번거로운 작업이다. 다행히 위의 매개변수가 없는 생성자의 경우와 마찬가지로 이 경우도 추가적인 플러그인을 통해 자동으로 모든 클래스를 open해줄 수 있다.

     

    우선 빌드 환경에 따라 build.gradle, build.gradle.kts, pom.xml에 All-open 플러그인을 추가해야 한다. 그런데 Spring Initializer를 통해 스프링부트 프로젝트를 생성하였다면 아래와 같이 기본적으로 kotlin-spring 플러그인이 추가되어 있을 것이다.

    plugins {
        ...
        id 'org.jetbrains.kotlin.plugin.spring' version '1.7.22'
        ...
    }

    이 kotlin-spring 플러그인은 All-open 플러그인을 포함하고 있기 때문에 kotlin-spring 플러그인이 추가된 상태라면 아래 코드에서 allOpen 블록의 내용만 설정해주면 된다.

     

    Gradle(Spring Boot 3.0.0 이상)

    plugins {
        ...
        id 'org.jetbrains.kotlin.plugin.allopen' version '{Kotlin버전}'	// Gradle(Groovy)
        kotlin("plugin.allopen") version "{Kotlin버전}"	// Gradle(Kotlin)
        ...
    }
    
    allOpen {
        annotation("jakarta.persistence.Entity")
        annotation("jakarta.persistence.Embeddable")
        annotation("jakarta.persistence.MappedSuperclass")
    }

     

    Gradle(Spring Boot 3.0.0 미만)

    plugins {
        ...
        id 'org.jetbrains.kotlin.plugin.allopen' version '{Kotlin버전}'	// Gradle(Groovy)
        kotlin("plugin.allopen") version "{Kotlin버전}"	// Gradle(Kotlin)
        ...
    }
    
    allOpen {
        annotation("javax.persistence.Entity")
        annotation("javax.persistence.Embeddable")
        annotation("javax.persistence.MappedSuperclass")
    }

     

    Maven(Spring Boot 3.0.0 이상)

    <plugin>
        <artifactId>kotlin-maven-plugin</artifactId>
        <groupId>org.jetbrains.kotlin</groupId>
        <version>{Kotlin버전}</version>
    
        <configuration>
            <compilerPlugins>
                <plugin>all-open</plugin>
            </compilerPlugins>
    
            <pluginOptions>
                <option>all-open:annotation=jakarta.persistence.Entity</option>
                <option>all-open:annotation=jakarta.persistence.MappedSuperclass</option>
                <option>all-open:annotation=jakarta.persistence.Embeddable</option>
            </pluginOptions>
        </configuration>
    
        <dependencies>
            <dependency>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-allopen</artifactId>
                <version>{Kotlin버전}</version>
            </dependency>
        </dependencies>
    </plugin>

     

    Maven(Spring Boot 3.0.0 미만)

    <plugin>
        <artifactId>kotlin-maven-plugin</artifactId>
        <groupId>org.jetbrains.kotlin</groupId>
        <version>{Kotlin버전}</version>
    
        <configuration>
            <compilerPlugins>
                <plugin>all-open</plugin>
            </compilerPlugins>
    
            <pluginOptions>
                <option>all-open:annotation=javax.persistence.Entity</option>
                <option>all-open:annotation=javax.persistence.MappedSuperclass</option>
                <option>all-open:annotation=javax.persistence.Embeddable</option>
            </pluginOptions>
        </configuration>
    
        <dependencies>
            <dependency>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-allopen</artifactId>
                <version>{Kotlin버전}</version>
            </dependency>
        </dependencies>
    </plugin>

     

    이렇게 하면 allOpen으로 설정해둔 @Entity, @Embeddable, @MappedSuperclass 어노테이션이 붙은 모든 클래스가 자동으로 open된다.

     

    스프링부트 3.0.0부터는 org.springframework.boot:spring-boot-starter-data-jpa의 JPA 어노테이션들의 최상위 패키지가 javax에서 jakarta로 변경되었기 때문에 스프링부트 버전에 따라서 allOpen에 설정할 어노테이션의 패키지 명을 잘 작성해야 한다.

    JPA의 엔티티에는 data class가 어울리지 않는다!

    코틀린에서는 Lombok의 toString(), equals(), hashCode(), copy()를 기본적으로 포함하고 구조 분해를 사용할 수 있는 data class를 제공한다. 하이버네이트 사용자 가이드를 따르면 equals()hashCode()는 특별한 상황이 아니라면 구현하지 않는 것이 좋다고 언급하고 있다. 또한, 이 특별한 상황에서도 ID값만을 이용하여 구현해야 한다고 언급한다. 하지만 매번 구현하기는 번거로우니 종종 data class로 엔티티를 만드는 경우가 있는데, 두가지 이유로 인해 data class는 JPA의 엔티티로 사용하기에는 적합하지 않다.

     

    첫번째로는 data class가 기본적으로 포함하는 메소드가 StackOverflow를 발생시킬 수 있다는 점이다. 이전 글에서 언급한 것과 같이 양방향 연관 관계를 갖는 두 엔티티 사이에서 자동으로 작성된 toString()이나 equals() 등의 메소드를 사용하면 서로가 상대방의 메소드를 무한히 호출하는 순환참조가 발생하여 StackOverflow를 발생시킨다. 이를 방지하기 위해선 원하는 프로퍼티에 한해서 toString(), equals(), hashCode()를 재정의해야 할텐데 이러면 data class를 쓸 이유가 없지 않을까 싶다.

     

    두번째 이유는 data class가 기본적으로 open될 수 없다는 점이다. Kotlin 공식 문서에는 다음과 같은 언급이 있다.

    Data classes cannot be abstract, open, sealed, or inner.

    그런데 위의 All-open 플러그인을 사용한다면 data class 또한 open이 가능하다. 공식 문서에 data class는 open될 수 없다고 되어 있는 내용과 상충된다.(Kotlin 공식 문서의 data class에 대한 내용과 All-open 플러그인에 대한 내용에서는 관련 내용을 찾아볼 수 없었다.) 어쨌든 순환참조의 문제도 있고 data class 자체가 JPA의 요구사항을 전혀 고려하지 않은 클래스이기 때문에 data class를 억지로 open하면서까지 data class로 엔티티를 만드는 것은 적합하지 않다고 생각한다.

    결론

    코틀린과 JPA를 함께 사용하려면 다음과 같은 점을 고려해야 할 것이다.

    • JPA의 엔티티는 매개변수가 없는 생성자를 가져야한다. → JPA 플러그인을 추가하는 것으로 해결.
    • 지연 로딩을 사용하기 위해선 엔티티 클래스가 open되어야 한다. → All-open 플러그인 설정을 통해 편리하게 해결 가능.
    • JPA엔티티를 data class로 만드는 것은 적합하지 않다.

    참조링크

     

    코틀린에서 하이버네이트를 사용할 수 있을까? | 우아한형제들 기술블로그

    {{item.name}} 신규 시스템을 개발하면서 코틀린과 하이버네이트를 함께 사용한 경험을 나누기 위해 작성해봅니다. 안녕하세요! 서비스플랫폼 팀에서 서버 개발을 하는 김지희, 김석홍입니다. 저희

    techblog.woowahan.com

     

    Spring Boot + Kotlin + JPA 적용하기 Entity 생성시 생각해볼 점들

    2020-05-12 우아한 형제들 기술 블로그 - 코틀린에서 하이버네이트를 사용할 수 있을까?에 나온 내용 추가합니다. 4. data class 사용에 대해 본글에서 적은 순환참조 이슈 외에도 다른 이슈가 나와있

    blog.junu.dev

     

    Hibernate ORM 6.1.7.Final User Guide

    Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

    docs.jboss.org

     

    Data classes

    It is not unusual to create classes whose main purpose is to hold data. In such classes, some standard functionality and some utility functions are often mechanically...

    kotlinlang.org

    댓글