Search
🌶️

자바와 코틀린을 함께 사용할 때 Lombok 라이브러리의 문제

Tags
SpringBoot
Date
2024/05/01

문제 상황

현재 진행중인 프로젝트는 멀티모듈로 레이어드 아키텍처를 구성하여 진행중입니다. 도메인 계층의 코드를 순수한 자바 코드로 관리하고, db에 접근하는 엔티티와 비즈니스 로직을 위한 자바 객체를 분리함으로서 역할을 나누기 위해서입니다. 자세한 설계는 아래와 같습니다.
. ├── build.gradle.kts ├── api // Kotlin │   ├── build.gradle.kts ├── build.gradle.kts ├── domain // Java │   └── build.gradle ├── settings.gradle.kts └── storage // Kotlin ├── core-mongo └── core-mysql ├── build.gradle.kts └── src
Plain Text
복사
domain 모듈에서는 @Getter 등 간결한 코드와 편리를 위해 Lombok 라이브러리를 사용 중입니다. 하지만 storage 모듈의 AccountEntity.kt 에서, 엔티티를 자바 객체로 변환하는 코드에서 Unresolved reference: builder 라는 에러가 발생합니다.
/** :storage:core-mysql AccountEntity(.kt) */ @Entity @Table(name="account") class AccountEntity( ) : BaseEntity() { fun to(): Account { return Account.builder().build() } }
Kotlin
복사
/** :domain Account(.java) */ @Getter public class Account { private final Long id; private final String email; private final String password; private final String riotName; private final String riotTag; private final String riotId; private final Boolean isAgreeToPersonalInfo; private final Boolean isAgreeToEmail; private final Boolean isCertificatedEmail; @Builder public Account(Long id, String email, String password, String riotName, String riotTag, String riotId, Boolean isAgreeToPersonalInfo, Boolean isAgreeToEmail, Boolean isCertificatedEmail) { this.id = id; this.email = email; this.password = password; this.riotName = riotName; this.riotTag = riotTag; this.riotId = riotId; this.isAgreeToPersonalInfo = isAgreeToPersonalInfo; this.isAgreeToEmail = isAgreeToEmail; this.isCertificatedEmail = isCertificatedEmail; } }
Java
복사

Lombok의 문제

