Kotlin 2.0.0의 새로운 기능(What's new in Kotlin 2.0.0)

Kotlin 2.0.0의 새로운 기능(What's new in Kotlin 2.0.0)

Kotlin 2.0.0이 출시됐고, 새로운 Kotlin K2 컴파일러가 이제 Stable이 됐어요. 그 외 주요 변경점들도 함께 소개할게요.

출처: Kotlin 2.0.0의 새로운 기능

본문

출시일: 2024년 5월 21일

Kotlin 2.0.0 릴리스가 나왔고, 새로운 Kotlin K2 컴파일러가 Stable이 됐어요. 그 외에도 주목할 만한 변경점이 몇 가지 있어요.

  • 새로운 Compose 컴파일러 Gradle 플러그인
  • invokedynamic을 사용한 람다 함수 생성
  • kotlinx-metadata-jvm 라이브러리가 이제 Stable
  • Apple 플랫폼에서 signposts로 Kotlin/Native의 GC 성능 모니터링
  • Kotlin/Native에서 Objective-C 메서드와의 충돌 해결
  • Kotlin/Wasm에서 named export 지원
  • Kotlin/Wasm의 @JsExport 함수에서 unsigned 기본 타입 지원
  • Binaryen을 사용한 기본 production 빌드 최적화
  • multiplatform 프로젝트를 위한 컴파일러 옵션의 새로운 Gradle DSL
  • enum class values 일반 함수의 Stable 대체
  • Stable AutoCloseable 인터페이스

Kotlin 2.0은 JetBrains 팀에게 아주 큰 이정표예요. 이 릴리스는 KotlinConf 2024의 핵심 주제였죠. 흥미로운 업데이트를 발표하고 Kotlin 언어의 최근 작업을 다룬 개막 기조연설을 확인해 보세요.

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

IDE 지원

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

  • Kotlin K2 컴파일러에 대한 IntelliJ IDEA의 지원에 대한 자세한 내용은 IDE에서의 지원을 참고하세요.
  • IntelliJ IDEA의 Kotlin 지원에 대한 자세한 내용은 Kotlin 릴리스를 참고하세요.

Kotlin K2 컴파일러

K2 컴파일러로 가는 길은 험난했지만, 이제 JetBrains 팀은 마침내 안정화를 발표할 준비가 됐어요. Kotlin 2.0.0에서는 새로운 Kotlin K2 컴파일러가 기본적으로 사용되며, JVM, Native, Wasm, JS 등 모든 타깃 플랫폼에서 Stable이 됐어요. 새 컴파일러는 성능을 크게 개선하고, 새 언어 기능 개발 속도를 높이며, Kotlin이 지원하는 모든 플랫폼을 통합하고, multiplatform 프로젝트에 더 나은 아키텍처를 제공해요.

JetBrains 팀은 선택된 사용자 및 내부 프로젝트의 1천만 줄의 코드를 성공적으로 컴파일하여 새 컴파일러의 품질을 보장했어요. 18,000명의 개발자가 안정화 과정에 참여해 총 80,000개 프로젝트에서 새 K2 컴파일러를 테스트하고 발견한 문제를 보고했어요.

새 컴파일러로의 마이그레이션 과정을 최대한 매끄럽게 만들기 위해 K2 컴파일러 마이그레이션 가이드를 만들었어요. 이 가이드는 컴파일러의 많은 이점을 설명하고, 마주칠 수 있는 변경점을 강조하며, 필요한 경우 이전 버전으로 되돌리는 방법을 설명해요.

블로그 게시물에서 다양한 프로젝트에서 K2 컴파일러의 성능을 살펴봤어요. K2 컴파일러가 실제로 어떻게 동작하는지에 대한 실제 데이터를 보고 싶고, 자신의 프로젝트에서 성능 벤치마크를 수집하는 방법을 알고 싶다면 확인해 보세요.

또한 KotlinConf 2024의 이 발표 영상도 볼 수 있어요. 수석 언어 설계자 Michail Zarečenskij가 Kotlin의 기능 진화와 K2 컴파일러에 대해 논의해요.

현재 K2 컴파일러의 제한 사항

Gradle 프로젝트에서 K2를 활성화하면 특정 제한 사항이 생기는데, Gradle 8.3 미만 버전을 사용하는 프로젝트에서 다음과 같은 경우에 영향을 줄 수 있어요.

  • buildSrc에서 소스 코드 컴파일.
  • 포함된 빌드(included build)에서 Gradle 플러그인 컴파일.
  • Gradle 8.3 미만 버전을 사용하는 프로젝트에서 다른 Gradle 플러그인을 사용하는 경우 해당 플러그인 컴파일.
  • Gradle 플러그인 의존성 빌드.

위에서 언급한 문제 중 하나라도 마주치면 다음 단계를 실행해서 해결할 수 있어요.

  • buildSrc, 모든 Gradle 플러그인 및 그 의존성에 대해 언어 버전을 설정하세요.
kotlin {
    compilerOptions {
        languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
        apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9)
    }
}

특정 태스크에 대해 언어 및 API 버전을 구성하면 이 값들이 compilerOptions 확장이 설정한 값을 재정의해요. 이 경우 언어 및 API 버전은 1.9보다 높으면 안 돼요.

  • 프로젝트의 Gradle 버전을 8.3 이상으로 업데이트하세요.

스마트 캐스트 개선

Kotlin 컴파일러는 특정 경우에 객체를 자동으로 타입으로 캐스트할 수 있어서 직접 명시적으로 캐스트할 필요가 없어져요. 이를 스마트 캐스트라고 해요. 이제 Kotlin K2 컴파일러는 이전보다 더 많은 시나리오에서 스마트 캐스트를 수행해요.

Kotlin 2.0.0에서는 다음 영역에서 스마트 캐스트와 관련된 개선 사항을 만들었어요.

  • 지역 변수와 이후 범위
  • 논리 or 연산자를 사용한 타입 검사
  • 인라인 함수
  • 함수 타입을 가진 프로퍼티
  • 예외 처리
  • 증가 및 감소 연산자

지역 변수와 이후 범위

이전에는 변수가 if 조건 안에서 null이 아닌 것으로 평가되면 그 변수가 스마트 캐스트됐어요. 그런 다음 이 변수에 대한 정보가 if 블록의 범위 내에서 더 공유됐죠.

하지만 변수를 if 조건 밖에서 선언했다면 if 조건 안에서는 변수에 대한 정보를 사용할 수 없어서 스마트 캐스트할 수 없었어요. 이 동작은 when 식과 while 루프에서도 나타났어요.

Kotlin 2.0.0부터는 if, when 또는 while 조건에서 사용하기 전에 변수를 선언하면, 컴파일러가 그 변수에 대해 수집한 모든 정보를 해당 블록에서 스마트 캐스팅에 사용할 수 있어요.

이 기능은 boolean 조건을 변수로 추출하고 싶을 때 유용해요. 그러면 변수에 의미 있는 이름을 지어서 코드 가독성을 높이고, 나중에 코드에서 변수를 재사용할 수 있어요. 예를 들어:

class Cat {
    fun purr() {
        println("Purr purr")
    }
}

fun petAnimal(animal: Any) {
    val isCat = animal is Cat
    if (isCat) {
        // In Kotlin 2.0.0, the compiler can access
        // information about isCat, so it knows that
        // animal was smart-cast to the type Cat.
        // Therefore, the purr() function can be called.
        // In Kotlin 1.9.20, the compiler doesn't know
        // about the smart cast, so calling the purr()
        // function triggers an error.
        animal.purr()
    }
}

fun main() {
    val kitty = Cat()
    petAnimal(kitty)
    // Purr purr
}

이 예시 코드의 주석도 함께 설명할게요. Kotlin 2.0.0에서는 컴파일러가 isCat에 대한 정보에 접근할 수 있어서 animalCat 타입으로 스마트 캐스트됐다는 것을 알아요. 그래서 purr() 함수를 호출할 수 있어요. 반면 Kotlin 1.9.20에서는 컴파일러가 스마트 캐스트를 알지 못해서 purr() 함수 호출 시 오류가 발생해요.

논리 또는 연산자를 사용한 타입 검사

Kotlin 2.0.0에서는 객체에 대한 타입 검사를 or 연산자(||)로 결합하면, 가장 가까운 공통 상위 타입으로 스마트 캐스트돼요. 이 변경 전에는 항상 Any 타입으로 스마트 캐스트됐어요.

이 경우 프로퍼티에 접근하거나 함수를 호출하기 전에 객체 타입을 수동으로 다시 검사해야 했어요. 예를 들어:

interface Status {
    fun signal() {}
}

interface Ok : Status
interface Postponed : Status
interface Declined : Status

fun signalCheck(signalStatus: Any) {
    if (signalStatus is Postponed || signalStatus is Declined) {
        // signalStatus is smart-cast to a common supertype Status
        signalStatus.signal()
        // Prior to Kotlin 2.0.0, signalStatus is smart cast 
        // to type Any, so calling the signal() function triggered an
        // Unresolved reference error. The signal() function can only 
        // be called successfully after another type check:

        // check(signalStatus is Status)
        // signalStatus.signal()
    }
}

공통 상위 타입은 union 타입의 근사치예요. Kotlin은 union 타입을 지원하지 않아요.

인라인 함수

Kotlin 2.0.0에서는 K2 컴파일러가 인라인 함수를 다르게 처리해서, 다른 컴파일러 분석과 결합해 스마트 캐스트가 안전한지 판단할 수 있어요.

구체적으로, 인라인 함수는 이제 암시적인 callsInPlace 계약이 있는 것으로 취급돼요. 즉, 인라인 함수에 전달된 모든 람다 함수는 제자리에서 호출돼요. 람다 함수가 제자리에서 호출되기 때문에 컴파일러는 람다 함수가 함수 본문에 포함된 변수에 대한 참조를 유출할 수 없다는 것을 알아요.

컴파일러는 이 지식을 다른 컴파일러 분석과 함께 사용해 캡처된 변수를 스마트 캐스트하는 것이 안전한지 판단해요. 예를 들어:

interface Processor {
    fun process()
}

inline fun inlineAction(f: () -> Unit) = f()

fun nextProcessor(): Processor? = null

fun runProcessor(): Processor? {
    var processor: Processor? = null
    inlineAction {
        // In Kotlin 2.0.0, the compiler knows that processor 
        // is a local variable, and inlineAction() is an inline function, so 
        // references to processor can't be leaked. Therefore, it's safe 
        // to smart-cast processor.

        // If processor isn't null, processor is smart-cast
        if (processor != null) {
            // The compiler knows that processor isn't null, so no safe call 
            // is needed
            processor.process()

            // In Kotlin 1.9.20, you have to perform a safe call:
            // processor?.process()
        }

        processor = nextProcessor()
    }

    return processor
}

여기서도 주석을 설명할게요. Kotlin 2.0.0에서는 컴파일러가 processor가 지역 변수이고 inlineAction()이 인라인 함수라는 것을 알기 때문에 processor에 대한 참조가 유출될 수 없어요. 그래서 processor를 스마트 캐스트하는 것이 안전해요. processor가 null이 아니면 스마트 캐스트되고, 컴파일러는 null이 아니라는 것을 알기 때문에 안전한 호출(safe call)이 필요 없어요. Kotlin 1.9.20에서는 processor?.process()처럼 안전한 호출을 해야 했어요.

함수 타입을 가진 프로퍼티

이전 Kotlin 버전에는 함수 타입을 가진 클래스 프로퍼티가 스마트 캐스트되지 않는 버그가 있었어요. Kotlin 2.0.0과 K2 컴파일러에서 이 동작을 고쳤어요. 예를 들어:

class Holder(val provider: (() -> Unit)?) {
    fun process() {
        // In Kotlin 2.0.0, if provider isn't null, then
        // provider is smart-cast
        if (provider != null) {
            // The compiler knows that provider isn't null
            provider()

            // In 1.9.20, the compiler doesn't know that provider isn't 
            // null, so it triggers an error:
            // Reference has a nullable type '(() -> Unit)?', use explicit '?.invoke()' to make a function-like call instead
        }
    }
}

이 변경은 invoke 연산자를 오버로드할 때도 적용돼요. 예를 들어:

interface Provider {
    operator fun invoke()
}

interface Processor : () -> String

class Holder(val provider: Provider?, val processor: Processor?) {
    fun process() {
        if (provider != null) {
            provider()
            // In 1.9.20, the compiler triggers an error: 
            // Reference has a nullable type 'Provider?' use explicit '?.invoke()' to make a function-like call instead
        }
    }
}

예외 처리

Kotlin 2.0.0에서는 예외 처리를 개선해서 스마트 캐스트 정보를 catchfinally 블록에 전달할 수 있게 됐어요. 이 변경으로 컴파일러가 객체가 nullable 타입인지 추적하므로 코드가 더 안전해져요. 예를 들어:

//sampleStart
fun testString() {
    var stringInput: String? = null
    // stringInput is smart-cast to String type
    stringInput = ""
    try {
        // The compiler knows that stringInput isn't null
        println(stringInput.length)
        // 0

        // The compiler rejects previous smart cast information for 
        // stringInput. Now stringInput has the String? type.
        stringInput = null

        // Trigger an exception
        if (2 > 1) throw Exception()
        stringInput = ""
    } catch (exception: Exception) {
        // In Kotlin 2.0.0, the compiler knows stringInput 
        // can be null, so stringInput stays nullable.
        println(stringInput?.length)
        // null

        // In Kotlin 1.9.20, the compiler says that a safe call isn't
        // needed, but this is incorrect.
    }
}

//sampleEnd
fun main() {
    testString()
}

이 예시를 살펴볼게요. stringInputString 타입으로 스마트 캐스트된 후 try 블록 안에서 다시 null로 설정되면, 컴파일러는 이전 스마트 캐스트 정보를 버려요. 이제 stringInputString? 타입이에요. 예외가 발생해 catch 블록으로 들어가면, Kotlin 2.0.0에서 컴파일러는 stringInput이 null일 수 있다는 것을 알기 때문에 nullable로 유지돼요. 반면 Kotlin 1.9.20에서는 안전한 호출이 필요 없다고 잘못 알려줬어요.

증가 및 감소 연산자

Kotlin 2.0.0 이전에는 컴파일러가 증가 또는 감소 연산자를 사용한 후 객체의 타입이 변할 수 있다는 것을 이해하지 못했어요. 컴파일러가 객체 타입을 정확히 추적할 수 없었기 때문에 unresolved reference 오류가 발생할 수 있었어요. Kotlin 2.0.0에서 이 문제가 고쳐졌어요.

interface Rho {
    operator fun inc(): Sigma = TODO()
}

interface Sigma : Rho {
    fun sigma() = Unit
}

interface Tau {
    fun tau() = Unit
}

fun main(input: Rho) {
    var unknownObject: Rho = input

    // Check if unknownObject inherits from the Tau interface
    // Note, it's possible that unknownObject inherits from both
    // Rho and Tau interfaces.
    if (unknownObject is Tau) {

        // Use the overloaded inc() operator from interface Rho.
        // In Kotlin 2.0.0, the type of unknownObject is smart-cast to
        // Sigma.
        ++unknownObject

        // In Kotlin 2.0.0, the compiler knows unknownObject has type
        // Sigma, so the sigma() function can be called successfully.
        unknownObject.sigma()

        // In Kotlin 1.9.20, the compiler doesn't perform a smart cast
        // when inc() is called so the compiler still thinks that 
        // unknownObject has type Tau. Calling the sigma() function 
        // throws a compile-time error.
        
        // In Kotlin 2.0.0, the compiler knows unknownObject has type
        // Sigma, so calling the tau() function throws a compile-time 
        // error.
        unknownObject.tau()
        // Unresolved reference 'tau'

        // In Kotlin 1.9.20, since the compiler mistakenly thinks that 
        // unknownObject has type Tau, the tau() function can be called,
        // but it throws a ClassCastException.
    }
}

이 예시는 오버로드된 inc() 연산자를 사용해 Rho 인터페이스에서 상속받는 동작을 설명해요. Kotlin 2.0.0에서는 unknownObject의 타입이 Sigma로 스마트 캐스트되기 때문에 sigma() 함수를 성공적으로 호출할 수 있고, tau() 함수 호출은 컴파일 타임 오류가 돼요.

Kotlin Multiplatform 개선 사항

Kotlin 2.0.0에서는 다음 영역에서 Kotlin Multiplatform과 관련된 K2 컴파일러 개선 사항을 만들었어요.

  • 컴파일 중 common 소스와 플랫폼 소스의 분리
  • expected 및 actual 선언의 서로 다른 가시성 수준

컴파일 중 common 소스와 플랫폼 소스의 분리

이전에는 Kotlin 컴파일러의 설계상 컴파일 시점에 공통 소스 세트와 플랫폼 소스 세트를 분리해서 유지할 수 없었어요. 그 결과 공통 코드가 플랫폼 코드에 접근할 수 있었고, 이로 인해 플랫폼 간 동작이 달라졌어요. 또한 공통 코드의 일부 컴파일러 설정과 의존성이 플랫폼 코드로 누출되기도 했어요.

Kotlin 2.0.0에서는 새 Kotlin K2 컴파일러 구현에 컴파일 체계를 재설계하여 공통 소스 세트와 플랫폼 소스 세트를 엄격히 분리하도록 했어요. 이 변경은 expected 및 actual 함수를 사용할 때 가장 눈에 띄어요. 이전에는 공통 코드의 함수 호출이 플랫폼 코드의 함수로 해석될 수 있었어요. 예를 들어:

fun foo(x: Any) = println("common foo")

fun exampleFunction() {
    foo(42)
}
// JVM
fun foo(x: Int) = println("platform foo")

// JavaScript
// There is no foo() function overload
// on the JavaScript platform

이 예시에서 공통 코드는 실행되는 플랫폼에 따라 동작이 달라요.

  • JVM 플랫폼에서 공통 코드의 foo() 함수를 호출하면 플랫폼 코드의 foo() 함수가 호출되어 platform foo가 출력돼요.
  • JavaScript 플랫폼에서 공통 코드의 foo() 함수를 호출하면 플랫폼 코드에 해당 함수가 없으므로 공통 코드의 foo() 함수가 호출되어 common foo가 출력돼요.

Kotlin 2.0.0에서는 공통 코드가 플랫폼 코드에 접근할 수 없으므로 두 플랫폼 모두 foo() 함수를 공통 코드의 foo() 함수로 해석해서 common foo가 출력돼요.

플랫폼 간 동작의 일관성 향상 외에도 IntelliJ IDEA 또는 Android Studio와 컴파일러 사이에 충돌하는 동작이 있던 경우를 고치는 데 많은 노력을 기울였어요. 예를 들어 expected 및 actual 클래스를 사용할 때 다음이 발생했어요.

expect class Identity {
    fun confirmIdentity(): String
}

fun common() {
    // Before 2.0.0,
    // it triggers an IDE-only error
    Identity().confirmIdentity()
    // RESOLUTION_TO_CLASSIFIER : Expected class
    // Identity has no default constructor.
}
actual class Identity {
    actual fun confirmIdentity() = "expect class fun: jvm"
}

이 예시에서 expected 클래스 Identity에는 기본 생성자가 없으므로 공통 코드에서 성공적으로 호출할 수 없어요. 이전에는 IDE에서만 오류가 보고되었지만 코드는 JVM에서 여전히 성공적으로 컴파일됐어요. 이제 컴파일러가 올바르게 오류를 보고해요.

Expected class 'expect class Identity : Any' does not have default constructor
해석 동작이 변하지 않는 경우

여전히 새 컴파일 체계로 마이그레이션하는 중이라 같은 소스 세트에 속하지 않은 함수를 호출할 때는 해석 동작이 여전히 동일해요. 이 차이는 주로 공통 코드에서 multiplatform 라이브러리의 오버로드를 사용할 때 눈에 띄어요.

서로 다른 시그니처를 가진 두 개의 whichFun() 함수가 있는 라이브러리가 있다고 가정해 볼게요.

// Example library

// MODULE: common
fun whichFun(x: Any) = println("common function")

// MODULE: JVM
fun whichFun(x: Int) = println("platform function")

공통 코드에서 whichFun() 함수를 호출하면 라이브러리에서 가장 관련성이 높은 인자 타입을 가진 함수가 해석돼요.

// A project that uses the example library for the JVM target

// MODULE: common
fun main() {
    whichFun(2)
    // platform function
}

반면에 whichFun()에 대한 오버로드를 같은 소스 세트 안에 선언하면, 공통 코드가 플랫폼별 버전에 접근할 수 없으므로 공통 코드의 함수가 해석돼요.

// Example library isn't used

// MODULE: common
fun whichFun(x: Any) = println("common function")

fun main() {
    whichFun(2)
    // common function
}

// MODULE: JVM
fun whichFun(x: Int) = println("platform function")

multiplatform 라이브러리와 유사하게, commonTest 모듈은 별도의 소스 세트에 있으므로 플랫폼별 코드에 여전히 접근할 수 있어요. 따라서 commonTest 모듈의 함수 호출 해석은 이전 컴파일 체계와 동일한 동작을 보여요.

앞으로 이러한 나머지 경우들은 새 컴파일 체계와 더 일관되게 만들 예정이에요.

expected 및 actual 선언의 서로 다른 가시성 수준

Kotlin 2.0.0 이전에는 Kotlin Multiplatform 프로젝트에서 expected 및 actual 선언을 사용할 때 두 선언이 같은 가시성 수준을 가져야 했어요. Kotlin 2.0.0부터는 실제 선언이 expected 선언보다 더 허용적인 경우에만 서로 다른 가시성 수준도 지원해요. 예를 들어:

expect internal class Attribute // Visibility is internal
actual class Attribute          // Visibility is public by default,
                                // which is more permissive

마찬가지로 actual 선언에서 타입 별칭을 사용한다면, 기본 타입의 가시성은 expected 선언과 같거나 더 허용적이어야 해요. 예를 들어:

expect internal class Attribute                 // Visibility is internal
internal actual typealias Attribute = Expanded

class Expanded                                  // Visibility is public by default,
                                                // which is more permissive

컴파일러 플러그인 지원

현재 Kotlin K2 컴파일러는 다음 Kotlin 컴파일러 플러그인을 지원해요.

또한 Kotlin K2 컴파일러는 다음을 지원해요.

추가 컴파일러 플러그인을 사용한다면 해당 플러그인이 K2와 호환되는지 설명서를 확인해 보세요.

실험적 Kotlin Power-assert 컴파일러 플러그인

Kotlin Power-assert 플러그인은 실험적(Experimental)이며 언제든 변경될 수 있어요.

Kotlin 2.0.0은 실험적 Power-assert 컴파일러 플러그인을 도입해요. 이 플러그인은 실패 메시지에 문맥 정보를 포함해 테스트 작성 경험을 개선하고, 디버깅을 더 쉽고 효율적으로 만들어요.

개발자들은 효과적인 테스트를 작성하기 위해 복잡한 assertion 라이브러리를 사용해야 하는 경우가 많아요. Power-assert 플러그인은 assertion 식의 중간 값을 포함하는 실패 메시지를 자동으로 생성하여 이 과정을 단순화해요. 이 덕분에 개발자가 테스트가 실패한 이유를 빠르게 이해할 수 있어요.

테스트에서 assertion이 실패하면 개선된 오류 메시지가 assertion 내의 모든 변수와 하위 식의 값을 보여줘서 조건의 어느 부분이 실패를 일으켰는지 분명하게 해줘요. 이는 여러 조건을 검사하는 복잡한 assertion에서 특히 유용해요.

프로젝트에서 플러그인을 활성화하려면 build.gradle(.kts) 파일에서 구성하세요.

plugins {
    kotlin("multiplatform") version "2.0.0"
    kotlin("plugin.power-assert") version "2.0.0"
}

powerAssert {
    functions = listOf("kotlin.assert", "kotlin.test.assertTrue")
}
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '2.0.0'
    id 'org.jetbrains.kotlin.plugin.power-assert' version '2.0.0'
}

