Kotlin 2.2.20의 새로운 기능

Kotlin 2.2.20의 새로운 기능

Kotlin 2.2.20 릴리스가 나왔어요. 웹 개발에 중요한 변화를 가져오는 릴리스예요. Kotlin/Wasm은 이제 Beta 단계로, JavaScript interop에서의 예외 처리 개선, npm 의존성 분리, 별도 설정 없는 브라우저 디버깅 지원, 그리인 js와 wasmJs target을 위한 새 공유 소스 세트 등이 포함돼요.

추가로 주요 하이라이트는 다음과 같아요:

Compose Multiplatform for web이 Beta로 나아가요. 자세한 내용은 블로그 포스트에서 확인해 보세요.

업데이트에 대한 간략한 개요도 이 영상에서 볼 수 있어요.

Kotlin 릴리스 주기에 대한 자세한 내용은 Kotlin 릴리스 프로세스 문서를 참고하세요.

출처: What's new in Kotlin 2.2.20

본문

IDE 지원

Kotlin 2.2.20을 지원하는 Kotlin 플러그인은 최신 IntelliJ IDEA와 Android Studio에 번들로 포함되어 있어요. 업데이트하려면 빌드 스크립트에서 Kotlin 버전을 2.2.20으로 변경하기만 하면 돼요.

자세한 내용은 새 릴리스로 업데이트하기를 참고하세요.

언어

Kotlin 2.2.20에서는 Kotlin 2.3.0에 계획된 차기 언어 기능을 시험해 볼 수 있어요. suspend 함수 타입 오버로드에 람다를 전달할 때의 오버로드 해석 개선명시적 반환 타입이 있는 식 본문에서의 return 문 지원이 그 예시예요. 이번 릴리스는 when 식의 exhaustiveness 검사 개선, reified Throwable catch, Kotlin contracts 개선도 포함해요.

suspend 함수 타입 오버로드에 대한 람다의 오버로드 해석 개선

이전에는 일반 함수 타입과 suspend 함수 타입을 둘 다 오버로드하면 람다를 전달할 때 모호성 오류가 발생했어요. 명시적 타입 캐스트로 이 오류를 해결할 수 있었지만, 컴파일러가 "No cast needed" 경고를 잘못 보고했어요:

// Defines two overloads
fun transform(block: () -> Int) {}
fun transform(block: suspend () -> Int) {}

fun test() {
    // Fails with overload resolution ambiguity
    transform({ 42 })

    // Uses an explicit cast, but the compiler incorrectly reports
    // a "No cast needed" warning
    transform({ 42 } as () -> Int)
}

이 변경으로 일반 함수 타입과 suspend 함수 타입 오버로드를 모두 정의하면, 캐스트가 없는 람다는 일반 오버로드로 해석돼요. suspend 키워드를 사용해 suspend 오버로드로 명시적으로 해석할 수도 있어요:

// Resolves to transform(() -> Int)
transform({ 42 })

// Resolves to transform(suspend () -> Int)
transform(suspend { 42 })

이 동작은 Kotlin 2.3.0에서 기본으로 활성화될 예정이에요. 지금 테스트하려면 다음 컴파일러 옵션으로 언어 버전을 2.3으로 설정하세요:

-language-version 2.3

또는 build.gradle(.kts) 파일에서 구성해도 돼요:

kotlin {
    compilerOptions {
        languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3)
    }
}

이슈 트래커 YouTrack에 피드백을 남겨 주시면 감사하겠어요.

명시적 반환 타입이 있는 식 본문에서의 return 문 지원

이전에는 식 본문에서 return을 사용하면 함수의 반환 타입이 Nothing으로 추론될 수 있어 컴파일러 오류가 발생했어요.

fun example() = return 42
// Error: Returns are prohibited for functions with an expression body

이 변경으로 이제 반환 타입을 명시적으로 작성하는 한 식 본문에서 return을 사용할 수 있어요:

// Specifies the return type explicitly
fun getDisplayNameOrDefault(userId: String?): String = getDisplayName(userId ?: return "default")

// Fails because it doesn't specify the return type explicitly
fun getDisplayNameOrDefault(userId: String?) = getDisplayName(userId ?: return "default")

마찬가지로 식 본문이 있는 함수의 람다와 중첩 식 안의 return 문은 이전에 의도와 다르게 컴파일되곤 했어요. 이제 Kotlin은 반환 타입이 명시적으로 지정된 한 이러한 경우를 지원해요. 명시적 반환 타입이 없는 경우는 Kotlin 2.3.0에서 deprecate될 예정이에요:

// Return type isn't explicitly specified, and the return statement is inside a lambda
// which will be deprecated
fun returnInsideLambda() = run { return 42 }

// Return type isn't explicitly specified, and the return statement is inside the initializer
// of a local variable, which will be deprecated
fun returnInsideIf() = when {
    else -> {
        val result = if (someCondition()) return "" else "value"
        result
    }
}

이 동작은 Kotlin 2.3.0에서 기본으로 활성화될 예정이에요. 지금 테스트하려면 다음 컴파일러 옵션으로 언어 버전을 2.3으로 설정하세요:

-language-version 2.3

또는 build.gradle(.kts) 파일에서 구성해도 돼요:

kotlin {
    compilerOptions {
        languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3)
    }
}

이슈 트래커 YouTrack에 피드백을 남겨 주시면 감사하겠어요.

when 식의 데이터 흐름 기반 exhaustiveness 검사

Kotlin 2.2.20은 when 식에 데이터 흐름 기반 exhaustiveness 검사를 도입했어요. 이전에는 컴파일러 검사가 when 식 자체에 한정되어 있어서 불필요한 else 분기를 자주 추가해야 했어요. 이 업데이트로 컴파일러가 이전 조건 검사와 조기 반환을 추적해서, 불필요한 else 분기를 제거할 수 있게 됐어요.

예를 들어 컴파일러는 이제 if 조건이 충족될 때 함수가 반환함을 인식하므로, when 식은 나머지 경우만 처리하면 돼요:

enum class UserRole { ADMIN, MEMBER, GUEST }

fun getPermissionLevel(role: UserRole): Int {
    // Covers the Admin case outside of the when expression
    if (role == UserRole.ADMIN) return 99

    return when (role) {
        UserRole.MEMBER -> 10
        UserRole.GUEST -> 1
        // You no longer have to include this else branch
        // else -> throw IllegalStateException()
    }
}

이 기능은 Experimental 단계예요. 활성화하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가하세요:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xdata-flow-based-exhaustiveness")
    }
}

catch 절에서 reified 타입 지원

Kotlin 2.2.20에서 컴파일러는 이제 inline 함수의 catch 절에서 reified 제네릭 타입 파라미터를 사용하는 것을 허용해요.

예시를 볼게요:

inline fun <reified ExceptionType : Throwable> handleException(block: () -> Unit) {
    try {
        block()
        // This is now allowed after the change
    } catch (e: ExceptionType) {
        println("Caught specific exception: ${e::class.simpleName}")
    }
}

fun main() {
    // Tries to perform an action that might throw an IOException
    handleException<java.io.IOException> {
        throw java.io.IOException("File not found")
    }
    // Caught specific exception: IOException
}

이전에는 inline 함수에서 reified Throwable 타입을 catch하려고 하면 오류가 발생했어요.

이 동작은 Kotlin 2.4.0에서 기본으로 활성화될 예정이에요. 지금 사용하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가하세요:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xallow-reified-type-in-catch")
    }
}

Kotlin 팀은 이 기능에 기여한 외부 기여자 Iven Krall 님께 감사를 전해요.

개선된 Kotlin contracts

Kotlin 2.2.20은 Kotlin contracts에 여러 개선을 도입했어요:

이 개선들은 Experimental 단계예요. 옵트인하려면 contracts를 선언할 때 여전히 @OptIn(ExperimentalContracts::class) 애노테이션을 사용해야 해요. holdsIn 키워드와 returnsNotNull() 함수도 @OptIn(ExperimentalExtendedContracts::class) 애노테이션이 필요해요.

