[인프런] 최태현님의 자바 개발자를 위한 코틀린 입문 강의 수강 중 발생한 에러에 관한 글입니다.
Lombok과 컴파일 오류
Lombok은 애너테이션을 기반으로 constructor, getter, setter 등 반복적으로 작성해야 하는 메서드를 자동으로 생성하는 라이브러리이다. 코드를 간결하게 만들기 때문에 많은 Java 기반 프로젝트에서 사용하고 있다. 네이버 예약 서비스에도 전반적으로 Lombok을 사용하는 상태였다. 하지만 Lombok을 사용하는 프로젝트에 Kotlin을 적용했을 때 컴파일 오류가 발생했다.
현상
Kotlin으로 작성한 코드에서 Lombok이 생성한 코드를 사용하려고 하면 컴파일 오류가 발생한다.
아래 코드는 Java로 간단하게 작성한 Person 클래스이다.
package oop;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@AllArgsConstructor
@ToString
@Getter @Setter
public class JavaPerson {
private final String name;
private int age;
}
Lombok 라이브러리를 통해 아래 클래스파일과 같이 컴파일 된다.
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package oop;
public class JavaPerson {
private final String name;
private int age;
public JavaPerson(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
String var10000 = this.getName();
return "JavaPerson(name=" + var10000 + ", age=" + this.getAge() + ")";
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
다음은 위의 Lombok 코드를 사용하는 Kotlin 코드의 예이다. 이 프로젝트를 컴파일하면 오류가 발생한다.
package oop
fun main() {
val kotlinPerson = Person("코틀린이재근", 100)
val javaPerson = JavaPerson("자바이재근", 50)
println("person.name = ${kotlinPerson.name}")
println("javaPerson.name = ${javaPerson.name}")
}
// Kotlin Person class
class Person(val name: String, var age: Int)
상단 코드 JavaPerson 생성 부분에서 컴파일 에러가 발생.
val javaPerson = JavaPerson("자바이재근", 50)
Kotlin: Too many arguments for public constructor JavaPerson() defined in oop.JavaPerson
원인
문제의 원인을 이해하려면 Java 코드와 Kotlin 코드가 섞여 있는 프로젝트의 빌드 과정을 살펴봐야 한다.
Java 코드와 Kotlin 코드의 빌드 과정은 다음과 같은 순서로 이루어진다.
- Kotlin 컴파일러가 Kotlin 코드를 컴파일해 .class 파일을 생성한다. 이 과정에서 Kotlin 코드가 참조하는 Java 코드가 함께 로딩되어 사용된다.
- Java 컴파일러가 Java 코드를 컴파일해 .class 파일을 생성한다. 이때 이미 Kotlin이 컴파일한 .class 파일의 경로를 클래스 패스에 추가해 컴파일한다.
두 번째 과정은 다시 세 단계로 나눌 수 있는데, Lombok이 코드를 생성하는 단계는 세 단계 중 Annotation Processing 단계이다. 하지만 이 단계는 Kotlin 코드가 컴파일된 이후이기 때문에 Kotlin 코드는 Lombok이 생성한 코드를 사용할 수 없게 된다.
해결 방법
해결방법은 정상영님이 작성하신 [NaverD2] Kotlin 도입 과정에서 만난 문제와 해결방법 의 글을 인용한다.
Lombok과 Kotlin의 컴파일 순서가 문제의 원인임을 이해하고 다음과 같은 해결 방법을 고려했다. 결론적으로는 Lombok을 제거하는 방법을 선택했다.
빌드 순서 조정
Kotlin 코드보다 Java 코드를 먼저 컴파일하도록 빌드 순서를 조정하면 Lombok 문제는 해결할 수 있다. 하지만 Java 코드에서 Kotlin 코드를 호출할 수 없게 된다.
Java와 Kotlin을 별도 모듈로 분리
Java와 Kotlin을 별도 모듈로 분리해서 컴파일하면 Lombok 문제는 해결된다. 하지만 모듈 간 의존성의 방향에 따라 Java 코드에서 Kotlin 코드를 호출하거나 Kotlin 코드에서 Java 코드를 호출하는 것이 불가능해진다.
빌드 전처리 과정에서 Delombok 실행
프로젝트 빌드 전에 Lombok이 제공하는 Delombok 기능을 활용해 Lombok이 코드를 미리 생성하게 한다. Delombok이 Gradle 플러그인을 공식으로 지원하지 않아 빌드 구성이 복잡해지는 단점이 있다.
Lombok이 적용된 코드를 Kotlin으로 변환
Lombok이 적용된 Java 클래스는 대부분 JPA 엔티티, DTO 등 데이터를 담는 용도의 클래스이다. 이런 유형의 클래스를 Kotlin의 data class로 변환하면 Lombok에서 주로 제공하는 constructor, getter, setter, equals(), hashCode(), toString() 메서드 등을 별도로 구현하지 않아도 손쉽게 사용할 수 있다. 다만 프로젝트의 규모가 클 때에는 일괄 변환이 안정성 측면에서 부담이 될 수 있다. Kotlin 코드에서 사용하는 코드부터 점진적으로 변환하는 것이 좀 더 실용적인 방법이다.
Lombok 제거
Delombok을 실행해 Lombok이 코드를 생성한 다음 생성된 코드를 저장소에 반영해 사용하는 방법이다. "Lombok이 적용된 코드를 Kotlin으로 변환"에서 사용한 방법과 마찬가지로 Lombok을 프로젝트에서 제거할 수 있는 방법이다.
이 방법에는 Java 코드가 Lombok 애너테이션을 적용하기 전 상태로 돌아가므로 코드의 간결함을 잃는다는 단점이 있다. 하지만 Lombok을 일괄로 제거해 반영한 후 점진적으로 Kotlin으로 변환하기로 결정했다.
프로젝트 규모가 작지 않음에도 불구하고 일괄 제거를 선택한 이유는 다음과 같다.
- 점진적인 제거가 안정적이기는 하지만 Kotlin 도입을 더디게 만드는 요소가 될 수 있다.
- 앞으로 Kotlin을 적극적으로 사용할 예정이므로 Kotlin 사용에 제약이 없는 환경을 만들자는 팀의 공감대가 있었다.
- Lombok이 제거되어 간결함을 잃은 코드는 점진적으로 Kotlin으로 변환해서 정리하는 것이 안정적일 것이다.
이번에 선택한 방법이 모든 상황에서 맞는 정답은 아니다. 프로젝트의 규모와 진행 상황에 따라 알맞은 접근 방법을 선택하길 바란다.