Kotlin 2.2.0의 새로운 기능

Kotlin 2.2.0의 새로운 기능

Kotlin 2.2.0 릴리스가 출시됐어요! 이번 릴리스의 주요 하이라이트는 다음과 같아요:

Kotlin Language Evolution 팀이 새로운 기능을 소개하고 질문에 답하는 이 영상도 확인해 볼 수 있어요.

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

출처: What's new in Kotlin 2.2.0

본문

IDE 지원

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

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

언어

이번 릴리스에서는 guard conditions, non-local breakcontinue, multi-dollar interpolation을 Stable승격시켰어요. 또한 context parameterscontext-sensitive resolution 같은 여러 기능이 preview로 도입됐어요.

Context parameters의 preview

Context parameters를 사용하면 함수와 프로퍼티가 주변 컨텍스트에서 암시적으로 사용할 수 있는 의존성을 선언할 수 있어요.

context parameters를 쓰면 서비스나 의존성처럼 함수 호출 집합에서 공유되고 잘 바뀌지 않는 값을 일일이 전달할 필요가 없어져요.

context parameters는 예전의 실험적 기능인 context receivers를 대체해요. context receivers에서 context parameters로 마이그레이션하려면 블로그 포스트에 설명된 대로 IntelliJ IDEA의 지원 기능을 이용할 수 있어요.

가장 큰 차이는 context parameters가 함수 본문에서 receiver로 도입되지 않는다는 점이에요. 그래서 컨텍스트가 암시적으로 제공되는 context receivers와 달리, 멤버에 접근하려면 context parameters의 이름을 사용해야 해요.

Kotlin의 context parameters는 단순화된 의존성 주입, 개선된 DSL 설계, 범위가 제한된 작업을 통해 의존성 관리를 크게 개선해 주는 기능이에요. 자세한 내용은 해당 기능의 KEEP을 확인하세요.

context parameters 선언하는 방법

context 키워드 뒤에 각각 name: Type 형태의 파라미터 목록을 붙여서 프로퍼티와 함수의 context parameters를 선언할 수 있어요. UserService 인터페이스에 의존하는 예시를 볼게요.

// UserService is the dependency required in the context
interface UserService {
    fun log(message: String)
    fun findUserById(id: Int): String
}

// Declares a function with a context parameter
context(users: UserService)
fun outputMessage(message: String) {
    // Uses log from the context
    users.log("Log: $message")
}

// Declares a property with a context parameter
context(users: UserService)
val firstUser: String
    // Uses findUserById from the context
    get() = users.findUserById(1)

context parameter 이름으로 _을 사용할 수도 있어요. 이 경우 파라미터 값은 해석에는 사용할 수 있지만 블록 안에서 이름으로 접근할 수는 없어요:

// Uses "_" as context parameter name
context(_: UserService)
fun logWelcome() {
    // Finds the appropriate log function from UserService
    outputMessage("Welcome!")
}

context parameters 활성화하는 방법

프로젝트에서 context parameters를 활성화하려면 커맨드 라인에서 다음 컴파일러 옵션을 쓰세요:

-Xcontext-parameters

또는 Gradle 빌드 파일의 compilerOptions {} 블록에 추가해도 돼요:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xcontext-parameters")
    }
}

-Xcontext-receivers-Xcontext-parameters 컴파일러 옵션을 동시에 지정하면 오류가 발생해요.

피드백 남기기

이 기능은 향후 Kotlin 릴리스에서 안정화되고 개선될 예정이에요. 이슈 트래커인 YouTrack에 피드백을 남겨 주시면 감사하겠어요.

Context-sensitive resolution의 preview

Kotlin 2.2.0은 preview로 context-sensitive resolution 구현을 도입했어요.

이 기능의 개요는 이 영상에서 확인할 수 있어요.

이전에는 타입이 컨텍스트에서 추론될 수 있을 때조차 enum entry나 sealed class 멤버의 전체 이름을 써야 했어요. 예를 들면:

enum class Problem {
    CONNECTION, AUTHENTICATION, DATABASE, UNKNOWN
}

fun message(problem: Problem): String = when (problem) {
    Problem.CONNECTION -> "connection"
    Problem.AUTHENTICATION -> "authentication"
    Problem.DATABASE -> "database"
    Problem.UNKNOWN -> "unknown"
}

이제 context-sensitive resolution을 사용하면 기대 타입이 알려진 컨텍스트에서 타입 이름을 생략할 수 있어요:

enum class Problem {
    CONNECTION, AUTHENTICATION, DATABASE, UNKNOWN
}

// Resolves enum entries based on the known type of problem
fun message(problem: Problem): String = when (problem) {
    CONNECTION -> "connection"
    AUTHENTICATION -> "authentication"
    DATABASE -> "database"
    UNKNOWN -> "unknown"
}

컴파일러는 이 컨텍스트 타입 정보를 사용해 올바른 멤버를 해석해요. 이 정보에는 다음과 같은 것들이 포함돼요:

  • when 식의 subject
  • 명시적 반환 타입
  • 선언된 변수 타입
  • 타입 검사(is)와 캐스트(as)
  • sealed class 계층의 알려진 타입
  • 파라미터의 선언 타입

context-sensitive resolution은 함수, 파라미터가 있는 프로퍼티, receiver가 있는 확장 프로퍼티에는 적용되지 않아요.

프로젝트에서 context-sensitive resolution을 시험해 보려면 커맨드 라인에서 다음 컴파일러 옵션을 쓰세요:

-Xcontext-sensitive-resolution

또는 Gradle 빌드 파일의 compilerOptions {} 블록에 추가해도 돼요:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xcontext-sensitive-resolution")
    }
}

이 기능은 향후 Kotlin 릴리스에서 안정화·개선할 계획이며, 이슈 트래커 YouTrack에 피드백을 남겨 주시면 감사하겠어요.

애노테이션 use-site target 기능의 preview

Kotlin 2.2.0은 애노테이션 use-site target을 더 편리하게 다루는 데 도움을 주는 기능 몇 가지를 도입했어요.

프로퍼티를 위한 @all 메타 타깃

Kotlin은 use-site target이라 불리는 선언의 특정 부분에 애노테이션을 붙일 수 있어요. 하지만 각 target을 개별적으로 붙이는 것은 복잡하고 오류가 발생하기 쉬웠어요:

data class User(
    val username: String,

    @param:Email      // Constructor parameter
    @field:Email      // Backing field
    @get:Email        // Getter method
    @property:Email   // Kotlin property reference
    val email: String,
) {
    @field:Email
    @get:Email
    @property:Email
    val secondaryEmail: String? = null
}

이를 단순화하기 위해 Kotlin은 프로퍼티를 위한 새로운 @all 메타 타깃을 도입했어요. 이 기능은 애노테이션을 프로퍼티의 모든 관련 부분에 적용하라고 컴파일러에 지시해요. 사용하면 @all은 다음 대상에 애노테이션을 적용하려 시도해요:

  • param: 기본 생성자에 선언된 경우 생성자 파라미터.
  • property: Kotlin 프로퍼티 자체.
  • field: 존재한다면 backing field.
  • get: getter 메서드.
  • setparam: 프로퍼티가 var로 정의된 경우 setter 메서드의 파라미터.
  • RECORD_COMPONENT: 클래스가 @JvmRecord라면 Java record component에 애노테이션이 적용돼요. 이 동작은 Java가 record component에서 애노테이션을 처리하는 방식을 모방한 거예요.

컴파일러는 주어진 프로퍼티의 대상에만 애노테이션을 적용해요.

아래 예시에서는 @Email 애노테이션이 각 프로퍼티의 모든 관련 target에 적용돼요:

