[Kotlin] Spring boot -> Kotlin 마이그레이션 해보기(1)

이번 블로그 포스팅에서는 기존 SpringBoot & Java로 구성되어있던 저의 졸업작품을 SpringBoot & kotlin으로 마이그레이션 하는 것을 포스팅 하겠습니다.
아래 코드는 저의 기존 프로젝트의 build.gradle코드입니다
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.13'
id 'io.spring.dependency-management' version '1.1.4'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
id 'jacoco'
//id 'checkstyle'
}
group = 'io.junseok'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
//jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//security
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
implementation 'org.springframework.boot:spring-boot-starter-security'
//test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
//etc
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-validation'
annotationProcessor 'org.projectlombok:lombok'
//mysql
runtimeOnly 'com.mysql:mysql-connector-j'
//swagger
implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
//s3
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.624'
implementation 'com.slack.api:slack-api-client:1.29.0'
//querydsl
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
}
repositories {
mavenCentral()
}
tasks.named('test') {
useJUnitPlatform()
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
뭐가 굉장히 많죠,,,,?
자 이게 이걸 하나씩 Kotlin으로 마이그레이션 작업을 진행해보겠습니다,,!
우선 plugins 부분부터 하나씩 살펴보겠습니다.
// Before
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.13'
id 'io.spring.dependency-management' version '1.1.4'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
// Afrer
plugins {
val springBootVersion = "2.7.13"
val kotlinVersion = "1.9.24"
val dependencyVersion = "1.1.4"
val lombokVersion = "8.1.0"
id("org.springframework.boot") version springBootVersion
id("io.spring.dependency-management") version dependencyVersion
kotlin("jvm") version kotlinVersion // Kotlin을 JVM 바이트코드로 컴파일하는데 필요
kotlin("plugin.spring") version kotlinVersion
kotlin("plugin.jpa") version kotlinVersion
kotlin("kapt") version kotlinVersion
kotlin("plugin.lombok") version kotlinVersion // Lombok을 Kotlin에서 사용가능하도록 도와줌
id("io.freefair.lombok") version lombokVersion // Lombok을 프로젝트에 쉽게 통합할 수 있도록 도와줌
id("jacoco")
}
val springBootVersion = "2.7.13"
val kotlinVersion = "1.9.24"
val dependencyVersion = "1.1.4"
val lombokVersion = "8.1.0"
저는 코드의 재활용성을 높이고자 버전들을 변수화하여 관리를 하였습니다.
id부분은 기존 Spring boot에서도 사용한 부분이기 때문에 따로 설명은 하지 않겠습니다.
아마 java의 gradle에서는 볼 수 없던 kotlin~으로 시작하는 부분을 보실 수 있는데
kotlin("jvm") version kotlinVersion
위 코드는 Kotlin를 자바의 가상머신인 JVM의 바이트코드로 컴파일하는데 사용하기에 선언을 하였습니다.
kotlin("plugin.spring") version kotlinVersion
kotlin("plugin.jpa") version kotlinVersion
위의 2개는 각각 Kotlin에서 Spring의 유용한 플러그인을 사용할 수 있게 도와주고, Jpa를 사용할 때 Kotlin의 기능을 활용할 수 있게 도와줄 수 있게 하기 위해 선언하였습니다.
사실 여기까진 Java & Spring Boot에서도 많이 사용하던 플러그인이라 그렇게 이상하지 않았습니다.
근데 제가 가장 어색했던 아이가 kapt? 라는 플러그인이었습니다..
KAPT (Kotlin Annotation Processing Tool)

흠,, 데이터 바인딩 어쩌구,, 잘 모르겠다,,
그래서 그냥 직접 뭔지 경험해보자 하고 kapt를 지우고 프로젝트를 빌드해봤다.
난 안될지 알았는데,,, 어라

왜 잘되지...
그래서 알아보니까 애노테이션 프로세서가 Kotlin을 직접 지원하거나, Kotlin이 해당 애노테이션 프로세서를 사용할 수 있는 방식으로 설계되었기때문에 제가 프로젝트에서 사용한 어노테이션은 기본적으로 Kotlin에서 지원하기 때문에 실행이 됐던거같다 ㅎ,,
그래도 호환성을 위해 선언해주는 것을 추천한다.
다음은 의존성 부분을 보겠습니다.
기존 querydsl을 설정하려면
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
...
}
...
dependencies {
...
//querydsl
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
}
...
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
위처럼 너무 많은 설정이 필요했고, 복잡했었습니다...
dependencies {
//query dsl
val querydslVersion = "5.0.0"
implementation("com.querydsl:querydsl-jpa:$querydslVersion")
kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa")
...
}
하지만 Kotlin gradle에서 querydsl을 설정하는 부분은 단 3줄로 마무리 되는 것을 볼 수 있었습니다 ㅎㅎㅎㅎ
물론 querydsl을 사용하지 않는 분들은 이마저도 안하셔도 무방합니다.
저기서 kapt를 사용하는 부분을 볼 수 있는데 컴파일 시에 코드 생성의 역할을 해줍니다.
compileOnly ("org.projectlombok:lombok")
kapt("org.projectlombok:lombok")
또한 kapt를 사용하기 때문에 Lombok의 의존성을 kapt로 선언을 하였습니다.
물론 기존의
annotationProcessor ("org.projectlombok:lombok")
을 사용해도 문제는 없을 수 있지만 annotationProcessor는 주로 Java 소스 코드에 사용되기 때문에
Kotlin 프로젝트에서는 kapt를 사용하는 것을 추천합니다.
kapt {
keepJavacAnnotationProcessors = true
}
위의 설정은 KAPT가 Kotlin 소스 코드를 처리할 때 Java 애노테이션 프로세서를 그대로 사용할 수 있게 도와줍니다.
만약 false로 설정을 한다면

