Kotlin 도입 과정에서 만난 문제와 해결 방법

Sangyeong Chung
네이버 플레이스 개발 블로그
10 min readSep 8, 2019

Kotlin은 JVM은 물론 Android, JavaScript, 네이티브 영역 등 다양한 플랫폼에서 사용할 수 있는 정적 타입(statically typed) 언어입니다. Java에 비해 코드를 간결하게 작성할 수 있으며, 좀 더 안전하게 null을 다룰 수 있는 문법을 제공하는 것이 주요 특징입니다. 또한 Java와 100% 호환되어 기존 라이브러리를 활용할 수 있습니다. 그래서 기존에 Java로 작성된 프로젝트에 점진적으로 Kotlin을 도입하는 것이 가능합니다.

이 글에서는 Java와 Spring Boot 기반의 네이버 예약 서비스에 Kotlin을 도입하면서 경험한 문제와 원인, 해결 방법을 간략하게 소개하겠습니다.

Lombok과 컴파일 오류

Lombok은 애너테이션을 기반으로 constructor, getter, setter 등 반복적으로 작성해야 하는 메서드를 자동으로 생성하는 라이브러리이다. 코드를 간결하게 만들기 때문에 많은 Java 기반 프로젝트에서 사용하고 있다. 네이버 예약 서비스에도 전반적으로 Lombok을 사용하는 상태였다. 하지만 Lombok을 사용하는 프로젝트에 Kotlin을 적용했을 때 컴파일 오류가 발생했다.

현상

Kotlin으로 작성한 코드에서 Lombok이 생성한 코드를 사용하려고 하면 컴파일 오류가 발생한다.

다음은 Lombok을 사용한 Java 코드의 예이다.

public class Person {
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Getter
private String name;
@Getter
private int age;
}

다음은 위에 정의된 class를 참조하는 Kotlin 코드의 예이다. 이 프로젝트를 컴파일하면 오류가 발생한다.

// Error: Cannot access 'name': it is 'private' in "Person"
fun
printName() {
val person = Person("Bob", 30)
println(person.name)
}

원인

문제의 원인을 이해하려면 Java 코드와 Kotlin 코드가 섞여 있는 프로젝트의 빌드 과정을 살펴봐야 한다.

Java 코드와 Kotlin 코드의 빌드 과정

Java 코드와 Kotlin 코드의 빌드 과정은 다음과 같은 순서로 이루어진다.

  1. Kotlin 컴파일러가 Kotlin 코드를 컴파일해 .class 파일을 생성한다. 이 과정에서 Kotlin 코드가 참조하는 Java 코드가 함께 로딩되어 사용된다.
  2. Java 컴파일러가 Java 코드를 컴파일해 .class 파일을 생성한다. 이때 이미 Kotlin이 컴파일한 .class 파일의 경로를 클래스 패스에 추가해 컴파일한다.

두 번째 과정은 다시 세 단계로 나눌 수 있는데, Lombok이 코드를 생성하는 단계는 세 단계 중 Annotation Processing 단계이다. 하지만 이 단계는 Kotlin 코드가 컴파일된 이후이기 때문에 Kotlin 코드는 Lombok이 생성한 코드를 사용할 수 없게 된다.

해결 방법

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으로 변환해서 정리하는 것이 안정적일 것이다.

이번에 선택한 방법이 모든 상황에서 맞는 정답은 아니다. 프로젝트의 규모와 진행 상황에 따라 알맞은 접근 방법을 선택하길 바란다.

Querydsl과 쿼리 타입 생성 오류

네이버 예약 서비스에서는 타입 안전성을 가지는 동적 쿼리 생성을 위해 Querydsl을 사용하고 있다. 그런데 JPA 엔티티 클래스를 Kotlin으로 작성했을 때 쿼리 타입이 생성되지 않았다.

원인과 해결 방법

JPA 엔티티 클래스의 쿼리 타입이 생성되지 않는 이유는 Java 컴파일러가 Annotation Processing을 실행하는 과정에서 Kotlin 코드를 인지할 수 없기 때문이다. Kotlin 코드의 Annotation Processing을 지원하도록 다음과 같이 kapt 플러그인을 적용해 문제를 해결했다.

apply plugin: 'kotlin-kapt'dependencies {
kapt "com.querydsl:querydsl-apt:4.2.1:hibernate"
}

남아 있는 문제

