Kotlin 2.4.20-RC3의 새로운 기능

Kotlin 2.4.20-RC3의 새로운 기능

릴리스: 2026년 9월 2일

이 문서는 Early Access Preview(EAP) 릴리스의 모든 기능을 다루지는 않지만, 주요 개선 사항을 짚어 줘요.

전체 변경 목록은 GitHub changelog에서 확인해 주세요.

Kotlin 2.4.20-RC3 릴리스가 나왔어요! 이 EAP 릴리스에 대한 몇 가지 세부 사항을 알려드릴게요.

  • 표준 라이브러리: 코루틴 스택 트레이스 복구 지원과 컬렉션 요소의 동등성·유일성 검사 기능
  • Kotlin/Native: 새로운 Swift export 기능과 SwiftPM 의존성을 위한 자동 생성 Package.swift 파일
  • Kotlin/Wasm: @JsFun 선언의 최상위 require() 호출 변경, 개선된 companion 객체 초기화 순서, Kotlin Gradle 플러그인의 Wasmtime 지원
  • Kotlin/JS: 브라우저 테스트용 새 DSL과 suspend 람다를 async 함수로 내보내기 지원
  • 빌드 도구 API: Kotlin/JS, Kotlin/Wasm, Kotlin 메타데이터 같은 새 타깃 지원
  • Kotlin 컴파일러: 네이티브 이미지의 실험적 릴리스

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

출처: What's new in Kotlin 2.4.20-RC3

본문

Kotlin 2.4.20-RC3로 업데이트하기

최신 버전의 Kotlin은 최신 버전의 IntelliJ IDEA와 Android Studio에 포함되어 있어요.

새 Kotlin 버전으로 업데이트하려면 IDE를 최신 버전으로 업데이트하고, 빌드 스크립트에서 Kotlin 버전을 2.4.20-RC3으로 변경해 주세요.

새로운 기능

이 릴리스에서 사용할 수 있는 사전 안정(pre-stable) 기능은 다음과 같아요. 여기에는 Beta, Alpha, Experimental 상태의 기능이 포함돼요.

  • 표준 라이브러리: 코루틴 스택 트레이스 복구 지원
  • 표준 라이브러리: 컬렉션 요소의 동등성과 유일성을 검사하는 새 함수
  • Kotlin/JS: 브라우저 테스트용 새 DSL
  • 빌드 도구 API: Kotlin/JS, Kotlin/Wasm, Kotlin 메타데이터 지원
  • Kotlin 컴파일러: 별도의 Kotlin 컴파일러 이미지

표준 라이브러리

Kotlin 2.4.20-RC3은 코루틴 스택 트레이스 복구 지원을 추가하고, 컬렉션 요소의 동등성과 유일성을 검사하는 새로운 함수를 도입해요.

코루틴 스택 트레이스 복구 지원

Kotlin 2.4.20-RC3은 표준 라이브러리에 StackTraceRecoverable 인터페이스를 추가해요. 이 인터페이스는 kotlinx.coroutines 라이브러리에 대한 의존성을 추가하지 않으면서도 스택 트레이스 복구를 위한 새 예외 인스턴스를 만드는 방법을 정의할 수 있게 해 줘서, kotlinx.coroutines 라이브러리와의 통합을 개선해요.

스택 트레이스 복구는 한 코루틴이 예외를 던지고 다른 코루틴이 그것을 다시 던질 때 디버깅에 도움을 줘요. 예외가 어디서 시작되었는지, 다른 코루틴이 어디서 다시 던지는지 볼 수 있게 해 줘요.

kotlinx.coroutines 라이브러리는 추가적인 코루틴 스택 트레이스 정보가 있는 새 예외 인스턴스를 만들어서 스택 트레이스 복구를 수행해요. 이것은 예외 메시지만, 원인(cause)만, 둘 다, 또는 인자를 받지 않는 생성자를 가진 예외에 대해 자동으로 일어나요.

