Kotlin 2.0.20의 새로운 기능(What's new in Kotlin 2.0.20)
Kotlin 2.0.20의 새로운 기능(What's new in Kotlin 2.0.20)
Kotlin 2.0.20이 출시됐어요! 이 버전은 Kotlin K2 컴파일러를 Stable로 발표한 Kotlin 2.0.0의 성능 개선과 버그 수정을 포함해요. 이번 릴리스의 주요 변경점들을 정리하면 다음과 같아요.
본문
출시일: 2024년 8월 22일
Kotlin 2.0.20 릴리스가 나왔어요! 이 버전은 Kotlin K2 컴파일러를 Stable로 발표한 Kotlin 2.0.0의 성능 개선과 버그 수정을 포함해요. 이번 릴리스의 주요 변경점들은 다음과 같아요.
- data class
copy함수가 생성자와 동일한 가시성을 가지도록 하는 변경 - multiplatform 프로젝트에서 기본 타깃 계층(default target hierarchy)의 소스 세트에 대한 정적 접근자 제공
- Kotlin/Native 가비지 컬렉터에서 동시 마킹(concurrent marking) 지원
- Kotlin/Wasm의
@ExperimentalWasmDsl어노테이션 위치 변경 - Gradle 8.6~8.8 버전 지원 추가
- Gradle 프로젝트 간 JVM 아티팩트를 클래스 파일로 공유할 수 있는 새 옵션
- Compose 컴파일러 업데이트
- 공통 Kotlin 표준 라이브러리에 UUID 지원 추가
Kotlin 릴리스 주기에 대한 자세한 내용은 Kotlin 릴리스 프로세스를 참고하세요.
IDE 지원
2.0.20을 지원하는 Kotlin 플러그인은 최신 IntelliJ IDEA와 Android Studio에 번들로 포함돼 있어요. IDE에서 Kotlin 플러그인을 업데이트할 필요는 없어요. 빌드 스크립트에서 Kotlin 버전을 2.0.20으로 변경하기만 하면 돼요.
자세한 내용은 새 릴리스로 업데이트를 참고하세요.
언어
Kotlin 2.0.20은 data class의 일관성을 개선하고 실험적 context receivers 기능을 대체하기 위한 변경을 도입하기 시작해요.
data class copy 함수가 생성자와 동일한 가시성을 가지도록
현재 private 생성자로 data class를 만들면 자동으로 생성되는 copy() 함수는 동일한 가시성을 가지지 않아요. 이로 인해 나중에 코드에서 문제가 발생할 수 있어요. 향후 Kotlin 릴리스에서는 copy() 함수의 기본 가시성이 생성자와 동일해지는 동작을 도입할 거예요. 이 변경은 코드를 최대한 매끄럽게 마이그레이션할 수 있도록 점진적으로 도입될 거예요.
마이그레이션 계획은 Kotlin 2.0.20에서 시작되는데, 이 버전에서 가시성이 향후 변경될 코드에 경고를 발생시켜요. 예를 들어:
// Triggers a warning in 2.0.20
data class PositiveInteger private constructor(val number: Int) {
companion object {
fun create(number: Int): PositiveInteger? = if (number > 0) PositiveInteger(number) else null
}
}
fun main() {
val positiveNumber = PositiveInteger.create(42) ?: return
// Triggers a warning in 2.0.20
val negativeNumber = positiveNumber.copy(number = -1)
// Warning: Non-public primary constructor is exposed via the generated 'copy()' method of the 'data' class.
// The generated 'copy()' will change its visibility in future releases.
}
마이그레이션 계획에 대한 최신 정보는 YouTrack의 해당 이슈를 참고하세요.
이 동작을 더 잘 제어할 수 있도록 Kotlin 2.0.20에서는 두 가지 어노테이션을 도입했어요.
@ConsistentCopyVisibility는 나중에 이 동작이 기본이 되기 전에 지금 이 동작을 옵트인하는 데 사용해요.@ExposedCopyVisibility는 이 동작을 옵트아웃하고 선언 위치에서 경고를 억제해요. 참고로 이 어노테이션을 사용해도 컴파일러는copy()함수가 호출될 때 여전히 경고를 보고해요.
개별 클래스가 아니라 모듈 전체에서 이미 2.0.20에서 새 동작을 옵트인하려면 -Xconsistent-data-class-copy-visibility 컴파일러 옵션을 사용할 수 있어요. 이 옵션은 모듈의 모든 data class에 @ConsistentCopyVisibility 어노테이션을 추가하는 것과 동일한 효과가 있어요.
context receivers를 context parameters로 단계적으로 대체
Kotlin 1.6.20에서 context receivers를 실험적(Experimental) 기능으로 도입했어요. 커뮤니티 피드백을 듣고 이 접근 방식을 계속하지 않기로 결정하고 다른 방향을 택했어요.
향후 Kotlin 릴리스에서 context receivers는 context parameters로 대체될 거예요. context parameters는 아직 설계 단계에 있으며 제안서는 KEEP에서 볼 수 있어요.
context parameters의 구현에는 컴파일러의 상당한 변경이 필요하기 때문에 컨텍스트 수신기와 컨텍스트 매개변수를 동시에 지원하지 않기로 결정했어요. 이 결정은 구현을 크게 단순화하고 불안정한 동작의 위험을 최소화해요.
많은 개발자가 이미 context receivers를 사용하고 있다는 것을 이해해요. 따라서 context receivers에 대한 지원을 점진적으로 제거하기 시작할 거예요. 마이그레이션 계획은 Kotlin 2.0.20에서 시작되며, -Xcontext-receivers 컴파일러 옵션으로 context receivers를 사용할 때 코드에 경고가 발생해요. 예를 들어:
class MyContext
context(MyContext)
// Warning: Experimental context receivers are deprecated and will be superseded by context parameters.
// Please don't use context receivers. You can either pass parameters explicitly or use members with extensions.
fun someFunction() {
}
이 경고는 향후 Kotlin 릴리스에서 오류가 될 거예요.
코드에서 context receivers를 사용한다면 다음 중 하나를 사용하도록 코드를 마이그레이션하는 것을 권장해요.
- 명시적 매개변수.
context(ContextReceiverType)
fun someFunction() {
contextReceiverMember()
}
fun someFunction(explicitContext: ContextReceiverType) {
explicitContext.contextReceiverMember()
}
- 확장 멤버 함수(가능한 경우).
context(ContextReceiverType)
fun contextReceiverMember() = TODO()
context(ContextReceiverType)
fun someFunction() {
contextReceiverMember()
}
class ContextReceiverType {
fun contextReceiverMember() = TODO()
}
fun ContextReceiverType.someFunction() {
contextReceiverMember()
}
또는 컴파일러에서 context parameters가 지원되는 Kotlin 릴리스까지 기다릴 수도 있어요. 참고로 context parameters는 처음에는 실험적 기능으로 도입될 거예요.
Kotlin Multiplatform
Kotlin 2.0.20은 multiplatform 프로젝트의 소스 세트 관리를 개선하고, Gradle의 최근 변경으로 인해 일부 Gradle Java 플러그인과의 호환성을 사용 중단해요.
기본 타깃 계층의 소스 세트에 대한 정적 접근자
Kotlin 1.9.20부터 기본 계층 템플릿이 모든 Kotlin Multiplatform 프로젝트에 자동으로 적용돼요. 그리고 기본 계층 템플릿의 모든 소스 세트에 대해 Kotlin Gradle 플러그인이 타입 안전한 접근자를 제공했어요. 이로써 마침내 by getting이나 by creating 구문을 사용하지 않고도 지정된 모든 타깃의 소스 세트에 접근할 수 있게 됐어요.
Kotlin 2.0.20은 IDE 경험을 더욱 개선하는 것을 목표로 해요. 이제 기본 계층 템플릿의 모든 소스 세트에 대해 sourceSets {} 블록에서 정적 접근자를 제공해요. 이 변경으로 이름으로 소스 세트에 접근하는 것이 더 쉽고 예측 가능해질 것이라고 믿어요.
이제 각 소스 세트에는 샘플이 포함된 자세한 KDoc 주석과, 해당 타깃을 먼저 선언하지 않고 소스 세트에 접근하려고 하면 경고와 함께 진단 메시지가 제공돼요.
kotlin {
jvm()
linuxX64()
linuxArm64()
mingwX64()
sourceSets {
commonMain.languageSettings {
progressiveMode = true
}
jvmMain { }
linuxX64Main { }
linuxArm64Main { }
// Warning: accessing source set without registering the target
iosX64Main { }
}
}
Kotlin Multiplatform의 계층적 프로젝트 구조에 대해 자세히 알아보세요.
Kotlin Multiplatform Gradle 플러그인과 Gradle Java 플러그인의 호환성 사용 중단
Kotlin 2.0.20에서 같은 프로젝트에 Kotlin Multiplatform Gradle 플러그인과 다음 Gradle Java 플러그인 중 하나를 적용하면 사용 중단 경고가 발생해요: Java, Java Library, Application. multiplatform 프로젝트의 다른 Gradle 플러그인이 Gradle Java 플러그인을 적용할 때도 경고가 나타나요. 예를 들어 Spring Boot Gradle Plugin은 Application 플러그인을 자동으로 적용해요.
Kotlin Multiplatform의 프로젝트 모델과 Gradle의 Java 생태계 플러그인 사이의 근본적인 호환성 문제 때문에 이 사용 중단 경고를 추가했어요. Gradle의 Java 생태계 플러그인은 현재 다른 플러그인이 다음을 수행할 수 있다는 것을 고려하지 않아요.
- Java 생태계 플러그인과 다른 방식으로 JVM 타깃용으로 게시하거나 컴파일할 수 있다는 점.
- 같은 프로젝트에 JVM과 Android 같은 두 개의 서로 다른 JVM 타깃이 있을 수 있다는 점.
- 잠재적으로 여러 non-JVM 타깃이 있는 복잡한 multiplatform 프로젝트 구조를 가질 수 있다는 점.
아쉽게도 Gradle은 현재 이러한 문제를 해결할 API를 제공하지 않아요.
이전에는 Kotlin Multiplatform에서 Java 생태계 플러그인과의 통합을 도와주는 몇 가지 해결 방법을 사용했어요. 하지만 이러한 해결 방법은 호환성 문제를 진정으로 해결하지 못했고, Gradle 8.8 릴리스 이후로는 이 해결 방법을 더 이상 사용할 수 없게 됐어요. 자세한 내용은 YouTrack 이슈를 참고하세요.
이 호환성 문제를 정확히 어떻게 해결할지 아직 알지 못하지만, Kotlin Multiplatform 프로젝트에서 어떤 형태로든 Java 소스 컴파일을 계속 지원할 것을 약속해요. 최소한 Java 소스 컴파일과 multiplatform 프로젝트 내에서 Gradle의 java-base 플러그인 사용을 지원할 거예요.
그동안 multiplatform 프로젝트에서 이 사용 중단 경고가 보이면 다음을 권장해요.
- 프로젝트에서 Gradle Java 플러그인이 실제로 필요한지 판단해 보세요. 필요 없다면 제거하는 것을 고려하세요.
- Gradle Java 플러그인이 단일 태스크에만 사용되는지 확인해 보세요. 그렇다면 큰 노력 없이 플러그인을 제거할 수 있을 거예요. 예를 들어 태스크가 Gradle Java 플러그인을 사용해 Javadoc JAR 파일을 만든다면 Javadoc 태스크를 수동으로 정의할 수 있어요.
그렇지 않으면 multiplatform 프로젝트에서 Kotlin Multiplatform Gradle 플러그인과 이 Gradle Java 플러그인을 모두 사용하려면 다음을 권장해요.
- multiplatform 프로젝트에 별도의 하위 프로젝트를 만드세요.
- 별도의 하위 프로젝트에서 Java용 Gradle 플러그인을 적용하세요.
- 별도의 하위 프로젝트에서 상위 multiplatform 프로젝트에 대한 의존성을 추가하세요.
별도의 하위 프로젝트는 multiplatform 프로젝트가 아니어야 하며, multiplatform 프로젝트에 대한 의존성을 설정하는 데만 사용해야 해요.
예를 들어 my-main-project라는 multiplatform 프로젝트가 있고 Application Gradle 플러그인을 사용해 JVM 애플리케이션을 실행하려고 해요.
하위 프로젝트를 만들고 이름을 subproject-A라고 하면 상위 프로젝트 구조는 다음과 같아야 해요.
.
├── build.gradle.kts
├── settings.gradle
├── subproject-A
└── build.gradle.kts
└── src
└── Main.java
하위 프로젝트의 build.gradle.kts 파일에서 plugins {} 블록에 Application 플러그인을 적용하세요.
plugins {
id("application")
}
plugins {
id('application')
}
하위 프로젝트의 build.gradle.kts 파일에서 상위 multiplatform 프로젝트에 대한 의존성을 추가하세요.
dependencies {
implementation(project(":my-main-project")) // The name of your parent multiplatform project
}
dependencies {
implementation project(':my-main-project') // The name of your parent multiplatform project
}
이제 상위 프로젝트가 두 플러그인과 함께 동작하도록 설정됐어요.
Kotlin/Native
Kotlin/Native는 가비지 컬렉터 개선과 Swift/Objective-C에서 Kotlin suspend 함수 호출 개선을 받았어요.
가비지 컬렉터의 동시 마킹
Kotlin 2.0.20에서 JetBrains 팀은 Kotlin/Native 런타임 성능 개선을 위한 또 하나의 단계를 밟아요. 가비지 컬렉터(GC)에 동시 마킹(concurrent marking)에 대한 실험적 지원을 추가했어요.
기본적으로 GC가 힙에서 객체를 마킹하는 동안 애플리케이션 스레드를 일시 중지해야 해요. 이는 Compose Multiplatform으로 만든 UI 애플리케이션처럼 지연 시간에 민감한 애플리케이션의 성능에 중요한 GC 일시 중지 시간의 길이에 큰 영향을 줘요.
이제 가비지 컬렉션의 마킹 단계가 애플리케이션 스레드와 동시에 실행될 수 있어요. 이는 GC 일시 중지 시간을 크게 단축하고 앱 응답성을 개선하는 데 도움이 돼요.
활성화 방법
이 기능은 현재 실험적(Experimental)이에요. 활성화하려면 gradle.properties 파일에 다음 옵션을 설정하세요.
kotlin.native.binary.gc=cms
문제가 있으면 이슈 트래커 YouTrack에 보고해 주세요.
bitcode 임베딩 지원 제거
Kotlin 2.0.20부터 Kotlin/Native 컴파일러는 더 이상 bitcode 임베딩을 지원하지 않아요. bitcode 임베딩은 Xcode 14에서 사용 중단되었고 모든 Apple 타깃에 대해 Xcode 15에서 제거됐어요.
이제 프레임워크 구성의 embedBitcode 매개변수와 -Xembed-bitcode, -Xembed-bitcode-marker 명령줄 인자가 사용 중단됐어요.
아직 이전 버전의 Xcode를 사용하고 있지만 Kotlin 2.0.20으로 업그레이드하고 싶다면 Xcode 프로젝트에서 bitcode 임베딩을 비활성화하세요.
signposts로 GC 성능 모니터링 변경
Kotlin 2.0.0은 Xcode Instruments를 통해 Kotlin/Native 가비지 컬렉터(GC)의 성능을 모니터링할 수 있게 했어요. Instruments에는 GC 일시 중지를 이벤트로 보여줄 수 있는 signposts 도구가 포함돼 있어요. 이는 iOS 앱에서 GC 관련 정지를 확인할 때 유용해요.
이 기능은 기본으로 활성화되어 있었지만, 불행히도 Xcode Instruments와 동시에 애플리케이션을 실행할 때 때때로 충돌을 일으켰어요. Kotlin 2.0.20부터 다음 컴파일러 옵션을 사용한 명시적 옵트인이 필요해요.
-Xbinary=enableSafepointSignposts=true
문서에서 GC 성능 분석에 대해 자세히 알아보세요.
비주 스레드에서 Swift/Objective-C로 Kotlin suspend 함수 호출
이전에는 Kotlin/Native에 Swift와 Objective-C에서 Kotlin suspend 함수를 호출하는 기능을 메인 스레드로만 제한하는 기본 제한이 있었어요. Kotlin 2.0.20은 이 제한을 해제해 어떤 스레드에서든 Swift/Objective-C에서 Kotlin suspend 함수를 실행할 수 있게 해줘요.
이전에 kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none 이진 옵션으로 비메인 스레드의 기본 동작을 전환했다면 이제 gradle.properties 파일에서 제거할 수 있어요.
Kotlin/Wasm
Kotlin 2.0.20에서 Kotlin/Wasm은 named export로의 마이그레이션을 계속하고 @ExperimentalWasmDsl 어노테이션을 이동해요.
기본 export 사용 시 오류
named export로의 마이그레이션의 일부로, 이전에는 JavaScript에서 Kotlin/Wasm export에 대한 기본 import를 사용할 때 콘솔에 경고 메시지가 출력됐어요.
named export를 완전히 지원하기 위해 이 경고가 이제 오류로 승격됐어요. 기본 import를 사용하면 다음 오류 메시지가 발생해요.
Do not use default import. Use the corresponding named import instead.
이 변경은 named export로 마이그레이션하기 위한 사용 중단 주기의 일부예요. 각 단계에서 기대할 수 있는 내용은 다음과 같아요.
- 2.0.0 버전: 콘솔에 경고 메시지가 출력되어 기본 export로 엔터티를 내보내는 것이 사용 중단되었음을 알려줘요.
- 2.0.20 버전: 해당 named import를 사용하라는 오류가 발생해요.
- 2.1.0 버전: 기본 import 사용이 완전히 제거돼요.
ExperimentalWasmDsl 어노테이션의 새 위치
이전에는 WebAssembly(Wasm) 기능을 위한 @ExperimentalWasmDsl 어노테이션이 Kotlin Gradle 플러그인의 다음 위치에 있었어요.
org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
2.0.20에서 @ExperimentalWasmDsl 어노테이션이 다음 위치로 이동했어요.
org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
이전 위치는 이제 사용 중단되었으며 unresolved reference로 인해 빌드 실패로 이어질 수 있어요.
@ExperimentalWasmDsl 어노테이션의 새 위치를 반영하려면 Gradle 빌드 스크립트의 import 문을 업데이트하세요. 새 @ExperimentalWasmDsl 위치에 대한 명시적 import를 사용하세요.
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
또는 이전 패키지의 다음 star import 문을 제거하세요.
import org.jetbrains.kotlin.gradle.targets.js.dsl.*
Kotlin/JS
Kotlin/JS는 JavaScript에서 정적 멤버를 지원하고 JavaScript에서 Kotlin 컬렉션을 생성하기 위한 몇 가지 실험적 기능을 도입해요.
JavaScript에서 Kotlin 정적 멤버 사용 지원
이 기능은 실험적(Experimental)이며 언제든 제거되거나 변경될 수 있어요. 평가 목적으로만 사용하세요. YouTrack에서 피드백을 남겨주시면 감사하겠어요.
Kotlin 2.0.20부터 @JsStatic 어노테이션을 사용할 수 있어요. 이 어노테이션은 @JvmStatic과 유사하게 동작하며 대상 선언에 대한 추가 정적 메서드를 생성하도록 컴파일러에 지시해요. 이를 통해 Kotlin 코드의 정적 멤버를 JavaScript에서 직접 사용할 수 있어요.
이름 있는 객체(named object)에서 정의한 함수와 클래스 및 인터페이스 안에 선언된 companion object의 함수에 @JsStatic 어노테이션을 사용할 수 있어요. 컴파일러는 객체의 정적 메서드와 객체 자체의 인스턴스 메서드를 모두 생성해요. 예를 들어:
class C {
companion object {
@JsStatic
fun callStatic() {}
fun callNonStatic() {}
}
}
이제 callStatic()은 JavaScript에서 정적이지만 callNonStatic()은 그렇지 않아요.
C.callStatic(); // Works, accessing the static function
C.callNonStatic(); // Error, not a static function in the generated JavaScript
C.Companion.callStatic(); // Instance method remains
C.Companion.callNonStatic(); // The only way it works
객체 또는 companion object의 프로퍼티에 @JsStatic 어노테이션을 적용하여 그 getter와 setter 메서드를 해당 객체 또는 companion object를 포함하는 클래스의 정적 멤버로 만들 수도 있어요.
JavaScript에서 Kotlin 컬렉션 생성
이 기능은 실험적(Experimental)이며 언제든 제거되거나 변경될 수 있어요. 평가 목적으로만 사용하세요. YouTrack에서 피드백을 남겨주시면 감사하겠어요.
Kotlin 2.0.0은 Kotlin 컬렉션을 JavaScript(및 TypeScript)로 내보내는 기능을 도입했어요. 이제 JetBrains 팀은 컬렉션 상호 운용성을 개선하기 위한 또 하나의 단계를 밟아요. Kotlin 2.0.20부터 JavaScript/TypeScript 쪽에서 직접 Kotlin 컬렉션을 생성할 수 있어요.
JavaScript에서 Kotlin 컬렉션을 만들어 내보낸 생성자나 함수에 인자로 전달할 수 있어요. 내보낸 선언 안에서 컬렉션을 언급하는 즉시 Kotlin은 JavaScript/TypeScript에서 사용할 수 있는 컬렉션용 팩토리를 생성해요.
다음 내보낸 함수를 살펴볼게요.
// Kotlin
@JsExport
fun consumeMutableMap(map: MutableMap<String, Int>)
MutableMap 컬렉션이 언급되었으므로 Kotlin은 JavaScript/TypeScript에서 사용할 수 있는 팩토리 메서드가 있는 객체를 생성해요. 이 팩토리 메서드는 JavaScript Map에서 MutableMap을 만들어요.
// JavaScript
import { consumeMutableMap } from "an-awesome-kotlin-module"
import { KtMutableMap } from "an-awesome-kotlin-module/kotlin-kotlin-stdlib"
consumeMutableMap(
KtMutableMap.fromJsMap(new Map([["First", 1], ["Second", 2]]))
)
이 기능은 Set, Map, List Kotlin 컬렉션 타입과 그 변경 가능한 대응물에서 사용할 수 있어요.
Gradle
Kotlin 2.0.20은 Gradle 6.8.3부터 8.6까지 완전히 호환돼요. Gradle 8.7과 8.8도 한 가지 예외를 제외하고 지원돼요. Kotlin Multiplatform Gradle 플러그인을 사용한다면 JVM 타깃에서 withJava() 함수를 호출할 때 multiplatform 프로젝트에서 사용 중단 경고가 보일 수 있어요. 이 문제는 가능한 한 빨리 고칠 계획이에요.
자세한 내용은 YouTrack의 이슈를 참고하세요.
최신 Gradle 릴리스까지 사용할 수도 있지만, 그렇게 하면 사용 중단 경고가 발생하거나 일부 새 Gradle 기능이 동작하지 않을 수 있다는 점을 명심하세요.
이 버전은 JVM history 파일을 기반으로 하는 기존 증분 컴파일 접근 방식의 사용 중단 절차 시작과 프로젝트 간 JVM 아티팩트를 공유하는 새로운 방식 같은 변경을 가져와요.
JVM history 파일 기반 증분 컴파일 사용 중단
Kotlin 2.0.20에서 JVM history 파일을 기반으로 하는 증분 컴파일 접근 방식이, Kotlin 1.8.20부터 기본으로 활성화된 새 증분 컴파일 접근 방식으로 대체되며 사용 중단됐어요.
JVM history 파일 기반 증분 컴파일 접근 방식은 Gradle의 빌드 캐시에서 작동하지 않고 컴파일 회피를 지원하지 않는 등의 제한이 있었어요. 반면 새 증분 컴파일 접근 방식은 이러한 제한을 극복하고 도입 이후 잘 작동해 왔어요.
새 증분 컴파일 접근 방식이 지난 두 번의 주요 Kotlin 릴리스 동안 기본으로 사용되었으므로 kotlin.incremental.useClasspathSnapshot Gradle 속성은 Kotlin 2.0.20에서 사용 중단됐어요. 따라서 이를 사용해 옵트아웃하면 사용 중단 경고가 보일 거예요.
프로젝트 간 JVM 아티팩트를 클래스 파일로 공유하는 옵션
이 기능은 실험적(Experimental)이며 언제든 제거되거나 변경될 수 있어요. 평가 목적으로만 사용하세요. YouTrack에서 피드백을 남겨주시면 감사하겠어요. 옵트인이 필요해요(자세한 내용은 아래 참조).
Kotlin 2.0.20에서는 JAR 파일 같은 Kotlin/JVM 컴파일의 출력이 프로젝트 간에 공유되는 방식을 변경하는 새 접근 방식을 도입해요. 이 접근 방식에서 Gradle의 apiElements 구성은 이제 컴파일된 .class 파일이 포함된 디렉터리에 대한 접근을 제공하는 보조 변형을 가져요. 구성되면 프로젝트는 컴파일 중에 압축된 JAR 아티팩트를 요청하는 대신 이 디렉터리를 사용해요. 이는 특히 증분 빌드에서 JAR 파일이 압축되고 압축 해제되는 횟수를 줄여줘요.
테스트 결과 이 새 접근 방식은 Linux와 macOS 호스트에서 빌드 성능을 개선할 수 있어요. 그러나 Windows 호스트에서는 파일 작업 시 Windows가 I/O 작업을 처리하는 방식 때문에 성능 저하가 관찰됐어요.
이 새 접근 방식을 시도하려면 gradle.properties 파일에 다음 속성을 추가하세요.
kotlin.jvm.addClassesVariant=true
기본적으로 이 속성은 false로 설정되며 Gradle의 apiElements 변형은 압축된 JAR 아티팩트를 요청해요.
Gradle에는 Java 전용 프로젝트에서 컴파일된 .class 파일이 포함된 디렉터리 대신 컴파일 중에 압축된 JAR 아티팩트만 노출하도록 사용할 수 있는 관련 속성이 있어요.
org.gradle.java.compile-classpath-packaging=true
이 속성과 그 목적에 대한 자세한 내용은 Windows에서 거대한 멀티 프로젝트의 심각한 빌드 성능 저하에 대한 Gradle 문서를 참고하세요.
이 새 접근 방식에 대한 피드백을 환영해요. 사용하면서 성능 개선을 발견했나요? YouTrack에 댓글을 추가해 알려주세요.
Kotlin Gradle 플러그인의 의존성 동작을 java-test-fixtures 플러그인과 정렬
Kotlin 2.0.20 이전에는 프로젝트에서 java-test-fixtures 플러그인을 사용한다면 의존성이 전파되는 방식에 Gradle과 Kotlin Gradle 플러그인 사이에 차이가 있었어요.
Kotlin Gradle 플러그인은 의존성을 다음과 같이 전파했어요.
java-test-fixtures플러그인의implementation및api의존성 타입에서test소스 세트 컴파일 클래스패스로.- 메인 소스 세트의
implementation및api의존성 타입에서java-test-fixtures플러그인의 소스 세트 컴파일 클래스패스로.
하지만 Gradle은 api 의존성 타입의 의존성만 전파했어요.
이 동작 차이로 일부 프로젝트에서 리소스 파일을 클래스패스에서 여러 번 찾게 되었어요.
Kotlin 2.0.20부터 Kotlin Gradle 플러그인의 동작이 Gradle의 java-test-fixtures 플러그인과 정렬되어 이 문제가 이 플러그인이나 다른 Gradle 플러그인에서 더 이상 발생하지 않아요.
이 변경의 결과로 test 및 testFixtures 소스 세트의 일부 의존성에 더 이상 접근할 수 없을 수 있어요. 이런 일이 발생하면 의존성 선언 타입을 implementation에서 api로 변경하거나 영향을 받는 소스 세트에 새 의존성 선언을 추가하세요.
컴파일 태스크가 아티팩트에 대한 의존성이 없는 드문 경우에 태스크 의존성 추가
2.0.20 이전에는 컴파일 태스크가 아티팩트 입력 중 하나에 대한 태스크 의존성이 없는 시나리오가 있다는 것을 발견했어요. 이는 아티팩트가 때로는 제때 생성되었지만 때로는 그렇지 않았기 때문에 의존 컴파일 태스크의 결과가 불안정하다는 것을 의미했어요.
이 문제를 해결하기 위해 Kotlin Gradle 플러그인은 이제 이러한 시나리오에서 필요한 태스크 의존성을 자동으로 추가해요.
아주 드문 경우에 이 새 동작이 순환 의존성 오류를 일으킬 수 있다는 것을 발견했어요. 예를 들어 한 컴파일이 다른 컴파일의 모든 내부 선언을 볼 수 있고 생성된 아티팩트가 두 컴파일 태스크의 출력에 의존하는 여러 컴파일이 있다면 다음과 같은 오류가 발생할 수 있어요.
FAILURE: Build failed with an exception.
What went wrong:
Circular dependency between the following tasks:
:lib:compileKotlinJvm
--- :lib:jvmJar
\--- :lib:compileKotlinJvm (*)
(*) - details omitted (listed previously)
이 순환 의존성 오류를 해결하기 위해 archivesTaskOutputAsFriendModule Gradle 속성을 추가했어요.
기본적으로 이 속성은 true로 설정되어 태스크 의존성을 추적해요. 태스크 의존성이 필요 없도록 컴파일 태스크에서 아티팩트 사용을 비활성화하려면 gradle.properties 파일에 다음을 추가하세요.
kotlin.build.archivesTaskOutputAsFriendModule=false
자세한 내용은 YouTrack의 이슈를 참고하세요.
Compose 컴파일러
Kotlin 2.0.20에서 Compose 컴파일러는 몇 가지 개선 사항을 받았어요.
2.0.0에서 도입된 불필요한 재구성 문제 수정
Compose 컴파일러 2.0.0에는 non-JVM 타깃이 있는 multiplatform 프로젝트에서 타입의 안정성을 잘못 추론하는 경우가 있는 문제가 있어요. 이로 인해 불필요한(또는 끝없는) 재구성이 발생할 수 있어요. Kotlin 2.0.0용으로 만든 Compose 앱을 버전 2.0.10 이상으로 업데이트할 것을 강력히 권장해요.
앱이 Compose 컴파일러 2.0.10 이상으로 빌드되었지만 2.0.0 버전으로 빌드된 의존성을 사용한다면 이 오래된 의존성들이 여전히 재구성 문제를 일으킬 수 있어요. 이를 방지하려면 의존성을 앱과 동일한 Compose 컴파일러로 빌드된 버전으로 업데이트하세요.
컴파일러 옵션 구성의 새로운 방식
최상위 매개변수의 변동을 피하기 위해 새 옵션 구성 메커니즘을 도입했어요. Compose 컴파일러 팀이 composeCompiler {} 블록의 최상위 항목을 만들거나 제거하여 무언가를 테스트하는 것은 더 어려워요. 그래서 강력한 스킵 모드(strong skipping mode)와 non-skipping group 최적화 같은 옵션은 이제 featureFlags 프로퍼티를 통해 활성화돼요. 이 프로퍼티는 결국 기본값이 될 새 Compose 컴파일러 옵션을 테스트하는 데 사용될 거예요.
이 변경은 Compose 컴파일러 Gradle 플러그인에도 적용됐어요. 앞으로 기능 플래그를 구성하려면 다음 구문을 사용하세요(이 코드는 모든 기본값을 뒤집을 거예요).
composeCompiler {
featureFlags = setOf(
ComposeFeatureFlag.IntrinsicRemember.disabled(),
ComposeFeatureFlag.OptimizeNonSkippingGroups,
ComposeFeatureFlag.StrongSkipping.disabled()
)
}
또는 Compose 컴파일러를 직접 구성한다면 다음 구문을 사용하세요.
-P plugin:androidx.compose.compiler.plugins.kotlin:featureFlag=IntrinsicRemember
따라서 enableIntrinsicRemember, enableNonSkippingGroupOptimization, enableStrongSkippingMode 프로퍼티는 사용 중단됐어요.
YouTrack에서 이 새 접근 방식에 대한 피드백을 남겨주시면 감사하겠어요.
기본으로 활성화된 강력한 스킵 모드
Compose 컴파일러의 강력한 스킵 모드(strong skipping mode)가 이제 기본으로 활성화돼요.
강력한 스킵 모드는 어떤 composable이 스킵될 수 있는지에 대한 규칙을 변경하는 Compose 컴파일러 구성 옵션이에요. 강력한 스킵 모드가 활성화되면 불안정한 매개변수를 가진 composable도 스킵될 수 있어요. 강력한 스킵 모드는 또한 composable 함수에 사용된 람다를 자동으로 기억(remember)하므로 재구성을 피하기 위해 람다를 더 이상 remember로 감쌀 필요가 없어요.
자세한 내용은 강력한 스킵 모드 문서를 참고하세요.
기본으로 활성화된 Composition trace 마커
includeTraceMarkers 옵션은 이제 컴파일러 플러그인의 기본값과 일치하도록 Compose 컴파일러 Gradle 플러그인에서 기본적으로 true로 설정돼요. 이를 통해 Android Studio 시스템 추적 프로파일러에서 composable 함수를 볼 수 있어요. composition 추적에 대한 자세한 내용은 Android Developers 블로그 게시물을 참고하세요.
Non-skipping group 최적화
이 릴리스는 새 컴파일러 옵션을 포함해요. 활성화하면 스킵 불가능하고 재시작 불가능한 composable 함수가 더 이상 composable 본문 주변에 group을 생성하지 않아요. 이로 인해 할당이 줄어들고 성능이 개선돼요. 이 옵션은 실험적이며 기본적으로 비활성화되어 있지만 위에 표시된 대로 OptimizeNonSkippingGroups 기능 플래그로 활성화할 수 있어요.
이 기능 플래그는 이제 더 넓은 테스트를 위한 준비가 됐어요. 이 기능을 활성화할 때 발견한 문제는 Google 이슈 트래커에 제출할 수 있어요.
추상 composable 함수의 기본 매개변수 지원
이제 추상 composable 함수에 기본 매개변수를 추가할 수 있어요.
이전에는 유효한 Kotlin 코드임에도 Compose 컴파일러가 이를 시도할 때 오류를 보고했어요. 이제 Compose 컴파일러에서 이에 대한 지원을 추가했고 제한이 제거됐어요. 이는 기본 Modifier 값을 포함할 때 특히 유용해요.
abstract class Composables {
@Composable
abstract fun Composable(modifier: Modifier = Modifier)
}
열린(open) composable 함수의 기본 매개변수는 2.0.20에서 여전히 제한돼요. 이 제한은 향후 릴리스에서 다룰 예정이에요.
표준 라이브러리
표준 라이브러리는 이제 실험적 기능으로 UUID(범용 고유 식별자)를 지원하며 Base64 디코딩에 몇 가지 변경 사항이 있어요.
공통 Kotlin 표준 라이브러리의 UUID 지원
이 기능은 실험적(Experimental)이에요. 옵트인하려면 @ExperimentalUuidApi 어노테이션이나 -opt-in=kotlin.uuid.ExperimentalUuidApi 컴파일러 옵션을 사용하세요.
Kotlin 2.0.20은 항목을 고유하게 식별하는 과제를 해결하기 위해 공통 Kotlin 표준 라이브러리에 UUID(범용 고유 식별자)를 나타내는 클래스를 도입해요.
또한 이 기능은 다음과 같은 UUID 관련 작업을 위한 API를 제공해요.
- UUID 생성.
- UUID를 문자열 표현으로 파싱하고 문자열 표현으로 포맷.
- 지정된 128비트 값에서 UUID 생성.
- UUID의 128비트에 접근.
다음 코드 예시는 이러한 작업을 보여줘요.
// Constructs a byte array for UUID creation
val byteArray = byteArrayOf(
0x55, 0x0E, 0x84.toByte(), 0x00, 0xE2.toByte(), 0x9B.toByte(), 0x41, 0xD4.toByte(),
0xA7.toByte(), 0x16, 0x44, 0x66, 0x55, 0x44, 0x00, 0x00
)
val uuid1 = Uuid.fromByteArray(byteArray)
val uuid2 = Uuid.fromULongs(0x550E8400E29B41D4uL, 0xA716446655440000uL)
val uuid3 = Uuid.parse("550e8400-e29b-41d4-a716-446655440000")
println(uuid1)
// 550e8400-e29b-41d4-a716-446655440000
println(uuid1 == uuid2)
// true
println(uuid2 == uuid3)
// true
// Accesses UUID bits
val version = uuid1.toLongs { mostSignificantBits, _ ->
((mostSignificantBits shr 12) and 0xF).toInt()
}
println(version)
// 4
// Generates a random UUID
val randomUuid = Uuid.random()
println(uuid1 == randomUuid)
// false
java.util.UUID를 사용하는 API와의 호환성을 유지하기 위해 Kotlin/JVM에는 java.util.UUID와 kotlin.uuid.Uuid 사이를 변환하는 두 개의 확장 함수가 있어요: .toJavaUuid()와 .toKotlinUuid(). 예를 들어:
val kotlinUuid = Uuid.parseHex("550e8400e29b41d4a716446655440000")
// Converts Kotlin UUID to java.util.UUID
val javaUuid = kotlinUuid.toJavaUuid()
val javaUuid = java.util.UUID.fromString("550e8400-e29b-41d4-a716-446655440000")
// Converts Java UUID to kotlin.uuid.Uuid
val kotlinUuid = javaUuid.toKotlinUuid()
이 기능과 제공된 API는 여러 플랫폼 간 코드 공유를 허용하여 multiplatform 소프트웨어 개발을 단순화해요. UUID는 또한 고유 식별자 생성이 어려운 환경에서 이상적이에요.
UUID를 포함하는 몇 가지 예시 사용 사례는 다음과 같아요.
- 데이터베이스 레코드에 고유 ID 할당.
- 웹 세션 식별자 생성.
- 고유 식별 또는 추적이 필요한 모든 시나리오.
HexFormat의 minLength 지원
HexFormat 클래스와 그 프로퍼티는 실험적(Experimental)이에요. 옵트인하려면 @OptIn(ExperimentalStdlibApi::class) 어노테이션이나 -opt-in=kotlin.ExperimentalStdlibApi 컴파일러 옵션을 사용하세요.
Kotlin 2.0.20은 NumberHexFormat 클래스에 새 minLength 프로퍼티를 추가하며, 이는 HexFormat.number로 접근해요. 이 프로퍼티는 숫자 값의 16진수 표현에서 최소 자릿수를 지정할 수 있게 해주며, 필요한 길이를 맞추기 위해 0으로 패딩할 수 있게 해줘요. 또한 removeLeadingZeros 프로퍼티를 사용해 앞의 0을 제거할 수 있어요.
fun main() {
println(93.toHexString(HexFormat {
number.minLength = 4
number.removeLeadingZeros = true
}))
// "005d"
}
minLength 프로퍼티는 파싱에 영향을 주지 않아요. 그러나 파싱 시 이제 추가 앞의 자릿수가 0이라면 16진수 문자열이 타입의 너비보다 더 많은 자릿수를 가질 수 있게 됐어요.
Base64 디코더 동작 변경
Base64 클래스와 관련 기능은 실험적(Experimental)이에요. 옵트인하려면 @OptIn(ExperimentalEncodingApi::class) 어노테이션이나 -opt-in=kotlin.io.encoding.ExperimentalEncodingApi 컴파일러 옵션을 사용하세요.
Kotlin 2.0.20에서 Base64 디코더 동작에 두 가지 변경이 도입됐어요.
- Base64 디코더는 이제 패딩을 요구해요.
- 패딩 구성을 위한
withPadding함수가 추가됐어요.
Base64 디코더는 이제 패딩을 요구
Base64 인코더는 이제 기본적으로 패딩을 추가하고, 디코더는 패딩을 요구하며 디코딩 시 0이 아닌 패딩 비트를 금지해요.
패딩 구성을 위한 withPadding 함수
기본 인코딩 및 디코딩의 패딩 동작을 제어할 수 있도록 새 .withPadding() 함수가 도입됐어요.
val base64 = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL)
이 함수를 사용하면 서로 다른 패딩 옵션을 가진 Base64 인스턴스를 만들 수 있어요.
PaddingOption |
인코딩 시 | 디코딩 시 |
|---|---|---|
PRESENT |
패딩 추가 | 패딩 필요 |
ABSENT |
패딩 생략 | 패딩 허용되지 않음 |
PRESENT_OPTIONAL |
패딩 추가 | 패딩 선택적 |
ABSENT_OPTIONAL |
패딩 생략 | 패딩 선택적 |
서로 다른 패딩 옵션을 가진 Base64 인스턴스를 만들고 이를 사용해 데이터를 인코딩하고 디코딩할 수 있어요.
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
fun main() {
// Example data to encode
val data = "fooba".toByteArray()
// Creates a Base64 instance with URL-safe alphabet and PRESENT padding
val base64Present = Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT)
val encodedDataPresent = base64Present.encode(data)
println("Encoded data with PRESENT padding: $encodedDataPresent")
// Encoded data with PRESENT padding: Zm9vYmE=
// Creates a Base64 instance with URL-safe alphabet and ABSENT padding
val base64Absent = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT)
val encodedDataAbsent = base64Absent.encode(data)
println("Encoded data with ABSENT padding: $encodedDataAbsent")
// Encoded data with ABSENT padding: Zm9vYmE
// Decodes the data back
val decodedDataPresent = base64Present.decode(encodedDataPresent)
println("Decoded data with PRESENT padding: ${String(decodedDataPresent)}")
// Decoded data with PRESENT padding: fooba
val decodedDataAbsent = base64Absent.decode(encodedDataAbsent)
println("Decoded data with ABSENT padding: ${String(decodedDataAbsent)}")
// Decoded data with ABSENT padding: fooba
}
문서 업데이트
Kotlin 문서에 몇 가지 눈에 띄는 변경이 있었어요.
- 개선된 표준 입력 페이지 - Java Scanner와
readln()사용법을 알아보세요. - 개선된 K2 컴파일러 마이그레이션 가이드 - 성능 개선, Kotlin 라이브러리와의 호환성, 사용자 정의 컴파일러 플러그인으로 무엇을 해야 하는지 알아보세요.
- 개선된 예외 페이지 - 예외, 예외를 던지고 잡는 방법을 알아보세요.
- 개선된 JVM에서 JUnit을 사용한 코드 테스트 - 튜토리얼 - JUnit을 사용해 테스트를 만드는 방법을 알아보세요.
- 개선된 Swift/Objective-C와의 상호 운용성 페이지 - Swift/Objective-C 코드에서 Kotlin 선언을, Kotlin 코드에서 Objective-C 선언을 사용하는 방법을 알아보세요.
- 개선된 Swift package export 설정 페이지 - Swift package manager 의존성이 소비할 수 있는 Kotlin/Native 출력을 설정하는 방법을 알아보세요.
Kotlin 2.0.20 설치
IntelliJ IDEA 2023.3과 Android Studio Iguana (2023.2.1) Canary 15부터 Kotlin 플러그인은 IDE에 포함된 번들 플러그인으로 배포돼요. 즉, 더 이상 JetBrains Marketplace에서 플러그인을 설치할 수 없어요.
새 Kotlin 버전으로 업데이트하려면 빌드 스크립트에서 Kotlin 버전을 2.0.20으로 변경하세요.