[Kotlin] 코틀린과 JPA를 함께 사용할 때 주의사항 모음

Loopy·2023년 5월 14일
5

삽질기록

목록 보기
18/28
post-thumbnail

코틀린으로 프로젝트를 진행하다가, JPA를 사용했을 때 자바와는 약간 차이점이 존재해 주의해야할 점이 생겨서 정리해보기로 하였다.

☁️ Data class 기본 생성자

Data Class는 기본으로 게터/세터, 생성자, equals와 hashcode, toString을 만들어주므로 실제 개발할 때는 자주 사용하게 되었다. 하지만 문제점은 Data class를 만들면, 기본 생성자는 만들어지지 않는다.

data class Request(
    @NotNull val title: String,
    @NotNull val content: String,
    @NotNull val subwayLine: Long,
    @NotNull val lostType: LostType,
    val imgUrls: List<String>?
)

1. Default Value 선언

코틀린 공식 문서에 따르면, 모든 필드에 기본값을 설정해주면 기본 생성자가 만들어진다.

data class Request(
    @NotNull val title: String = "",
    @NotNull val content: String = "",
    @NotNull val subwayLine: Long = 0L,
    @NotNull val lostType: LostType = LostType.ACQUIRE,
    val imgUrls: List<String>? = listof("1")
)

2. 부생성자 사용

data class Request(
    @NotNull val title: String,
    @NotNull val content: String,
    @NotNull val subwayLine: Long,
    @NotNull val lostType: LostType,
    val imgUrls: List<String>?
) {
    constructor() : this("", "", 0L, LostType.ACQUIRE, listof("1"))
}

하지만 부 생성자 역시 주 생성자 호출이 반드시 필요하므로, 코드가 너무 지저분해진다는 단점이 있다. 따라서, 주로 첫 번째 방법을 선호한다고 한다.

☁️ Final 클래스와 JPA 엔티티

코틀린은 기본적으로 생성되는 모든 클래스들은 final 이다. final 키워드가 붙으면, 상속이 불가능해지는데 이렇게 되면 어떠한 문제들이 발생할까?

  1. AOP 가 적용이 되지 않는다.

Spring AOP는 CGLIB 프록시 기반으로 동작하기 때문에, 상속을 금지하게 되면 당연히 프록시 생성이 불가능하므로 final 클래스이면 안된다.

@Service
@Transactional(readOnly = true) // 적용 되지 않음
class LostPostService(
)
  1. 지연로딩(Lazy Initialization)시 프록시 객체를 생성하지 못한다.

하이버네이트 공식 문서에 따르면, 기본 JPA 스펙에서는 영속성 클래스와 메서드에 final 이 붙으면 안된다고 나와있다. 그리고 그 이유는, 바로 영속성 컨텍스트의 지연 로딩과 밀접한 관련이 있다.

JPA에서는, 조회하려는 객체의 연관된 엔티티의 필드값을 나중에 가져오고 싶을 때 불필요한 SQL 조회 쿼리가 나가는 것을 막기 위해 원본 엔티티를 상속받은 프록시 객체를 생성한다. 프록시 객체는 ID만 존재하기 때문에, 필드에 데이터가 없어도 먼저 조회할 수 있게 되는 것이다.

따라서 다음과 같은 과정이 발생하는데, 당연히 상속을 막아두면 지연 로딩이 힘들어진다.

  1. 프록시 객체를 생성하고 ID 값만 세팅한다.
  2. 이후 프록시에 존재하지 않는 필드 사용이 필요해지면 조회 쿼리를 날린다.
  3. DB에서 가져온 값을 실제 엔티티에 세팅한다.
  4. 프록시 엔티티는 실제 엔티티의 참조를 가지고 있기 때문에 프록시 메서드 호출하면 실제 엔티티가 호출된다.