data class User(
    val username: String,

    // Applies @Email to param, property, field,
    // get, and setparam (if var)
    @all:Email val email: String,
) {
    // Applies @Email to property, field, and get
    // (no param since it's not in the constructor)
    @all:Email val secondaryEmail: String? = null
}

@all 메타 타깃은 기본 생성자 안팎에서 모든 프로퍼티에 사용할 수 있어요. 하지만 여러 애노테이션과 함께 @all 메타 타깃을 사용할 수는 없어요.

이 새 기능은 문법을 단순화하고, 일관성을 보장하며, Java records와의 상호 운용성을 개선해요.

프로젝트에서 @all 메타 타깃을 활성화하려면 커맨드 라인에서 다음 컴파일러 옵션을 쓰세요:

-Xannotation-target-all

또는 Gradle 빌드 파일의 compilerOptions {} 블록에 추가해도 돼요:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xannotation-target-all")
    }
}

이 기능은 preview 단계예요. 문제가 있으면 이슈 트래커 YouTrack에 보고해 주세요. @all 메타 타깃에 대한 자세한 내용은 이 KEEP 제안서를 읽어 보세요.

use-site 애노테이션 target의 새로운 기본 규칙

Kotlin 2.2.0은 애노테이션을 파라미터, 필드, 프로퍼티로 전파하는 새로운 기본 규칙을 도입했어요. 이전에는 기본적으로 애노테이션이 param, property, field 중 하나에만 적용되었는데, 이제 기본값이 애노테이션에 기대되는 동작에 더 부합하게 되었어요.

적용 가능한 target이 여러 개라면 다음 규칙에 따라 하나 이상이 선택돼요:

  • 생성자 파라미터 target(param)이 적용 가능하면 그것이 사용돼요.
  • 프로퍼티 target(property)이 적용 가능하면 그것이 사용돼요.
  • 필드 target(field)이 적용 가능하고 property는 아니라면 field가 사용돼요.

target이 여러 개인데 param, property, field 중 어느 것도 적용 가능하지 않으면 애노테이션은 오류가 돼요.

이 기능을 활성화하려면 Gradle 빌드 파일의 compilerOptions {} 블록에 추가하세요:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xannotation-default-target=param-property")
    }
}

또는 컴파일러의 커맨드 라인 인수를 사용해도 돼요:

-Xannotation-default-target=param-property

이전 동작을 사용하고 싶다면 다음처럼 할 수 있어요:

  • 특정한 경우에는 필요한 target을 명시적으로 정의해서 사용해요. 예를 들어 @Annotation 대신 @param:Annotation을 쓰면 돼요.
  • 전체 프로젝트에 대해서는 Gradle 빌드 파일에서 다음 플래그를 사용해요:
// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xannotation-default-target=first-only")
    }
}

이 기능은 preview 단계예요. 문제가 있으면 이슈 트래커 YouTrack에 보고해 주세요. 애노테이션 use-site target의 새로운 기본 규칙에 대한 자세한 내용은 이 KEEP 제안서를 읽어 보세요.

중첩 타입 별칭(Nested type aliases) 지원

Kotlin 2.2.0은 다른 선언 안에서 타입 별칭을 정의하는 것을 지원해요.

이 기능의 개요는 이 영상에서 확인할 수 있어요.

이전에는 타입 별칭을 Kotlin 파일의 최상위 레벨에서만 선언할 수 있었어요. 그래서 내부용이거나 도메인 특화된 타입 별칭조차 사용하는 클래스 밖에 둬야 했어요.

2.2.0부터는 외부 클래스의 타입 파라미터를 캡처하지 않는 한, 다른 선언 안에서도 타입 별칭을 정의할 수 있어요:

class Dijkstra {
    typealias VisitedNodes = Set<Node>

    private fun step(visited: VisitedNodes, ...) = ...
}

중첩 타입 별칭에는 타입 파라미터를 언급할 수 없다는 등의 몇 가지 추가 제약이 있어요. 전체 규칙은 문서에서 확인하세요.

중첩 타입 별칭은 캡슐화를 개선하고 패키지 수준의 어수선함을 줄이며 내부 구현을 단순화해서 더 깔끔하고 유지보수하기 쉬운 코드를 만들어 줘요.

중첩 타입 별칭 활성화하는 방법

프로젝트에서 중첩 타입 별칭을 활성화하려면 커맨드 라인에서 다음 컴파일러 옵션을 쓰세요:

-Xnested-type-aliases

또는 Gradle 빌드 파일의 compilerOptions {} 블록에 추가해도 돼요:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xnested-type-aliases")
    }
}

의견 공유하기

중첩 타입 별칭은 현재 Beta 단계예요. 문제가 있으면 이슈 트래커 YouTrack에 보고해 주세요. 이 기능에 대한 자세한 내용은 이 KEEP 제안서를 읽어 보세요.

Stable 기능: Guard conditions, non-local break and continue, multi-dollar interpolation

Kotlin 2.1.0에서 여러 새로운 언어 기능이 preview로 도입되었는데, 이번 릴리스에서 다음 언어 기능이 Stable이 되었다는 소식을 전하게 되어 기뻐요:

Kotlin 언어 설계 기능과 제안의 전체 목록을 확인해 보세요.

Kotlin 컴파일러: 컴파일러 경고의 통합 관리

Kotlin 2.2.0은 새 컴파일러 옵션인 -Xwarning-level을 도입했어요. Kotlin 프로젝트에서 컴파일러 경고를 통합적으로 관리하는 방법을 제공하기 위한 것이에요.

이전에는 -nowarn으로 모든 경고를 끄거나, -Werror로 모든 경고를 컴파일 오류로 바꾸거나, -Wextra로 추가 컴파일러 검사를 켜는 등 모듈 전반에 적용되는 일반 규칙만 사용할 수 있었어요. 특정 경고에 대해 조정하는 유일한 방법은 -Xsuppress-warning 옵션이었죠.

새로운 솔루션을 사용하면 일반 규칙을 재정의하고 특정 진단을 일관된 방식으로 제외할 수 있어요.

적용 방법

새 컴파일러 옵션의 구문은 다음과 같아요:

-Xwarning-level=DIAGNOSTIC_NAME:(error|warning|disabled)
  • error: 지정한 경고를 오류로 승격해요.
  • warning: 경고를 내보내며 기본으로 활성화돼요.
  • disabled: 지정한 경고를 모듈 전체에서 완전히 억제해요.

새 컴파일러 옵션으로는 경고의 심각도 수준만 구성할 수 있다는 점을 기억하세요.

사용 사례

새 솔루션을 사용하면 일반 규칙과 특정 규칙을 결합해 프로젝트의 경고 보고를 더 세밀하게 조정할 수 있어요. 사용 사례를 골라 보세요.

경고 억제하기

Command Description
-nowarn 컴파일 중 모든 경고를 억제해요.
-Xwarning-level=DIAGNOSTIC_NAME:disabled 지정한 경고만 억제해요.
-nowarn -Xwarning-level=DIAGNOSTIC_NAME:warning 지정한 경고를 제외한 모든 경고를 억제해요.

경고를 오류로 올리기

Command Description
-Werror 모든 경고를 컴파일 오류로 올려요.
-Xwarning-level=DIAGNOSTIC_NAME:error 지정한 경고만 오류로 올려요.
-Werror -Xwarning-level=DIAGNOSTIC_NAME:warning 지정한 경고를 제외한 모든 경고를 오류로 올려요.

추가 컴파일러 경고 활성화하기

Command Description
-Wextra 참이면 경고를 내보내는 모든 추가 선언·식·타입 컴파일러 검사를 활성화해요.
-Xwarning-level=DIAGNOSTIC_NAME:warning 지정한 추가 컴파일러 검사만 활성화해요.
-Wextra -Xwarning-level=DIAGNOSTIC_NAME:disabled 지정한 검사를 제외한 모든 추가 검사를 활성화해요.