이 개선들을 사용하려면 각 섹션에 설명된 컴파일러 옵션도 추가해야 해요.

이슈 트래커에 피드백을 남겨 주시면 감사하겠어요.

contract 타입 단언에서의 제네릭 지원

이제 제네릭 타입에 타입 단언을 수행하는 contracts를 작성할 수 있어요:

import kotlin.contracts.*

sealed class Failure {
    class HttpError(val code: Int) : Failure()
    // Insert other failure types here
}

sealed class Result<out T, out F : Failure> {
    class Success<T>(val data: T) : Result<T, Nothing>()
    class Failed<F : Failure>(val failure: F) : Result<Nothing, F>()
}

@OptIn(ExperimentalContracts::class)
// Uses a contract to assert a generic type
fun <T, F : Failure> Result<T, F>.isHttpError(): Boolean {
    contract {
        returns(true) implies (this@isHttpError is Result.Failed<Failure.HttpError>)
    }
    return this is Result.Failed && this.failure is Failure.HttpError
}

이 예시에서 contract는 Result 객체에 타입 단언을 수행해서, 컴파일러가 이를 스마트 캐스트로 안전하게 단언된 제네릭 타입으로 변환하게 해줘요.

이 기능은 Experimental 단계예요. 옵트인하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가하세요:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xallow-contracts-on-more-functions")
    }
}

프로퍼티 접근자와 특정 operator 함수 내부에서의 contracts 지원

이제 프로퍼티 접근자와 특정 operator 함수 내부에서 contracts를 정의할 수 있어요. 이를 통해 더 많은 종류의 선언에 contracts를 사용할 수 있어 더 유연해져요.

예를 들어 getter 안에서 contract를 사용해 receiver 객체의 스마트 캐스팅을 활성화할 수 있어요:

import kotlin.contracts.*

val Any.isHelloString: Boolean
    get() {
        @OptIn(ExperimentalContracts::class)
        // Enables smart casting the receiver to String when the getter returns true
        contract { returns(true) implies (this@isHelloString is String) }
        return "hello" == this
    }

fun printIfHelloString(x: Any) {
    if (x.isHelloString) {
        // Prints the length after the smart cast of the receiver to String
        println(x.length)
        // 5
    }
}

또한 다음 operator 함수에서 contracts를 사용할 수 있어요:

  • invoke
  • contains
  • rangeTo, rangeUntil
  • componentN
  • iterator
  • unaryPlus, unaryMinus, not
  • inc, dec

람다 안에서 변수의 초기화를 보장하기 위해 operator 함수에서 contract를 사용하는 예시를 볼게요:

import kotlin.contracts.*

class Runner {
    @OptIn(ExperimentalContracts::class)
    // Enables initialization of variables assigned inside the lambda
    operator fun invoke(block: () -> Unit) {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        block()
    }
}

fun testOperator(runner: Runner) {
    val number: Int
    runner {
        number = 1
    }
    // Prints the value after definite initialization guaranteed by the contract
    println(number)
    // 1
}

이 기능은 Experimental 단계예요. 옵트인하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가하세요:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xallow-contracts-on-more-functions")
    }
}

contracts에서 returnsNotNull() 함수 지원

Kotlin 2.2.20은 contracts용 returnsNotNull() 함수를 도입했어요. 이 함수를 사용해 특정 조건이 충족될 때 함수가 non-null 값을 반환하도록 보장할 수 있어요. 별도의 nullable과 non-nullable 함수 오버로드를 하나의 간결한 함수로 대체해서 코드를 단순화해 줘요:

import kotlin.contracts.*

@OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class)
fun decode(encoded: String?): String? {
    contract {
        // Guarantees a non-null return value when the input is non-null
        (encoded != null) implies (returnsNotNull())
    }
    if (encoded == null) return null
    return java.net.URLDecoder.decode(encoded, "UTF-8")
}

fun useDecodedValue(s: String?) {
    // Uses a safe call since the return value may be null
    decode(s)?.length
    if (s != null) {
        // Treats the return value as non-null after the smart cast
        decode(s).length
    }
}

이 예시에서 decode() 함수의 contract는 입력이 non-null일 때 컴파일러가 반환 값을 스마트 캐스트할 수 있게 해줘, 추가 null 검사나 여러 오버로드가 필요 없어져요.

이 기능은 Experimental 단계예요. 옵트인하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가하세요:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xallow-condition-implies-returns-contracts")
    }
}

새 holdsIn 키워드

Kotlin 2.2.20은 contracts용 새 holdsIn 키워드를 도입했어요. 이를 사용해 특정 람다 안에서 불리언 조건이 true라고 가정되도록 보장할 수 있어요. 이를 통해 contracts로 조건부 스마트 캐스트가 있는 DSL을 만들 수 있어요.

예시를 볼게요:

import kotlin.contracts.*

@OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class)
fun <T> T.alsoIf(condition: Boolean, block: (T) -> Unit): T {
    contract {
        // Declares that the lambda runs at most once
        callsInPlace(block, InvocationKind.AT_MOST_ONCE)
        // Declares that the condition is assumed to be true inside the lambda
        condition holdsIn block
    }
    if (condition) block(this)
    return this
}

fun useApplyIf(input: Any) {
    val result = listOf(1, 2, 3)
        .first()
        .alsoIf(input is Int) {
            // The input parameter is smart cast to Int inside the lambda
            // Prints the sum of input and first list element
            println(input + it)
            // 2
        }
        .toString()
}

이 기능은 Experimental 단계예요. 옵트인하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가하세요:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xallow-holdsin-contract")
    }
}

Kotlin/JVM: when 식에서 invokedynamic 지원

Kotlin 2.2.20에서 이제 when 식을 invokedynamic으로 컴파일할 수 있어요. 이전에는 타입 검사가 여러 개 있는 when 식이 바이트코드에서 긴 instanceof 검사 체인으로 컴파일됐어요.

이제 다음 조건이 충족되면 when 식에 invokedynamic을 사용해 Java switch 문이 생성하는 바이트코드처럼 더 작은 바이트코드를 만들 수 있어요:

  • else를 제외한 모든 조건이 is 또는 null 검사예요.
  • 식에 guard conditions (if)가 없어요.
  • 조건에 직접 타입 검사할 수 없는 타입이 포함되지 않아요. 예: 가변 Kotlin 컬렉션(MutableList) 또는 함수 타입(kotlin.Function1, kotlin.Function2 등).
  • else 외에 최소 두 개 이상의 조건이 있어요.
  • 모든 분기가 when 식의 동일한 subject를 검사해요.

예를 들면:

open class Example

class A : Example()
class B : Example()
class C : Example()

fun test(e: Example) = when (e) {
    // Uses invokedynamic with SwitchBootstraps.typeSwitch
    is A -> 1
    is B -> 2
    is C -> 3
    else -> 0
}

새 기능을 활성화하면 이 예시의 when 식은 여러 instanceof 검사 대신 단일 invokedynamic 타입 스위치로 컴파일돼요.

이 기능을 활성화하려면 Kotlin 코드를 JVM target 21 이상으로 컴파일하고 다음 컴파일러 옵션을 추가하세요:

-Xwhen-expressions=indy

또는 build.gradle(.kts) 파일의 compilerOptions {} 블록에 추가해도 돼요:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xwhen-expressions=indy")
    }
}

이 기능은 Experimental 단계예요. 이슈 트래커 YouTrack에 피드백을 남겨 주시면 감사하겠어요.

Kotlin Multiplatform

Kotlin 2.2.20은 Kotlin Multiplatform에 중요한 변화를 가져와요. Swift export가 기본으로 제공되고, 새 공유 소스 세트가 있으며, 공통 의존성 관리의 새로운 방식을 시험해 볼 수 있어요.

Swift export가 기본으로 제공