powerAssert {
    functions = ["kotlin.assert", "kotlin.test.assertTrue"]
}

Kotlin Power-assert 플러그인에 대해 문서에서 자세히 알아보세요.

Kotlin K2 컴파일러를 활성화하는 방법

Kotlin 2.0.0부터 Kotlin K2 컴파일러가 기본으로 활성화돼요. 추가 작업은 필요 없어요.

Kotlin Playground에서 Kotlin K2 컴파일러 사용해 보기

Kotlin Playground는 2.0.0 릴리스를 지원해요. 확인해 보세요!

IDE에서의 지원

기본적으로 IntelliJ IDEA와 Android Studio는 코드 분석, 코드 완성, 하이라이팅 및 기타 IDE 관련 기능에 여전히 이전 컴파일러를 사용해요. IDE에서 Kotlin 2.0 경험을 온전히 누리려면 K2 모드를 활성화하세요.

IDE에서 Settings | Languages & Frameworks | Kotlin으로 이동해 Enable K2 mode 옵션을 선택하세요. 그러면 IDE가 K2 모드로 코드를 분석해요.

K2 모드를 활성화한 후에는 컴파일러 동작 변화로 인해 IDE 분석에 차이가 있을 수 있어요. 마이그레이션 가이드에서 새 K2 컴파일러가 이전 컴파일러와 어떻게 다른지 알아보세요.

  • 블로그에서 K2 모드에 대해 자세히 알아보세요.
  • K2 모드에 대한 피드백을 적극적으로 수집하고 있으니 공개 Slack 채널에서 의견을 공유해 주세요.

