본문 바로가기
Java & Kotlin/Spring

[Spring] kotlin spring lazy loading 삽질기

by heekng 2022. 8. 13.
반응형

Kotlin spring lazy loading 삽질기

kotlin+spring 시작하기에 이어 기존에 만들었던 crud 위중의 프로젝트를 kotlin+spring 프로젝트로 변경하던 중 JPA와 Jackson 그리고 Jpa의 LazyLoading으로 인해 몇 시간 동안 붙잡게 되었습니다.
이를 해결하기 위해 진행했던 방법, 결과적으로 잘못되었던 점에 대해 짚어보려 합니다.

allOpen 옵션 열어두기

먼저 kotlin의 open에 대해 알아보면, java와 다르게 kotlin의 클래스는 기본적으로 final으로 설정되어 있습니다.
hibernate에서 사용하는 CGLIB는 상속을 기반으로 프록시 기술을 사용하기 떄문에 코틀린 클래스에 대해 상속을 열어두어야 프록시 기술을 사용할 수 있습니다.

기존의 build.gradle에 org.jetbrains.kotlin.plugin.jpaorg.jetbrains.kotlin.plugin.spring 플러그인을 추가하였습니다.

Spring 플러그인

org.jetbrains.kotlin.plugin.spring 플러그인은 All-open 플러그인과 noArg 플러그인을 포함하고 있습니다.

all-open 플러그인은 @Component, @Async, @Transactional, @Cacheable, @SpringBootTest, @Component 어노테이션에 대해 open을 사용한 효과를 제공합니다.

allOpen {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.Embeddable")
    annotation("javax.persistence.MappedSuperclass")
}

또한 위와 같이 allOpen{}을 이용하여 특정 어노테이션이 붙은 클래스에 대해 open을 설정할 수 있는 옵션을 제공합니다.

allOpen을 이용하여 @Entity, @Embeddable, @MappedSuperclass 어노테이션에 대해 open을 열어두었습니다.

Jpa 플러그인

org.jetbrains.kotlin.plugin.jpa 플러그인은 Jpa 사용시 Entity 생성에 필요한 기본 생성자를 만들어주는 역할을 합니다.

Kotlin의 경우 기본 생성자를 만들지 않고 사용하기 때문에, 이를 해결해주는 noArg 옵션을 제공합니다.

해당 플러그인에서 noArg 설정을 기본 제공하는 어노테이션은 @Entity, @Embeddable, @MappedSuperClass 이며, all-open과 동일하게

noArg {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.MappedSuperclass")
    annotation("javax.persistence.Embeddable")
}

위와 같이 직접 어노테이션을 선택하여 추가할 수 있습니다. (위의 예시는 기본적으로 noArg가 설정되어 있습니다.)

또한, jpa 플러그인은 리플렉션시에만 사용하는 기본 생성자를 제공하기 때문에, java에서 lombok을 사용해 Protected로 생성하던 기본생성자를 접근제한을 생각하지 않고 사용할 수 있습니다.

build.gradle -> build.gradle.kts 로 변환하기

위 과정을 진행했음에도 API 응답 시 지연로딩이 되지 않아 같은 오류를 보게 되었습니다.
allOpen{}이 정상 작동하지 않기 때문에 지연로딩이 발생한다 판단하였고, Java to kotlin spring에서 가장 마지막으로 미뤄두었던 build.gradle을 build.gradle.kts로 변경하는 것을 진행했습니다.

before

plugins {
    id 'org.springframework.boot' version '2.6.4'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'

    // kotlin 추가
    id 'org.jetbrains.kotlin.jvm' version '1.6.21'
    id 'org.jetbrains.kotlin.plugin.jpa' version '1.6.21'
    id 'org.jetbrains.kotlin.plugin.spring' version '1.6.21'
}

group = 'com.heekng'
version = '0.0.1-SNAPSHOT'+new Date().format("yyyyMMddHHmmss")
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0' //쿼리 파라미터를 더 깔끔하게 확인할 수 있게 도와주는 라이브러리
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.h2database:h2'
    implementation 'org.postgresql:postgresql'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // kotlin 추가
    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
    implementation 'org.jetbrains.kotlin:kotlin-reflect:1.6.21'
    implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.13.3'

}

jar {
    enabled = false;
}

tasks.named('test') {
    useJUnitPlatform()
}

compileKotlin {
    kotlinOptions {
        jvmTarget = "11"
    }
}

compileTestKotlin{
    kotlinOptions {
        jvmTarget = "11"
    }
}

after

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

group = "com.heekng"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

plugins {
    id("org.springframework.boot") version "2.6.4"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"

    val kotlinVersion = "1.6.21"

    // kotlin 추가
    kotlin("jvm") version kotlinVersion
    kotlin("plugin.jpa") version kotlinVersion
    kotlin("plugin.spring") version kotlinVersion
    kotlin("kapt") version kotlinVersion
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0") //쿼리 파라미터를 더 깔끔하게 확인할 수 있게 도와주는 라이브러리
    implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity5")
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    runtimeOnly("com.h2database:h2")
    implementation("org.postgresql:postgresql")
    runtimeOnly("mysql:mysql-connector-java")
    testImplementation("org.springframework.boot:spring-boot-starter-test")

    // kotlin 추가
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
}

allOpen {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.Embeddable")
    annotation("javax.persistence.MappedSuperclass")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

tasks.getByName<Jar>("jar") {
    enabled = false
}

몇 가지 문법상의 차이를 제외한 나머지는 변경이 없었습니다.

그런데!! 테스트코드에서는 정상적으로 지연로딩이 되지만, API에서 사용할 때에는 지연로딩이 되지 않는 문제가 나타났습니다.

위 과정까지는 당연히 해야했던 설정… 뭐가 문제일까?

도대체 무슨이유로 원하는대로 작동되지 않았을까?

이유는 Entity -> DTO 클래스로 변경하는 과정에 있었습니다.

1번

2번

위 두가지 코드의 차이점이 무엇일까?

바로 생성자의 차이입니다.

자바에서 코틀린으로 변경하는 과정에서 매번 클래스명 오른쪽의 ()가 생성자의 역할도 하지만, 필드를 명시할 수 있다는 점을 잊고 있었습니다.
때문에 staffCount를 가져오는 과정에서 shop 엔티티의 staffList가 프록시객체에서 엔티티 객체로 변경되었고, 생성자에 명시되어있는 shop이 필드에 포함되어 shop -> staff -> shop의 형태로 순환참조하게 되었습니다.

이를 막기 위해 기존의 필드를 주 생성자로 옮기고, Shop 객체를 통해 필드를 채워주는 것을 부 생성자로 옮겨 ListResponse dto 객체는 id, name, info, staffCount 네가지 필드만 소유하게 수정하였습니다.

마치며

holix spring 관리자님의 말씀

언어를 배우는 가장 쉬운 방법은 해당 언어로 무언가 계속해 만들어가는 것이라 생각한다.
이는 코딩테스트 문제던, 웹이던 코드를 작성하는 것을 의미한다.

기존의 코드를 kotlin spring 프로젝트로 옮기는 것을 완료했다.
확실하게 느낀 것은 코드가 단순해지고, 이해하기 쉬워졌으며 빙산의 일각이겠지만 코틀린이라는 언어의 매력을 느끼고 있는 것 같다.

여러분 코틀린하세요..!!

반응형