Kotlin Gradle 플러그인의 컴파일과 캐시
Kotlin Gradle 플러그인의 컴파일과 캐시 (Compilation and caches in the Kotlin Gradle plugin)
이 페이지에서는 다음 주제들을 배울 수 있어요.
- 증분 컴파일
- Gradle 빌드 캐시 지원
- Gradle 구성 캐시 지원
- Kotlin 데몬과 Gradle에서의 사용법
- 이전 컴파일러로 되돌리기
- Kotlin 컴파일러 실행 전략 정의
- Kotlin 컴파일러 폴백 전략
- 최신 언어 버전 시도하기
- 빌드 리포트
본문
증분 컴파일 (Incremental compilation)
Kotlin Gradle 플러그인은 증분 컴파일을 지원하며, Kotlin/JVM과 Kotlin/JS 프로젝트에서는 기본적으로 활성화되어 있어요. 증분 컴파일은 빌드 사이에 클래스패스의 파일 변화를 추적해서, 그 변경의 영향을 받는 파일만 컴파일해요. 이 방식은 Gradle의 빌드 캐시와 함께 동작하며 컴파일 회피(compilation avoidance)를 지원해요.
Kotlin/JVM에서 증분 컴파일은 클래스패스 스냅샷에 의존하는데, 이 스냅샷은 언제 재컴파일이 필요한지 결정하기 위해 모듈의 API 구조를 포착해요. 전체 파이프라인을 최적화하기 위해 Kotlin 컴파일러는 두 가지 유형의 클래스패스 스냅샷을 사용해요.
- 세밀한 스냅샷(Fine-grained snapshots): 프로퍼티나 함수 같은 클래스 구성원에 대한 상세한 정보를 포함해요. 구성원 수준의 변경이 감지되면 Kotlin 컴파일러는 수정된 구성원에 의존하는 클래스만 재컴파일해요. 성능을 유지하기 위해 Kotlin Gradle 플러그인은 Gradle 캐시의
.jar파일에 대해서는 거친 스냅샷을 만들어요. - 거친 스냅샷(Coarse-grained snapshots): 클래스의 ABI 해시만 담아요. ABI의 일부가 바뀌면 Kotlin 컴파일러는 변경된 클래스에 의존하는 모든 클래스를 재컴파일해요. 외부 라이브러리처럼 자주 바뀌지 않는 클래스에 유용해요.
Kotlin/JS 프로젝트는 히스토리 파일에 기반한 다른 증분 컴파일 방식을 사용해요.
증분 컴파일을 비활성화하는 방법은 여러 가지가 있어요.
- Kotlin/JVM에
kotlin.incremental=false설정하기. - Kotlin/JS 프로젝트에
kotlin.incremental.js=false설정하기. - 커맨드 라인 파라미터로
-Pkotlin.incremental=false또는-Pkotlin.incremental.js=false사용하기.
이 파라미터는 이후의 매 빌드에 추가해야 해요.
증분 컴파일을 비활성화하면 빌드 이후 증분 캐시가 무효화돼요. 첫 번째 빌드는 절대 증분식이 아니에요.
때로는 증분 컴파일 문제가 실패가 발생한 지 몇 라운드 뒤에야 눈에 보이게 되기도 해요. 빌드 리포트를 사용해 변경 내역과 컴파일 히스토리를 추적하세요. 그러면 재현 가능한 버그 리포트를 만드는 데 도움이 돼요.
현재 증분 컴파일 방식이 어떻게 동작하고 이전 방식과 어떻게 비교되는지 더 자세히 알고 싶다면 블로그 글을 확인해 보세요.
Gradle 빌드 캐시 지원 (Gradle build cache support)
Kotlin 플러그인은 Gradle 빌드 캐시를 사용하는데, 이 캐시는 빌드 출력물을 저장해서 이후 빌드에서 재사용해요.
모든 Kotlin 작업의 캐싱을 비활성화하려면 시스템 프로퍼티 kotlin.caching.enabled를 false로 설정하세요(-Dkotlin.caching.enabled=false 인자로 빌드 실행).
Gradle 구성 캐시 지원 (Gradle configuration cache support)
Kotlin 플러그인은 Gradle 구성 캐시를 사용하는데, 이 캐시는 구성 단계의 결과를 이후 빌드에서 재사용해 빌드 과정을 빠르게 해줘요.
구성 캐시를 활성화하는 방법은 Gradle 문서를 참고하세요. 이 기능을 켜면 Kotlin Gradle 플러그인이 자동으로 사용하기 시작해요.
Kotlin 데몬과 Gradle에서의 사용법 (The Kotlin daemon and how to use it with Gradle)
- Gradle 데몬과 함께 실행되며 프로젝트를 컴파일해요.
- IntelliJ IDEA 내장 빌드 시스템으로 프로젝트를 컴파일할 때는 Gradle 데몬과 별도로 실행돼요.
Kotlin 데몬은 Kotlin 컴파일 작업 중 하나가 소스를 컴파일하기 시작할 때 Gradle 실행 단계(execution stage)에서 시작돼요. Kotlin 데몬은 Gradle 데몬과 함께 종료되거나, Kotlin 컴파일 없이 2시간이 지나면 종료돼요.
Kotlin 데몬은 Gradle 데몬과 같은 JDK를 사용해요.
Kotlin 데몬의 JVM 인자 설정하기 (Setting Kotlin daemon's JVM arguments)
다음의 각 인자 설정 방식은 그보다 앞선 방식을 덮어써요.
Gradle 데몬 인자 상속 (Gradle daemon arguments inheritance)
기본적으로 Kotlin 데몬은 Gradle 데몬에서 특정 인자 집합을 상속하지만, Kotlin 데몬에 직접 지정된 JVM 인자가 있으면 그것으로 덮어써요. 예를 들어 gradle.properties 파일에 다음 JVM 인자를 추가하면:
org.gradle.jvmargs=-Xmx1500m -Xms500m -XX:MaxMetaspaceSize=1g
이 인자들이 Kotlin 데몬의 JVM 인자에 추가돼요.
-Xmx1500m -XX:ReservedCodeCacheSize=320m -XX:MaxMetaspaceSize=1g -XX:UseParallelGC -ea -XX:+UseCodeCacheFlushing -XX:+HeapDumpOnOutOfMemoryError -Djava.awt.headless=true -Djava.rmi.server.hostname=127.0.0.1 --add-exports=java.base/sun.nio.ch=ALL-UNNAMED
Kotlin 데몬이 JVM 인자로 동작하는 기본 방식에 대해 더 알아보려면 JVM 인자에 대한 Kotlin 데몬의 동작을 참고하세요.
kotlin.daemon.jvm.options 시스템 프로퍼티 (kotlin.daemon.jvm.options system property)
Gradle 데몬의 JVM 인자에 kotlin.daemon.jvm.options 시스템 프로퍼티가 있다면 이 프로퍼티를 gradle.properties 파일에서 사용할 수 있어요.
org.gradle.jvmargs=-Dkotlin.daemon.jvm.options=-Xmx1500m,Xms500m
인자를 전달할 때는 다음 규칙을 따라야 해요.
- 마이너스 기호
-는Xmx,XX:MaxMetaspaceSize,XX:ReservedCodeCacheSize인자 앞에서만 사용하세요. - 인자는 공백 없이 쉼표(
,)로 구분하세요. 공백 뒤에 오는 인자는 Kotlin 데몬이 아니라 Gradle 데몬에 사용돼요.
다음 조건이 모두 충족되면 Gradle은 이 프로퍼티들을 무시해요.
- Gradle이 JDK 1.9 이상을 사용 중.
- Gradle 버전이 7.0 이상 7.1.1 이하.
- Gradle이 Kotlin DSL 스크립트를 컴파일 중.
- Kotlin 데몬이 실행 중이 아님.
이를 해결하려면 Gradle을 7.2 이상으로 업그레이드하거나 kotlin.daemon.jvmargs 프로퍼티를 사용하세요. 아래 섹션에서 설명할게요.
kotlin.daemon.jvmargs 프로퍼티 (kotlin.daemon.jvmargs property)
gradle.properties 파일에 kotlin.daemon.jvmargs 프로퍼티를 추가할 수 있어요.
kotlin.daemon.jvmargs=-Xmx1500m -Xms500m
여기나 Gradle의 JVM 인자에서 ReservedCodeCacheSize 인자를 지정하지 않으면 Kotlin Gradle 플러그인은 기본값 320m을 적용한다는 점을 기억하세요.
-Xmx1500m -XX:ReservedCodeCacheSize=320m -Xms500m
kotlin 확장 (kotlin extension)
kotlin 확장에서 인자를 지정할 수 있어요.
kotlin {
kotlinDaemonJvmArgs = listOf("-Xmx486m", "-Xms256m", "-XX:+UseParallelGC")
}
kotlin {
kotlinDaemonJvmArgs = ["-Xmx486m", "-Xms256m", "-XX:+UseParallelGC"]
}
특정 작업 정의 (Specific task definition)
특정 작업에 대한 인자를 지정할 수 있어요.
tasks.withType<CompileUsingKotlinDaemon>().configureEach {
kotlinDaemonJvmArguments.set(listOf("-Xmx486m", "-Xms256m", "-XX:+UseParallelGC"))
}
tasks.withType(CompileUsingKotlinDaemon).configureEach { task ->
task.kotlinDaemonJvmArguments = ["-Xmx1g", "-Xms512m"]
}
이 경우 작업 실행 시 새 Kotlin 데몬 인스턴스가 시작될 수 있어요. JVM 인자에 대한 Kotlin 데몬의 동작에 대해 더 알아보세요.
JVM 인자에 대한 Kotlin 데몬의 동작 (Kotlin daemon's behavior with JVM arguments)
Kotlin 데몬의 JVM 인자를 구성할 때 다음을 기억하세요.
- 서로 다른 서브프로젝트나 작업이 서로 다른 JVM 인자 집합을 가지면 같은 시점에 여러 Kotlin 데몬 인스턴스가 실행되는 것이 정상이에요.
- 새 Kotlin 데몬 인스턴스는 Gradle이 관련 컴파일 작업을 실행하는데 기존 Kotlin 데몬들이 같은 JVM 인자 집합을 가지지 않을 때만 시작돼요. 프로젝트에 서브프로젝트가 많다고 상상해 보세요. 대부분은 Kotlin 데몬에 어느 정도의 힙 메모리가 필요한데, 한 모듈은 아주 많은 메모리를 요구해요(그런데 자주 컴파일되진 않아요). 이런 경우 그 모듈에 대해 다른 JVM 인자 집합을 제공해야 해요. 그래야 더 큰 힙 크기의 Kotlin 데몬이 이 특정 모듈을 건드리는 개발자에게만 시작되거든요.
- 이미 컴파일 요청을 처리할 만큼 충분한 힙 크기를 가진 Kotlin 데몬이 실행 중이라면, 다른 요청된 JVM 인자가 다르더라도 새 데몬을 시작하지 않고 기존 데몬을 재사용해요.
다음 인자를 지정하지 않으면 Kotlin 데몬은 Gradle 데몬에서 상속해요.
-Xmx-XX:MaxMetaspaceSize-XX:ReservedCodeCacheSize. 지정하거나 상속하지 않으면 기본값은320m이에요.
Kotlin 데몬은 다음과 같은 기본 JVM 인자를 가져요.
-XX:UseParallelGC. 다른 가비지 컬렉터가 지정되지 않은 경우에만 적용돼요.-ea-XX:+UseCodeCacheFlushing-Djava.awt.headless=true-D{java.servername.property}={localhostip}--add-exports=java.base/sun.nio.ch=ALL-UNNAMED. JDK 16 이상 버전에서만 적용돼요.
Kotlin 데몬의 기본 JVM 인자 목록은 버전마다 다를 수 있어요. VisualVM 같은 도구를 사용해 Kotlin 데몬 같은 실행 중인 JVM 프로세스의 실제 설정을 확인할 수 있어요.
이전 컴파일러로 되돌리기 (Rolling back to the previous compiler)
Kotlin 2.0.0부터 K2 컴파일러가 기본으로 사용돼요.
Kotlin 2.0.0부터 이전 컴파일러를 사용하려면 둘 중 하나를 하세요.
build.gradle.kts파일에서 언어 버전을1.9로 설정하기.- 다음 컴파일러 옵션 사용하기:
-language-version 1.9.
K2 컴파일러의 장점에 대해 더 알아보려면 K2 컴파일러 마이그레이션 가이드를 확인해 보세요.
최신 언어 버전 시도하기 (Trying the latest language version)
Kotlin 2.0.0부터 최신 언어 버전을 시도하려면 gradle.properties 파일에 kotlin.experimental.tryNext 프로퍼티를 설정하세요. 이 프로퍼티를 사용하면 Kotlin Gradle 플러그인이 언어 버전을 현재 Kotlin 버전의 기본값보다 한 단계 높은 값으로 올려요. 예를 들어 Kotlin 2.0.0에서 기본 언어 버전은 2.0이므로, 이 프로퍼티는 언어 버전을 2.1로 설정해요.
또는 다음 명령을 실행할 수 있어요.
./gradlew assemble -Pkotlin.experimental.tryNext=true
빌드 리포트에서 각 작업을 컴파일하는 데 사용된 언어 버전을 확인할 수 있어요.
빌드 리포트 (Build reports)
빌드 리포트에는 서로 다른 컴파일 단계의 소요 시간과 컴파일이 증분식이 될 수 없었던 이유가 담겨 있어요. 컴파일 시간이 너무 길거나 같은 프로젝트에서 컴파일 시간이 다를 때 빌드 리포트로 성능 문제를 조사할 수 있어요.
Kotlin 빌드 리포트는 단일 Gradle 작업을 세부 단위로 삼는 Gradle 빌드 스캔보다 빌드 성능 문제를 더 효율적으로 조사하는 데 도움을 줘요.
오래 걸리는 컴파일에 대한 빌드 리포트를 분석해서 해결할 수 있는 흔한 경우가 두 가지 있어요.
- 빌드가 증분식이 아니었을 때. 원인을 분석하고 근본적인 문제를 해결하세요.
- 빌드가 증분식이었지만 시간이 너무 오래 걸렸을 때. 소스 파일을 재구성해 보세요 — 큰 파일을 나누고, 개별 클래스를 다른 파일에 저장하고, 큰 클래스를 리팩터링하고, 최상위 함수를 다른 파일에 선언하는 식이에요.
빌드 리포트는 프로젝트에서 사용된 Kotlin 버전도 보여줘요. 게다가 Kotlin 1.9.0부터 Gradle 빌드 스캔에서 코드를 컴파일하는 데 어떤 컴파일러가 사용됐는지 볼 수 있어요.
빌드 리포트 읽는 법과 JetBrains가 빌드 리포트를 사용하는 방법을 알아보세요.
빌드 리포트 활성화하기 (Enabling build reports)
빌드 리포트를 활성화하려면 gradle.properties에서 빌드 리포트 출력을 저장할 위치를 선언하세요.
kotlin.build.report.output=file
출력에 사용할 수 있는 값과 그 조합은 다음과 같아요.
| 옵션 | 설명 |
|---|---|
file |
빌드 리포트를 사람이 읽을 수 있는 형식으로 로컬 파일에 저장해요. 기본값은 ${project_folder}/build/reports/kotlin-build/${project_name}-timestamp.txt예요. |
single_file |
빌드 리포트를 객체(object) 형식으로 지정된 로컬 파일에 저장해요. |
build_scan |
빌드 리포트를 빌드 스캔의 custom values 섹션에 저장해요. Gradle Enterprise 플러그인이 custom value의 개수와 길이를 제한한다는 점을 기억하세요. 큰 프로젝트에서는 일부 값이 유실될 수 있어요. |
http |
HTTP(S)로 빌드 리포트를 전송해요. POST 메서드는 메트릭을 JSON 형식으로 보내요. 전송되는 데이터의 현재 버전은 Kotlin 리포지토리에서 볼 수 있어요. HTTP 엔드포인트 샘플은 이 블로그 글에서 찾을 수 있어요. |
json |
빌드 리포트를 JSON 형식으로 로컬 파일에 저장해요. 기본값은 ${project_folder}/build/reports/kotlin-build/${project_name}-build-<date-time>-<index>.json이에요. |
kotlin.build.report에 사용할 수 있는 옵션 목록은 다음과 같아요.
# Required outputs. Any combination is allowed
kotlin.build.report.output=file,single_file,http,build_scan,json
# Mandatory if single_file output is used. Where to put reports
# Use instead of the deprecated `kotlin.internal.single.build.metrics.file` property
kotlin.build.report.single_file=my/directory/path/some_filename
# Optional. Output directory for file-based or JSON reports. Default: build/reports/kotlin-build/
kotlin.build.report.file.output_dir=kotlin-reports
# Optional. Label for marking your build report (for example, debug parameters)
kotlin.build.report.label=some_label
HTTP에만 적용되는 옵션은 다음과 같아요.
# Mandatory. Where to post HTTP(S)-based reports
kotlin.build.report.http.url=http://127.0.0.1:8080
# Optional. User and password if the HTTP endpoint requires authentication
kotlin.build.report.http.user=someUser
kotlin.build.report.http.password=somePassword
# Optional. Add a Git branch name of a build to a build report
kotlin.build.report.http.include_git_branch.name=true|false
# Optional. Add compiler arguments to a build report
# If a project contains many modules, its compiler arguments in the report can be very heavy and not that helpful
kotlin.build.report.include_compiler_arguments=true|false
Custom value의 한계 (Limit of custom values)
빌드 스캔 통계를 수집하기 위해 Kotlin 빌드 리포트는 Gradle의 custom value를 사용해요. 사용자와 여러 Gradle 플러그인이 custom value에 데이터를 쓸 수 있어요. custom value의 개수에는 한계가 있어요. 현재 최대 custom value 개수는 Build scan 플러그인 문서에서 확인할 수 있어요.
큰 프로젝트라면 custom value의 개수가 꽤 많아질 수 있어요. 이 개수가 한계를 초과하면 로그에 다음 메시지가 표시될 수 있어요.
Maximum number of custom values (1,000) exceeded
Kotlin 플러그인이 만드는 custom value의 개수를 줄이려면 gradle.properties에서 다음 프로퍼티를 사용할 수 있어요.
kotlin.build.report.build_scan.custom_values_limit=500
프로젝트·시스템 프로퍼티 수집 끄기 (Switching off collecting project and system properties)
HTTP 빌드 통계 로그에는 일부 프로젝트·시스템 프로퍼티가 포함될 수 있어요. 이 프로퍼티들은 빌드의 동작을 바꿀 수 있으므로 빌드 통계에 기록해 두면 유용해요. 그런데 이 프로퍼티들은 비밀번호나 프로젝트의 전체 경로 같은 민감한 데이터를 담을 수도 있어요.
gradle.properties에 kotlin.build.report.http.verbose_environment 프로퍼티를 추가하면 이 통계 수집을 끌 수 있어요.
JetBrains는 이 통계를 수집하지 않아요. 리포트를 저장할 위치는 사용자가 직접 선택해요.
다음 단계 (What's next?)
더 알아보세요.