예외 생성자에 줄 번호나 오류 코드 같은 추가 필수 인자가 있다면 StackTraceRecoverable 인터페이스를 구현해서 kotlinx.coroutines 라이브러리가 해당 예외의 새 인스턴스를 만드는 방법을 정의해 주세요.

인터페이스를 구현하려면 copyForStackTraceRecovery() 함수를 재정의해 주세요. 재정의에서 스택 트레이스 복구를 위한 새 예외 인스턴스를 반환하거나, kotlinx.coroutines 라이브러리가 예외를 복사하지 않게 하려면 null을 반환하면 돼요.

StackTraceRecoverable 인터페이스는 모든 타깃에서 사용할 수 있지만, kotlinx.coroutines 라이브러리는 JVM에서만 그것을 스택 트레이스 복구에 사용해요.

이 API들은 Experimental 상태이고 @OptIn(ExperimentalStdlibCoroutineSupportApi::class) 어노테이션으로 옵트인이 필요해요.

스택 트레이스 복구를 위해 새 인스턴스를 만들 때 line 속성을 보존하는 커스텀 예외의 예를 볼게요.

import kotlin.coroutines.ExperimentalStdlibCoroutineSupportApi
import kotlin.coroutines.debug.StackTraceRecoverable

@OptIn(ExperimentalStdlibCoroutineSupportApi::class)
class FileEditException
// The implementation requires a private constructor
// to pass the cause to the IllegalStateException constructor
private constructor(
    val line: Int,
    private val detail: String,
    cause: Throwable?,
) : IllegalStateException("When editing line $line: $detail", cause),
    // Implements StackTraceRecoverable for stack trace recovery
    StackTraceRecoverable<FileEditException> {

    constructor(line: Int, detail: String) : this(line, detail, null)

    // Copies the line number and message details
    override fun copyForStackTraceRecovery(): FileEditException =
        FileEditException(line, detail, this)
    }

fun main() {
    val original = FileEditException(15, "Unexpected token")

    // Normally, you don't need to call this function directly unless you're testing its behavior
    // The kotlinx.coroutines library invokes it automatically during stack trace recovery
    val copy = original.copyForStackTraceRecovery()

    println(copy.message)
    // When editing line 15: Unexpected token

    println(copy.cause == original)
    // true
}

자세한 내용은 이 기능의 KEEP을 참고해 주세요.

YouTrack에 피드백을 남겨 주시면 감사하겠어요.

컬렉션 요소의 동등성과 유일성 검사 함수

Kotlin 2.4.20-RC3 이전에는 컬렉션 요소가 모두 서로 다르거나 모두 같은지 검사하고 싶다면 비효율적인 코드 패턴을 사용해야 했어요.

Kotlin 2.4.20-RC3은 이 공백을 메우는 실험적 함수를 도입해요.

함수 검사 내용
.allDistinct() 컬렉션의 모든 값이 유일하다.
.allDistinctBy() 선택한 속성에 대해 모든 객체가 유일한 값을 가진다.
.allEqual() 컬렉션의 모든 값이 같다.
.allEqualBy() 선택한 속성에 대해 모든 객체가 같은 값을 가진다.

이 함수들은 컬렉션, 시퀀스, 배열에서 사용할 수 있어요. 다른 컬렉션 연산처럼 구조적 동등성으로 요소를 비교해요.

이 함수들은 Experimental 상태이고 @OptIn(ExperimentalStdlibApi::class) 어노테이션이나 -opt-in=kotlin.ExperimentalStdlibApi 컴파일러 옵션으로 옵트인이 필요해요.

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    data class Response(
        val participantId: String,
        val answer: String,
        val responseDate: String
    )

    val responses = listOf(
        Response("P001", "Yes", "2026-07-21"),
        Response("P002", "Maybe", "2026-07-21"),
        Response("P003", "No", "2026-07-21")
    )

    // Checks if all participants gave the same answer
    println(responses.allEqualBy { it.answer })
    // false

    // Checks for duplicate participants
    println(responses.allDistinctBy { it.participantId })
    // true

    // Checks if all responses were submitted on the same date
    println(responses.allEqualBy { it.responseDate })
    // true

    val answers = responses.map { it.answer }

    // Checks if answers are identical
    println(answers.allEqual())
    // false

    // Checks if answers are distinct
    println(answers.allDistinct())
    // true
}