@Entity @Table(name="account") class AccountEntity( ) : BaseEntity() { fun to(): Account { return Account(id, email, password, riotName, riotTag, riotId, isAgreeToPersonalInfo, isAgreeToEmail, isCertificatedEMail) } }
Java
복사
하지만 위와 같은 방식으로, Builder 를 사용하지 않고 객체를 생성하게 되면 문제가 발생하지 않습니다. 이는 Lombok 라이브러리의 특성 때문입니다. 자바와 코틀린을 함께 사용할 때는, 코틀린 쪽에서 자바의 Lombok을 인식하지 못합니다. Lombok 은 자바 코드(.java)가 자바 컴파일러를 통해 자바 바이트 코드(.class)로 컴파일 될 때, 소스 코드에 적용된 어노테이션을 분석하고, 어노테이션이 있는 필드/메서드를 기반으로 코드를 생성합니다. 필드 → 생성자 순으로 자동으로 코드를 생성한 후, 컴파일 시점에 원본 소스 코드에 주입됩니다. 자바 바이트 코드(.class)에는 Lombok 어노테이션이 존재하지 않게 됩니다.
dependencies { compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") }
Plain Text
복사
gradle에서 Lombok 라이브러리가 compileOnly 인 이유입니다. Lombok 라이브러리는 자바 코드에서 자바 바이트 코드로 컴파일 될 때만 사용됩니다.
하지만 자바와 코틀린이 함께 사용된 프로젝트를 빌드할 때는 아래 순서로 빌드가 진행됩니다.
1. .kt -> .class 컴파일 2. .java -> .class 컴파일(Lombok) 3. 런타임 실행
Plain Text
복사
때문에 당연히 Lombok 에서 주입되지 않은 코드를 확인할 수 없으므로 문제가 발생하게 됩니다.

해결 방안 1. Lombok을 사용하지 않기

장단점이 뚜렷한 방법입니다.
장점: 문제가 발생할 가능성이 Zero 단점: 단순 타이핑 시간이 많아짐
Plain Text
복사
변환된 코드만 빠르게 보고 넘어가겠습니다.
/** :domain Account(.java) */ public class Account { private String id; private String email; private String password; private String riotName; private String riotTag; private String riotId; private Account(String id, String email, String password, String riotName, String riotTag, String riotId) { this.id = id; this.email = email; this.password = password; this.riotName = riotName; this.riotTag = riotTag; this.riotId = riotId; } public static class Builder { private String id; private String email; private String password; private String riotName; private String riotTag; private String riotId; public Builder id(String id) { this.id = id; return this; } public Builder email(String email) { this.email = email; return this; } public Builder password(String password) { this.password = password; return this; } public Builder riotName(String riotName) { this.riotName = riotName; return this; } public Builder riotTag(String riotTag) { this.riotTag = riotTag; return this; } public Builder riotId(String riotId) { this.riotId = riotId; return this; } public Account build() { return new Account(id, email, password, riotName, riotTag, riotId); } } // Getters... }
Java
복사
어차피 도메인 모듈은 자바 코드로만 작성하는 것이 목표입니다. Kotlin으로 도메인 객체를 작성하게 되면, @Setter 를 열어두어야 하는데, 개인적으로 필요하지 않은 메서드를 열어두어야 하는 것이 좋아보이지 않기 때문입니다.

해결 방안 2. 컴파일 순서 조정하기

> Task :api:checkKotlinGradlePluginConfigurationErrors > Task :domain:checkKotlinGradlePluginConfigurationErrors > Task :domain:kaptGenerateStubsKotlin UP-TO-DATE > Task :domain:kaptKotlin SKIPPED > Task :domain:compileKotlin NO-SOURCE > Task :domain:compileJava UP-TO-DATE > Task :domain:processResources UP-TO-DATE > Task :domain:classes UP-TO-DATE > Task :domain:jar UP-TO-DATE > Task :api:kaptGenerateStubsKotlin UP-TO-DATE > Task :api:kaptKotlin SKIPPED > Task :api:compileKotlin UP-TO-DATE > Task :api:compileJava NO-SOURCE > Task :api:processResources UP-TO-DATE > Task :api:classes UP-TO-DATE > Task :api:kaptGenerateStubsTestKotlin UP-TO-DATE > Task :api:kaptTestKotlin SKIPPED > Task :api:compileTestKotlin UP-TO-DATE > Task :api:compileTestJava NO-SOURCE > Task :api:processTestResources NO-SOURCE > Task :api:testClasses UP-TO-DATE > Task :storage:core-mysql:checkKotlinGradlePluginConfigurationErrors > Task :storage:core-mysql:kaptGenerateStubsKotlin UP-TO-DATE > Task :storage:core-mysql:kaptKotlin SKIPPED > Task :storage:core-mysql:compileKotlin UP-TO-DATE > Task :storage:core-mysql:compileJava NO-SOURCE > Task :storage:core-mysql:processResources UP-TO-DATE > Task :storage:core-mysql:classes UP-TO-DATE > Task :storage:core-mysql:jar UP-TO-DATE
Plain Text
복사
setting.gradle.kts 파일과 각 모듈 속의 build.gradle.kts 파일을 잘 조정하면 빌드 순서를 조정할 수 있습니다.
/** * api module의 build.gradle.kts */ dependencies { implementation(project(":domain")) runtimeOnly(project(":storage:core-mysql")) implementation("org.springframework.boot:spring-boot-starter-web") }
Groovy
복사
/** * setting.gradle.kts */ include ( "domain", "api", "mail", "common", "storage:core-mysql" )
Groovy
복사
⇒ 이렇게 되면 api 모듈의 @SpringBootApplication 실행 시, 별다른 코드 변경 없이 잘 실행됩니다.

결론 및 후속 문제

하지만 api 모듈의 테스트 코드를 실행할 때 컴파일 에러가 발생합니다.
> Task :api:checkKotlinGradlePluginConfigurationErrors > Task :domain:checkKotlinGradlePluginConfigurationErrors > Task :domain:kaptGenerateStubsKotlin UP-TO-DATE > Task :domain:kaptKotlin SKIPPED > Task :domain:compileKotlin NO-SOURCE > Task :domain:compileJava UP-TO-DATE > Task :domain:processResources UP-TO-DATE > Task :domain:classes UP-TO-DATE > Task :domain:jar UP-TO-DATE > Task :api:kaptGenerateStubsKotlin UP-TO-DATE > Task :api:kaptKotlin SKIPPED > Task :api:compileKotlin UP-TO-DATE > Task :api:compileJava NO-SOURCE > Task :api:processResources UP-TO-DATE > Task :api:classes UP-TO-DATE > Task :api:kaptGenerateStubsTestKotlin UP-TO-DATE > Task :api:kaptTestKotlin SKIPPED > Task :api:compileTestKotlin UP-TO-DATE > Task :api:compileTestJava NO-SOURCE > Task :api:processTestResources NO-SOURCE > Task :api:testClasses UP-TO-DATE > Task :storage:core-mysql:checkKotlinGradlePluginConfigurationErrors > Task :storage:core-mysql:kaptGenerateStubsKotlin UP-TO-DATE > Task :storage:core-mysql:kaptKotlin SKIPPED > Task :storage:core-mysql:processResources UP-TO-DATE > Task :storage:core-mysql:compileKotlin FAILED e: file:/co/storage/core-mysql/src/main/kotlin/boaz/lol/co/entity/account/AccountEntity.kt:38:24 Unresolved reference: builder FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':storage:core-mysql:compileKotlin'. > A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction > Compilation error. See log for more details
Groovy
복사
:storage:core-mysql 모듈에서 compileKotlin 작업을 수행할 때 에러가 발생하네요.
아쉬운 결론이지만, 도메인 모듈을 코틀린으로 작성하는 것으로 변경하였습니다. Kotlin에서 data class 를 선언해서 도메인 객체에 대한 비즈니스 코드를 작성할 때 setter 가 열리게 되는 것은 아쉬운 부분이지만, 코드에 기여하는 개발 팀원들이 setter의 활용을 지양하는 이유를 이미 알고 있고, DeLombok을 진행해서 자바 코드를 작성하기에 너무 많은 개발 비용과 시간이 투자되기 때문입니다.
두 언어의 장단점을 각각 살리기 위해 굳이 혼용하기보다, 장점(코틀린의 경우 빠른 빌드 속도와 적은 코드량)을 극대화 하는 게 낫겠다는 결론을 내렸습니다.

References

아래 글에서 모듈 설계에 대한 내용을 더 찾아볼 수 있습니다.