새 K2 컴파일러에 대한 피드백 남기기

어떤 피드백이든 환영해요!

Kotlin/JVM

버전 2.0.0부터 컴파일러는 Java 22 바이트코드를 포함하는 클래스를 생성할 수 있어요. 이 버전은 또한 다음 변경점을 가져와요.

  • invokedynamic을 사용한 람다 함수 생성
  • kotlinx-metadata-jvm 라이브러리가 이제 Stable

invokedynamic을 사용한 람다 함수 생성

Kotlin 2.0.0은 invokedynamic을 사용해 람다 함수를 생성하는 새로운 기본 방법을 도입해요. 이 변경은 기존의 익명 클래스 생성에 비해 애플리케이션의 바이너리 크기를 줄여줘요.

첫 버전부터 Kotlin은 람다를 익명 클래스로 생성했어요. 하지만 Kotlin 1.5.0부터 -Xlambdas=indy 컴파일러 옵션을 사용해 invokedynamic 생성을 선택할 수 있었어요. Kotlin 2.0.0에서 invokedynamic이 람다 생성의 기본 방법이 됐어요. 이 방법은 더 가벼운 바이너리를 생성하고 Kotlin을 JVM 최적화와 맞춰 애플리케이션이 현재 및 미래의 JVM 성능 개선의 혜택을 받을 수 있게 해줘요.

현재 일반적인 람다 컴파일과 비교해 세 가지 제한 사항이 있어요.

  • invokedynamic으로 컴파일된 람다는 직렬화할 수 없어요.
  • 실험적 reflect() API는 invokedynamic으로 생성된 람다를 지원하지 않아요.
  • 그러한 람다에서 .toString()을 호출하면 읽기 어려운 문자열 표현이 생성돼요.
fun main() {
    println({})

    // With Kotlin 1.9.24 and reflection, returns
    // () -> kotlin.Unit
    
    // With Kotlin 2.0.0, returns
    // FileKt$$Lambda$13/0x00007f88a0004608@506e1b77
}

람다 함수를 생성하는 기존 동작을 유지하려면 다음 중 하나를 선택할 수 있어요.

  • 특정 람다에 @JvmSerializableLambda를 붙이세요.
  • -Xlambdas=class 컴파일러 옵션을 사용해 모듈의 모든 람다를 기존 방식으로 생성하세요.

kotlinx-metadata-jvm 라이브러리가 Stable

Kotlin 2.0.0에서 kotlinx-metadata-jvm 라이브러리가 Stable이 됐어요. 이제 라이브러리가 kotlin 패키지와 좌표로 변경되었으므로 kotlin-metadata-jvm("x" 없이)으로 찾을 수 있어요.

이전에는 kotlinx-metadata-jvm 라이브러리가 자체 게시 체계와 버전을 가졌어요. 이제 Kotlin 릴리스 주기의 일부로 kotlin-metadata-jvm 업데이트를 빌드하고 게시하며, Kotlin 표준 라이브러리와 동일한 하위 호환성 보장을 제공해요.

kotlin-metadata-jvm 라이브러리는 Kotlin/JVM 컴파일러가 생성한 바이너리 파일의 메타데이터를 읽고 수정하는 API를 제공해요.

Kotlin/Native

이 버전은 다음 변경점을 가져와요.

  • signposts로 GC 성능 모니터링
  • Objective-C 메서드와의 충돌 해결
  • Kotlin/Native에서 컴파일러 인자의 로그 수준 변경
  • Kotlin/Native에 표준 라이브러리와 플랫폼 의존성 명시적 추가
  • Gradle 구성 캐시의 태스크 오류

Apple 플랫폼에서 signposts로 GC 성능 모니터링

이전에는 로그를 살펴봐야만 Kotlin/Native의 가비지 컬렉터(GC) 성능을 모니터링할 수 있었어요. 하지만 이 로그는 iOS 앱 성능 문제 조사에 널리 쓰이는 도구인 Xcode Instruments와 통합되지 않았어요.