경고 목록

일반 규칙에서 제외하려는 경고가 많다면 @argfile을 통해 별도의 파일에 나열할 수 있어요.

피드백 남기기

새 컴파일러 옵션은 여전히 Experimental 단계예요. 문제가 있으면 이슈 트래커 YouTrack에 보고해 주세요.

Kotlin/JVM

Kotlin 2.2.0은 JVM에 많은 업데이트를 가져와요. 컴파일러가 이제 Java 24 바이트코드를 지원하고, 인터페이스 함수의 기본 메서드 생성 방식에 변화가 생겼어요. 또한 이 릴리스는 Kotlin metadata에서 애노테이션을 더 쉽게 다루고, 인라인 값 클래스와의 Java 상호 운용성을 개선하며, JVM records에 애노테이션을 붙이는 지원을 강화했어요.

인터페이스 함수의 기본 메서드 생성 변경

Kotlin 2.2.0부터 인터페이스에 선언된 함수는 별도 설정이 없으면 JVM 기본 메서드로 컴파일돼요. 이 변경은 구현이 있는 Kotlin 인터페이스 함수가 바이트코드로 컴파일되는 방식에 영향을 줘요.

이 동작은 더 이상 사용되지 않는 -Xjvm-default 옵션을 대체하는 새 안정 컴파일러 옵션 -jvm-default로 제어돼요.

-jvm-default 옵션의 동작은 다음 값으로 제어할 수 있어요:

  • enable(기본값): 인터페이스에 기본 구현을 생성하고 서브클래스와 DefaultImpls 클래스에 bridge 함수를 포함해요. 이 모드는 이전 Kotlin 버전과의 바이너리 호환성을 유지하는 데 사용해요.
  • no-compatibility: 인터페이스에 기본 구현만 생성해요. 이 모드는 호환성 bridge와 DefaultImpls 클래스를 건너뛰므로 새 코드에 적합해요.
  • disable: 인터페이스의 기본 구현을 비활성화해요. Kotlin 2.2.0 이전처럼 bridge 함수와 DefaultImpls 클래스만 생성돼요.

-jvm-default 컴파일러 옵션을 구성하려면 Gradle Kotlin DSL에서 jvmDefault 프로퍼티를 설정하세요:

// build.gradle.kts
kotlin {
    compilerOptions {
        jvmDefault = JvmDefaultMode.NO_COMPATIBILITY
    }
}

Kotlin metadata에서 애노테이션 읽기·쓰기 지원

이전에는 컴파일된 JVM 클래스 파일에서 reflection이나 바이트코드 분석으로 애노테이션을 읽고, 시그니처를 기준으로 metadata 항목과 수동으로 일치시켜야 했어요. 이 과정은 특히 오버로드된 함수에서 오류가 발생하기 쉬웠죠.

이제 Kotlin 2.2.0에서 Kotlin Metadata JVM 라이브러리가 Kotlin metadata에 저장된 애노테이션을 읽는 것을 지원해요.

컴파일된 파일의 metadata에서 애노테이션을 사용할 수 있게 하려면 다음 컴파일러 옵션을 추가하세요:

-Xannotations-in-metadata

또는 Gradle 빌드 파일의 compilerOptions {} 블록에 추가해도 돼요:

// build.gradle.kts
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xannotations-in-metadata")
    }
}

이 옵션을 활성화하면 Kotlin 컴파일러가 JVM 바이트코드와 함께 metadata에 애노테이션을 작성해서 kotlin-metadata-jvm 라이브러리에서 접근할 수 있게 해줘요.

이 라이브러리는 애노테이션 접근을 위해 다음 API를 제공해요:

  • KmClass.annotations
  • KmFunction.annotations
  • KmProperty.annotations
  • KmConstructor.annotations
  • KmPropertyAccessorAttributes.annotations
  • KmValueParameter.annotations
  • KmFunction.extensionReceiverAnnotations
  • KmProperty.extensionReceiverAnnotations
  • KmProperty.backingFieldAnnotations
  • KmProperty.delegateFieldAnnotations
  • KmEnumEntry.annotations

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

Kotlin metadata에서 애노테이션을 읽는 예시를 볼게요:

@file:OptIn(ExperimentalAnnotationsInMetadata::class)

import kotlin.metadata.ExperimentalAnnotationsInMetadata
import kotlin.metadata.jvm.KotlinClassMetadata

annotation class Label(val value: String)

@Label("Message class")
class Message

fun main() {
    val metadata = Message::class.java.getAnnotation(Metadata::class.java)
    val kmClass = (KotlinClassMetadata.readStrict(metadata) as KotlinClassMetadata.Class).kmClass
    println(kmClass.annotations)
    // [@Label(value = StringValue("Message class"))]
}

프로젝트에서 kotlin-metadata-jvm 라이브러리를 사용한다면, 애노테이션을 지원하도록 코드를 테스트하고 업데이트하는 것을 권장해요. 그렇지 않으면 향후 Kotlin 버전에서 metadata의 애노테이션이 기본으로 활성화될 때 프로젝트가 잘못되거나 불완전한 metadata를 생성할 수 있어요.

문제가 발생하면 이슈 트래커에 보고해 주세요.

인라인 값 클래스와의 Java 상호 운용성 개선

Kotlin 2.2.0은 새 실험적 애노테이션인 @JvmExposeBoxed을 도입했어요. 이 애노테이션은 인라인 값 클래스를 Java에서 더 쉽게 소비할 수 있게 해줘요.

이 기능의 개요는 이 영상에서 확인할 수 있어요.

기본적으로 Kotlin은 인라인 값 클래스를 unboxed 표현으로 컴파일해요. 이 표현은 성능은 좋지만 Java에서 사용하기 어렵거나 아예 불가능한 경우가 많아요. 예를 들면:

@JvmInline value class PositiveInt(val number: Int) {
    init { require(number >= 0) }
}

이 경우 클래스가 unboxed이기 때문에 Java가 호출할 수 있는 생성자가 없어요. 또한 number가 양수임을 보장하기 위해 init 블록을 Java가 트리거할 방법도 없어요.

클래스에 @JvmExposeBoxed을 붙이면 Kotlin이 Java가 직접 호출할 수 있는 public 생성자를 생성하면서 init 블록도 실행되도록 보장해요.

@JvmExposeBoxed 애노테이션은 클래스, 생성자, 함수 레벨에 적용해서 Java에 무엇을 노출할지 세밀하게 제어할 수 있어요.

예를 들어 다음 코드에서 확장 함수 .timesTwoBoxed()는 Java에서 접근할 수 없어요:

@JvmInline
value class MyInt(val value: Int)

fun MyInt.timesTwoBoxed(): MyInt = MyInt(this.value * 2)

Java 코드에서 MyInt 클래스의 인스턴스를 만들고 .timesTwoBoxed() 함수를 호출할 수 있게 하려면 클래스와 함수 양쪽에 @JvmExposeBoxed 애노테이션을 추가하세요:

@JvmExposeBoxed
@JvmInline
value class MyInt(val value: Int)

@JvmExposeBoxed
fun MyInt.timesTwoBoxed(): MyInt = MyInt(this.value * 2)

이 애노테이션들을 사용하면 Kotlin 컴파일러가 MyInt 클래스에 Java가 접근 가능한 생성자를 생성해요. 또한 값 클래스의 boxed 형태를 사용하는 확장 함수의 오버로드도 생성해요. 그 결과 다음 Java 코드가 성공적으로 실행돼요:

MyInt input = new MyInt(5);
MyInt output = ExampleKt.timesTwoBoxed(input);