Kotlin 2.2.20은 Swift export에 대한 실험적 지원을 도입했어요. Kotlin 소스를 직접 내보내고 Swift에서 관용적으로 Kotlin 코드를 호출할 수 있게 해줘서 Objective-C 헤더가 필요 없어져요.

이는 Apple target을 위한 multiplatform 개발을 크게 개선해야 해요. 예를 들어 최상위 함수가 있는 Kotlin 모듈이 있다면, Swift export는 헷갈리는 Objective-C 밑줄과 맹글된 이름을 제거하고 깔끔한 모듈별 import를 가능하게 해줘요.

주요 기능들은 다음과 같아요:

  • 멀티 모듈 지원. 각 Kotlin 모듈이 별도의 Swift 모듈로 내보내져 함수 호출을 단순화해요.
  • 패키지 지원. 내보내기 중에 Kotlin 패키지가 명시적으로 보존되어 생성된 Swift 코드의 명명 충돌을 피해요.
  • 타입 별칭. Kotlin 타입 별칭이 Swift로 내보내지고 보존되어 가독성을 높여요.
  • 프리미티브의 향상된 nullability. Int? 같은 타입을 KotlinInt 같은 래퍼 클래스로 박싱해서 nullability를 보존해야 했던 Objective-C interop과 달리, Swift export는 nullability 정보를 직접 변환해요.
  • 오버로드. 모호함 없이 Swift에서 Kotlin의 오버로드된 함수를 호출할 수 있어요.
  • 평탄화된 패키지 구조. Kotlin 패키지를 Swift enum으로 변환해 생성된 Swift 코드에서 패키지 접두사를 제거할 수 있어요.
  • 모듈 이름 커스터마이징. Kotlin 프로젝트의 Gradle 구성에서 결과 Swift 모듈 이름을 커스터마이징할 수 있어요.

Swift export 활성화하는 방법

이 기능은 현재 Experimental 단계이며, 직접 통합으로 iOS 프레임워크를 Xcode 프로젝트에 연결하는 프로젝트에서만 작동해요. 이는 IntelliJ IDEA의 Kotlin Multiplatform 플러그인이나 웹 마법사로 만든 multiplatform 프로젝트의 표준 구성이에요.

Swift export를 시험해 보려면 Xcode 프로젝트를 구성하세요:

  • Xcode에서 프로젝트 설정을 엽니다.
  • Build Phases 탭에서 embedAndSignAppleFrameworkForXcode task가 있는 Run Script 단계를 찾아요.
  • run script 단계에서 대신 embedSwiftExportForXcode task가 나타나도록 스크립트를 조정해요:
./gradlew :<Shared module name>:embedSwiftExportForXcode
  • 프로젝트를 빌드해요. Swift 모듈이 빌드 출력 디렉터리에 생성돼요.

이 기능은 기본으로 제공돼요. 이전 릴리스에서 이미 활성화했다면 이제 gradle.properties 파일에서 kotlin.experimental.swift-export.enabled를 제거할 수 있어요.

시간을 아끼려면 Swift export가 이미 설정된 공개 샘플을 클론해 보세요.

Swift export에 대한 자세한 내용은 문서를 참고하세요.

피드백 남기기

향후 Kotlin 릴리스에서 Swift export 지원을 확장하고 점진적으로 안정화할 계획이에요. Kotlin 2.2.20 이후에는 특히 coroutines와 flows를 중심으로 Kotlin과 Swift 사이의 상호 운용성 개선에 집중할 거예요.

Swift export 지원은 Kotlin Multiplatform에 큰 변화예요. 여러분의 피드백을 기다리고 있어요:

  • Kotlin Slack에서 개발팀에 직접 연락 – 초대 받기#swift-export 채널에 참여하세요.
  • Swift export에서 겪는 문제를 YouTrack에 보고해 주세요.

js와 wasmJs target을 위한 공유 소스 세트

이전에는 Kotlin Multiplatform에 JavaScript(js)와 WebAssembly(wasmJs) 웹 target을 위한 공유 소스 세트가 기본으로 포함되지 않았어요. jswasmJs 사이에서 코드를 공유하려면 커스텀 소스 세트를 수동으로 구성하거나, js용과 wasmJs용 버전을 따로 두 곳에 코드를 작성해야 했어요. 예를 들면:

// commonMain
expect suspend fun readCopiedText(): String

// jsMain
external interface Navigator { val clipboard: Clipboard }
// Different interop in JS and Wasm
external interface Clipboard { fun readText(): Promise<String> }
external val navigator: Navigator

suspend fun readCopiedText(): String {
    // Different interop in JS and Wasm
    return navigator.clipboard.readText().await()
}

// wasmJsMain
external interface Navigator { val clipboard: Clipboard }
external interface Clipboard { fun readText(): Promise<JsString> }
external val navigator: Navigator

suspend fun readCopiedText(): String {
    return navigator.clipboard.readText().await().toString()
}

이번 릴리스부터 기본 계층 템플릿을 사용하면 Kotlin Gradle 플러그인이 웹용 새 공유 소스 세트(webMainwebTest로 구성)를 추가해요.

이 변경으로 web 소스 세트가 jswasmJs 소스 세트 양쪽의 부모가 돼요. 업데이트된 소스 세트 계층은 다음과 같아요.

새 소스 세트 덕분에 jswasmJs target 둘 다를 위한 코드 한 벌을 작성할 수 있어요. 공유 코드를 webMain에 두면 양쪽 모두에서 자동으로 작동해요:

// commonMain
expect suspend fun readCopiedText(): String

// webMain
@OptIn(ExperimentalWasmJsInterop::class)
private suspend fun <R : JsAny?> Promise<R>.await(): R = suspendCancellableCoroutine { continuation ->
    this.then(
        onFulfilled = { continuation.resumeWith(Result.success(it)); null },
        onRejected = { continuation.resumeWithException(it.asJsException()); null }
    )
}

external interface Navigator { val clipboard: Clipboard }
external interface Clipboard { fun readText(): Promise<JsString> }
external val navigator: Navigator

actual suspend fun readCopiedText(): String {
    return navigator.clipboard.readText().await().toString()
}

이 업데이트는 jswasmJs target 사이의 코드 공유를 단순화해요. 특히 두 경우에 유용해요:

  • 라이브러리 작성자이고 코드를 중복하지 않고 jswasmJs target을 모두 지원하고 싶을 때.
  • 웹을 대상으로 하는 Compose Multiplatform 애플리케이션을 개발하면서 더 넓은 브라우저 호환성을 위해 jswasmJs target 양쪽의 크로스 컴파일을 활성화할 때. 이 폴백 모드 덕분에 웹사이트를 만들면 모든 브라우저에서 바로 작동해요. 최신 브라우저는 wasmJs를, 오래된 브라우저는 js를 사용하기 때문이에요.

이 기능을 시험해 보려면 build.gradle(.kts) 파일의 kotlin {} 블록에서 기본 계층 템플릿을 사용하세요:

kotlin {
    js()
    wasmJs()

    // Enables the default source set hierarchy, including webMain and webTest
    applyDefaultHierarchyTemplate()
}

기본 계층을 사용하기 전에, 커스텀 공유 소스 세트가 있거나 js("web") target의 이름을 바꾼 프로젝트의 잠재적 충돌을 신중히 고려하세요. 충돌을 해결하려면 충돌하는 소스 세트나 target의 이름을 바꾸거나, 기본 계층을 사용하지 마세요.

Kotlin 라이브러리의 안정적인 크로스 플랫폼 컴파일

Kotlin 2.2.20은 중요한 로드맵 항목을 완료해 Kotlin 라이브러리의 크로스 플랫폼 컴파일을 안정화했어요.

이제 지원되는 host를 사용해 Kotlin 라이브러리 게시용 .klib 산출물을 만들 수 있어요. 이는 특히 이전에 Mac 머신이 필요했던 Apple target의 게시 과정을 크게 간소화해요.

이 기능은 기본으로 제공돼요. kotlin.native.enableKlibsCrossCompilation=true로 이미 크로스 컴파일을 활성화했다면 gradle.properties 파일에서 제거할 수 있어요.