Kotlin 2.0.0부터 GC는 Instruments에서 사용할 수 있는 signposts로 일시 중지를 보고해요. Signposts는 앱 내에서 사용자 정의 로깅을 허용하므로, 이제 iOS 앱 성능을 디버깅할 때 GC 일시 중지가 애플리케이션 정지에 해당하는지 확인할 수 있어요.

문서에서 GC 성능 분석에 대해 자세히 알아보세요.

Objective-C 메서드와의 충돌 해결

Objective-C 메서드는 이름이 다르지만 매개변수의 개수와 타입이 같을 수 있어요. 예를 들어 locationManager:didEnterRegion:locationManager:didExitRegion:이 있어요. Kotlin에서 이 메서드들은 시그니처가 같아서 사용하면 충돌하는 오버로드 오류가 발생해요.

이전에는 이 컴파일 오류를 피하려면 충돌하는 오버로드를 수동으로 억제해야 했어요. Kotlin과 Objective-C의 상호 운용성을 개선하기 위해 Kotlin 2.0.0은 새 @ObjCSignatureOverride 어노테이션을 도입해요.

이 어노테이션은 인자 타입이 같지만 인자 이름이 다른 여러 함수가 Objective-C 클래스에서 상속되는 경우 컴파일러가 충돌하는 오버로드를 무시하도록 지시해요.

이 어노테이션을 적용하는 것은 일반적인 오류 억제보다 더 안전해요. 이 어노테이션은 지원되고 테스트된 Objective-C 메서드를 재정의하는 경우에만 사용할 수 있는 반면, 일반적인 억제는 중요한 오류를 숨기고 조용히 깨진 코드로 이어질 수 있어요.

컴파일러 인자의 로그 수준 변경

이 릴리스에서는 Kotlin/Native Gradle 태스크의 컴파일러 인자 로그 수준이 compile, link, cinterop 등에서 info에서 debug로 변경됐어요.

debug가 기본값이므로 로그 수준이 다른 Gradle 컴파일 태스크와 일관되며 모든 컴파일러 인자를 포함한 자세한 디버깅 정보를 제공해요.

Kotlin/Native에 표준 라이브러리와 플랫폼 의존성 명시적 추가

이전에는 Kotlin/Native 컴파일러가 표준 라이브러리와 플랫폼 의존성을 암시적으로 해결했기 때문에 Kotlin 타깃 전반에서 Kotlin Gradle 플러그인이 동작하는 방식에 불일치가 발생했어요.

이제 각 Kotlin/Native Gradle 컴파일은 compileDependencyFiles 컴파일 매개변수를 통해 표준 라이브러리와 플랫폼 의존성을 컴파일 타임 라이브러리 경로에 명시적으로 포함해요.

Gradle 구성 캐시의 태스크 오류

Kotlin 2.0.0부터 invocation of Task.project at execution time is unsupported와 같은 메시지가 있는 구성 캐시 오류가 발생할 수 있어요.

이 오류는 NativeDistributionCommonizerTaskKotlinNativeCompile 같은 태스크에서 나타나요.

하지만 이는 오탐(거짓 양성) 오류예요. 근본적인 문제는 publish* 태스크처럼 Gradle 구성 캐시와 호환되지 않는 태스크가 존재한다는 것이에요.

오류 메시지가 다른 근본 원인을 암시하기 때문에 이 불일치가 즉시 명확하지 않을 수 있어요.

정확한 원인이 오류 보고서에 명시적으로 언급되지 않기 때문에 Gradle 팀이 이미 보고서를 고치기 위해 문제를 해결하고 있어요.

Kotlin/Wasm

Kotlin 2.0.0은 JavaScript와의 성능 및 상호 운용성을 개선해요.

  • Binaryen을 사용한 기본 production 빌드 최적화
  • named export 지원
  • @JsExport 함수에서 unsigned 기본 타입 지원
  • Kotlin/Wasm에서 TypeScript 선언 파일 생성
  • JavaScript 예외 포착 지원
  • 새 예외 처리 제안을 옵션으로 지원
  • withWasm() 함수가 JS와 WASI 변형으로 분리

Binaryen을 사용한 기본 production 빌드 최적화

Kotlin/Wasm 툴체인은 이제 이전의 수동 설정 접근 방식 대신 production 컴파일 중 모든 프로젝트에 Binaryen 도구를 적용해요. 우리 추정으로는 프로젝트의 런타임 성능이 향상되고 바이너리 크기가 줄어들 거예요.

이 변경은 production 컴파일에만 영향을 줘요. 개발 컴파일 과정은 그대로예요.

named export 지원

이전에는 Kotlin/Wasm의 모든 내보낸 선언이 기본 export를 사용해 JavaScript로 가져와졌어요.

//JavaScript:
import Module from "./index.mjs"

Module.add()

이제 @JsExport로 표시된 각 Kotlin 선언을 이름으로 가져올 수 있어요.

// Kotlin:
@JsExport
fun add(a: Int, b: Int) = a + b
//JavaScript:
import { add } from "./index.mjs"

named export는 Kotlin과 JavaScript 모듈 간의 코드 공유를 더 쉽게 만들어요. 가독성을 개선하고 모듈 간 의존성 관리를 도와줘요.

@JsExport 함수에서 unsigned 기본 타입 지원

Kotlin 2.0.0부터 JavaScript 코드에서 Kotlin/Wasm 함수를 사용할 수 있게 하는 @JsExport 어노테이션이 있는 외부 선언과 함수 안에서 unsigned 기본 타입을 사용할 수 있어요.

이로써 unsigned 기본 타입을 내보낸 선언과 외부 선언 안에서 직접 사용하지 못하게 했던 기존 제한이 완화돼요. 이제 unsigned 기본 타입을 반환 타입이나 매개변수 타입으로 사용하는 함수를 내보낼 수 있고, unsigned 기본 타입을 반환하거나 사용하는 외부 선언을 사용할 수 있어요.

Kotlin/Wasm과 JavaScript의 상호 운용성에 대한 자세한 내용은 문서를 참고하세요.

Kotlin/Wasm에서 TypeScript 선언 파일 생성

Kotlin/Wasm에서 TypeScript 선언 파일을 생성하는 기능은 실험적(Experimental)이며 언제든 제거되거나 변경될 수 있어요.

Kotlin 2.0.0에서 Kotlin/Wasm 컴파일러는 Kotlin 코드의 모든 @JsExport 선언에서 TypeScript 정의를 생성할 수 있게 됐어요. 이 정의는 IDE와 JavaScript 도구가 코드 자동 완성, 타입 검사 지원을 제공하고 Kotlin 코드를 JavaScript에 포함하기 쉽게 만드는 데 사용될 수 있어요.

Kotlin/Wasm 컴파일러는 @JsExport로 표시된 모든 최상위 함수를 수집하고 .d.ts 파일에 TypeScript 정의를 자동으로 생성해요.

TypeScript 정의를 생성하려면 build.gradle(.kts) 파일의 wasmJs {} 블록에 generateTypeScriptDefinitions() 함수를 추가하세요.

kotlin {
    wasmJs {
        binaries.executable()
        browser {
        }
        generateTypeScriptDefinitions()
    }
}

JavaScript 예외 포착 지원

이전에는 Kotlin/Wasm 코드가 JavaScript 예외를 포착할 수 없어서 프로그램의 JavaScript 쪽에서 발생한 오류를 처리하기 어려웠어요.

Kotlin 2.0.0에서는 Kotlin/Wasm 내에서 JavaScript 예외를 포착하는 지원을 구현했어요. 이 구현은 Throwable 또는 JsException 같은 특정 타입과 함께 try-catch 블록을 사용해 이러한 오류를 올바르게 처리할 수 있게 해줘요.

또한 예외와 관계없이 코드를 실행하는 데 도움을 주는 finally 블록도 올바르게 동작해요. JavaScript 예외 포착 지원을 도입하지만 호출 스택 같은 JavaScript 예외가 발생할 때 추가 정보는 제공되지 않아요. 하지만 이 구현을 작업 중이에요.

새 예외 처리 제안을 옵션으로 지원

이 릴리스에서는 Kotlin/Wasm 내에서 WebAssembly의 예외 처리 제안의 새 버전을 지원해요.

이 업데이트로 새 제안이 Kotlin 요구 사항과 일치하여, 제안의 최신 버전만 지원하는 가상 머신에서 Kotlin/Wasm을 사용할 수 있게 해줘요.

기본적으로 꺼져 있는 -Xwasm-use-new-exception-proposal 컴파일러 옵션을 사용해 새 예외 처리 제안을 활성화하세요.

withWasm() 함수가 JS와 WASI 변형으로 분리

계층 템플릿에 Wasm 타깃을 제공하던 withWasm() 함수는 전문화된 withWasmJs()withWasmWasi() 함수로 대체되며 더 이상 사용되지 않아요.

이제 트리 정의에서 WASI와 JS 타깃을 서로 다른 그룹으로 분리할 수 있어요.

Kotlin/JS

다른 변경점들 중에서, 이 버전은 ES2015 표준의 더 많은 기능을 지원하는 현대적인 JS 컴파일을 Kotlin에 가져와요.

  • 새 컴파일 타깃
  • ES2015 generator로서의 suspend 함수
  • main 함수에 인자 전달
  • Kotlin/JS 프로젝트의 파일별 컴파일
  • 컬렉션 상호 운용성 개선
  • createInstance() 지원
  • 타입 안전한 일반 JavaScript 객체 지원
  • npm 패키지 매니저 지원
  • 컴파일 태스크 변경
  • 레거시 Kotlin/JS JAR 아티팩트 중단

새 컴파일 타깃

Kotlin 2.0.0에서는 Kotlin/JS에 새 컴파일 타깃 es2015를 추가해요. 이렇게 하면 Kotlin에서 지원되는 모든 ES2015 기능을 한 번에 활성화할 수 있는 새로운 방법이 돼요.