노출하려는 인라인 값 클래스의 모든 부분에 애노테이션을 붙이고 싶지 않다면, 애노테이션을 모듈 전체에 효과적으로 적용할 수 있어요. 모듈에 이 동작을 적용하려면 -Xjvm-expose-boxed 옵션으로 컴파일하세요. 이 옵션으로 컴파일하면 모듈의 모든 선언에 @JvmExposeBoxed 애노테이션이 붙은 것과 동일한 효과가 있어요.

이 새 애노테이션은 Kotlin이 값 클래스를 내부적으로 컴파일하거나 사용하는 방식을 바꾸지 않으며, 기존 컴파일 코드는 모두 유효해요. 단지 Java 상호 운용성을 개선하는 새 기능을 추가할 뿐이에요. 값 클래스를 사용하는 Kotlin 코드의 성능에는 영향이 없어요.

@JvmExposeBoxed 애노테이션은 멤버 함수의 boxed 변형을 노출하고 boxed 반환 타입을 받으려는 라이브러리 작성자에게 유용해요. 이 기능 덕분에 인라인 값 클래스(효율적이지만 Kotlin 전용)와 data class(Java 호환이지만 항상 boxed) 사이에서 선택을 강요받지 않아도 돼요.

@JvmExposedBoxed 애노테이션이 어떻게 동작하고 어떤 문제를 해결하는지에 대한 더 자세한 설명은 이 KEEP 제안서를 참고하세요.

JVM records에 애노테이션 붙이기 지원 개선

Kotlin은 Kotlin 1.5.0부터 JVM records를 지원했어요. 이제 Kotlin 2.2.0은 record component에서 Kotlin이 애노테이션을 처리하는 방식을 개선했어요. 특히 Java의 RECORD_COMPONENT target과 관련해서요.

먼저 RECORD_COMPONENT를 애노테이션 target으로 사용하려면 Kotlin(@Target)과 Java용 애노테이션을 수동으로 추가해야 해요. Kotlin의 @Target 애노테이션이 RECORD_COMPONENT를 지원하지 않기 때문이에요. 예를 들면:

@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
@java.lang.annotation.Target(ElementType.CLASS, ElementType.RECORD_COMPONENT)
annotation class exampleClass

두 목록을 수동으로 유지하는 것은 오류가 발생하기 쉬우므로, Kotlin 2.2.0은 Kotlin과 Java target이 일치하지 않으면 컴파일러 경고를 도입했어요. 예를 들어 Java target 목록에서 ElementType.CLASS를 빠뜨리면 컴파일러가 다음처럼 보고해요:

Incompatible annotation targets: Java target 'CLASS' missing, corresponding to Kotlin targets 'CLASS'.

둘째, record에서 애노테이션을 전파하는 방식은 Kotlin과 Java가 다르다는 점이 있어요. Java에서는 record component의 애노테이션이 backing field, getter, 생성자 파라미터에 자동으로 적용돼요. Kotlin은 기본적으로 그렇게 하지 않지만, 이제 @all: use-site target을 사용해 그 동작을 재현할 수 있어요.

예를 들면:

@JvmRecord
data class Person(val name: String, @all:Positive val age: Int)

@JvmRecord@all:과 함께 사용하면 Kotlin이 이제:

  • 애노테이션을 프로퍼티, backing field, 생성자 파라미터, getter로 전파해요.
  • 애노테이션이 Java의 RECORD_COMPONENT를 지원한다면 record component에도 애노테이션을 적용해요.

Kotlin/Native

2.2.0부터 Kotlin/Native는 LLVM 19를 사용해요. 이번 릴리스는 메모리 소비를 추적하고 조정하는 몇 가지 실험적 기능도 가져와요.

객체별 메모리 할당(Per-object memory allocation)

Kotlin/Native의 메모리 할당기는 이제 객체 단위로 메모리를 예약할 수 있어요. 어떤 경우에는 엄격한 메모리 제한을 충족하거나 애플리케이션 시작 시 메모리 소비를 줄이는 데 도움이 될 수 있어요.

이 새 기능은 기본 산술 할당기 대신 시스템 메모리 할당기를 활성화하던 -Xallocator=std 컴파일러 옵션을 대체하도록 설계됐어요. 이제 메모리 할당기를 바꾸지 않고도 버퍼링(할당의 paging)을 비활성화할 수 있어요.

이 기능은 현재 Experimental 단계예요. 활성화하려면 gradle.properties 파일에 다음 옵션을 설정하세요:

kotlin.native.binary.pagedAllocator=false

문제가 있으면 이슈 트래커 YouTrack에 보고해 주세요.

런타임에서 Latin-1 인코딩 문자열 지원

Kotlin은 이제 JVM과 유사하게 Latin-1 인코딩 문자열을 지원해요. 이는 애플리케이션의 바이너리 크기를 줄이고 메모리 소비를 조정하는 데 도움이 돼요.

기본적으로 Kotlin의 문자열은 각 문자가 2바이트로 표현되는 UTF-16 인코딩으로 저장돼요. 어떤 경우에는 이 때문에 문자열이 소스 코드에 비해 바이너리에서 두 배의 공간을 차지하고, 단순 ASCII 파일에서 데이터를 읽는 데 파일을 디스크에 저장하는 것보다 두 배의 메모리가 들 수 있어요.

반면 Latin-1 (ISO 8859-1) 인코딩은 처음 256개의 유니코드 문자를 각각 1바이트로 표현해요. Latin-1 지원을 활성화하면 모든 문자가 그 범위에 들어오는 한 문자열이 Latin-1 인코딩으로 저장돼요. 그렇지 않으면 기본 UTF-16 인코딩이 사용돼요.

Latin-1 지원 활성화하는 방법

이 기능은 현재 Experimental 단계예요. 활성화하려면 gradle.properties 파일에 다음 옵션을 설정하세요:

kotlin.native.binary.latin1Strings=true

알려진 문제

이 기능이 Experimental인 동안에는 cinterop 확장 함수인 String.pin, String.usePinned, String.refTo가 덜 효율적이게 돼요. 각 호출이 문자열을 UTF-16으로 자동 변환하도록 트리거할 수 있어요.

Kotlin 팀은 이 기능을 구현해 준 Google과 특히 Sonya Valchuk 동료분들께 감사를 전해요.

Kotlin의 메모리 소비에 대한 자세한 내용은 문서를 참고하세요.

Apple 플랫폼에서 메모리 소비 추적 개선

Kotlin 2.2.0부터 Kotlin 코드가 할당하는 메모리에 태그가 붙어요. 이는 Apple 플랫폼에서 메모리 문제를 디버깅하는 데 도움이 돼요.

애플리케이션의 높은 메모리 사용량을 검사할 때 이제 Kotlin 코드가 얼마나 많은 메모리를 예약하는지 식별할 수 있어요. Kotlin의 몫은 식별자로 태그가 붙으며 Xcode Instruments의 VM Tracker 같은 도구로 추적할 수 있어요.

이 기능은 기본으로 활성화되지만 다음 조건이 모두 충족될 때만 Kotlin/Native 기본 메모리 할당기에서 사용할 수 있어요:

  • 태깅 활성화: 메모리가 유효한 식별자로 태그되어야 해요. Apple은 240에서 255 사이의 숫자를 권장하며 기본값은 246이에요. kotlin.native.binary.mmapTag=0 Gradle 프로퍼티를 설정하면 태깅이 비활성화돼요.
  • mmap을 통한 할당: 할당기가 파일을 메모리에 매핑하기 위해 mmap 시스템 호출을 사용해야 해요. kotlin.native.binary.disableMmap=true Gradle 프로퍼티를 설정하면 기본 할당기가 mmap 대신 malloc을 사용해요.
  • paging 활성화: 할당의 paging(버퍼링)이 활성화되어야 해요. kotlin.native.binary.pagedAllocator=false Gradle 프로퍼티를 설정하면 대신 객체 단위로 메모리가 예약돼요.