아쉽게도 몇 가지 제한이 아직 남아 있어요. 다음 경우에는 여전히 Mac 머신이 필요해요:

multiplatform 라이브러리 게시에 대한 자세한 내용은 문서를 참고하세요.

공통 의존성 선언의 새로운 방식

Gradle로 multiplatform 프로젝트 설정을 단순화하기 위해, Kotlin 2.2.20은 이제 프로젝트가 Gradle 8.8 이상을 사용할 때 kotlin {} 블록 안에서 최상위 dependencies {} 블록을 사용해 공통 의존성을 선언할 수 있게 해줘요. 이 의존성은 commonMain 소스 세트에 선언된 것처럼 동작해요. 이 기능은 Kotlin/JVM과 Android 전용 프로젝트에 사용하는 dependencies 블록과 비슷하며, Kotlin Multiplatform에서는 이제 Experimental이 되었어요.

프로젝트 수준에서 공통 의존성을 선언하면 소스 세트 전체에서 반복적인 구성을 줄이고 빌드 설정을 간소화해 줘요. 필요에 따라 각 소스 세트에 플랫폼별 의존성을 계속 추가할 수 있어요.

이 기능을 시험해 보려면 최상위 dependencies {} 블록 앞에 @OptIn(ExperimentalKotlinGradlePluginApi::class) 애노테이션을 추가해 옵트인하세요. 예를 들면:

kotlin {
    @OptIn(ExperimentalKotlinGradlePluginApi::class)
    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
    }
}

이 기능에 대한 피드백은 YouTrack에 남겨 주시면 감사하겠어요.

의존성의 target 지원을 위한 새 진단

Kotlin 2.2.20 이전에는 빌드 스크립트의 의존성이 소스 세트가 요구하는 모든 target을 지원하지 않으면, Gradle이 생성하는 오류 메시지로 문제를 이해하기 어려웠어요.

Kotlin 2.2.20은 각 의존성이 어떤 target을 지원하고 어떤 target을 지원하지 않는지를 명확히 보여주는 새 진단을 도입했어요.

이 진단은 기본으로 활성화돼요. 어떤 이유로 비활성화해야 한다면 이 YouTrack 이슈에 의견을 남겨 주세요. gradle.properties 파일에서 다음 Gradle 프로퍼티를 사용해 진단을 비활성화할 수 있어요:

Property Description
kotlin.kmp.eagerUnresolvedDependenciesDiagnostic=false metadata 컴파일과 import에서만 진단을 실행해요.
kotlin.kmp.unresolvedDependenciesDiagnostic=false 진단을 완전히 비활성화해요.

Kotlin/Native

이번 릴리스는 Xcode 26 지원, Objective-C/Swift와의 상호 운용성 개선, 디버깅, 새 바이너리 옵션을 가져와요.

Xcode 26 지원

Kotlin 2.2.21부터 Kotlin/Native 컴파일러가 최신 안정 버전인 Xcode 26을 지원해요. 이제 Xcode를 업데이트하고 최신 API에 접근해 Apple 운영체제용 Kotlin 프로젝트를 계속 작업할 수 있어요.

바이너리의 stack canaries 지원

Kotlin 2.2.20부터 Kotlin은 결과 Kotlin/Native 바이너리에 stack canaries 지원을 추가해요. 스택 보호의 일부로, 이 보안 기능은 스택 스매싱으로부터 보호해 일반적인 애플리케이션 취약점을 완화해요. Swift와 Objective-C에서 이미 사용 가능했으며 이제 Kotlin에서도 지원돼요.

Kotlin/Native의 스택 보호 구현은 Clang의 스택 보호기 동작을 따르는 방식으로 동작해요.

stack canaries를 활성화하려면 gradle.properties 파일에 다음 바이너리 옵션을 추가하세요:

kotlin.native.binary.stackProtector=yes

이 프로퍼티는 스택 스매싱에 취약한 모든 Kotlin 함수에 대해 기능을 활성화해요. 대체 모드는 다음과 같아요:

  • kotlin.native.binary.stackProtector=strong, 스택 스매싱에 취약한 함수에 더 강한 휴리스틱을 사용해요.
  • kotlin.native.binary.stackProtector=all, 모든 함수에 stack protectors를 활성화해요.

어떤 경우에는 스택 보호가 성능 비용을 동반할 수 있다는 점에 유의하세요.

릴리스 바이너리의 크기 축소

Kotlin 2.2.20은 릴리스 바이너리의 크기를 줄이는 데 도움을 줄 수 있는 smallBinary 옵션을 도입했어요. 새 옵션은 LLVM 컴파일 단계에서 컴파일러의 기본 최적화 인수로 -Oz를 효과적으로 설정해요.

smallBinary 옵션을 활성화하면 릴리스 바이너리를 더 작게 만들고 빌드 시간을 개선할 수 있어요. 다만 어떤 경우에는 런타임 성능에 영향을 줄 수 있어요.

새 기능은 현재 Experimental 단계예요. 프로젝트에서 시험해 보려면 gradle.properties 파일에 다음 바이너리 옵션을 추가하세요:

kotlin.native.binary.smallBinary=true

Kotlin 팀은 이 기능 구현을 도와준 Troels Lund 님께 감사를 전해요.

개선된 디버거 객체 요약

Kotlin/Native는 이제 LLDB와 GDB 같은 디버거 도구에 더 명확한 객체 요약을 생성해요. 이를 통해 생성되는 디버그 정보의 가독성이 개선되고 디버깅 경험이 단순해져요.

예를 들어 다음 객체를 생각해 보세요:

class Point(val x: Int, val y: Int)
val point = Point(1, 2)

이전에는 검사 시 객체의 메모리 주소 포인터를 포함해 제한된 정보만 보여줬어요:

(lldb) v point
(ObjHeader *) point = [x: ..., y: ...]
(lldb) v point->x
(int32_t *) x = 0x0000000100274048

Kotlin 2.2.20에서는 디버거가 실제 값을 포함해 더 풍부한 세부 정보를 보여줘요:

(lldb) v point
(ObjHeader *) point = Point(x=1, y=2)
(lldb) v point->x
(int32_t) point->x = 1

Kotlin 팀은 이 기능 구현을 도와준 Nikita Nazarov 님께 감사를 전해요.

Kotlin/Native 디버깅에 대한 자세한 내용은 문서를 참고하세요.

Objective-C 헤더의 block 타입에서 명시적 이름

Kotlin 2.2.20은 Kotlin/Native 프로젝트에서 내보낸 Objective-C 헤더의 Kotlin 함수 타입에 명시적 파라미터 이름을 추가하는 옵션을 도입했어요. 파라미터 이름은 Xcode의 자동 완성 제안을 개선하고 Clang 경고를 피하는 데 도움이 돼요.

이전에는 block 타입에서 파라미터 이름이 생성된 Objective-C 헤더에 생략되었어요. 이런 경우 Xcode의 자동 완성은 Objective-C block에서 파라미터 이름 없이 이러한 함수를 호출하도록 제안했어요. 생성된 block은 Clang 경고를 트리거했어요.

예를 들어 다음 Kotlin 코드에 대해:

// Kotlin:
fun greetUser(block: (name: String) -> Unit) = block("John")

생성된 Objective-C 헤더에는 파라미터 이름이 없었어요:

// Objective-C:
+ (void)greetUserBlock:(void (^)(NSString *))block __attribute__((swift_name("greetUser(block:)")));

그래서 Xcode에서 Objective-C의 greetUserBlock() 함수를 호출할 때 IDE가 다음을 제안했어요:

// Objective-C:
greetUserBlock:^(NSString *) {
    // ...
};

제안에서 파라미터 이름 (NSString *)가 없어서 Clang 경고가 발생했어요.

새 옵션을 사용하면 Kotlin이 파라미터 이름을 Kotlin 함수 타입에서 Objective-C block 타입으로 전달하므로 Xcode가 제안에 이를 사용해요:

// Objective-C:
greetUserBlock:^(NSString *name) {
    // ...
};

명시적 파라미터 이름을 활성화하려면 gradle.properties 파일에 다음 바이너리 옵션을 추가하세요:

kotlin.native.binary.objcExportBlockExplicitParameterNames=true

Kotlin 팀은 이 기능을 구현한 Yijie Jiang 님께 감사를 전해요.

Kotlin/Native 배포판 크기 축소

Kotlin/Native 배포판에는 컴파일러 코드가 담긴 JAR 파일 두 개가 포함되어 있었어요:

  • konan/lib/kotlin-native.jar
  • konan/lib/kotlin-native-compiler-embeddable.jar.

Kotlin 2.2.20부터 kotlin-native.jar는 더 이상 게시되지 않아요.

제거된 JAR 파일은 더 이상 필요하지 않은 embeddable 컴파일러의 레거시 버전이에요. 이 변경은 배포판 크기를 크게 줄여 줘요.

결과적으로 다음 옵션이 이제 deprecate되고 제거됐어요:

  • kotlin.native.useEmbeddableCompilerJar=false Gradle 프로퍼티. 대신 Kotlin/Native 프로젝트에는 항상 embeddable 컴파일러 JAR 파일이 사용돼요.
  • KotlinCompilerPluginSupportPlugin.getPluginArtifactForNative() 함수. 대신 getPluginArtifact() 함수가 항상 사용돼요.

자세한 내용은 YouTrack 이슈를 참고하세요.

KDoc을 Objective-C 헤더로 기본 내보내기

Kotlin/Native 최종 바이너리 컴파일 중 Objective-C 헤더를 생성할 때 KDoc 주석을 이제 기본으로 내보내요.

이전에는 빌드 파일에 -Xexport-kdoc 옵션을 수동으로 추가해야 했어요. 이제 자동으로 컴파일 작업에 전달돼요.

이 옵션은 KDoc 주석을 klib에 포함시키고 Apple 프레임워크를 만들 때 klib에서 주석을 추출해요. 그 결과 클래스와 메서드의 주석이 예를 들어 Xcode의 자동 완성 중에 나타나요.

KDoc 주석을 klib에서 생성된 Apple 프레임워크로 내보내는 것을 build.gradle(.kts) 파일의 binaries {} 블록에서 비활성화할 수 있어요:

import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi

kotlin {
    iosArm64 {
        binaries {
            framework {
                baseName = "sdk"
                @OptIn(ExperimentalKotlinGradlePluginApi::class)
                exportKdoc.set(false)
            }
        }
    }
}

자세한 내용은 우리 문서를 참고하세요.

x86_64 Apple target의 deprecation

Apple은 몇 년 전에 Intel 칩이 탑재된 기기 생산을 중단했고, 최근 macOS Tahoe 26이 Intel 기반 아키텍처를 지원하는 마지막 OS 버전이 될 것이라고 발표했어요.

이 때문에 특히 macOS 26에 포함되는 지원 Xcode 버전을 업데이트할 향후 Kotlin 릴리스에서 이러한 target을 빌드 에이전트에서 제대로 테스트하는 것이 점점 어려워지고 있어요.

Kotlin 2.2.20부터 macosX64iosX64 target은 지원 등급 2(tier 2)로 강등돼요. 즉 해당 target은 컴파일을 보장하기 위해 CI에서 정기적으로 테스트되지만, 실행을 보장하기 위해 자동 테스트되지는 않을 수 있어요.

우리는 모든 x86_64 Apple target을 점진적으로 deprecate하고 Kotlin 2.2.20~2.4.0 릴리스 주기 동안 지원을 제거할 계획이에요. 여기에는 다음 target이 포함돼요:

  • macosX64
  • iosX64
  • tvosX64
  • watchosX64

지원 등급에 대한 자세한 내용은 Kotlin/Native target 지원을 참고하세요.

Kotlin/Wasm

Kotlin/Wasm이 이제 Beta 단계예요. 더 큰 안정성과 함께 분리된 npm 의존성, JavaScript interop을 위한 개선된 예외 처리, 별도 설정 없는 브라우저 디버깅 지원 등의 개선을 제공해요.

분리된 npm 의존성

이전에는 Kotlin/Wasm 프로젝트에서 모든 npm 의존성이 프로젝트 폴더에 함께 설치되었어요. Kotlin 도구 의존성과 여러분의 의존성 둘 다 포함이죠. 또한 프로젝트의 lock 파일(package-lock.json 또는 yarn.lock)에도 함께 기록됐어요.

그 결과 Kotlin 도구 의존성이 업데이트될 때마다, 아무것도 추가하거나 변경하지 않았어도 lock 파일을 업데이트해야 했어요.

Kotlin 2.2.20부터 Kotlin 도구 npm 의존성이 프로젝트 밖에 설치돼요. 이제 도구와 여러분(사용자) 의존성이 별도의 디렉터리를 가져요:

  • 도구 의존성 디렉터리:

<kotlin-user-home>/kotlin-npm-tooling/<yarn|npm>/hash/node_modules

  • 사용자 의존성 디렉터리:

build/wasm/node_modules

또한 프로젝트 디렉터리 안의 lock 파일에는 사용자 정의 의존성만 포함돼요.

이 개선은 lock 파일이 여러분의 의존성에만 집중하도록 유지하고, 더 깔끔한 프로젝트를 유지하며, 파일의 불필요한 변경을 줄여 줘요.

이 변경은 wasm-js target에 대해 기본으로 활성화돼요. 이 변경은 아직 js target에는 구현되지 않았어요. 향후 릴리스에서 구현할 계획이 있지만, Kotlin 2.2.20에서 js target의 npm 의존성 동작은 이전과 동일해요.

Kotlin/Wasm과 JavaScript interop에서 개선된 예외 처리

이전에는 Kotlin이 JavaScript(JS)에서 발생해 Kotlin/Wasm 코드로 넘어오는 예외(오류)를 이해하는 데 어려움이 있었어요.

어떤 경우에는 예외가 Wasm 코드에서 JS로 전달·통과되면서 세부 정보 없이 WebAssembly.Exception으로 감싸지는 반대 방향에서도 문제가 발생했어요. 이러한 Kotlin 예외 처리 문제는 디버깅을 어렵게 만들었어요.

Kotlin 2.2.20부터 예외 관련 개발자 경험이 양방향 모두 개선돼요:

  • JS에서 예외가 발생하면 Kotlin 쪽에서 더 많은 정보를 볼 수 있어요. 그러한 예외가 Kotlin을 통해 JS로 다시 전파될 때 더 이상 WebAssembly로 감싸지지 않아요.
  • Kotlin에서 예외가 발생하면 이제 JS 쪽에서 JS 오류로 잡을 수 있어요.

새 예외 처리는 WebAssembly.JSTag 기능을 지원하는 최신 브라우저에서 자동으로 작동해요:

  • Chrome 115+
  • Firefox 129+
  • Safari 18.4+

이전 브라우저에서는 예외 처리 동작이 변경되지 않아요.

별도 설정 없는 브라우저 디버깅 지원

이전에는 브라우저가 디버깅에 필요한 Kotlin/Wasm 프로젝트 소스에 자동으로 접근할 수 없었어요. 브라우저에서 Kotlin/Wasm 애플리케이션을 디버깅하려면 build.gradle(.kts) 파일에 다음 스니펫을 추가해 이러한 소스를 제공하도록 빌드를 수동으로 구성해야 했어요:

devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
    static = (static ?: mutableListOf()).apply {
        add(project.rootDir.path)
    }
}

Kotlin 2.2.20부터 최신 브라우저에서 애플리케이션 디버깅이 곧바로 작동해요. Gradle 개발 task(*DevRun)를 실행하면 Kotlin이 소스 파일을 브라우저에 자동으로 제공해서, 추가 설정 없이 중단점 설정, 변수 검사, Kotlin 코드 단계별 실행을 할 수 있어요.