이 함수들에 대한 여러분의 경험을 YouTrack에 알려 주시면 감사하겠어요.

Kotlin/Native

Kotlin 2.4.20-RC3은 sealed 클래스와 크로스 언어 상속 지원을 포함한 새로운 Swift export 기능과, SwiftPM 의존성을 위한 Package.swift 파일의 자동 생성을 가져와요.

새로운 Swift export 기능

Sealed 클래스

Kotlin 2.4.20-RC3은 Swift export에 sealed 클래스와 인터페이스 지원을 추가해요.

이전에는 sealed 타입에 대한 모든 switch 문에 default 케이스를 작성해야 했어요. 이제 Kotlin에서 정의된 sealed 계층 구조가 Swift enum으로 매핑되어, Xcode에서 전체 자동 완성을 지원하는 완전한(exhaustive) switch 문을 가능하게 해요.

Swift export는 각 sealed 타입에 .sealedType() 메서드를 생성해요. 이 메서드는 sealed 계층 구조의 직접 하위 클래스와 일치하는 케이스를 가진 Swift enum을 반환해요. 이런 호출을 중첩해서 더 깊은 계층 수준을 매칭할 수 있어요.

예를 들어 Kotlin에서 클래스 계층 구조를 가진 sealed 인터페이스를 선언해 볼게요.

// Kotlin
sealed interface Shape

class Circle : Shape {
   override fun toString(): String = "Circle"
}

class Rectangle : Shape {
   override fun toString(): String = "Rectangle"
}

fun createCircle(): Shape = Circle()

Swift 쪽에서는 default 케이스 없이 완전한 switch를 사용할 수 있어요.

// Swift
let shape = createCircle()

let name = switch shape.sealedType() {
   case let .circle(type): "It's a \(type.value)"
   case let .rectangle(type): "It's a \(type.value)"
}
// name == "It's a Circle"

switch가 완전하기 때문에 sealed 계층 구조에 새 하위 클래스가 추가되면 컴파일러가 경고를 주어서, default 케이스에 의존하는 대신 즉시 처리할 수 있어요.

Swift export의 크로스 언어 상속

Kotlin 2.4.20-RC3은 Swift export에 크로스 언어 상속 지원을 도입해요.

이 기능의 일반적인 유스 케이스는 역방향 import 패턴(reverse import pattern)인데, Kotlin에서 계약(contract)을 정의하고 Swift 쪽에서 플랫폼별 구현을 제공하는 거예요. 이것은 Kotlin에 직접 import할 수 없는 순수 Swift 라이브러리를 사용해야 할 때 특히 유용해요.

패턴을 구현하려면 Swift 구현이 상속할 Kotlin 슈퍼클래스와 Kotlin 인터페이스를 선언하세요. 그런 다음 Swift에서 인터페이스를 구현하고, 그 인터페이스를 받는 Kotlin 함수에 Swift 객체를 전달해요. 예를 들어 CryptoKit 라이브러리의 경우:

  1. Kotlin 쪽에서 open 기본 클래스와 그것을 받는 함수가 있는 Kotlin 인터페이스를 선언해요.
// Kotlin
interface CryptoProvider {
   fun hashMD5(input: String): String
}

fun processHash(provider: CryptoProvider, input: String): String = provider.hashMD5(input)

open class SwiftBase
  1. Swift 쪽에서 export된 SwiftBase 클래스를 상속하고 순수 Swift 라이브러리로 인터페이스를 구현한 다음, 객체를 Kotlin에 다시 전달해요.
// Swift
import CryptoKit