Kotlin의 메모리 소비에 대한 자세한 내용은 문서를 참고하세요.

LLVM 16에서 19로 업데이트

Kotlin 2.2.0에서 LLVM을 버전 16에서 19로 업데이트했어요. 새 버전에는 성능 개선, 버그 수정, 보안 업데이트가 포함돼요.

이 업데이트는 코드에 영향을 주지 않아야 하지만, 문제가 발생하면 이슈 트래커에 보고해 주세요.

Windows 7 target 지원 중단

Kotlin 2.2.0부터 지원 최소 Windows 버전이 Windows 7에서 Windows 10으로 올라갔어요. Microsoft가 2025년 1월에 Windows 7 지원을 종료했으므로, 레거시 target의 지원 중단도 결정했어요.

자세한 내용은 Kotlin/Native 지원 target과 host를 참고하세요.

Kotlin/Wasm

이번 릴리스에서는 Wasm target의 빌드 인프라가 JavaScript target에서 분리되었어요. 또한 프로젝트나 모듈별로 Binaryen 도구를 구성할 수 있게 되었어요.

JavaScript target에서 분리된 Wasm target 빌드 인프라

이전에는 wasmJs target이 js target과 동일한 인프라를 공유했어요. 그 결과 두 target 모두 같은 디렉터리(build/js)에 위치했고 같은 NPM task와 구성을 사용했어요.

이제 wasmJs target은 js target과 분리된 자체 인프라를 갖게 됐어요. 이를 통해 Wasm task와 타입이 JavaScript와 구별되어 독립적으로 구성할 수 있게 됐어요.

또한 Wasm 관련 프로젝트 파일과 NPM 의존성은 이제 별도의 build/wasm 디렉터리에 저장돼요.

Wasm을 위한 새 NPM 관련 task가 도입되었고, 기존 JavaScript task는 이제 JavaScript 전용이 됐어요:

Wasm tasks JavaScript tasks
kotlinWasmNpmInstall kotlinNpmInstall
wasmRootPackageJson rootPackageJson

마찬가지로 새 Wasm 전용 선언도 추가됐어요:

Wasm declarations JavaScript declarations
WasmNodeJsRootPlugin NodeJsRootPlugin
WasmNodeJsPlugin NodeJsPlugin
WasmYarnPlugin YarnPlugin
WasmNodeJsRootExtension NodeJsRootExtension
WasmNodeJsEnvSpec NodeJsEnvSpec
WasmYarnRootEnvSpec YarnRootEnvSpec

이제 JavaScript target과 독립적으로 Wasm target을 작업할 수 있어서 구성 과정이 단순해졌어요.

이 변경은 기본으로 활성화되며 추가 설정이 필요 없어요.

프로젝트별 Binaryen 구성

Kotlin/Wasm에서 프로덕션 빌드를 최적화하는 데 사용하는 Binaryen 도구는 이전에는 루트 프로젝트에서 한 번 설정했어요.

이제 Binaryen 도구를 프로젝트나 모듈별로 구성할 수 있어요. 이 변경은 Gradle의 모범 사례와 일치하며 project isolation 같은 기능을 더 잘 지원해서 복잡한 빌드에서 빌드 성능과 안정성을 개선해요.

또한 필요하다면 모듈마다 다른 Binaryen 버전을 구성할 수도 있어요.

이 기능은 기본으로 활성화돼요. 하지만 Binaryen의 커스텀 구성을 갖고 있다면 이제 루트 프로젝트뿐 아니라 프로젝트별로 적용해야 해요.

Kotlin/JS

이번 릴리스는 @JsPlainObject 인터페이스의 copy() 함수 수정, @JsModule 애노테이션이 있는 파일의 타입 별칭 등 다른 Kotlin/JS 기능을 개선해요.

@JsPlainObject 인터페이스의 copy() 수정

Kotlin/JS에는 js-plain-objects라는 실험적 플러그인이 있는데, @JsPlainObject로 애노테이션된 인터페이스에 copy() 함수를 도입했어요. copy() 함수를 사용해 객체를 조작할 수 있어요.

하지만 초기 copy() 구현은 상속과 호환되지 않아서, @JsPlainObject 인터페이스가 다른 인터페이스를 확장할 때 문제가 발생했어요.

plain object의 제약을 피하기 위해 copy() 함수를 객체 자체에서 companion object로 옮겼어요:

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

fun main() {
    val user = User(name = "SomeUser", age = 21)
    // This syntax is not valid anymore
    val copy = user.copy(age = 35)
    // This is the correct syntax
    val copy = User.copy(user, age = 35)
}

이 변경은 상속 계층의 충돌을 해결하고 모호성을 제거해요. Kotlin 2.2.0부터 기본으로 활성화돼요.

@JsModule 애노테이션이 있는 파일의 타입 별칭 지원

이전에는 JavaScript 모듈에서 선언을 가져오기 위해 @JsModule로 애노테이션된 파일이 external 선언으로만 제한되어 있었어요. 즉 이러한 파일에서 typealias를 선언할 수 없었죠.

Kotlin 2.2.0부터 @JsModule로 표시된 파일 안에서 타입 별칭을 선언할 수 있어요:

@file:JsModule("somepackage")
package somepackage
typealias SomeClass = Any

이 변경은 Kotlin/JS 상호 운용성 제한의 한 측면을 줄여 주며, 향후 릴리스에서 더 많은 개선이 계획되어 있어요.

@JsModule이 있는 파일의 타입 별칭 지원은 기본으로 활성화돼요.

multiplatform expect 선언에서 @JsExport 지원

Kotlin Multiplatform 프로젝트에서 expect/actual 메커니즘을 작업할 때, 공통 코드의 expect 선언에는 @JsExport 애노테이션을 사용할 수 없었어요.

이번 릴리스부터 expect 선언에 직접 @JsExport를 적용할 수 있어요:

// commonMain

// Produced error, but now works correctly
@JsExport
expect class WindowManager {
    fun close()
}

@JsExport
fun acceptWindowManager(manager: WindowManager) {
    ...
}

// jsMain

@JsExport
actual class WindowManager {
    fun close() {
        window.close()
    }
}

JavaScript source set의 해당 actual 구현에도 @JsExport로 애노테이션해야 하며, export 가능한 타입만 사용해야 해요.

이 수정 덕분에 commonMain에 정의된 공유 코드를 JavaScript로 올바르게 내보낼 수 있어요. 수동 해결 방법을 쓰지 않고도 multiplatform 코드를 JavaScript 사용자에게 노출할 수 있게 됐어요.

이 변경은 기본으로 활성화돼요.

@JsExport를 Promise 타입과 함께 사용하는 기능

이전에는 @JsExport 애노테이션으로 Promise<Unit> 타입을 반환하는 함수를 내보내려 하면 Kotlin 컴파일러가 오류를 생성했어요.

Promise<Int> 같은 반환 타입은 올바르게 작동했지만, Promise<Unit>을 사용하면 TypeScript에서 Promise<void>로 올바르게 매핑됨에도 "non-exportable type" 경고가 발생했어요.

이 제한이 제거됐어요. 이제 다음 코드가 오류 없이 컴파일돼요:

// Worked correctly before
@JsExport
fun fooInt(): Promise<Int> = GlobalScope.promise {
    delay(100)
    return@promise 42
}

// Produced error, but now works correctly
@JsExport
fun fooUnit(): Promise<Unit> = GlobalScope.promise {
    delay(100)
}

이 변경은 Kotlin/JS 상호 운용성 모델에서 불필요한 제한을 제거해요. 이 수정은 기본으로 활성화돼요.

Gradle