이 변경은 수동 구성의 필요성을 제거해 디버깅을 단순화해요. 필요한 구성이 이제 Kotlin Gradle 플러그인에 포함되어 있어요. 이전에 build.gradle(.kts) 파일에 이 구성을 추가했다면 충돌을 피하기 위해 제거해야 해요.

브라우저 디버깅은 모든 Gradle *DevRun task에 대해 기본으로 활성화돼요. 이 task들은 애플리케이션뿐 아니라 소스 파일도 제공하므로 로컬 개발에만 사용하고, 소스가 공개적으로 노출될 클라우드나 프로덕션 환경에서는 실행하지 마세요.

디버깅 중 반복 리로드 처리

소스를 기본으로 제공하면 Kotlin 컴파일과 번들링이 완료되기 전에 브라우저에서 애플리케이션이 반복 리로드될 수 있어요. 해결 방법으로, webpack 구성을 조정해 Kotlin 소스 파일을 무시하고 제공되는 정적 파일 감시를 비활성화하세요. 프로젝트 루트의 webpack.config.d 디렉터리에 다음 내용의 .js 파일을 추가하세요:

config.watchOptions = config.watchOptions || {
    ignored: ["**/*.kt", "**/node_modules"]
}

if (config.devServer) {
    config.devServer.static = config.devServer.static.map(file => {
        if (typeof file === "string") {
        return { directory: file,
                 watch: false,
        }
    } else {
        return file
    }
    })
}

빈 yarn.lock 파일 제거

이전에는 Kotlin Gradle 플러그인(KGP)이 Kotlin 도구 체인에 필요한 npm 패키지 정보와 프로젝트나 사용된 라이브러리의 기존 npm 의존성을 포함하는 yarn.lock 파일을 자동으로 생성했어요.

이제 KGP는 도구 체인 의존성을 별도로 관리하며, 프로젝트에 npm 의존성이 없으면 프로젝트 수준의 yarn.lock 파일이 더 이상 생성되지 않아요.

KGP는 npm 의존성이 추가되면 yarn.lock 파일을 자동으로 만들고, npm 의존성이 제거되면 yarn.lock 파일을 삭제해요.

이 변경은 프로젝트 구조를 정리하고 실제 npm 의존성이 언제 도입되는지 추적하기 쉽게 만들어줘요.

이 동작을 구성하는 데 추가 단계가 필요 없어요. Kotlin 2.2.20부터 Kotlin/Wasm 프로젝트에서 기본으로 적용돼요.

정규화된 클래스 이름의 새 컴파일러 오류

Kotlin/Wasm에서 컴파일러는 기본적으로 생성된 바이너리에 클래스의 정규화된 이름(FQN)을 저장하지 않아요. 이 방식은 애플리케이션 크기가 커지는 것을 피하기 위한 것이에요.

그 결과 이전 Kotlin 릴리스에서는 KClass::qualifiedName 프로퍼티를 호출하면 클래스의 정규화된 이름 대신 빈 문자열을 반환했어요.

Kotlin 2.2.20부터 정규화된 이름 기능을 명시적으로 활성화하지 않는 한 Kotlin/Wasm 프로젝트에서 KClass::qualifiedName 프로퍼티를 사용하면 컴파일러가 오류를 보고해요.

이 변경은 qualifiedName 프로퍼티 호출 시 예상치 못한 빈 문자열을 방지하고, 컴파일 시점에 문제를 잡아 개발자 경험을 개선해요.

진단은 기본으로 활성화되며 오류가 자동으로 보고돼요. 진단을 비활성화하고 Kotlin/Wasm에서 FQN 저장을 허용하려면 build.gradle(.kts) 파일에 다음 옵션을 추가해 모든 클래스에 정규화된 이름을 저장하도록 컴파일러에 지시하세요:

kotlin {
    wasmJs {
        ...
        compilerOptions {
            freeCompilerArgs.add("-Xwasm-kclass-fqn")
        }
    }
}

이 옵션을 활성화하면 애플리케이션 크기가 커진다는 점을 기억하세요.

Kotlin/JS

Kotlin 2.2.20은 Kotlin의 Long 타입을 나타내는 데 BigInt 타입을 사용하는 것을 지원해서 내보낸 선언에서 Long을 사용할 수 있게 해줘요. 또한 이번 릴리스는 Node.js 인수를 정리하는 DSL 함수를 추가해요.

Kotlin의 Long 타입을 나타내는 BigInt 타입 사용

ES2020 표준 이전에는 JavaScript(JS)가 53비트보다 큰 정밀한 정수를 위한 기본 타입을 지원하지 않았어요.

이 때문에 Kotlin/JS는 Long 값(64비트 폭)을 두 개의 number 프로퍼티를 포함하는 JavaScript 객체로 표현했어요. 이 커스텀 구현은 Kotlin과 JavaScript 사이의 상호 운용성을 더 복잡하게 만들었어요.

Kotlin 2.2.20부터 Kotlin/JS는 최신 JavaScript(ES2020)로 컴파일할 때 Kotlin의 Long 값을 나타내기 위해 JavaScript의 내장 BigInt 타입을 사용해요.

이 변경은 Kotlin 2.2.20에서 함께 도입된 Long 타입을 JavaScript로 내보내기를 가능하게 해줘요. 그 결과 Kotlin과 JavaScript 사이의 상호 운용성이 단순화돼요.

활성화하려면 build.gradle(.kts) 파일에 다음 컴파일러 옵션을 추가해야 해요:

kotlin {
    js {
        ...
        compilerOptions {
            freeCompilerArgs.add("-Xes-long-as-bigint")
        }
    }
}

이 기능은 Experimental 단계예요. 이슈 트래커 YouTrack에 피드백을 남겨 주시면 감사하겠어요.

내보낸 선언에서 Long 사용

Kotlin/JS가 커스텀 Long 표현을 사용했기 때문에 JavaScript에서 Kotlin의 Long과 상호 작용하는 간단한 방법을 제공하기 어려웠어요. 그 결과 Long 타입을 사용하는 Kotlin 코드를 JavaScript로 내보낼 수 없었어요. 이 문제는 함수 파라미터, 클래스 프로퍼티, 생성자처럼 Long을 사용하는 모든 코드에 영향을 줬어요.

이제 Kotlin의 Long 타입을 JavaScript의 BigInt 타입으로 컴파일할 수 있으므로, Kotlin/JS는 Long 값을 JavaScript로 내보내는 것을 지원해 Kotlin과 JavaScript 코드 사이의 상호 운용성을 단순화해요.

이 기능을 활성화하려면:

  • build.gradle(.kts) 파일의 freeCompilerArgs 속성에 다음 컴파일러 옵션을 추가해 Kotlin/JS에서 Long 내보내기를 허용하세요:
kotlin {
    js {
        ...
        compilerOptions {
            freeCompilerArgs.add("-XXLanguage:+JsAllowLongInExportedDeclarations")
        }
    }
}

더 깔끔한 인수를 위한 새 DSL 함수

Node.js로 Kotlin/JS 애플리케이션을 실행할 때 프로그램에 전달되는 인수(args)에는 다음이 포함되곤 했어요:

  • 실행 파일 Node의 경로.
  • 스크립트의 경로.
  • 직접 제공한 실제 커맨드 라인 인수.

하지만 args에 대한 기대 동작은 커맨드 라인 인수만 포함하는 것이었어요. 이를 달성하려면 build.gradle(.kts) 파일이나 Kotlin 코드에서 drop() 함수로 처음 두 인수를 수동으로 건너뛰어야 했어요:

fun main(args: Array<String>) {
    println(args.drop(2).joinToString(", "))
}

이 해결 방법은 반복적이고 오류가 발생하기 쉬우며 플랫폼 간 코드 공유 시 잘 작동하지 않았어요.

이 문제를 해결하기 위해 Kotlin 2.2.20은 passCliArgumentsToMainFunction()이라는 새 DSL 함수를 도입했어요.