build.gradle(.kts) 파일에서 다음과 같이 설정할 수 있어요.

kotlin {
    js {
        compilerOptions {
            target.set("es2015")
        }
    }
}

새 타깃은 ES 클래스와 모듈 및 새로 지원되는 ES generator를 자동으로 켜요.

ES2015 generator로서의 suspend 함수

이 릴리스는 suspend 함수를 컴파일하기 위한 ES2015 generator에 대한 실험적(Experimental) 지원을 도입해요.

상태 머신 대신 generator를 사용하면 프로젝트의 최종 번들 크기가 개선될 수 있어요. 예를 들어 JetBrains 팀은 ES2015 generator를 사용해 Space 프로젝트의 번들 크기를 20% 줄였어요.

공식 문서에서 ES2015(ECMAScript 2015, ES6)에 대해 자세히 알아보세요.

main 함수에 인자 전달

Kotlin 2.0.0부터 main() 함수의 args 소스를 지정할 수 있어요. 이 기능은 명령줄 작업을 더 쉽게 만들고 인자를 전달하는 데 도움을 줘요.

이렇게 하려면 문자열 배열을 반환하는 새 passAsArgumentToMainFunction() 함수로 js {} 블록을 정의하세요.

kotlin {
    js {
        binary.executable()
        passAsArgumentToMainFunction("Deno.args")
    }
}

이 함수는 런타임에 실행돼요. JavaScript 식을 받아 main() 함수 호출 대신 args: Array<String> 인자로 사용해요.

또한 Node.js 런타임을 사용한다면 특별한 별칭을 활용할 수 있어요. 매번 수동으로 추가하는 대신 process.argvargs 매개변수에 한 번에 전달할 수 있게 해줘요.

kotlin {
    js {
        binary.executable()
        nodejs {
            passProcessArgvToMainFunction()
        }
    }
}

Kotlin/JS 프로젝트의 파일별 컴파일

Kotlin 2.0.0은 Kotlin/JS 프로젝트 출력에 대한 새로운 세분성 옵션을 도입해요. 이제 각 Kotlin 파일에 대해 하나의 JavaScript 파일을 생성하는 파일별 컴파일을 설정할 수 있어요. 이는 최종 번들의 크기를 크게 최적화하고 프로그램의 로딩 시간을 개선하는 데 도움을 줘요.

이전에는 출력 옵션이 두 가지만 있었어요. Kotlin/JS 컴파일러는 전체 프로젝트에 대해 단일 .js 파일을 생성할 수 있었어요. 하지만 이 파일은 너무 커서 사용하기 불편할 수 있었어요. 프로젝트의 함수를 사용하려면 전체 JavaScript 파일을 의존성으로 포함해야 했어요. 또는 각 프로젝트 모듈에 대해 별도의 .js 파일 컴파일을 구성할 수 있었어요. 이것이 여전히 기본 옵션이에요.

모듈 파일도 너무 클 수 있으므로 Kotlin 2.0.0에서는 각 Kotlin 파일에 대해 하나(파일에 내보낸 선언이 있으면 두 개)의 JavaScript 파일을 생성하는 더 세분화된 출력을 추가해요. 파일별 컴파일 모드를 활성화하려면:

  1. ECMAScript 모듈을 지원하도록 빌드 파일에 useEsModules() 함수를 추가하세요.
// build.gradle.kts
kotlin {
    js(IR) {
        useEsModules() // Enables ES2015 modules
        browser()
    }
}

이를 위해 새 es2015 컴파일 타깃을 사용할 수도 있어요.

  1. -Xir-per-file 컴파일러 옵션을 적용하거나 gradle.properties 파일을 다음과 같이 업데이트하세요.
# gradle.properties
kotlin.js.ir.output.granularity=per-file // `per-module` is the default

컬렉션 상호 운용성 개선

Kotlin 2.0.0부터 시그니처에 Kotlin 컬렉션 타입이 포함된 선언을 JavaScript(및 TypeScript)로 내보낼 수 있어요. 이는 Set, Map, List 컬렉션 타입과 그 변경 가능한 대응물에 적용돼요.

JavaScript에서 Kotlin 컬렉션을 사용하려면 먼저 필요한 선언을 @JsExport 어노테이션으로 표시하세요.

// Kotlin
@JsExport
data class User(
    val name: String,
    val friends: List<User> = emptyList()
)

@JsExport
val me = User(
    name = "Me",
    friends = listOf(User(name = "Kodee"))
)

그런 다음 JavaScript에서 일반적인 JavaScript 배열로 사용할 수 있어요.

// JavaScript
import { User, me, KtList } from "my-module"

const allMyFriendNames = me.friends
    .asJsReadonlyArrayView()
    .map(x => x.name) // ['Kodee']

아쉽게도 JavaScript에서 Kotlin 컬렉션을 만드는 것은 아직 불가능해요. 이 기능은 Kotlin 2.0.20에 추가할 예정이에요.

createInstance() 지원

Kotlin 2.0.0부터 Kotlin/JS 타깃에서 createInstance() 함수를 사용할 수 있어요. 이전에는 JVM에서만 사용할 수 있었어요.

KClass 인터페이스의 이 함수는 지정된 클래스의 새 인스턴스를 만들어요. 이는 Kotlin 클래스에 대한 런타임 참조를 얻는 데 유용해요.

타입 안전한 일반 JavaScript 객체 지원

js-plain-objects 플러그인은 실험적(Experimental)이며 언제든 제거되거나 변경될 수 있어요. js-plain-objects 플러그인은 K2 컴파일러만 지원해요.

JavaScript API 작업을 더 쉽게 하기 위해 Kotlin 2.0.0에서는 새 플러그인 js-plain-objects를 제공해요. 이를 사용해 타입 안전한 일반 JavaScript 객체를 만들 수 있어요. 플러그인은 @JsPlainObject 어노테이션이 있는 외부 인터페이스가 있는지 코드를 검사하고 다음을 추가해요.

  • 생성자로 사용할 수 있는 companion object 안의 인라인 invoke 연산자 함수.
  • 일부 프로퍼티를 조정하면서 객체의 복사본을 만들 수 있는 .copy() 함수.

예를 들어:

import kotlinx.js.JsPlainObject

@JsPlainObject
external interface User {
    var name: String
    val age: Int
    val email: String?
}

fun main() {
    // Creates a JavaScript object
    val user = User(name = "Name", age = 10)
    // Copies the object and adds an email
    val copy = user.copy(age = 11, email = "[email protected]")

    println(JSON.stringify(user))
    // { "name": "Name", "age": 10 }
    println(JSON.stringify(copy))
    // { "name": "Name", "age": 11, "email": "[email protected]" }
}

이 접근 방식으로 만든 모든 JavaScript 객체는 런타임에만 오류를 볼 수 있었던 대신 컴파일 타임에 오류를 보거나 IDE에서 하이라이팅까지 볼 수 있기 때문에 더 안전해요.

외부 인터페이스로 JavaScript 객체의 모양을 설명하며 fetch() 함수를 사용해 JavaScript API와 상호 작용하는 다음 예시를 고려해 보세요.

import kotlinx.js.JsPlainObject

@JsPlainObject
external interface FetchOptions {
    val body: String?
    val method: String
}

// A wrapper for Window.fetch
suspend fun fetch(url: String, options: FetchOptions? = null) = TODO("Add your custom behavior here")

// A compile-time error is triggered as "metod" is not recognized
// as method
fetch("https://google.com", options = FetchOptions(metod = "POST"))
// A compile-time error is triggered as method is required
fetch("https://google.com", options = FetchOptions(body = "SOME STRING")) 

이 예시에서 metodmethod로 인식되지 않기 때문에 컴파일 타임 오류가 발생하고, body만 제공하면 필수인 method가 없어서 컴파일 타임 오류가 발생해요.

반대로 JavaScript 객체를 만들기 위해 js() 함수를 대신 사용하면 오류는 런타임에만 발견되거나 전혀 발생하지 않아요.

suspend fun fetch(url: String, options: FetchOptions? = null) = TODO("Add your custom behavior here")

// No error is triggered. As "metod" is not recognized, the wrong method 
// (GET) is used.
fetch("https://google.com", options = js("{ metod: 'POST' }"))

// By default, the GET method is used. A runtime error is triggered as 
// body shouldn't be present.
fetch("https://google.com", options = js("{ body: 'SOME STRING' }"))
// TypeError: Window.fetch: HEAD or GET Request cannot have a body

js-plain-objects 플러그인을 사용하려면 build.gradle(.kts) 파일에 다음을 추가하세요.

plugins {
    kotlin("plugin.js-plain-objects") version "2.0.0"
}
plugins {
    id "org.jetbrains.kotlin.plugin.js-plain-objects" version "2.0.0"
}

npm 패키지 매니저 지원

이전에는 Kotlin Multiplatform Gradle 플러그인이 npm 의존성을 다운로드하고 설치하기 위해 패키지 매니저로 Yarn만 사용할 수 있었어요. Kotlin 2.0.0부터는 npm을 패키지 매니저로 대신 사용할 수 있어요. npm을 패키지 매니저로 사용하면 설정 중 관리해야 할 도구가 하나 줄어들어요.

하위 호환성을 위해 Yarn이 여전히 기본 패키지 매니저예요. npm을 패키지 매니저로 사용하려면 gradle.properties 파일에 다음 속성을 설정하세요.

kotlin.js.yarn = false

컴파일 태스크 변경

이전에는 webpackdistributeResources 컴파일 태스크가 모두 동일한 디렉터리를 타깃으로 했어요. 게다가 distribution 태스크도 dist를 출력 디렉터리로 선언했어요. 이로 인해 출력이 겹치고 컴파일 경고가 발생했어요.

그래서 Kotlin 2.0.0부터 다음 변경을 구현했어요.

  • webpack 태스크는 이제 별도의 폴더를 타깃으로 해요.
  • distributeResources 태스크가 완전히 제거됐어요.
  • distribution 태스크는 이제 Copy 타입을 가지며 dist 폴더를 타깃으로 해요.