Kotlin 2.2.0은 Gradle 7.6.3부터 8.14까지와 완전히 호환돼요. 최신 Gradle 릴리스까지의 Gradle 버전도 사용할 수 있어요. 다만 그렇게 하면 deprecation 경고가 발생하거나, 일부 새 Gradle 기능이 동작하지 않을 수 있다는 점을 알아두세요.

이번 릴리스에서 Kotlin Gradle 플러그인은 진단 기능에 몇 가지 개선을 가져왔어요. 또한 바이너리 호환성 검증의 실험적 통합을 도입해서 라이브러리 작업을 더 쉽게 만들었어요.

Kotlin Gradle 플러그인에 포함된 바이너리 호환성 검증

라이브러리 버전 간 바이너리 호환성 검사를 더 쉽게 하기 위해, 바이너리 호환성 검증기의 기능을 Kotlin Gradle 플러그인(KGP)으로 옮기는 것을 실험 중이에요. 장난감 프로젝트에서는 시도해 볼 수 있지만 아직 프로덕션 사용은 권장하지 않아요.

원래 바이너리 호환성 검증기는 이 실험 단계 동안 계속 유지 관리돼요.

Kotlin 라이브러리는 JVM 클래스 파일과 klib 중 하나의 바이너리 형식을 사용할 수 있어요. 이 형식들은 호환되지 않으므로 KGP는 각각을 따로 처리해요.

바이너리 호환성 검증 기능 세트를 활성화하려면 build.gradle.kts 파일의 kotlin{} 블록에 다음을 추가하세요:

// build.gradle.kts
kotlin {
    @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
    abiValidation {
        // Use the set() function to ensure compatibility with older Gradle versions
        enabled.set(true)
    }
}

바이너리 호환성을 검사하고 싶은 모듈이 여러 개 있다면, 각 모듈에서 이 기능을 개별적으로 구성하세요. 각 모듈은 자체 커스텀 구성을 가질 수 있어요.

활성화되면 checkLegacyAbi Gradle task를 실행해 바이너리 호환성 문제를 검사해요. 이 task는 IntelliJ IDEA나 프로젝트 디렉터리의 커맨드 라인에서 실행할 수 있어요:

./gradlew checkLegacyAbi

이 task는 현재 코드에서 애플리케이션 바이너리 인터페이스(ABI) 덤프를 UTF-8 텍스트 파일로 생성해요. 그런 다음 새 덤프를 이전 릴리스의 덤프와 비교해요. 차이점이 발견되면 오류로 보고해요. 오류를 검토한 후 변경이 허용 가능하다고 판단되면 updateLegacyAbi Gradle task를 실행해 기준 ABI 덤프를 업데이트할 수 있어요.

클래스 필터링

이 기능을 사용하면 ABI 덤프에서 클래스를 필터링할 수 있어요. 이름이나 부분 이름, 또는 클래스를 표시하는 애노테이션(이나 애노테이션 이름의 일부)으로 클래스를 명시적으로 포함하거나 제외할 수 있어요.

예를 들어 이 샘플은 com.company 패키지의 모든 클래스를 제외해요:

// build.gradle.kts
kotlin {
    @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
    abiValidation {
        filters.excluded.byNames.add("com.company.**")
    }
}

바이너리 호환성 검증기 구성에 대해 더 알아보려면 KGP API 레퍼런스를 살펴보세요.

Multiplatform 제약

multiplatform 프로젝트에서 host가 모든 target을 크로스 컴파일하지 못한다면, KGP는 다른 target의 ABI 덤프를 확인해 지원되지 않는 target의 ABI 변경을 추론하려 해요. 이 방식은 나중에 모든 target을 컴파일할 수 있는 host로 전환할 때 잘못된 검증 실패를 피하는 데 도움이 돼요.

KGP가 지원되지 않는 target의 ABI 변경을 추론하지 않도록 이 기본 동작을 변경하려면 build.gradle.kts 파일에 다음을 추가하세요:

// build.gradle.kts
kotlin {
    @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
    abiValidation {
        klib {
            keepUnsupportedTargets = false
        }
    }
}

하지만 프로젝트에 지원되지 않는 target이 있다면, ABI 덤프를 만들 수 없으므로 checkLegacyAbi task 실행이 실패해요. 다른 target의 추론된 ABI 변경 때문에 호환되지 않는 변경을 놓치기보다는 검사가 실패하는 것이 더 중요하다면 이 동작이 바람직할 수 있어요.

Kotlin Gradle 플러그인의 콘솔 리치 출력 지원

Kotlin 2.2.0에서는 Gradle 빌드 과정에서 콘솔의 색상과 기타 리치 출력을 지원해, 보고되는 진단을 더 쉽게 읽고 이해할 수 있게 해요.

리치 출력은 Linux와 macOS의 지원되는 터미널 에뮬레이터에서 사용할 수 있으며, Windows 지원도 작업 중이에요.

이 기능은 기본으로 활성화되지만, 재정의하려면 gradle.properties 파일에 다음 Gradle 프로퍼티를 추가하세요:

org.gradle.console=plain

이 프로퍼티와 옵션에 대한 자세한 내용은 Gradle 문서의 로그 형식 커스터마이징을 참고하세요.

KGP 진단 내 Problems API 통합

이전에는 Kotlin Gradle 플러그인(KGP)이 경고와 오류 같은 진단을 콘솔이나 로그에 일반 텍스트로만 보고할 수 있었어요.

2.2.0부터 KGP는 추가 보고 메커니즘을 도입했어요. 이제 빌드 과정에서 풍부하고 구조화된 문제 정보를 보고하는 표준화된 방법인 Gradle의 Problems API를 사용해요.

KGP 진단은 이제 읽기 더 쉬워지고 Gradle CLI와 IntelliJ IDEA 같은 다양한 인터페이스에서 더 일관되게 표시돼요.

이 통합은 Gradle 8.6 이상부터 기본으로 활성화돼요. API가 아직 발전 중이므로 최신 개선을 활용하려면 가장 최근 Gradle 버전을 사용하세요.

KGP와 --warning-mode 호환성

Kotlin Gradle 플러그인(KGP) 진단은 고정된 심각도 수준으로 문제를 보고해서, Gradle의 --warning-mode 커맨드 라인 옵션이 KGP가 오류를 표시하는 방식에 영향을 주지 못했어요.

이제 KGP 진단이 --warning-mode 옵션과 호환되어 더 큰 유연성을 제공해요. 예를 들어 모든 경고를 오류로 변환하거나 경고를 완전히 비활성화할 수 있어요.

이 변경으로 KGP 진단은 선택된 warning mode에 따라 출력을 조정해요:

  • --warning-mode=fail로 설정하면 Severity.Warning 진단이 이제 Severity.Error로 승격돼요.
  • --warning-mode=none으로 설정하면 Severity.Warning 진단이 로그되지 않아요.

이 동작은 2.2.0부터 기본으로 활성화돼요.

--warning-mode 옵션을 무시하려면 gradle.properties 파일에 다음 Gradle 프로퍼티를 설정하세요:

kotlin.internal.diagnostics.ignoreWarningMode=true

새로운 실험적 빌드 도구 API

Kotlin은 Gradle, Maven, Amper 등 다양한 빌드 시스템과 함께 사용할 수 있어요. 하지만 각 시스템에 Kotlin을 통합해 증분 컴파일, Kotlin 컴파일러 플러그인과의 호환성, daemon, Kotlin Multiplatform 같은 전체 기능 세트를 지원하려면 상당한 노력이 필요해요.

이 과정을 단순화하기 위해 Kotlin 2.2.0은 새 실험적 빌드 도구 API(BTA)를 도입했어요. BTA는 빌드 시스템과 Kotlin 컴파일러 생태계 사이의 추상화 계층 역할을 하는 범용 API예요. 이 접근 방식 덕분에 각 빌드 시스템은 단일 BTA 진입점만 지원하면 돼요.