이 함수를 사용하면 커맨드 라인 인수만 포함되고 Node와 스크립트 경로는 제외돼요:

fun main(args: Array<String>) {
    // No need for drop() and only your custom arguments are included
    println(args.joinToString(", "))
}

이 변경은 보일러플레이트 코드를 줄이고, 수동 인수 제거로 인한 실수를 방지하며, 크로스 플랫폼 호환성을 개선해요.

이 기능을 활성화하려면 build.gradle(.kts) 파일에 다음 DSL 함수를 추가하세요:

kotlin {
    js {
        nodejs {
            passCliArgumentsToMainFunction()
        }
    }
}

Gradle

Kotlin 2.2.20은 Gradle 빌드 보고서에서 Kotlin/Native task에 대한 새 컴파일러 성능 지표를 추가하고, 증분 컴파일을 질적 개선해요.

Kotlin/Native task의 빌드 보고서 새 컴파일러 성능 지표

Kotlin 1.7.0에서 컴파일러 성능을 추적하는 데 도움을 주는 빌드 보고서를 도입했어요. 이후 성능 문제를 조사하는 데 이 보고서를 더 상세하고 유용하게 만들기 위해 지표를 계속 추가하고 있어요.

Kotlin 2.2.20에서는 빌드 보고서에 Kotlin/Native task에 대한 컴파일러 성능 지표가 이제 포함돼요.

빌드 보고서와 구성 방법에 대해 더 알아보려면 빌드 보고서 활성화를 참고하세요.

Kotlin/JVM에 대한 개선된 증분 컴파일 preview

Kotlin 2.0.0은 최적화된 프론트엔드가 있는 새 K2 컴파일러를 도입했어요. Kotlin 2.2.20은 이에 더해 새 프론트엔드를 사용해 Kotlin/JVM의 특정 복잡한 증분 컴파일 시나리오에서 성능을 개선해요.

이 개선들은 동작을 안정화하는 동안 기본으로 비활성화돼 있어요. 활성화하려면 gradle.properties 파일에 다음 프로퍼티를 추가하세요:

kotlin.incremental.jvm.fir=true

현재 kapt 컴파일러 플러그인은 이 새 동작과 호환되지 않아요. 향후 Kotlin 릴리스에서 지원을 추가하는 작업 중이에요.

이 기능에 대한 피드백은 YouTrack에 남겨 주시면 감사하겠어요.

증분 컴파일이 inline 함수의 람다 변경을 감지

Kotlin 2.2.20 이전에는 증분 컴파일을 활성화하고 inline 함수 안의 람다에 있는 로직을 변경해도, 컴파일러가 다른 모듈의 해당 inline 함수 호출 지점을 다시 컴파일하지 않았어요. 그 결과 해당 호출 지점에서 이전 버전의 람다를 사용해 예상치 못한 동작이 발생할 수 있었어요.

Kotlin 2.2.20에서 컴파일러는 이제 inline 함수의 람다 변경을 감지하고 해당 호출 지점을 자동으로 다시 컴파일해요.

라이브러리 게시 개선

Kotlin 2.2.20은 라이브러리 게시를 더 쉽게 만드는 새 Gradle task를 추가해요. 이러한 task는 키 쌍 생성, 공개 키 업로드, 그리고 Maven Central 리포지토리에 업로드하기 전에 검증 과정이 성공하는지 확인하는 로컬 검사를 돕습니다.

게시 과정의 일부로 이러한 task를 사용하는 방법에 대한 자세한 내용은 라이브러리를 Maven Central에 게시를 참고하세요.

PGP 키 생성·업로드를 위한 새 Gradle task

Kotlin 2.2.20 이전에는 multiplatform 라이브러리를 Maven Central 리포지토리에 게시하려면 게시물 서명용 키 쌍을 생성하기 위해 gpg 같은 타사 프로그램을 설치해야 했어요. 이제 Kotlin Gradle 플러그인에 키 쌍을 생성하고 공개 키를 업로드할 수 있는 Gradle task가 함께 제공되어 다른 프로그램을 설치할 필요가 없어요.

키 쌍 생성

generatePgpKeys task는 키 쌍을 생성해요. 실행할 때 개인 키 저장소의 비밀번호와 이름을 다음 형식으로 제공해야 해요:

./gradlew -Psigning.password=example-password generatePgpKeys --name "John Smith <[email protected]>"

이 task는 키 쌍을 build/pgp 디렉터리에 저장해요.

실수로 삭제되거나 무단 접근을 방지하려면 키 쌍을 안전한 위치로 옮기세요.

공개 키 업로드

uploadPublicPgpKey task는 공개 키를 Ubuntu의 키 서버인 keyserver.ubuntu.com에 업로드해요. 실행할 때 .asc 형식의 공개 키 경로를 제공하세요:

./gradlew uploadPublicPgpKey --keyring /path_to/build/pgp/public_KEY_ID.asc

검증을 로컬에서 테스트하는 새 Gradle task

Kotlin 2.2.20은 라이브러리를 Maven Central 리포지토리에 업로드하기 전에 검증을 로컬에서 테스트하는 Gradle task도 추가해요.

Kotlin Gradle 플러그인을 Gradle의 Signing PluginMaven Publish Plugin과 함께 사용한다면, checkSigningConfigurationcheckPomFileFor<PUBLICATION_NAME>Publication task를 실행해 설정이 Maven Central의 요구 사항을 충족하는지 확인할 수 있어요. <PUBLICATION_NAME>을 게시물 이름으로 바꾸세요.

이 task들은 buildcheck Gradle task의 일부로 자동 실행되지 않으므로 수동으로 실행해야 해요. 예를 들어 KotlinMultiplatform 게시물이 있다면:

./gradlew checkSigningConfiguration checkPomFileForKotlinMultiplatformPublication

checkSigningConfiguration task는 다음을 확인해요:

  • Signing Plugin에 키가 구성되어 있는지.
  • 구성된 공개 키가 keyserver.ubuntu.com 또는 keys.openpgp.org 키 서버에 업로드되었는지.
  • 모든 게시물에 서명이 활성화되어 있는지.

이 검사 중 하나라도 실패하면 task는 문제 해결 방법 정보와 함께 오류를 반환해요.

checkPomFileFor<PUBLICATION_NAME>Publication task는 pom.xml 파일이 Maven Central의 요구 사항을 충족하는지 확인해요. 충족하지 않으면 task는 pom.xml 파일의 어느 부분이 비준수인지에 대한 세부 정보와 함께 오류를 반환해요.

Maven: kotlin-maven-plugin의 Kotlin daemon 지원

Kotlin 2.2.20은 Kotlin 2.2.0에 도입된 빌드 도구 API를 한 단계 더 발전시켜 kotlin-maven-plugin에서 Kotlin daemon 지원을 추가했어요. Kotlin daemon을 사용하면 Kotlin 컴파일러가 별도의 격리된 프로세스에서 실행되어 다른 Maven 플러그인이 시스템 프로퍼티를 재정의하는 것을 방지해요. 예시는 이 YouTrack 이슈에서 볼 수 있어요.

Kotlin 2.2.20부터 Kotlin daemon이 기본으로 사용돼요. 이전 동작으로 되돌리려면 pom.xml 파일에서 다음 프로퍼티를 false로 설정해 옵트아웃하세요:

<properties>
    <kotlin.compiler.daemon>false</kotlin.compiler.daemon>
</properties>

Kotlin 2.2.20은 Kotlin daemon의 기본 JVM 인수를 커스터마이징하는 데 사용할 수 있는 새 jvmArgs 프로퍼티도 도입했어요. 예를 들어 -Xmx-Xms 옵션을 재정의하려면 pom.xml 파일에 다음을 추가하세요:

<properties>
    <kotlin.compiler.daemon.jvmArgs>Xmx1500m,Xms500m</kotlin.compiler.daemon.jvmArgs>
</properties>

Kotlin 컴파일러 옵션의 새 공통 스키마