레거시 Kotlin/JS JAR 아티팩트 중단

Kotlin 2.0.0부터 Kotlin 배포판에 더 이상 .jar 확장자를 가진 레거시 Kotlin/JS 아티팩트가 포함되지 않아요. 레거시 아티팩트는 지원되지 않는 이전 Kotlin/JS 컴파일러에서 사용되었고, klib 형식을 사용하는 IR 컴파일러에는 불필요해요.

Gradle 개선 사항

Kotlin 2.0.0은 Gradle 6.8.3부터 8.5까지 완전히 호환돼요. 최신 Gradle 릴리스까지 사용할 수도 있지만, 그렇게 하면 사용 중단 경고가 발생하거나 일부 새 Gradle 기능이 동작하지 않을 수 있다는 점을 명심하세요.

이 버전은 다음 변경점을 가져와요.

  • multiplatform 프로젝트를 위한 컴파일러 옵션의 새로운 Gradle DSL
  • 새로운 Compose 컴파일러 Gradle 플러그인
  • JVM과 Android 게시 라이브러리를 구분하는 새 속성
  • Kotlin/Native에서 CInteropProcess의 Gradle 의존성 처리 개선
  • Gradle의 가시성 변경
  • Gradle 프로젝트에서 Kotlin 데이터를 위한 새 디렉터리
  • 필요할 때 다운로드되는 Kotlin/Native 컴파일러
  • 컴파일러 옵션을 정의하는 기존 방식의 사용 중단
  • 지원되는 최소 AGP 버전 상향
  • 최신 언어 버전을 시험하기 위한 새 Gradle 속성
  • 빌드 리포트의 새 JSON 출력 형식
  • kapt 구성이 상위 구성에서 어노테이션 프로세서 상속
  • Kotlin Gradle 플러그인이 더 이상 사용 중단된 Gradle 규약을 사용하지 않음

multiplatform 프로젝트를 위한 컴파일러 옵션의 새로운 Gradle DSL

이 기능은 실험적(Experimental)이며 언제든 제거되거나 변경될 수 있어요. 평가 목적으로만 사용하세요. YouTrack에서 피드백을 남겨주시면 감사하겠어요.

Kotlin 2.0.0 이전에는 Gradle로 multiplatform 프로젝트에서 컴파일러 옵션을 구성하는 것이 태스크, 컴파일 또는 소스 세트 단위 같은 낮은 수준에서만 가능했어요. 프로젝트에서 컴파일러 옵션을 더 일반적으로 구성하기 쉽게 만들기 위해 Kotlin 2.0.0은 새 Gradle DSL과 함께 제공돼요.

이 새 DSL을 사용하면 commonMain 같은 모든 타깃과 공유 소스 세트에 대해 확장 수준에서, 그리고 특정 타깃에 대해 타깃 수준에서 컴파일러 옵션을 구성할 수 있어요.

kotlin {
    compilerOptions {
        // Extension-level common compiler options that are used as defaults
        // for all targets and shared source sets
        allWarningsAsErrors.set(true)
    }
    jvm {
        compilerOptions {
            // Target-level JVM compiler options that are used as defaults
            // for all compilations in this target
            noJdk.set(true)
        }
    }
}

전체 프로젝트 구성은 이제 세 개의 계층으로 구성돼요. 가장 높은 곳은 확장 수준이고, 그다음 타깃 수준, 가장 낮은 곳은 컴파일 단위(보통 컴파일 태스크)예요.

더 높은 수준의 설정은 더 낮은 수준의 규약(기본값)으로 사용돼요.

  • 확장 컴파일러 옵션의 값은 commonMain, nativeMain, commonTest 같은 공유 소스 세트를 포함한 타깃 컴파일러 옵션의 기본값이에요.
  • 타깃 컴파일러 옵션의 값은 compileKotlinJvmcompileTestKotlinJvm 태스크 같은 컴파일 단위(태스크) 컴파일러 옵션의 기본값으로 사용돼요.

반대로 더 낮은 수준에서 만든 구성은 더 높은 수준의 관련 설정을 재정의해요.

  • 태스크 수준 컴파일러 옵션은 타깃 또는 확장 수준의 관련 구성을 재정의해요.
  • 타깃 수준 컴파일러 옵션은 확장 수준의 관련 구성을 재정의해요.

프로젝트를 구성할 때 컴파일러 옵션을 설정하는 일부 기존 방식이 사용 중단되었음을 명심하세요.

이 DSL을 multiplatform 프로젝트에서 사용해 보고 YouTrack에 피드백을 남겨주시길 권장해요. 이 DSL을 컴파일러 옵션 구성의 권장 방식으로 만들 계획이에요.

새로운 Compose 컴파일러 Gradle 플러그인

composable을 Kotlin 코드로 변환하는 Jetpack Compose 컴파일러가 이제 Kotlin 저장소로 통합됐어요. 이로 인해 Compose 컴파일러가 항상 Kotlin과 동시에 배포되므로 Compose 프로젝트를 Kotlin 2.0.0으로 전환하는 데 도움이 될 거예요. 또한 Compose 컴파일러 버전이 2.0.0으로 올라가요.

프로젝트에서 새 Compose 컴파일러를 사용하려면 build.gradle(.kts) 파일에 org.jetbrains.kotlin.plugin.compose Gradle 플러그인을 적용하고 그 버전을 Kotlin 2.0.0과 동일하게 설정하세요.

이 변경에 대해 자세히 알아보고 마이그레이션 지침을 보려면 Compose 컴파일러 문서를 참고하세요.

JVM과 Android 게시 라이브러리를 구분하는 새 속성

Kotlin 2.0.0부터 org.gradle.jvm.environment Gradle 속성은 모든 Kotlin 변형과 함께 기본적으로 게시돼요.

이 속성은 Kotlin Multiplatform 라이브러리의 JVM과 Android 변형을 구분하는 데 도움을 줘요. 특정 라이브러리 변형이 특정 JVM 환경에 더 적합하다는 것을 나타내요. 대상 환경은 "android", "standard-jvm", 또는 "no-jvm"일 수 있어요.

이 속성을 게시하면 Java 전용 프로젝트 같은 non-multiplatform 클라이언트에서도 JVM과 Android 타깃이 있는 Kotlin Multiplatform 라이브러리를 사용하는 것이 더 견고해져야 해요.

필요하면 속성 게시를 비활성화할 수 있어요. 그러려면 gradle.properties 파일에 다음 Gradle 옵션을 추가하세요.

kotlin.publishJvmEnvironmentAttribute=false

Kotlin/Native에서 CInteropProcess의 Gradle 의존성 처리 개선

이 릴리스에서는 Kotlin/Native 프로젝트에서 더 나은 Gradle 태스크 의존성 관리를 보장하기 위해 defFile 프로퍼티 처리 방식을 개선했어요.

이 업데이트 전에는 defFile 프로퍼티가 아직 실행되지 않은 다른 태스크의 출력으로 지정된 경우 Gradle 빌드가 실패할 수 있었어요. 이 문제의 해결 방법은 이 태스크에 의존성을 추가하는 것이었어요.

kotlin {
    macosArm64("native") {
        compilations.getByName("main") {
            cinterops {
                val cinterop by creating {
                    defFileProperty.set(createDefFileTask.flatMap { it.defFile.asFile })
                    project.tasks.named(interopProcessingTaskName).configure {
                        dependsOn(createDefFileTask)
                    }
                }
            }
        }
    }
}

이를 해결하기 위해 definitionFile이라는 새 RegularFileProperty 프로퍼티가 추가됐어요. 이제 Gradle은 빌드 과정의 나중에 연결된 태스크가 실행된 후 definitionFile 프로퍼티의 존재를 지연(lazily) 확인해요. 이 새 접근 방식은 추가 의존성의 필요성을 없애줘요.

CInteropProcess 태스크와 CInteropSettings 클래스는 defFiledefFileProperty 대신 definitionFile 프로퍼티를 사용해요.

kotlin {
    macosArm64("native") {
        compilations.getByName("main") {
            cinterops {
                val cinterop by creating {
                    definitionFile.set(project.file("def-file.def"))
                }
            }
        }
    }
}
kotlin {
    macosArm64("native") {
        compilations.main {
            cinterops {
                cinterop {
                    definitionFile.set(project.file("def-file.def"))
                }
            }
        }
    }
}

defFiledefFileProperty 매개변수는 사용 중단됐어요.

Gradle에서의 가시성 변경

이 변경은 Kotlin DSL 사용자에게만 영향을 줘요.

Kotlin 2.0.0에서는 빌드 스크립트에서 더 나은 제어와 안전을 위해 Kotlin Gradle 플러그인을 수정했어요. 이전에는 특정 DSL 컨텍스트를 위한 일부 Kotlin DSL 함수와 프로퍼티가 실수로 다른 DSL 컨텍스트로 누출됐어요. 이 누출로 올바르지 않은 컴파일러 옵션 사용, 설정이 여러 번 적용되는 문제, 기타 잘못된 구성이 발생할 수 있었어요.

kotlin {
    // Target DSL couldn't access methods and properties defined in the
    // kotlin{} extension DSL
    jvm {
        // Compilation DSL couldn't access methods and properties defined
        // in the kotlin{} extension DSL and Kotlin jvm{} target DSL
        compilations.configureEach {
            // Compilation task DSLs couldn't access methods and
            // properties defined in the kotlin{} extension, Kotlin jvm{}
            // target or Kotlin compilation DSL
            compileTaskProvider.configure {
                // For example:
                explicitApi()
                // ERROR as it is defined in the kotlin{} extension DSL
                mavenPublication {}
                // ERROR as it is defined in the Kotlin jvm{} target DSL
                defaultSourceSet {}
                // ERROR as it is defined in the Kotlin compilation DSL
            }
        }
    }
}

