Kotlin 환경에서의 Querydsl 적용 과정 중 발생한 이슈들에 관한 정리
새롭게 시작한 사이드 프로젝트는 Kotlin 공부를 위하여 Kopring + JPA + Querydsl으로 진행하기로 했다. Spring은 java 진영의 웹 프레임워크이기에 이를 편하게 사용하려면 추가적인 설정들이 필요했다. 또한 기존에 쓰던 jdk11이 아닌 jdk17을 적용함으로써 발생한 이슈들 또한 존재하였다.
Spring의 Bean 관리 방식은 CGLIB Prxoy 방식으로 이는 Target Class를 상속받아 생성된다. 하지만 Kotlin의 기본 클래스는 접근 제한자가 final
로 설정돼있어 상속을 할 수가 없다. 이를 허용하려면 open
키워드를 사용하여 상속이 가능한 상태로 열어주어야 하는데, 이를 플러그인을 추가하여 도움을 받을 수 있다.
또한 JPA가 지원하는 Proxy를 통한 Lazy Loading
을 사용하기 위해선 마찬가지로 open
명시해야하는데, 이 또한 아래 플러그인을 통해 함께 해결할 수 있다. 단, 이 플러그인은 @Entity, @Embeddable, @MappedSuperclass들을 적용 대상으로 삼지 않기 때문에 별도로 정의해줘야 한다.
// 동일한 기능 (Kotlin 관련 플러그인에 특화된 경우 아래처럼 추가할 수 있음)
// id("org.jetbrains.kotlin.plugin.spring") version "1.9.21"
kotlin("plugin.spring") version "targetVersion"
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.Embeddable")
annotation("jakarta.persistence.MappedSuperclass")
}
마지막으로 JPA의 경우 Entity 혹은 DTO에 기본 생성자가 존재하지 않으면 Reflection을 진행하지 못해 에러가 발생
한다. 따라서 기본 생성자를 default로 제공해주게 해주는 플러그인 또한 추가해줘 개발 편의성을 증대시킬 수 있다.
// id("org.jetbrains.kotlin.plugin.jpa") version version "targetVersion"
kotlin("plugin.jpa") version "targetVersion"
Kotlin을 선택하면서 빌드 도구 또한 build.gradle이 아닌 build.gradle.kts로 변경되었다. 따라서 자연스럽게 annotationProcessor에서 kapt로 Annotation Processor이 변경되었다.
Annotaion Processor란?
컴파일 단계에서 Annotation에 컴파일 단계에서 정의된 코드베이스를 검사, 수정, 생성하는 역할
이를 사용하기 위해선 아래와 같은 플러그인을 추가해야 한다.
kotlin("kapt") version version "targetVersion"
springboot의 버전을 3으로 올렸기에 jdk는 강제적으로 17 이상을 선택할 수 밖에 없다. 이로 인해 기존 javax 패키지가 jakarta 패키지로 변경됐으며 따라서 의존성을 추가할 때 이를 고려해서 추가하여야 한다.
1-3 이슈의 연장선으로, 기존 Querydsl 방식과는 사뭇 다르게 이를 적용해야한다.
// querydsl
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
implementation("com.querydsl:querydsl-apt:5.0.0:jakarta")
implementation("jakarta.persistence:jakarta.persistence-api")
implementation("jakarta.annotation:jakarta.annotation-api")
kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
kapt("org.springframework.boot:spring-boot-configuration-processor")
implementation 부분을 보면 뒤에 classifier에 jakarta
가 붙어있는 것을 볼 수 있다. 이처럼 classifier를 명확하게 명시함으로써 springboot3, jdk17과 Querydsl을 올바르게 연동할 수 있다.
위 코드 중 아래의 kapt 부분을 보자.
// Querydsl Q Class 생성해주는 Annotation Processor
kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
// SrpingBoot @ConfigurationProperties
kapt("org.springframework.boot:spring-boot-configuration-processor")
위의 Annotation Processor은 Q Class를 생성해주는 기능을 담당하기에 Querydsl을 사용하는 경우 필수이다. 만약 이 부분의 classifier가 jpa로 설정되어있다면 build 시점에 아래와 같은 에러가 발생한다.
(명령어: ./gradlew build --stacktrace)
Execution failed for task ':kaptKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask$KaptExecutionWorkAction
> java.lang.reflect.InvocationTargetException (no error message)
-- 중단 생략... --
Caused by: java.lang.NoClassDefFoundError: javax/persistence/Entity
at com.querydsl.apt.jpa.JPAAnnotationProcessor.createConfiguration(JPAAnnotationProcessor.java:37)
at com.querydsl.apt.AbstractQuerydslProcessor.process(AbstractQuerydslProcessor.java:82)
at org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor.process(incrementalProcessors.kt:90)
at org.jetbrains.kotlin.kapt3.base.ProcessorWrapper.process(annotationProcessing.kt:209)
at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:1023)
... 44 more
Caused by: java.lang.ClassNotFoundException: javax.persistence.Entity
... 49 more
intellij에서 단순히 project build를 하면 상단의 에러 3줄만 노출되어 원인을 찾는데 상당한 시간을 낭비하였다ㅠㅠ...
핵심은 Caused by: java.lang.ClassNotFoundException: javax.persistence.Entity
이 부분으로 jakarta 패키지로 이관된 기존의 javax.persistence.* 내부 클래스들에 Querydsl Annotation Processor가 접근하려고 하자 문제가 발생한 것이다.
위에 언급했다싶이 처음엔 원인을 알지 못했기에 해당 kapt를 주석 처리하고 build를 진행해보았다. 즉, 아래의 SpringBoot 관련 kapt는 남겨둔 채로 build를 하였는데 아이러니하게도 QClass가 정상적으로 생성되고 Querydsl 기능 또한 정상적으로 동작하였다.
당장 원인을 알 수 없었기에 세부적으로 다시 한 번 해당 kapt에 관하여 공부하는 시간을 가져야겠다.
최종적으로 위의 내용들을 모두 적용하고 정상 작동을 확인한 build.gradle.kts는 아래와 같다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript {
repositories {
mavenCentral()
}
}
plugins {
id("org.springframework.boot") version "3.2.1"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.21"
kotlin("kapt") version "1.9.21"
kotlin("plugin.spring") version "1.9.21"
kotlin("plugin.jpa") version "1.9.21"
}
group = "com.turnover.my"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// querydsl
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
implementation("com.querydsl:querydsl-apt:5.0.0:jakarta")
implementation("jakarta.persistence:jakarta.persistence-api")
implementation("jakarta.annotation:jakarta.annotation-api")
kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
kapt("org.springframework.boot:spring-boot-configuration-processor")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
//tasks.named("test") {
// (this as org.gradle.api.tasks.testing.Test).useJUnitPlatform()
//// useJUnitPlatform()
//}
이 포스팅을 작성한 가장 큰 이유는 새 프로젝트를 생성할 때 마다 다르게 설정 방식이 달라지는 Querydsl의 세팅 환경에 대하여 정리하고 싶었다는 점이다. 실제로 Querydsl은 JPA, jdk에 종속적인 라이브러리고 이들의 버전이 바뀔 때마다 세팅하는 방법이 조금씩 달라진다. 구글링하여 본 포스팅들 또한 각각의 방법이 다 다르고, 무엇보다 적용되지 않는 상세한 이유에대한 포스팅은 찾지 못하였다. 결과적으론 직접 적으며 다시 한 번 제대로 정리하게 되어 보람찬 포스팅이 되었다.
정리하면서도 부족한 부분은 다음에 다른 주제로 포스팅을 해봐도 좋을 것 같다.
좋은 정리네요! 고생하셨습니다 :)