현재 BTA는 Kotlin/JVM만 지원해요. JetBrains의 Kotlin 팀은 Kotlin Gradle 플러그인(KGP)과 kotlin-maven-plugin에서 이미 사용하고 있어요. 이 플러그인을 통해 BTA를 시험해 볼 수 있지만, API 자체는 아직 여러분의 자체 빌드 도구 통합에 일반적으로 사용할 준비가 되지 않았어요. BTA 제안에 궁금하거나 피드백을 공유하고 싶다면 이 KEEP 제안서를 참고하세요.

BTA를 시험해 보려면:

  • KGP에서는 gradle.properties 파일에 다음 프로퍼티를 추가하세요:
kotlin.compiler.runViaBuildToolsApi=true
  • Maven에서는 아무것도 할 필요 없어요. 기본으로 활성화돼 있어요.

BTA는 현재 Maven 플러그인에 직접적인 이점이 없지만, Kotlin daemon 지원이나 증분 컴파일 안정화 같은 새 기능을 더 빠르게 제공하기 위한 탄탄한 기반을 마련해요.

KGP의 경우 BTA를 사용하면 이미 다음과 같은 이점이 있어요:

개선된 "in process" 컴파일러 실행 전략

KGP는 세 가지 Kotlin 컴파일러 실행 전략을 지원해요. Gradle daemon 프로세스 안에서 컴파일러를 실행하는 "in process" 전략은 이전에 증분 컴파일을 지원하지 않았어요.

이제 BTA를 사용하면 "in-process" 전략이 증분 컴파일을 지원해요. 사용하려면 gradle.properties 파일에 다음 프로퍼티를 추가하세요:

kotlin.compiler.execution.strategy=in-process

Kotlin에서 서로 다른 컴파일러 버전을 구성하는 유연성

때로는 KGP는 이전 버전으로 유지하면서 코드에서는 더 새로운 Kotlin 컴파일러 버전을 사용하고 싶을 수 있어요. 예를 들어 빌드 스크립트 deprecation을 처리하면서 새 언어 기능을 시험해 보려는 경우죠. 또는 KGP 버전은 업데이트하되 Kotlin 컴파일러 버전은 이전 것을 유지하고 싶을 수도 있어요.

BTA가 이를 가능하게 해줘요. build.gradle.kts 파일에서 다음과 같이 구성할 수 있어요:

// build.gradle.kts
import org.jetbrains.kotlin.buildtools.api.ExperimentalBuildToolsApi
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi

plugins {
    kotlin("jvm") version "2.2.0"
}

group = "org.jetbrains.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

kotlin {
    jvmToolchain(8)
    @OptIn(ExperimentalBuildToolsApi::class, ExperimentalKotlinGradlePluginApi::class)
    compilerVersion.set("2.1.21") // Different version than 2.2.0
}

BTA는 KGP 및 Kotlin 컴파일러 버전을 이전 세 개의 주요 버전과 이후 한 개의 주요 버전으로 구성하는 것을 지원해요. 따라서 KGP 2.2.0에서는 Kotlin 컴파일러 버전 2.1.x, 2.0.x, 1.9.25가 지원돼요. KGP 2.2.0은 향후 Kotlin 컴파일러 버전 2.2.x와 2.3.x와도 호환돼요.

다만 컴파일러 플러그인과 함께 서로 다른 컴파일러 버전을 사용하면 Kotlin 컴파일러 예외가 발생할 수 있다는 점을 기억하세요. Kotlin 팀은 향후 릴리스에서 이런 종류의 문제를 해결할 계획이에요.

이 플러그인들로 BTA를 시험해 보고, KGPMaven 플러그인 전용 YouTrack 티켓에 피드백을 보내 주세요.

표준 라이브러리

Kotlin 2.2.0에서 Base64 APIHexFormat API가 이제 Stable이 되었어요.

Stable Base64 인코딩·디코딩

Kotlin 1.8.20은 Base64 인코딩·디코딩에 대한 Experimental 지원을 도입했어요. Kotlin 2.2.0에서 Base64 API는 이제 Stable이며, 이번 릴리스에서 새로 추가된 Base64.Pem을 포함해 네 가지 인코딩 방식을 갖췄어요:

  • Base64.Default는 표준 Base64 인코딩 방식을 사용해요. Base64.DefaultBase64 클래스의 companion object예요. 그래서 Base64.Default.encode()Base64.Default.decode() 대신 Base64.encode()Base64.decode()로 함수를 호출할 수 있어요.
  • Base64.UrlSafe"URL and Filename safe" 인코딩 방식을 사용해요.
  • Base64.MimeMIME 인코딩 방식을 사용하며, 인코딩 시 76자마다 줄 구분자를 넣고 디코딩 시 잘못된 문자를 건너뜁니다.
  • Base64.PemBase64.Mime처럼 데이터를 인코딩하지만 줄 길이를 64자로 제한해요.

Base64 API를 사용해 바이너리 데이터를 Base64 문자열로 인코딩하고 다시 바이트로 디코딩할 수 있어요.

예시를 볼게요:

val foBytes = "fo".map { it.code.toByte() }.toByteArray()
Base64.Default.encode(foBytes) // "Zm8="
// Alternatively:
// Base64.encode(foBytes)

val foobarBytes = "foobar".map { it.code.toByte() }.toByteArray()
Base64.UrlSafe.encode(foobarBytes) // "Zm9vYmFy"

Base64.Default.decode("Zm8=") // foBytes
// Alternatively:
// Base64.decode("Zm8=")

Base64.UrlSafe.decode("Zm9vYmFy") // foobarBytes

JVM에서는 .encodingWith().decodingWith() 확장 함수를 사용해 입출력 스트림으로 Base64를 인코딩·디코딩할 수 있어요:

import kotlin.io.encoding.*
import java.io.ByteArrayOutputStream

fun main() {
    val output = ByteArrayOutputStream()
    val base64Output = output.encodingWith(Base64.Default)

    base64Output.use { stream ->
        stream.write("Hello World!!".encodeToByteArray())
    }

    println(output.toString())
    // SGVsbG8gV29ybGQhIQ==
}

HexFormat API로 Stable 16진수 파싱·포맷팅

Kotlin 1.9.0에서 도입된 HexFormat API는 이제 Stable이 되었어요. 숫자 값과 16진수 문자열 사이의 변환에 사용할 수 있어요.

예를 들면:

fun main() {
    println(93.toHexString())
}

자세한 내용은 Hexadecimals을 포맷하고 파싱하는 새로운 HexFormat 클래스를 참고하세요.

Compose 컴파일러

이번 릴리스에서 Compose 컴파일러는 composable 함수 레퍼런스를 지원하고 여러 feature flag의 기본값을 변경했어요.

@Composable 함수 레퍼런스 지원

Compose 컴파일러는 Kotlin 2.2.0 릴리스부터 composable 함수 레퍼런스의 선언과 사용을 지원해요:

val content: @Composable (String) -> Unit = ::Text

@Composable fun App() {
    content("My App")
}

Composable 함수 레퍼런스는 런타임에서 composable lambda 객체와는 약간 다르게 동작해요. 특히 composable lambda는 ComposableLambda 클래스를 확장해 skipping을 더 세밀하게 제어할 수 있어요. 함수 레퍼런스는 KCallable 인터페이스를 구현할 것으로 기대되므로 동일한 최적화를 적용할 수 없어요.

PausableComposition feature flag 기본 활성화

