Home
🌶️

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

문제 상황

현재 진행중인 프로젝트는 멀티모듈로 레이어드 아키텍처를 구성하여 진행중입니다. 도메인 계층의 코드를 순수한 자바 코드로 관리하고, 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

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