이 문제를 해결하기 위해 @KotlinGradlePluginDsl 어노테이션을 추가해서 Kotlin Gradle 플러그인 DSL 함수와 프로퍼티가 의도되지 않은 수준으로 노출되는 것을 방지했어요. 다음 수준들이 서로 분리됐어요.

  • Kotlin 확장
  • Kotlin 타깃
  • Kotlin 컴파일
  • Kotlin 컴파일 태스크

가장 일반적인 경우에는 빌드 스크립트가 잘못 구성된 경우 수정 방법에 대한 제안과 함께 컴파일러 경고를 추가했어요. 예를 들어:

kotlin {
    jvm {
        sourceSets.getByName("jvmMain").dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
        }
    }
}

이 경우 sourceSets에 대한 경고 메시지는 다음과 같아요.

[DEPRECATION] 'sourceSets: NamedDomainObjectContainer<KotlinSourceSet>' is deprecated.Accessing 'sourceSets' container on the Kotlin target level DSL is deprecated. Consider configuring 'sourceSets' on the Kotlin extension level.

이 변경에 대한 피드백을 환영해요! #gradle Slack 채널에서 Kotlin 개발자에게 직접 의견을 공유해 주세요. Slack 초대 받기.

Gradle 프로젝트에서 Kotlin 데이터를 위한 새 디렉터리

.kotlin 디렉터리를 버전 관리에 커밋하지 마세요. 예를 들어 Git을 사용한다면 .kotlin을 프로젝트의 .gitignore 파일에 추가하세요.

Kotlin 1.8.20에서 Kotlin Gradle 플러그인은 데이터를 Gradle 프로젝트 캐시 디렉터리 <project-root-directory>/.gradle/kotlin에 저장하는 방식으로 전환했어요. 하지만 .gradle 디렉터리는 Gradle 전용으로 예약되어 있어서 미래 지향적이지 않아요.

이를 해결하기 위해 Kotlin 2.0.0부터 기본적으로 Kotlin 데이터를 <project-root-directory>/.kotlin에 저장해요. 하위 호환성을 위해 일부 데이터는 계속 .gradle/kotlin 디렉터리에 저장할 거예요.

구성할 수 있는 새로운 Gradle 속성은 다음과 같아요.

Gradle 속성 설명
kotlin.project.persistent.dir 프로젝트 수준 데이터가 저장되는 위치를 구성해요. 기본값: <project-root-directory>/.kotlin
kotlin.project.persistent.dir.gradle.disableWrite .gradle 디렉터리로 Kotlin 데이터 쓰기를 비활성화할지 제어하는 boolean 값. 기본값: false

이 속성들이 적용되려면 프로젝트의 gradle.properties 파일에 추가하세요.

필요할 때 다운로드되는 Kotlin/Native 컴파일러

Kotlin 2.0.0 이전에는 multiplatform 프로젝트의 Gradle 빌드 스크립트에 Kotlin/Native 타깃이 구성되어 있으면 Gradle이 항상 구성 단계에서 Kotlin/Native 컴파일러를 다운로드했어요.

이는 실행 단계에서 실행할 Kotlin/Native 타깃용 코드 컴파일 태스크가 없더라도 발생했어요. 이렇게 Kotlin/Native 컴파일러를 다운로드하는 것은 프로젝트의 JVM 또는 JavaScript 코드만 확인하려는 사용자에게 특히 비효율적이었어요. 예를 들어 CI 과정의 일부로 Kotlin 프로젝트로 테스트나 검사를 수행하려는 경우를 말해요.

Kotlin 2.0.0에서는 Kotlin Gradle 플러그인에서 이 동작을 변경해 Kotlin/Native 컴파일러가 실행 단계에서, 그리고 Kotlin/Native 타깃에 대한 컴파일이 요청된 경우에만 다운로드되도록 했어요.

또한 Kotlin/Native 컴파일러의 의존성도 컴파일러의 일부가 아니라 실행 단계에서 다운로드되도록 바뀌었어요.

새 동작에 문제가 있으면 gradle.properties 파일에 다음 Gradle 속성을 추가해 이전 동작으로 일시적으로 되돌릴 수 있어요.

kotlin.native.toolchain.enabled=false

Kotlin 1.9.20-Beta부터 Kotlin/Native 배포판은 CDN과 함께 Maven Central에도 게시돼요.

이로써 Kotlin이 필요한 아티팩트를 찾고 다운로드하는 방식을 변경할 수 있었어요. 기본적으로 CDN 대신 프로젝트의 repositories {} 블록에 지정한 Maven 저장소를 사용해요.

gradle.properties 파일에 다음 Gradle 속성을 설정해 이 동작을 일시적으로 되돌릴 수 있어요.

kotlin.native.distribution.downloadFromMaven=false

문제가 있으면 이슈 트래커 YouTrack에 보고해 주세요. 기본 동작을 변경하는 이 두 Gradle 속성은 모두 임시적이며 향후 릴리스에서 제거될 거예요.

컴파일러 옵션을 정의하는 기존 방식의 사용 중단

이 릴리스에서는 컴파일러 옵션을 설정하는 방식을 계속 개선하고 있어요. 서로 다른 방식 사이의 모호함을 해결하고 프로젝트 구성을 더 간단하게 만들어야 해요.

Kotlin 2.0.0부터 컴파일러 옵션을 지정하기 위한 다음 DSL이 사용 중단됐어요.

  • 모든 Kotlin 컴파일 태스크를 구현하는 KotlinCompile 인터페이스의 kotlinOptions DSL. 대신 KotlinCompilationTask<CompilerOptions>를 사용하세요.
  • KotlinCompilation 인터페이스의 HasCompilerOptions 타입을 가진 compilerOptions 프로퍼티. 이 DSL은 다른 DSL과 일관되지 않았고, KotlinCompilation.compileTaskProvider 컴파일 태스크 안의 compilerOptions와 동일한 KotlinCommonCompilerOptions 객체를 구성해 혼란을 일으켰어요. 대신 Kotlin 컴파일 태스크의 compilerOptions 프로퍼티를 사용하는 것을 권장해요.
kotlinCompilation.compileTaskProvider.configure {
    compilerOptions { ... }
}

예를 들어:

kotlin {
    js(IR) {
        compilations.all {
            compileTaskProvider.configure {
                compilerOptions.freeCompilerArgs.add("-Xir-minimized-member-names=false")
            }
        }
    }
}
  • KotlinCompilation 인터페이스의 kotlinOptions DSL.
  • KotlinNativeArtifactConfig 인터페이스, KotlinNativeLink 클래스, KotlinNativeLinkArtifactTask 클래스의 kotlinOptions DSL. 대신 toolOptions DSL을 사용하세요.
  • KotlinJsDce 인터페이스의 dceOptions DSL. 대신 toolOptions DSL을 사용하세요.

Kotlin Gradle 플러그인에서 컴파일러 옵션을 지정하는 방법에 대한 자세한 내용은 옵션 정의 방법을 참고하세요.

지원되는 최소 AGP 버전 상향

Kotlin 2.0.0부터 지원되는 최소 Android Gradle 플러그인 버전은 7.1.3이에요.

최신 언어 버전을 시험하기 위한 새 Gradle 속성

Kotlin 2.0.0 이전에는 새 K2 컴파일러를 시험해 보기 위해 kotlin.experimental.tryK2라는 Gradle 속성이 있었어요. 이제 Kotlin 2.0.0에서 K2 컴파일러가 기본으로 활성화되었으므로, 이 속성을 프로젝트에서 최신 언어 버전을 시험해 볼 수 있는 새 형태로 발전시키기로 결정했어요: kotlin.experimental.tryNext. gradle.properties 파일에서 이 속성을 사용하면 Kotlin Gradle 플러그인이 언어 버전을 Kotlin 버전의 기본값보다 하나 높게 올려요. 예를 들어 Kotlin 2.0.0에서 기본 언어 버전은 2.0이므로 이 속성은 언어 버전 2.1을 구성해요.

이 새 Gradle 속성은 이전에 kotlin.experimental.tryK2에서와 같이 빌드 리포트에서 유사한 지표를 생성해요. 구성된 언어 버전이 출력에 포함돼요. 예를 들어:

##### 'kotlin.experimental.tryNext' results #####
:app:compileKotlin: 2.1 language version
:lib:compileKotlin: 2.1 language version
##### 100% (2/2) tasks have been compiled with Kotlin 2.1 #####

빌드 리포트를 활성화하고 그 내용을 알아보려면 빌드 리포트를 참고하세요.

빌드 리포트의 새 JSON 출력 형식

Kotlin 1.7.0에서 컴파일러 성능을 추적하는 데 도움을 주기 위해 빌드 리포트를 도입했어요. 시간이 지나면서 성능 문제를 조사할 때 리포트를 더 자세하고 유용하게 만들기 위해 더 많은 지표를 추가했어요. 이전에는 로컬 파일의 유일한 출력 형식이 *.txt 형식이었어요. Kotlin 2.0.0에서는 다른 도구로 분석하기 더 쉽도록 JSON 출력 형식을 지원해요.

빌드 리포트의 JSON 출력 형식을 구성하려면 gradle.properties 파일에 다음 속성을 선언하세요.

kotlin.build.report.output=json

// The directory to store your build reports
kotlin.build.report.json.directory=my/directory/path

또는 다음 명령을 실행할 수 있어요.

./gradlew assemble -Pkotlin.build.report.output=json -Pkotlin.build.report.json.directory="my/directory/path"

구성이 끝나면 Gradle은 지정한 디렉터리에 ${project_name}-date-time-<sequence_number>.json이라는 이름으로 빌드 리포트를 생성해요.

다음은 빌드 메트릭과 집계 메트릭을 포함한 JSON 출력 형식의 빌드 리포트 예시 스니펫이에요.