위와 같이 어노테이션을 읽지 못하고 Error을 발생시키는 것을 볼 수 있습니다..ㅎ
tasks {
...
compileKotlin {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
...
}
마지막으로 위의 코드를 살펴보면 아마 처음보는 명령어가 있을 겁니다.. ( -Xjsr305=strict >> 이거)
이건 Kotlin과 Java의 특성을 생각하면 이해가 쉽다.
Kotlin은 Java와 달리 null을 굉장히 엄격하게 관리하는 특징을 지니고 있는 반면 Java는 null에 대해 너그러운 편이다.
그래서 Java에서 Kotlin으로 마이그레이션을 할 때 Kotlin의 특징을 보호하기 위해 선언을 하는데 구글에 검색해보면
Java의 JSR-305 애노테이션을 엄격하게 처리함,,,,어쩌구 저쩌구
저렇게 나와있는데 쉽게 말해서
- @Nonnull: 해당 요소가 null이 되어서는 안 된다는 것을 나타냄.
- @Nullable: 해당 요소가 null일 수 있음을 나타냄.
- @CheckForNull: 해당 요소가 null일 수도 있으며, 호출 후 null 검사를 해야 함을 나타냄.
- @ParametersAreNonnullByDefault: 메소드의 매개변수가 기본적으로 null이 될 수 없음을 나타냄.
- @ReturnValuesAreNonnullByDefault: 메소드의 반환 값이 기본적으로 null이 될 수 없음을 나타냄.
이러한 어노테이션을 사용하는 부분에서 null-safety를 강제하도록 도와주는 것을 의미한다..!
잠깐이지만 다시 한번 Kotlin이 지향하는 점이 무엇인지 깨닫게 되었다.
NPE에 이렇게 진심인 언어는 처음본다,,
이제 Java에서 Kotlin으로 마이그레이션 하기 위한 발걸음을 뗐다,,
다음 블로그에선 이제 프로덕트 코드를 마이그레이션 하는 거에 대해서 포스팅하겠다.