해당 문제는, 코틀린에서 상속을 허용하는 open 을 붙이면 해결할 수 있다. 하지만 모든 클래스에 매번 open 을 붙이기 번거로우니, 자동화해주는 스프링 플러그인을 사용해보자.

Spring Plugin

역시나 공식 문서에 따르면 스프링 플러그인@Component, @Async, @Transactional, @Cacheable, @SpringBootTest 와 같은 거의 모든 클래스에 자동으로 open 키워드를 추가해준다.

하지만 @Entity, @Embeddable 과 같은 JPA와 관련한 어노테이션들은 대상에서 제외되므로, 아래와 같이 build.gradle에 수동으로 추가해주어야 한다.

allOpen {
    annotation("jakarta.persistence.Entity")
    annotation("jakarta.persistence.MappedSuperclass")
    annotation("javax.persistence.Embeddable")
}
plugins {
    kotlin("plugin.spring") version "1.7.22"
}

🔖 참고사항
하이버네이트 5.0 버전에서는 bytecode enhancement 을 통해 final 클래스 유무에 상관 없이 지연 로딩이 가능하다고 한다. 즉 하이버네이트와 같은 구현체에서는, 위의 스펙들을 반드시 지키지 않았을 때의 문제를 회피할 수 있도록 유연하게 동작한다.

☁️ 기본 생성자와 JPA 엔티티

위에서 언급했듯이, 기본 값을 설정해주거나 따로 보조 생성자를 만들어주지 않는 이상 기본 생성자가 존재하지 않는다. 하지만 하이버네이트는 리플렉션을 통해 엔티티 클래스를 인스턴스화하기 때문에, 반드시 기본 생성자가 필요하다.

리플렉션을 통한 객체 생성

리플렉션은 정보를 얻기 위해 static 영역에 저장되어 있는 클래스 정보들을 활용한다. 따라서, 클래스 이름만 알고 있다면 가시성 정책에 영향을 받지 않고 private 메서드라도 접근이 가능해진다.

동적으로 객체를 생성해야 할때 리플렉션을 사용할 수도 있는데, 아래와 같이 기본 생성자를 만들고 newInstance() 를 호출하면 된다.

fun createNewInstance() {
    val tempClass: Class<*> = Temp::class.java
    val constructor: Constructor = tempClass.getConstructor()  // 인자가 없는 생성자 
    val result: Temp = constructor.newInstance() as Temp
}

물론, 인자가 있는 생성자에 대해서도 인자로 전달될 데이터만 알고 있다면 아래와 같이 적용이 가능하다.

fun createNewInstance() {
    val tempClass: Class<*> = Temp::class.java
    val constructor: Constructor = tempClass.getConstructor(String::class.java)  // 인자가 있는 생성자 
    val result: Temp = constructor.newInstance("parameter") as Temp
}

하지만, 자바 리플렉션을 통해 가져올 수 없는 정보 중 하나가 바로 생성자의 인자 정보들이다. 따라서, 당연하게 파라미터가 있는 생성자만 있다면 인자 값을 모르니 객체를 생성할 수 없게 된다.

@Entity 에 대한 기본 생성자는 kotlin-jpa 플러그인이 자동으로 생성해주므로, 이를 통해 간단하게 문제를 해결할 수 있다.

JPA Plugin

noArg {
    annotation("jakarta.persistence.Entity")
}
plugins {
    kotlin("plugin.jpa") version "1.7.22"
}

🔖 주의사항
kotlin-jpa 플러그인은 자동으로 기본 생성자를 만들어주기는 하지만, 우리가 코드에서 빈 객체를 생성할 때와 같이 기본 생성자를 직접적으로는 호출하지는 못한다고 한다.

참고 자료
https://spring.io/guides/tutorials/spring-boot-kotlin/
https://1-7171771.tistory.com/123
https://hbase.tistory.com/350#:~:text=객체%20생성,를%20생성할%20수도%20있다.&text=Constructor%20객체를%20이용해서,의%20객체가%20생성된다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글