PausableComposition feature flag는 Kotlin 2.2.0부터 기본으로 활성화돼요. 이 flag는 restartable 함수에 대한 Compose 컴파일러 출력을 조정해서, 런타임이 jumping 동작을 강제하고 각 함수를 건너뛰어 composition을 효과적으로 일시 중지할 수 있게 해줘요. 이를 통해 무거운 composition을 프레임 간에 분할할 수 있으며, 향후 릴리스의 prefetching에서 사용될 예정이에요.

이 feature flag를 비활성화하려면 Gradle 구성에 다음을 추가하세요:

// build.gradle.kts
composeCompiler {
    featureFlag = setOf(ComposeFeatureFlag.PausableComposition.disabled())
}

OptimizeNonSkippingGroups feature flag 기본 활성화

OptimizeNonSkippingGroups feature flag는 Kotlin 2.2.0부터 기본으로 활성화돼요. 이 최적화는 non-skipping composable 함수에 대해 생성된 group 호출을 제거해 런타임 성능을 개선해요. 런타임에서 관찰 가능한 동작 변화는 없어야 해요.

문제가 발생하면 feature flag를 비활성화해 이 변경이 원인인지 검증할 수 있어요. 문제는 Jetpack Compose 이슈 트래커에 보고해 주세요.

OptimizeNonSkippingGroups flag를 비활성화하려면 Gradle 구성에 다음을 추가하세요:

composeCompiler {
    featureFlag = setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups.disabled())
}

Deprecated feature flag

StrongSkippingIntrinsicRemember feature flag는 이제 더 이상 사용되지 않으며 향후 릴리스에서 제거될 예정이에요. 이 feature flag를 비활성화하게 만드는 문제가 발생하면 Jetpack Compose 이슈 트래커에 보고해 주세요.

주요 변경 사항과 deprecation

이 섹션은 주목할 만한 주요 변경 사항과 deprecation을 강조해요. 이번 릴리스의 모든 주요 변경 사항과 deprecation의 전체 개요는 Compatibility guide를 참고하세요.

  • Kotlin 2.2.0부터 컴파일러는 -language-version=1.6이나 -language-version=1.7을 더 이상 지원하지 않아요. 1.8보다 오래된 언어 기능 세트는 지원되지 않지만, 언어 자체는 Kotlin 1.0과 완전히 하위 호환을 유지해요.
  • Ant 빌드 시스템 지원이 deprecate됐어요. Ant에 대한 Kotlin 지원은 오랫동안 활발히 개발되지 않았고, 사용자 기반이 상대적으로 작아 더 이상 유지할 계획이 없어요. 2.3.0에서 Ant 지원을 제거할 계획이에요.
  • Kotlin 2.2.0은 Gradle의 kotlinOptions{} 블록의 deprecation 수준을 오류로 올려요. 대신 compilerOptions{} 블록을 사용하세요. 빌드 스크립트 업데이트 지침은 kotlinOptions{}에서 compilerOptions{}로 마이그레이션을 참고하세요.
  • Kotlin scripting은 Kotlin 생태계의 중요한 부분으로 남지만, 더 나은 경험을 제공하기 위해 custom scripting과 gradle.kts, main.kts 스크립트 같은 특정 사용 사례에 집중하고 있어요. 자세한 내용은 업데이트된 블로그 포스트를 참고하세요. 그 결과 Kotlin 2.2.0은 다음을 deprecate해요:
    • REPL: kotlinc를 통해 REPL을 계속 사용하려면 -Xrepl 컴파일러 옵션으로 옵트인하세요.
    • JSR-223: 이 JSR이 Withdrawn 상태이므로, JSR-223 구현은 language version 1.9에서 계속 작동하지만 향후 K2 컴파일러를 사용하도록 마이그레이션되지는 않아요.
    • KotlinScriptMojo Maven 플러그인: 이 플러그인에서는 충분한 관심을 보지 못했어요. 계속 사용하면 컴파일러 경고가 표시될 거예요.
  • Kotlin 2.2.0에서 KotlinCompileToolsetSource() 함수는 이제 구성된 소스를 추가하는 대신 대체해요. 기존 소스를 대체하지 않고 추가하려면 source() 함수를 사용하세요.
  • BaseKaptannotationProcessorOptionProviders 타입이 MutableList에서 MutableList로 변경됐어요. 현재 코드가 단일 요소로 리스트를 추가한다면 add() 함수 대신 addAll() 함수를 사용하세요.
  • 레거시 Kotlin/JS 백엔드에 사용된 dead code elimination(DCE) 도구가 deprecate된 이후, DCE와 관련된 나머지 DSL이 Kotlin Gradle 플러그인에서 제거됐어요:
    • org.jetbrains.kotlin.gradle.dsl.KotlinJsDce 인터페이스
    • org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsBrowserDsl.dceTask(body: Action<KotlinJsDce>) 함수
    • org.jetbrains.kotlin.gradle.dsl.KotlinJsDceCompilerToolOptions 인터페이스
    • org.jetbrains.kotlin.gradle.dsl.KotlinJsDceOptions 인터페이스
    • 현재 JS IR 컴파일러는 기본적으로 DCE를 지원하며, @JsExport 애노테이션을 사용해 DCE 중 어떤 Kotlin 함수와 클래스를 유지할지 지정할 수 있어요.
  • deprecate된 kotlin-android-extensions 플러그인은 Kotlin 2.2.0에서 제거됐어요. Parcelable 구현 생성기에는 kotlin-parcelize 플러그인을, synthetic view에는 Android Jetpack의 view bindings를 사용하세요.
  • 실험적 kotlinArtifacts API는 Kotlin 2.2.0에서 deprecate됐어요. 최종 네이티브 바이너리 빌드에는 Kotlin Gradle 플러그인에서 사용할 수 있는 현재 DSL을 사용하세요. 마이그레이션에 충분하지 않다면 이 YouTrack 이슈에 의견을 남겨 주세요.
  • Kotlin 1.9.0에서 deprecate된 KotlinCompilation.source는 이제 Kotlin Gradle 플러그인에서 제거됐어요.
  • 실험적 commonization 모드의 파라미터는 Kotlin 2.2.0에서 deprecate됐어요. 잘못된 컴파일 산출물을 삭제하려면 commonization 캐시를 비우세요.
  • deprecate된 konanVersion 프로퍼티는 이제 CInteropProcess task에서 제거됐어요. 대신 CInteropProcess.kotlinNativeVersion을 사용하세요.
  • deprecate된 destinationDir 프로퍼티를 사용하면 이제 오류가 발생해요. 대신 CInteropProcess.destinationDirectory.set()을 사용하세요.

문서 업데이트

이번 릴리스는 Kotlin Multiplatform 문서를 KMP 포털로 마이그레이션한 것을 포함해 주목할 만한 문서 변경을 가져와요.

또한 새 페이지와 튜토리얼을 만들고 기존 것을 개편했어요.

새롭고 새로 단장한 튜토리얼

새롭고 새로 단장한 페이지

  • AI용 Kotlin 개요 – AI 기반 애플리케이션을 구축하는 Kotlin의 역량을 알아보세요.
  • Dokka 마이그레이션 가이드 – Dokka Gradle 플러그인의 v2로 마이그레이션하는 방법을 배워요.
  • Kotlin Metadata JVM 라이브러리 – JVM용으로 컴파일된 Kotlin 클래스의 metadata를 읽고, 수정하고, 생성하는 지침을 살펴보세요.
  • CocoaPods 통합 – 튜토리얼과 샘플 프로젝트를 통해 환경을 설정하고, Pod 의존성을 추가하거나, Kotlin 프로젝트를 CocoaPod 의존성으로 사용하는 방법을 배워요.
  • iOS 안정 릴리스를 지원하기 위한 Compose Multiplatform 새 페이지:
  • Exposed migrations – Exposed가 데이터베이스 스키마 변경 관리에 제공하는 도구를 배워요.

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

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

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

더 알아보기