"buildOperationRecord": [
    {
     "path": ":lib:compileKotlin",
      "classFqName": "org.jetbrains.kotlin.gradle.tasks.KotlinCompile_Decorated",
      "startTimeMs": 1714730820601,
      "totalTimeMs": 2724,
      "buildMetrics": {
        "buildTimes": {
          "buildTimesNs": {
            "CLEAR_OUTPUT": 713417,
            "SHRINK_AND_SAVE_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION": 19699333,
            "IR_TRANSLATION": 281000000,
            "NON_INCREMENTAL_LOAD_CURRENT_CLASSPATH_SNAPSHOT": 14088042,
            "CALCULATE_OUTPUT_SIZE": 1301500,
            "GRADLE_TASK": 2724000000,
            "COMPILER_INITIALIZATION": 263000000,
            "IR_GENERATION": 74000000,
...
          }
        }
...
 "aggregatedMetrics": {
    "buildTimes": {
      "buildTimesNs": {
        "CLEAR_OUTPUT": 782667,
        "SHRINK_AND_SAVE_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION": 22031833,
        "IR_TRANSLATION": 333000000,
        "NON_INCREMENTAL_LOAD_CURRENT_CLASSPATH_SNAPSHOT": 14890292,
        "CALCULATE_OUTPUT_SIZE": 2370750,
        "GRADLE_TASK": 3234000000,
        "COMPILER_INITIALIZATION": 292000000,
        "IR_GENERATION": 89000000,
...
      }
    }

kapt 구성이 상위 구성에서 어노테이션 프로세서 상속

Kotlin 2.0.0 이전에는 별도의 Gradle 구성에서 어노테이션 프로세서의 공통 집합을 정의하고 하위 프로젝트의 kapt 전용 구성에서 이 구성을 확장하려면, kapt가 어노테이션 프로세서를 찾을 수 없어서 어노테이션 처리를 건너뛰었어요. Kotlin 2.0.0에서 kapt는 어노테이션 프로세서에 대한 간접 의존성이 있음을 성공적으로 감지할 수 있어요.

예를 들어 Dagger를 사용하는 하위 프로젝트의 경우 build.gradle(.kts) 파일에서 다음 구성을 사용하세요.

val commonAnnotationProcessors by configurations.creating
configurations.named("kapt") { extendsFrom(commonAnnotationProcessors) }

dependencies {
    implementation("com.google.dagger:dagger:2.48.1")
    commonAnnotationProcessors("com.google.dagger:dagger-compiler:2.48.1")
}

이 예시에서 commonAnnotationProcessors Gradle 구성은 모든 프로젝트에서 사용하려는 어노테이션 처리를 위한 공통 구성이에요. extendsFrom() 메서드를 사용해 commonAnnotationProcessors를 상위 구성으로 추가해요. kapt는 commonAnnotationProcessors Gradle 구성이 Dagger 어노테이션 프로세서에 대한 의존성을 가지고 있음을 봐요. 따라서 kapt는 어노테이션 처리 구성에 Dagger 어노테이션 프로세서를 포함해요.

구현을 제공한 Christoph Loy에게 감사드려요!

Kotlin Gradle 플러그인이 더 이상 사용 중단된 Gradle 규약을 사용하지 않음

Kotlin 2.0.0 이전에는 Gradle 8.2 이상을 사용하면 Kotlin Gradle 플러그인이 Gradle 8.2에서 사용 중단된 Gradle 규약을 잘못 사용했어요. 이로 인해 Gradle이 빌드 사용 중단을 보고했어요. Kotlin 2.0.0에서 Kotlin Gradle 플러그인은 Gradle 8.2 이상을 사용할 때 이러한 사용 중단 경고를 더 이상 발생시키지 않도록 업데이트됐어요.

표준 라이브러리

이 릴리스는 Kotlin 표준 라이브러리에 더 많은 안정성을 가져오고 더 많은 기존 함수를 모든 플랫폼에 공통으로 만들어요.

  • enum class values 일반 함수의 Stable 대체
  • Stable AutoCloseable 인터페이스
  • 공통 보호 프로퍼티 AbstractMutableList.modCount
  • 공통 보호 함수 AbstractMutableList.removeRange
  • 공통 String.toCharArray(destination)

enum class values 일반 함수의 Stable 대체

Kotlin 2.0.0에서 enumEntries<T>() 함수가 Stable이 됐어요. enumEntries<T>() 함수는 일반 enumValues<T>() 함수의 대체예요. 새 함수는 주어진 enum 타입 T의 모든 enum 항목 목록을 반환해요. enum class의 entries 프로퍼티는 이전에 도입되어 합성 values() 함수를 대체하기 위해 안정화됐어요. entries 프로퍼티에 대한 자세한 내용은 Kotlin 1.8.20의 새로운 기능을 참고하세요.

enumValues<T>() 함수는 여전히 지원되지만, 성능 영향이 적으므로 enumEntries<T>() 함수를 대신 사용하는 것을 권장해요. enumValues<T>()를 호출할 때마다 새 배열이 생성되는 반면, enumEntries<T>()를 호출할 때마다 동일한 목록이 반환되어 훨씬 더 효율적이에요.

예를 들어:

enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    print(enumEntries<T>().joinToString { it.name })
}

printAllValues<RGB>()
// RED, GREEN, BLUE

Stable AutoCloseable 인터페이스

Kotlin 2.0.0에서 공통 AutoCloseable 인터페이스가 Stable이 됐어요. 리소스를 쉽게 닫을 수 있고 몇 가지 유용한 함수를 포함해요.

  • use() 확장 함수는 선택한 리소스에서 주어진 블록 함수를 실행한 다음 예외가 발생하는지 여부와 관계없이 리소스를 올바르게 닫아요.
  • AutoCloseable() 생성자 함수는 AutoCloseable 인터페이스의 인스턴스를 만들어요.

아래 예시에서 XMLWriter 인터페이스를 정의하고 이를 구현하는 리소스가 있다고 가정해요. 예를 들어 이 리소스는 파일을 열고 XML 콘텐츠를 작성한 다음 파일을 닫는 클래스일 수 있어요.

interface XMLWriter {
    fun document(encoding: String, version: String, content: XMLWriter.() -> Unit)
    fun element(name: String, content: XMLWriter.() -> Unit)
    fun attribute(name: String, value: String)
    fun text(value: String)

    fun flushAndClose()
}

fun writeBooksTo(writer: XMLWriter) {
    val autoCloseable = AutoCloseable { writer.flushAndClose() }
    autoCloseable.use {
        writer.document(encoding = "UTF-8", version = "1.0") {
            element("bookstore") {
                element("book") {
                    attribute("category", "fiction")
                    element("title") { text("Harry Potter and the Prisoner of Azkaban") }
                    element("author") { text("J. K. Rowling") }
                    element("year") { text("1999") }
                    element("price") { text("29.99") }
                }
                element("book") {
                    attribute("category", "programming")
                    element("title") { text("Kotlin in Action") }
                    element("author") { text("Dmitry Jemerov") }
                    element("author") { text("Svetlana Isakova") }
                    element("year") { text("2017") }
                    element("price") { text("25.19") }
                }
            }
        }
    }
}

이 예시에서 AutoCloseable { writer.flushAndClose() }는 리소스를 닫을 동작을 정의하고, use {} 블록 안에서 작업을 수행한 후 리소스를 올바르게 닫아줘요.

공통 보호 프로퍼티 AbstractMutableList.modCount

이 릴리스에서 AbstractMutableList 인터페이스의 modCount protected 프로퍼티가 공통이 됐어요. 이전에는 modCount 프로퍼티가 각 플랫폼에서 사용할 수 있었지만 공통 타깃에서는 사용할 수 없었어요. 이제 AbstractMutableList의 사용자 정의 구현을 만들고 공통 코드에서 프로퍼티에 접근할 수 있어요.

이 프로퍼티는 컬렉션에 가해진 구조적 수정 횟수를 추적해요. 여기에는 컬렉션 크기를 변경하거나 진행 중인 반복이 잘못된 결과를 반환하게 만들 수 있는 방식으로 목록을 변경하는 작업이 포함돼요.

사용자 정의 목록을 구현할 때 modCount 프로퍼티를 사용해 동시 수정을 등록하고 감지할 수 있어요.

공통 보호 함수 AbstractMutableList.removeRange

이 릴리스에서 AbstractMutableList 인터페이스의 removeRange() protected 함수가 공통이 됐어요. 이전에는 각 플랫폼에서 사용할 수 있었지만 공통 타깃에서는 사용할 수 없었어요. 이제 AbstractMutableList의 사용자 정의 구현을 만들고 공통 코드에서 함수를 재정의할 수 있어요.

이 함수는 지정된 범위에 따라 이 목록에서 요소를 제거해요. 이 함수를 재정의하면 사용자 정의 구현을 활용하고 목록 작업의 성능을 개선할 수 있어요.

공통 String.toCharArray(destination) 함수

이 릴리스는 공통 String.toCharArray(destination) 함수를 도입해요. 이전에는 JVM에서만 사용할 수 있었어요.

기존 String.toCharArray() 함수와 비교해 볼게요. 이 함수는 지정된 문자열의 문자를 포함하는 새 CharArray를 만들어요. 반면 새 공통 String.toCharArray(destination) 함수는 String 문자를 기존 대상 CharArray로 옮겨요. 이미 채우고 싶은 버퍼가 있다면 유용해요.

fun main() {
    val myString = "Kotlin is awesome!"
    val destinationArray = CharArray(myString.length)

    // Convert the string and store it in the destinationArray:
    myString.toCharArray(destinationArray)

    for (char in destinationArray) {
        print("$char ")
        // K o t l i n   i s   a w e s o m e ! 
    }
}

Kotlin 2.0.0 설치

IntelliJ IDEA 2023.3과 Android Studio Iguana (2023.2.1) Canary 15부터 Kotlin 플러그인은 IDE에 포함된 번들 플러그인으로 배포돼요. 즉, 더 이상 JetBrains Marketplace에서 플러그인을 설치할 수 없어요.

더 알아보기