final class IosCryptoProvider: SwiftBase, CryptoProvider {
   func hashMD5(input: String) -> String {
       guard let data = input.data(using: .utf8) else { return "failed" }
       return Insecure.MD5.hash(data: data).description
   }
}

let provider = IosCryptoProvider()

// The call is dispatched to the Swift implementation
print(processHash(provider: provider, input: "Hello, world!"))

Kotlin이 Swift 객체를 받으면 그것을 일반 인터페이스의 구현처럼 취급해서 Swift 코드를 실행해요.

Swift export에 대한 자세한 내용은 우리 문서를 참고해 주세요.

SwiftPM 의존성을 위한 생성된 Package.swift

SwiftPM 패키지에 의존하는 XCFramework를 export할 때 제대로 해석되도록 결과 SwiftPM 패키지를 게시해야 해요. 이를 돕기 위해 assembleSharedXCFramework Gradle 태스크가 이제 XCFramework와 함께 배포할 Package.swift 파일을 생성해요.

자세한 내용은 SwiftPM export 페이지를 참고해 주세요.

Kotlin/Wasm

Kotlin 2.4.20-RC3은 Kotlin/Wasm이 @JsFun 선언에서 최상위 require() 호출을 처리하는 방식을 변경하고, companion 객체 초기화 순서를 JVM 동작과 일치시키며, Kotlin Gradle 플러그인의 wasmWasi 타깃 런타임으로 Wasmtime 지원을 추가해요.

@JsFun 선언의 최상위 require() 호출 변경

Kotlin/Wasm은 이제 @JsFun 선언이 최상위 require() 함수를 사용하면 오류를 보고해요.

이전에는 컴파일러가 import-object.mjs 파일에 require 변수를 생성해서, @JsFun 선언이 require()를 호출할 수 있었어요.

이 동작은 의도치 않게 컴파일러 구현 세부 사항을 노출했어요. 이것에서 벗어나는 마이그레이션을 지원하기 위해 Kotlin/Wasm은 생성된 require 선언을 제거하고, 컴파일러가 이제 그런 호출에 대해 오류를 보고해요. 예를 들어:

// Reports an error
@JsFun("(mod) => require(mod)")
external fun loadModule(mod: String): JsAny

이 변경을 준비하려면 @JsFun 선언의 최상위 require() 호출을 @JsModule 어노테이션으로 대체해 주세요.

@JsModule("module")
external val module: Module

external interface Module {
    // Defines the expected module members
}

동적 모듈 로딩을 위해서는 import() 표현식을 사용하세요. webpack이 동적 import를 파싱하지 못하게 하려면 /* webpackIgnore: true */ 매직 주석을 추가해 주세요.

@JsFun("""
    ((module) => () => module)(
        await import(/* webpackIgnore: true */ "module")
    )
""")
private external fun loadModuleDynamically(): JsAny?

import() 표현식을 조건부로 사용할 수도 있어요. 예를 들어 Node.js에서 실행할 때만 모듈을 로드할 수 있어요.

@JsFun("""
    ((module) => () => module)(
        ((typeof process !== "undefined") && (process.release.name === "node"))
            ? await import(/* webpackIgnore: true */ "module")
            : null
    )
""")
private external fun loadNodeModule(): JsAny?

프로젝트가 최상위 require() 함수를 요구하는 의존성에 의존한다면, 해결 방법으로 그것을 globalThis의 속성으로 추가해 주세요.

@JsFun("""
    ((module) => {
        globalThis.require = module.default.createRequire(import.meta.url)
        return () => {}
    })(await import("node:module"))
""")
external fun defineRequire()

문제가 생기면 이슈 트래커에 피드백을 남겨 주세요.

개선된 companion 객체 초기화 순서

Kotlin/Wasm은 이제 슈퍼클래스의 companion 객체를 하위 클래스의 companion 객체보다 먼저 초기화해서 JVM 동작과 일치해요. 이전에는 초기화 순서가 뒤집힐 수 있어서 플랫폼 간 동작이 일관되지 않았어요.