kapt 플러그인으로 Querydsl에서 발생하는 문제를 해결했다. 하지만 Lombok을 사용하는 프로젝트에서는 “Lombok과 컴파일 오류”에서 설명한 문제와 비슷한 상황이 발생한다. Annotation Processing 과정을 살펴보고 파악한 원인은 다음과 같다.

  1. Lombok은 기존 클래스에 코드를 추가하기 위해 Annotation Processing 과정에서 추상 구문 트리(abstract syntax tree)를 수정한다.
  2. Java의 Annotation Processing API가 기존 클래스의 추상 구문 트리를 수정하는 기능을 제공하지 않기 때문에 Lombok은 Java 컴파일러의 비공식 API를 사용해 추상 구문 트리를 수정한다.
  3. kapt 플러그인을 적용하면 Kotlin 컴파일러가 Annotation Processing을 실행하기 때문에 Lombok이 실행하는 Annotation Processing이 정상적으로 진행되지 않는다.

Querydsl을 사용하려면 Lombok을 제거해야 하는 상황이 되었다.

SonarQube와 테스트 커버리지 측정 문제

네이버 예약 서비스에서는 프로젝트의 정적 분석 및 테스트 커버리지 측정을 위해 SonarQube를 사용하고 있다. SonarQube를 기존처럼 사용하면 Kotlin 코드의 테스트 커버리지가 정상적으로 측정되지 않는다.

다음 과정을 따라 Gradle 설정을 변경하면 SonarQube가 Kotlin 코드의 테스트 커버리지를 정상적으로 측정한다.

SonarKotlin 플러그인 설치
SonarQube로 Kotlin 코드를 분석하려면 먼저 SonarQube 서버에 SonarKotlin 플러그인을 설치해야 한다. SonarKotlin 플러그인 설치와 사용에 관한 자세한 내용은 SonarQube 사이트의 “SonarKotlin” 문서를 참고한다.

1. SonarQube 분석 대상에 Kotlin 경로를 추가한다.

Java 코드와 Kotlin 코드의 빌드 결과물이 저장되는 경로가 다르므로 함께 분석될 수 있도록 SonarQube 분석 대상에 Kotlin 경로를 추가한다.

sonarqube {  
properties {
property 'sonar.java.binaries', "$buildDir/classes/java/main,$buildDir/classes/kotlin/main"
}
}

2. JaCoCo 결과 리포트 형식을 변경한다.

SonarQube의 바이너리 형식 결과 리포트는 Java만 지원한다. Kotlin에 대한 결과 리포트를 생성하려면 XML 형식으로 결과 리포트가 생성되도록 다음과 같이 설정을 변경한다(“Java Unit Tests and Coverage Results Import” 참고).

test {
jacocoTestReport {
reports {
xml.enabled true
xml.destination
file("$buildDir/jacoco/jacocoTest.xml")
}
}
}
sonarqube {
properties {
property 'sonar.coverage.jacoco.xmlReportPaths', "$buildDir/jacoco/jacocoTest.xml"
}
}

3. JaCoCo를 업그레이드한다.

Kotlin 인라인 함수를 사용할 때 컴파일러가 생성한 바이트코드와 소스 코드의 줄 수가 일치하지 않아 다음과 같은 오류가 발생한다. JaCoCo 0.8.3에서 문제가 수정되었으므로 JaCoCo 0.8.3 이후 버전을 사용한다(“Add filter for instructions inlined by Kotlin compiler” 참고).

Cannot import coverage information for file 'src/main/.../SomeClass.kt', coverage data is invalid. Error: {}
java.lang.IllegalStateException: Line 305 is out of range in the file src/main/.../SomeClass.kt (lines: 304)

위와 같은 순서로 설정을 변경하면 테스트 커버리지가 정상적으로 측정된다. 하지만 Kotlin으로 작성된 테스트 코드가 유닛 테스트 개수 집계에 포함되지 않는 문제가 아직 남아 있다.

마치며

지금까지 네이버 예약 서비스에 Kotlin을 도입하면서 경험한 문제와 해결 과정을 살펴보았다. 문제 해결 이후 프로젝트 내에서 Kotlin을 제약 없이 사용할 수 있는 환경이 갖추어졌고, 신규 코드에서 많은 부분이 Kotlin으로 작성되고 있다. 필요에 따라 기존 Java 코드도 계속해서 Kotlin으로 변환해 나갈 예정이다.

Kotlin은 다양한 장점이 있는 언어이다. 특히 생산성과 유지 보수 측면에서 Java보다 우위인 부분이 있어 앞으로 Java의 대안으로 자리할 것이라고 생각한다. 서비스 개발에 Kotlin 도입을 고려하는 개발자에게 이 글이 도움이 되기를 바란다.

--

--