Kotlin 2.2.20은 org.jetbrains.kotlin:kotlin-compiler-arguments-description 아래에 게시된 모든 컴파일러 옵션에 대한 공통 스키마를 도입했어요. 이 아티팩트에는 모든 컴파일러 옵션, 설명, 그리고 각 옵션이 도입되거나 안정화된 버전 같은 메타데이터의 코드 표현과 JSON 버전(비 JVM 소비자용)이 모두 포함돼 있어요. 이 스키마를 사용해 옵션의 커스텀 뷰를 생성하거나 필요에 따라 분석할 수 있어요.

표준 라이브러리

이번 릴리스는 표준 라이브러리에 새 실험적 기능들을 도입해요. Kotlin/JS에서 인터페이스 타입을 식별하는 reflection 지원, 공통 atomic 타입을 위한 update 함수, 배열 크기 조정을 위한 copyOf() 오버로드가 그 예시예요.

Kotlin/JS에서 reflection으로 인터페이스 타입 식별 지원

Kotlin 2.2.20은 Experimental KClass.isInterface 프로퍼티를 Kotlin/JS 표준 라이브러리에 추가했어요.

이 프로퍼티를 사용하면 클래스 레퍼런스가 Kotlin 인터페이스를 나타내는지 이제 확인할 수 있어요. 이는 KClass.java.isInterface로 클래스가 인터페이스를 나타내는지 확인할 수 있는 Kotlin/JVM과 동일한 수준으로 Kotlin/JS를 끌어올려 줘요.

옵트인하려면 @OptIn(ExperimentalStdlibApi::class) 애노테이션을 사용하세요:

@OptIn(ExperimentalStdlibApi::class)
fun inspect(klass: KClass<*>) {
    // Prints true for interfaces
    println(klass.isInterface)
}

이슈 트래커 YouTrack에 피드백을 남겨 주시면 감사하겠어요.

공통 atomic 타입의 새 update 함수

Kotlin 2.2.20은 공통 atomic 타입과 그 배열 대응 요소를 업데이트하는 새 실험적 함수를 도입해요. 각 함수는 이러한 update 함수 중 하나를 사용해 새 값을 원자적으로 계산하고 현재 값을 대체하며, 반환 값은 어떤 함수를 사용하는지에 따라 달라져요:

이 함수들을 사용해 곱셈이나 비트 연산처럼 기본으로 지원되지 않는 원자적 변환을 구현할 수 있어요. 이 변경 이전에는 공통 atomic 타입을 증가시키고 이전 값을 읽으려면 compareAndSet() 함수와 함께 루프가 필요했어요.

공통 atomic 타입의 모든 API처럼 이 함수들도 Experimental 단계예요. 옵트인하려면 @OptIn(ExperimentalAtomicApi::class) 애노테이션을 사용하세요.

다른 종류의 업데이트를 수행하고 이전 값 또는 업데이트된 값을 반환하는 코드 예시를 볼게요:

import kotlin.concurrent.atomics.*
import kotlin.random.Random

@OptIn(ExperimentalAtomicApi::class)
fun main() {
    val counter = AtomicLong(Random.nextLong())
    val minSetBitsThreshold = 20

    // Sets a new value without using the result
    counter.update { if (it < 0xDECAF) 0xCACA0 else 0xC0FFEE }

    // Retrieves the current value, then updates it
    val previousValue = counter.fetchAndUpdate { 0x1CEDL.shl(Long.SIZE_BITS - it.countLeadingZeroBits()) or it }

    // Updates the value, then retrieves the result
    val current = counter.updateAndFetch {
        if (it.countOneBits() < minSetBitsThreshold) it.shl(20) or 0x15BADL else it
    }

    val hexFormat = HexFormat {
        upperCase = true
        number {
            removeLeadingZeros = true
        }
    }
    println("Previous value: ${previousValue.toHexString(hexFormat)}")
    println("Current value: ${current.toHexString(hexFormat)}")
    println("Expected status flag set: ${current and 0xBAD != 0xBADL}")
}

이슈 트래커 YouTrack에 피드백을 남겨 주시면 감사하겠어요.

배열을 위한 copyOf() 오버로드 지원

Kotlin 2.2.20은 copyOf() 함수의 실험적 오버로드를 도입해요. 이는 제네릭 타입 Array<T>와 모든 프리미티브 배열 타입에서 사용할 수 있어요.

이 함수를 사용해 배열을 더 크게 만들고 initializer 람다의 값으로 새 요소를 채울 수 있어요. 이를 통해 커스텀 보일러플레이트 코드를 줄이고, 제네릭 Array<T>의 크기를 조정할 때 nullable 결과(Array<T?>)가 생성되던 일반적인 불편을 해결해 줘요.

예시를 볼게요:

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    val row1: Array<String> = arrayOf("one", "two")
    // Resizes the array and populates the new elements using the lambda
    val row2: Array<String> = row1.copyOf(4) { "default" }
    println(row2.contentToString())
    // [one, two, default, default]
}

이 API는 Experimental 단계예요. 옵트인하려면 @OptIn(ExperimentalStdlibApi::class) 애노테이션을 사용하세요.

이슈 트래커에 피드백을 남겨 주시면 감사하겠어요.

Compose 컴파일러

이번 릴리스에서 Compose 컴파일러는 새 경고를 추가하고 빌드 지표 출력을 더 읽기 쉽게 개선해 삶의 질을 높였어요.

기본 파라미터의 언어 버전 제한

이번 릴리스에서 Compose 컴파일러는 컴파일에 지정된 언어 버전이 abstract나 open composable 함수의 기본 파라미터를 지원하는 데 필요한 버전보다 낮으면 오류를 보고해요.

기본 파라미터는 Compose 컴파일러에서 Kotlin 2.1.0부터 abstract 함수, Kotlin 2.2.0부터 open 함수에 대해 지원돼요. 더 새로운 Compose 컴파일러 버전을 사용하면서 더 오래된 Kotlin 언어 버전을 대상으로 할 때, 라이브러리 개발자는 언어 버전이 지원하지 않더라도 abstract나 open 함수의 기본 파라미터가 공개 API에 계속 나타날 수 있음을 알아야 해요.

K2 컴파일러를 위한 Composable target 경고

이번 릴리스는 K2 컴파일러를 사용할 때 @ComposableTarget 불일치에 대한 경고를 추가해요.

예를 들면:

@Composable fun App() {
  Box { // <-- `Box` is a `@UiComposable`
    Path(...) // <-- `Path` is a `@VectorComposable`
    ^^^^^^^^^
    warning: Calling a Vector composable function where a UI composable was expected
  }
}

빌드 지표의 정규화된 이름

빌드 지표에 보고되는 클래스와 함수 이름이 이제 정규화되어, 다른 패키지에 있는 같은 이름의 선언을 구별하기 쉬워져요.

또한 빌드 지표는 이제 기본 파라미터에서 복잡한 식의 덤프를 더 이상 포함하지 않아 더 읽기 쉬워요.

주요 변경 사항과 deprecation

이 섹션은 주목할 만한 주요 변경 사항과 deprecation을 강조해요:

  • kapt 컴파일러 플러그인은 이제 기본으로 K2 컴파일러를 사용해요. 그 결과 플러그인이 K2 컴파일러를 사용할지 제어하던 kapt.use.k2 프로퍼티가 deprecate됐어요. K2 컴파일러 사용을 옵트아웃하려고 이 프로퍼티를 false로 설정하면 Gradle이 경고를 보여줘요.

문서 업데이트

Kotlin 문서에 주목할 만한 변경이 몇 가지 있었어요:

Kotlin 2.2.20으로 업데이트하는 방법

Kotlin 플러그인은 IntelliJ IDEA와 Android Studio에 번들 플러그인으로 배포돼요.

새 Kotlin 버전으로 업데이트하려면 빌드 스크립트에서 Kotlin 버전을 2.2.20으로 변경하세요.

더 알아보기