이 업데이트는 크로스 플랫폼 일관성을 개선하고 클래스 초기화 동작의 플랫폼별 차이를 줄여요. 또한 중간 클래스가 companion 객체를 선언하지 않는 경우를 포함해 더 깊은 상속 계층에서 companion 객체 초기화를 올바르게 처리할 수 있게 해 줘요.

Kotlin Gradle 플러그인의 Wasmtime 지원

Kotlin 2.4.20-RC3은 Kotlin Gradle 플러그인의 wasmWasi 타깃 런타임으로 Wasmtime 지원을 도입해요.

이전에는 wasmWasi 타깃이 Node.js 런타임만 지원해서 WASI 애플리케이션을 실행하려면 JavaScript 부트스트랩이 필요했어요. Wasmtime 지원 덕분에 이제 독립형 WebAssembly 런타임에서 Kotlin/Wasm 애플리케이션을 실행할 수 있어요.

wasmWasi 타깃의 런타임으로 Wasmtime을 사용하려면 Gradle 빌드 파일에 wasmtime()을 추가해 주세요.

kotlin {
    wasmWasi {
        wasmtime()
    }
}

YouTrack에 피드백을 남겨 주시면 감사하겠어요.

Kotlin/JS

Kotlin 2.4.20-RC3은 브라우저 테스트를 위한 새 실험적 DSL을 도입하고, suspend 람다를 JavaScript async 함수로 내보내는 것을 지원해요.

브라우저 테스트를 위한 새 DSL

Kotlin 2.4.20-RC3은 브라우저 환경에서 Kotlin/JS 테스트를 실행하기 위한 새로운 실험적 DSL을 도입해요.

현재 Kotlin Gradle 플러그인은 다양한 브라우저에서 JavaScript 테스트를 실행하기 위해 브라우저 런처로 Karma를 사용해요. Karma 프로젝트는 이미 2년 동안 deprecated 되어서 브라우저 테스트를 지원할 다른 방법을 모색하게 되었어요.

새 DSL은 내부에서 다양한 도구를 관리하는 Karma를 대체하려는 것으로, 다음을 포함해요.

  • 테스트 러너로 Mocha.
  • 번들러로 Webpack(향후 릴리스에서 Vite로 대체 예정).
  • Chromium, Firefox, WebKit(Safari) 브라우저 엔진을 지원하는 브라우저 드라이버이자 배포 관리자로 Playwright.

새 테스트 DSL을 사용해 보려면 Kotlin/JS 타깃의 browser{} 안에 옵트인 test{} 블록을 추가해 주세요.

import org.jetbrains.kotlin.gradle.ExperimentalJsTestDsl
import kotlin.time.Duration.Companion.seconds

kotlin {
    js {
        browser {
            @OptIn(ExperimentalJsTestDsl::class)
            // Add and configure the new test{} block
            test {
                // Configure default timeout for all runners
                timeout = 2.seconds
                // Configure headless mode using Gradle providers
                headless = providers
                    .environmentVariable("IS_IN_CI")
                    .map { it.toBoolean() }
                    .orElse(false)
                // Enable and configure Chromium test runner
                chromium {
                    // Override the common timeout option
                    timeout = 5.seconds
                    // Add extra launch arguments
                    launchArgs.add("--no-sandbox")
                }
                // Enable Firefox test runner
                firefox()
                // Enable WebKit test runner
                webkit()
                // Enable and configure an additional WebKit test runner
                webkit("noheadless") {
                    // Set up custom options
                    headless = false
                }
            }
        }
    }
}

새 DSL은 활발히 개발 중이에요. YouTrack에 피드백을 남겨 주시면 감사하겠어요.

suspend 람다를 async 함수로 내보내기 지원

Kotlin 2.4.20-RC3부터 이제 suspend 람다 표현식을 JavaScript async 함수로 내보낼 수 있어요.

이전에는 Kotlin/JS 라이브러리에서 suspend 람다를 포함한 선언을 내보낼 방법이 없었어요. 이제 Kotlin 컴파일러가 Kotlin의 suspend 함수와 JavaScript 네이티브 async/await 모델 사이의 브리징을 자동으로 처리해서, Kotlin/TypeScript 혼합 코드베이스에 유용해요.

이 기능을 활성화하려면 build.gradle.kts 파일에 다음 컴파일러 옵션을 추가해 주세요.

kotlin {
    js {
        compilations.all {
            compileTaskProvider.configure {
                compilerOptions {
                    freeCompilerArgs.add("-Xsuspend-lambda-exporting")
                }
            }
        }
    }
}

그런 다음 관련 선언을 @JsExport로 표시해 주세요.

// Kotlin
@JsExport
class TaskRunner {
    suspend fun runTask(task: suspend () -> String): String {
        return task()
    }
}

TypeScript 쪽에서는 suspend 람다가 일반 async 함수로 나타나요.

// TypeScript
import { TaskRunner } from "..."

const runner = new TaskRunner();
const result = await runner.runTask(async () => "done");
console.log(result); // "done"

@JsExport 어노테이션에 대한 자세한 내용은 우리 문서를 참고해 주세요.

빌드 도구 API

Kotlin/JS, Kotlin/Wasm, Kotlin 메타데이터 지원

Kotlin 2.2.0에서 빌드 도구 API(BTA)가 Kotlin/JVM에서 사용 가능해졌어요. Kotlin 2.4.20-RC3은 Kotlin/JS, Kotlin/Wasm, Kotlin 메타데이터 같은 새 타깃 지원을 추가해서 BTA 안정화를 향한 다음 단계를 밟아요.

이로써 Kotlin Gradle 플러그인이 컴파일러와 더 일관되게 상호작용해요. 어떤 경우에는 더 빠르고 안정적인 컴파일의 혜택을 볼 수도 있어요.

BTA는 빌드 시스템과 Kotlin 컴파일러 생태계 사이의 추상화 레이어 역할을 하는 범용 API예요. 이 API는 사용 가능한 빌드 도구에서 Kotlin 기능과 Kotlin 컴파일러 호환성을 지원하는 데 도움을 줘요.

Kotlin 2.4.20-RC3에서 BTA는 새 타깃에 대해 옵트인으로 사용할 수 있어요. 사용해 보려면 gradle.properties 파일에 해당 속성을 추가해 주세요.

kotlin.wasm.runViaBuildToolsApi=true
kotlin.js.runViaBuildToolsApi=true
kotlin.metadata.runViaBuildToolsApi=true

Kotlin 2.5.0부터는 Kotlin/JS, Kotlin/Wasm, Kotlin 메타데이터에서 BTA를 기본으로 활성화할 계획이에요.

BTA 제안에 관심이 있거나 피드백을 공유하고 싶다면 이 KEEP을 참고해 주세요.

Kotlin 컴파일러: 네이티브 이미지

Kotlin 2.4.20-RC3은 Kotlin 컴파일러 네이티브 이미지의 첫 번째 Experimental 릴리스를 선보여요. 네이티브 이미지는 표준 kotlinc 명령줄 도구의 드롭인 대체품으로, 더 빠른 시작 시간과 더 높은 성능을 제공해요.

네이티브 이미지를 사용해 보려면 GitHub Releases에서 빌드를 다운로드해 주세요.

네이티브 이미지는 -Xplugin 또는 -Xcompiler-plugin CLI 옵션과 함께 사용할 수 있는 다음 컴파일러 플러그인도 번들로 포함해요.

  • Serialization
  • Compose compiler
  • All-open
  • no-arg
  • SAM with receiver
  • Assignment
  • Lombok
  • Power-assert

Kotlin 컴파일러 네이티브 이미지에 대한 자세한 내용은 그 README를 참고해 주세요.

더 알아보기