Kotlin 1.6.20의 새로운 기능
Kotlin 1.6.20의 새로운 기능
Kotlin 1.6.20은 향후 언어 기능의 프리뷰를 공개하고, 멀티플랫폼 프로젝트의 기본값을 계층적 구조로 만들며, 다른 컴포넌트들에 진화적인 개선을 가져와요. 2022년 4월 4일에 공개된 이 버전의 변경 사항에 대한 짧은 개요는 아래 영상에서 확인할 수 있어요.
본문
언어 (Language)
Kotlin 1.6.20에서는 두 개의 새 언어 기능을 시도할 수 있어요:
Kotlin/JVM용 컨텍스트 리시버 프로토타입
Kotlin 1.6.20부터는 리시버 하나로 제한되지 않아요. 더 필요하다면 선언에 컨텍스트 리시버를 추가해서 함수, 프로퍼티, 클래스를 컨텍스트 의존적(즉, 컨텍스트 기반)으로 만들 수 있어요. 컨텍스트 기반 선언은 다음을 수행해요:
- 선언된 모든 컨텍스트 리시버가 호출자 범위에 암시적 리시버로 존재하도록 요구해요.
- 선언된 컨텍스트 리시버를 그 본문 범위에 암시적 리시버로 가져와요.
interface LoggingContext {
val log: Logger // 이 컨텍스트는 로거에 대한 참조를 제공한다
}
context(LoggingContext)
fun startBusinessOperation() {
// LoggingContext가 암시적 리시버이므로 log 프로퍼티에 접근할 수 있다
log.info("Operation has started")
}
fun test(loggingContext: LoggingContext) {
with(loggingContext) {
// startBusinessOperation()을 호출하려면
// LoggingContext가 암시적 리시버로 범위에 있어야 한다
startBusinessOperation()
}
}
프로젝트에서 컨텍스트 리시버를 켜려면 -Xcontext-receivers 컴파일러 옵션을 사용하면 돼요. 기능과 문법에 대한 자세한 설명은 KEEP에서 확인할 수 있어요.
구현이 프로토타입이라는 점을 유의하세요:
-Xcontext-receivers를 켜면 컴파일러가 프로덕션 코드에서 사용할 수 없는 사전 릴리스 바이너리를 생성해요.- 컨텍스트 리시버에 대한 IDE 지원은 지금은 최소한이에요.
장난감(toy) 프로젝트에서 기능을 시도하고 생각과 경험을 이 YouTrack 이슈에서 공유해 주세요. 문제가 생기면 새 이슈를 등록해 주세요.
확실히 non-null 타입 (Definitely non-nullable types)
제네릭 Java 클래스와 인터페이스를 확장할 때 더 나은 상호 운용성을 제공하기 위해, Kotlin 1.6.20은 새 문법 T & Any로 사용 지점에서 제네릭 타입 파라미터를 확실히 non-null로 표시할 수 있게 해요. 이 문법 형태는 교집합 타입(intersection types) 표기에서 왔으며, 현재 & 왼쪽에 nullable 상한을 가진 타입 파라미터, 오른쪽에 non-null Any로 제한돼요:
fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y
fun main() {
// OK
elvisLike<String>("", "").length
// Error: 'null' cannot be a value of a non-null type
elvisLike<String>("", null).length
// OK
elvisLike<String?>(null, "").length
// Error: 'null' cannot be a value of a non-null type
elvisLike<String?>(null, null).length
}
이 기능을 켜려면 언어 버전을 1.7로 설정하면 돼요:
kotlin {
sourceSets.all {
languageSettings.apply {
languageVersion = "1.7"
}
}
}
kotlin {
sourceSets.all {
languageSettings {
languageVersion = '1.7'
}
}
}
확실히 non-null 타입에 대해 KEEP에서 더 알아보세요.
Kotlin/JVM
Kotlin 1.6.20은 다음을 도입해요:
- JVM 인터페이스의 기본 메서드 호환성 개선: 인터페이스용 새 @JvmDefaultWithCompatibility 애노테이션과 -Xjvm-default 모드의 호환성 변화
- JVM 백엔드에서 단일 모듈의 병렬 컴파일 지원
- 함수형 인터페이스 생성자에 대한 호출 가능 참조 지원
인터페이스용 새 @JvmDefaultWithCompatibility 애노테이션
Kotlin 1.6.20은 새 애노테이션 @JvmDefaultWithCompatibility를 도입해요. -Xjvm-default=all 컴파일러 옵션과 함께 사용해서 어떤 Kotlin 인터페이스의 비추상 멤버든 JVM 인터페이스에 기본 메서드를 만들 수 있어요.
-Xjvm-default=all 옵션 없이 컴파일된 Kotlin 인터페이스를 사용하는 클라이언트가 있다면, 그 클라이언트는 이 옵션으로 컴파일된 코드와 바이너리 호환되지 않을 수 있어요. Kotlin 1.6.20 이전에는 이 호환성 문제를 피하기 위해 권장되는 접근 방식이 -Xjvm-default=all-compatibility 모드와, 이런 유형의 호환성이 필요 없는 인터페이스에는 @JvmDefaultWithoutCompatibility 애노테이션을 함께 사용하는 것이었어요.
이 접근 방식에는 몇 가지 단점이 있었어요:
- 새 인터페이스가 추가될 때 애노테이션을 잊어버리기 쉬웠어요.
- 보통 공개 API보다 비공개 부분에 인터페이스가 더 많아서, 코드 여러 곳에 이 애노테이션이 있게 됐어요.
이제 -Xjvm-default=all 모드를 사용하고 인터페이스에 @JvmDefaultWithCompatibility 애노테이션을 달면 돼요. 이렇게 하면 공개 API의 모든 인터페이스에 이 애노테이션을 한 번 추가할 수 있고, 새 비공개 코드에는 어떤 애노테이션도 사용할 필요가 없어요.
이 새 애노테이션에 대한 피드백을 이 YouTrack 티켓에 남겨 주세요.
-Xjvm-default 모드의 호환성 변화
Kotlin 1.6.20은 기본 모드(-Xjvm-default=disable 컴파일러 옵션)로 컴파일된 모듈을 -Xjvm-default=all 또는 -Xjvm-default=all-compatibility 모드로 컴파일된 모듈에 대해 컴파일하는 옵션을 추가해요. 이전처럼 모든 모듈이 -Xjvm-default=all 또는 -Xjvm-default=all-compatibility 모드라면 컴파일이 성공해요. 이 YouTrack 이슈에 피드백을 남길 수 있어요.
Kotlin 1.6.20은 컴파일러 옵션 -Xjvm-default의 compatibility와 enable 모드를 deprecated 해요. 다른 모드들의 설명에는 호환성 관련 변화가 있지만, 전체 로직은 같아요. 업데이트된 설명을 확인할 수 있어요.
Java 상호 운용에서 기본 메서드에 대한 자세한 정보는 상호 운용성 문서와 이 블로그 포스트를 참고하세요.
JVM 백엔드에서 단일 모듈의 병렬 컴파일 지원
우리는 새 JVM IR 백엔드 컴파일 시간을 개선하는 작업을 계속하고 있어요. Kotlin 1.6.20에서 모듈의 모든 파일을 병렬로 컴파일하는 실험적 JVM IR 백엔드 모드를 추가했어요. 병렬 컴파일은 전체 컴파일 시간을 최대 15% 줄일 수 있어요.
컴파일러 옵션 -Xbackend-threads로 실험적 병렬 백엔드 모드를 켜세요. 이 옵션에 다음 인자들을 사용해요:
N— 사용하려는 스레드 수예요. CPU 코어 수보다 크면 안 돼요. 그렇지 않으면 스레드 간 컨텍스트 전환 때문에 병렬화 효과가 사라져요.0— 각 CPU 코어에 별도 스레드를 사용해요.
Gradle은 태스크를 병렬로 실행할 수 있지만, 이 유형의 병렬화는 프로젝트(또는 프로젝트의 주요 부분)가 Gradle 관점에서 단지 하나의 큰 태스크일 때는 큰 도움이 되지 않아요. 매우 큰 모놀리식 모듈이 있다면 병렬 컴파일을 사용해서 더 빨리 컴파일하세요. 프로젝트가 많은 작은 모듈로 구성되어 있고 Gradle에 의해 빌드가 병렬화된다면, 또 다른 병렬화 계층을 추가하는 것은 컨텍스트 전환 때문에 성능을 해칠 수 있어요.
함수형 인터페이스 생성자에 대한 호출 가능 참조 지원
함수형 인터페이스 생성자에 대한 호출 가능 참조 지원은 생성자 함수가 있는 인터페이스에서 함수형 인터페이스로 마이그레이션하는 소스 호환 방식(source-compatible way)을 추가해요.
다음 코드를 고려해 볼까요:
interface Printer {
fun print()
}
fun Printer(block: () -> Unit): Printer = object : Printer { override fun print() = block() }
함수형 인터페이스 생성자에 대한 호출 가능 참조를 켜면 이 코드를 그냥 함수형 인터페이스 선언으로 바꿀 수 있어요:
fun interface Printer {
fun print()
}
그 생성자가 암시적으로 만들어질 것이고, ::Printer 함수 참조를 사용하는 어떤 코드든 컴파일돼요. 예를 들어:
documentsStorage.addPrinter(::Printer)
레거시 함수 Printer를 @Deprecated 애노테이션과 DeprecationLevel.HIDDEN으로 표시해서 바이너리 호환성을 보존하세요:
@Deprecated(message = "Your message about the deprecation", level = DeprecationLevel.HIDDEN)
fun Printer(...) {...}
이 기능을 켜려면 컴파일러 옵션 -XXLanguage:+KotlinFunInterfaceConstructorReference를 사용하면 돼요.
Kotlin/Native
Kotlin/Native 1.6.20은 새 컴포넌트들의 지속적인 개발을 나타내요. 우리는 다른 플랫폼에서의 Kotlin과 일관된 경험을 향해 다시 한 걸음 나아갔어요:
- 새 메모리 매니저 업데이트
- 새 메모리 매니저의 sweep 단계 동시 구현
- 애노테이션 클래스 인스턴스화
- Swift async/await 상호 운용: KotlinUnit 대신 Swift의 Void 반환
- libbacktrace로 더 나은 스택 트레이스
- 독립 실행형 Android 실행 파일 지원
- 성능 개선
- cinterop 모듈 import 중 개선된 오류 처리
- Xcode 13 라이브러리 지원
새 메모리 매니저 업데이트
Kotlin 1.6.20에서 새 Kotlin/Native 메모리 매니저의 Alpha 버전을 시도할 수 있어요. 이것은 멀티플랫폼 프로젝트에서 일관된 개발자 경험을 제공하기 위해 JVM과 Native 플랫폼 사이의 차이를 제거해요. 예를 들어 Android와 iOS 모두에서 동작하는 새 크로스 플랫폼 모바일 애플리케이션을 만드는 것이 훨씬 쉬워져요.
새 Kotlin/Native 메모리 매니저는 스레드 간 객체 공유에 대한 제한을 없애요. 또한 특별한 관리나 애노테이션을 요구하지 않는 안전한 누수 없는(leak-free) 동시 프로그래밍 프리미티브를 제공해요.
새 메모리 매니저는 향후 버전에서 기본이 될 것이므로, 지금 시도해 보시길 권해요. 새 메모리 매니저에 대해 더 알아보고 데모 프로젝트를 탐색하려면 블로그 포스트를 확인하거나, 직접 시도해 보려면 마이그레이션 지침으로 바로 갈 수 있어요.
새 메모리 매니저를 프로젝트에서 사용해 어떻게 동작하는지 확인하고 이슈 트래커 YouTrack에 피드백을 공유해 주세요.
새 메모리 매니저의 sweep 단계 동시 구현
이미 Kotlin 1.6에서 발표된 새 메모리 매니저로 전환했다면 실행 시간이 크게 개선된 것을 눈치챘을 거예요. 벤치마크는 평균 35% 개선을 보여줘요. 1.6.20부터는 새 메모리 매니저에 sweep 단계의 동시 구현도 사용할 수 있어요. 이것은 또한 성능을 개선하고 가비지 컬렉터 일시 정지 기간을 줄여야 해요.
새 Kotlin/Native 메모리 매니저에서 이 기능을 켜려면 다음 컴파일러 옵션을 전달하면 돼요:
-Xgc=cms
새 메모리 매니저 성능에 대한 피드백을 이 YouTrack 이슈에서 자유롭게 공유해 주세요.
애노테이션 클래스 인스턴스화
Kotlin 1.6.0에서 애노테이션 클래스 인스턴스화가 Kotlin/JVM과 Kotlin/JS에서 Stable이 됐어요. 1.6.20 버전은 Kotlin/Native에 대한 지원을 제공해요.
애노테이션 클래스 인스턴스화에 대해 더 알아보세요.
Swift async/await 상호 운용: KotlinUnit 대신 Void 반환
우리는 Swift의 async/await와의 실험적 상호 운용(Swift 5.5부터 사용 가능) 작업을 계속했어요. Kotlin 1.6.20은 Unit 반환 타입을 가진 suspend 함수를 다루는 방식에서 이전 버전과 달라요.
이전에는 그런 함수가 Swift에서 KotlinUnit을 반환하는 async 함수로 제시됐어요. 그러나 그들에게 적절한 반환 타입은 non-suspending 함수와 유사하게 Void예요.
기존 코드를 깨지 않기 위해, Unit을 반환하는 suspend 함수를 Void 반환 타입의 async Swift로 변환하게 하는 Gradle 프로퍼티를 도입해요:
# gradle.properties
kotlin.native.binary.unitSuspendFunctionObjCExport=proper
우리는 향후 Kotlin 릴리스에서 이 동작을 기본으로 만들 계획이에요.
libbacktrace로 더 나은 스택 트레이스
Kotlin/Native는 이제 linux*(linuxMips32와 linuxMipsel32 제외)와 androidNative* 타깃을 더 잘 디버깅하기 위해 파일 위치와 줄 번호가 있는 상세 스택 트레이스를 생성할 수 있어요.
이 기능은 내부적으로 libbacktrace 라이브러리를 사용해요. 차이의 예시를 보려면 다음 코드를 살펴보세요:
fun main() = bar()
fun bar() = baz()
inline fun baz() {
error("")
}
- 1.6.20 이전:
e: Uncaught Kotlin exception: kotlin.IllegalStateException: - libbacktrace를 사용한 1.6.20:
e: Uncaught Kotlin exception: kotlin.IllegalStateException: at fooKt.baz(main.kt:11)
스택 트레이스에 이미 파일 위치와 줄 번호가 있던 Apple 타깃에서 libbacktrace는 inline 함수 호출에 대한 더 많은 상세 정보를 제공해요.
- 1.6.20 이전:
at fooKt.baz(kotlin-native/main.kt:11) - libbacktrace를 사용한 1.6.20:
at fooKt.baz(main.kt:11) at fooKt.bar(main.kt:10)
libbacktrace로 더 나은 스택 트레이스를 생성하려면 gradle.properties에 다음 줄을 추가하면 돼요:
# gradle.properties
kotlin.native.binary.sourceInfoType=libbacktrace
libbacktrace로 Kotlin/Native를 디버깅하는 것이 어떻게 동작하는지 이 YouTrack 이슈에서 알려주세요.
독립 실행형 Android 실행 파일 지원
이전에는 Kotlin/Native의 Android Native 실행 파일이 실제 실행 파일이 아니라 NativeActivity로 사용할 수 있는 공유 라이브러리였어요. 이제 Android Native 타깃에 대해 표준 실행 파일을 생성하는 옵션이 있어요.
그러려면 프로젝트의 build.gradle(.kts) 부분에서 androidNative 타깃의 executable 블록을 구성하면 돼요. 다음 바이너리 옵션을 추가하세요:
kotlin {
androidNativeX64("android") {
binaries {
executable {
binaryOptions["androidProgramType"] = "standalone"
}
}
}
}
이 기능은 Kotlin 1.7.0에서 기본이 될 것이라는 점을 유의하세요. 현재 동작을 보존하고 싶다면 다음 설정을 사용하면 돼요:
binaryOptions["androidProgramType"] = "nativeActivity"
구현에 감사한 Mattia Iavarone에게 감사를 전해요!
성능 개선
우리는 컴파일 과정을 빠르게 하고 개발 경험을 개선하기 위해 Kotlin/Native에 열심히 작업하고 있어요.
Kotlin 1.6.20은 Kotlin이 생성하는 LLVM IR에 영향을 주는 일부 성능 업데이트와 버그 수정을 가져와요. 내부 프로젝트의 벤치마크에 따르면 평균적으로 다음 성능 향상을 달성했어요:
- 실행 시간 15% 감소
- 릴리스와 디버그 바이너리 모두의 코드 크기 20% 감소
- 릴리스 바이너리의 컴파일 시간 26% 감소
또한 이 변화들은 큰 내부 프로젝트에서 디버그 바이너리의 컴파일 시간 10% 감소를 제공해요.
이를 위해 컴파일러가 생성하는 일부 합성 객체에 대한 정적 초기화를 구현하고, 모든 함수에 대해 LLVM IR을 구성하는 방식을 개선하며, 컴파일러 캐시를 최적화했어요.
cinterop 모듈 import 중 개선된 오류 처리
이 릴리스는 cinterop 도구로 Objective-C 모듈을 import할 때(CocoaPods pods에서 일반적인 경우) 발생하는 오류에 대한 개선된 처리를 도입해요. 이전에는 Objective-C 모듈을 다룰 때 오류가 나면(예: 헤더의 컴파일 오류를 다룰 때) fatal error: could not build module $name 같은 정보 없는 오류 메시지를 받았어요. 우리는 cinterop 도구의 이 부분을 확장해서, 이제 확장된 설명이 있는 오류 메시지를 받을 수 있어요.
Xcode 13 라이브러리 지원
Xcode 13과 함께 제공되는 라이브러리들이 이 릴리스부터 완전히 지원돼요. Kotlin 코드 어디서든 자유롭게 접근할 수 있어요.
Kotlin Multiplatform
1.6.20은 Kotlin Multiplatform에 다음 주목할 만한 업데이트를 가져와요:
멀티플랫폼 프로젝트를 위한 계층적 구조 지원
Kotlin 1.6.20은 계층적 구조 지원을 기본으로 켜고 와요. Kotlin 1.4.0에서 도입한 이후 프론트엔드를 크게 개선하고 IDE import를 안정적으로 만들었어요.
이전에는 멀티플랫폼 프로젝트에 코드를 추가하는 두 가지 방법이 있었어요. 첫 번째는 플랫폼별 소스셋에 넣는 것인데, 한 타깃으로 제한되어 다른 플랫폼이 재사용할 수 없어요. 두 번째는 Kotlin이 현재 지원하는 모든 플랫폼에서 공유되는 공통 소스셋을 사용하는 거예요.
이제 많은 공통 로직과 제3자 API를 재사용하는 여러 유사한 네이티브 타깃 사이에서 소스 코드를 공유할 수 있어요. 이 기술은 올바른 기본 의존성을 제공하고 공유 코드에서 사용할 수 있는 정확한 API를 찾아줘요. 이것은 복잡한 빌드 설정과 네이티브 타깃 간 소스셋 공유에 대한 IDE 지원을 얻기 위한 우회 방법을 없애 줘요. 또한 다른 타깃을 위한 안전하지 않은 API 사용을 방지하는 데도 도움이 돼요.
이 기술은 라이브러리 작성자에게도 유용해요. 계층적 프로젝트 구조 덕분에 타깃의 일부에 대한 공통 API를 가진 라이브러리를 퍼블리시하고 소비할 수 있기 때문이에요.
기본적으로 계층적 프로젝트 구조로 퍼블리시된 라이브러리는 계층적 구조 프로젝트에서만 호환돼요.
프로젝트에서 더 나은 코드 공유
계층적 구조 지원이 없으면 일부(전부는 아닌) Kotlin 타깃 사이에서 코드를 공유하는 간단한 방법이 없어요. 인기 있는 예시는 모든 iOS 타깃에서 코드를 공유하고 Foundation 같은 iOS 특화 의존성에 접근하는 거예요.
계층적 프로젝트 구조 지원 덕분에 이제 박스로 바로 이것을 달성할 수 있어요. 새 구조에서 소스셋은 계층을 형성해요. 주어진 소스셋이 컴파일되는 각 타깃에서 사용할 수 있는 플랫폼별 언어 기능과 의존성을 사용할 수 있어요.
예를 들어 iosArm64와 iosX64라는 두 타깃(각각 iOS 기기와 시뮬레이터용)을 가진 전형적인 멀티플랫폼 프로젝트를 생각해 보세요. Kotlin 툴링은 두 타깃이 같은 함수를 가진다는 것을 이해하고 중간 소스셋 iosMain에서 그 함수에 접근할 수 있게 해줘요.
Kotlin 툴체인은 Kotlin/Native stdlib이나 네이티브 라이브러리 같은 올바른 기본 의존성을 제공해요. 게다가 Kotlin 툴링은 공유 코드에서 사용할 수 있는 API 표면 영역을 정확히 찾으려 최선을 다해요. 이것은 예를 들어 Windows용으로 공유된 코드에서 macOS 특화 함수를 사용하는 것 같은 경우를 방지해요.
라이브러리 작성자를 위한 더 많은 기회
멀티플랫폼 라이브러리가 퍼블리시되면 중간 소스셋의 API가 이제 그와 함께 제대로 퍼블리시되어 소비자에게 제공돼요. 다시 한번 Kotlin 툴체인은 JVM용 API를 JS 코드에서 사용하는 것 같은 안전하지 않은 사용을 주의 깊게 살피면서 소비자 소스셋에서 사용할 수 있는 API를 자동으로 파악해요. 라이브러리에서 코드 공유에 대해 더 알아보세요.
구성과 설정
Kotlin 1.6.20부터 모든 새 멀티플랫폼 프로젝트는 계층적 프로젝트 구조를 가져요. 추가 설정이 필요 없어요.
- 이미 수동으로 켰다면
gradle.properties에서 deprecated 옵션을 제거할 수 있어요:# gradle.properties kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.native.enableDependencyPropagation=false // 또는 이전 설정에 따라 'true' - Kotlin 1.6.20에서는 최상의 경험을 위해 Android Studio 2021.1.1(Bumblebee) 이상을 사용하는 것을 권장해요.
- 옵트아웃할 수도 있어요. 계층적 구조 지원을 끄려면
gradle.properties에 다음 옵션을 설정하면 돼요:# gradle.properties kotlin.mpp.hierarchicalStructureSupport=false
피드백 남기기
이것은 전체 생태계에 대한 중요한 변화예요. 더 좋게 만들기 위해 여러분의 피드백을 환영해요.
지금 시도해 보고 만나는 어려움이 있으면 이슈 트래커로 보고해 주세요.
Kotlin CocoaPods Gradle 플러그인
CocoaPods 통합을 단순화하기 위해 Kotlin 1.6.20은 다음 기능들을 제공해요:
- CocoaPods 플러그인에는 이제 등록된 모든 타깃으로 XCFramework를 빌드하고 Podspec 파일을 생성하는 태스크가 있어요. Xcode와 직접 통합하고 싶지 않고, 아티팩트를 빌드해서 로컬 CocoaPods 저장소에 배포하고 싶을 때 유용할 수 있어요. XCFramework 빌드에 대해 더 알아보세요.
- 프로젝트에서 CocoaPods 통합을 사용한다면 전체 Gradle 프로젝트에 대해 요구되는 Pod 버전을 지정하는 데 익숙할 거예요. 이제 더 많은 옵션이 있어요:
cocoapods블록에서 직접 Pod 버전 지정- Gradle 프로젝트 버전 계속 사용 이 프로퍼티 중 어느 것도 구성되지 않으면 오류를 받아요.
- 이제 전체 Gradle 프로젝트의 이름을 바꾸는 대신
cocoapods블록에서 CocoaPod 이름을 구성할 수 있어요. - CocoaPods 플러그인은 새
extraSpecAttributes프로퍼티를 도입해요. 이것으로 이전에는 하드코딩됐던 Podspec 파일의 프로퍼티(libraries나vendored_frameworks같은)를 구성할 수 있어요.
kotlin {
cocoapods {
version = "1.0"
name = "MyCocoaPod"
extraSpecAttributes["social_media_url"] = 'https://twitter.com/kotlin'
extraSpecAttributes["vendored_frameworks"] = 'CustomFramework.xcframework'
extraSpecAttributes["libraries"] = 'xml'
}
}
전체 Kotlin CocoaPods Gradle 플러그인 DSL 참조를 확인하세요.
Kotlin/JS
1.6.20의 Kotlin/JS 개선은 주로 IR 컴파일러에 영향을 줘요:
- 개발 바이너리를 위한 증분 컴파일(IR)
- 최상위 프로퍼티의 지연 초기화가 기본(IR)
- 프로젝트 모듈별 별도 JS 파일이 기본(IR)
- Char 클래스 최적화(IR)
- 내보내기 개선(IR 및 레거시 백엔드 모두)
- 비동기 테스트에 대한 @AfterTest 보장
IR 컴파일러로 개발 바이너리를 위한 증분 컴파일
IR 컴파일러로 Kotlin/JS 개발을 더 효율적으로 만들기 위해 새 증분 컴파일 모드를 도입해요.
이 모드에서 compileDevelopmentExecutableKotlinJs Gradle 태스크로 개발 바이너리를 빌드할 때, 컴파일러는 모듈 수준에서 이전 컴파일의 결과를 캐시해요. 이후 컴파일에서 변경되지 않은 소스 파일에 대해 캐시된 컴파일 결과를 사용해서, 특히 작은 변경에 대해 더 빨리 완료되게 해요. 이 개선은 오직 개발 과정(편집-빌드-디버그 주기 단축)만 대상으로 하고 프로덕션 아티팩트 빌드에는 영향을 주지 않는다는 점을 유의하세요.
개발 바이너리를 위한 증분 컴파일을 켜려면 프로젝트의 gradle.properties에 다음 줄을 추가하면 돼요:
# gradle.properties
kotlin.incremental.js.ir=true // 기본은 false
테스트 프로젝트에서 새 모드는 증분 컴파일을 최대 30%까지 빠르게 만들었어요. 그러나 이 모드의 클린 빌드는 캐시를 만들고 채워야 하기 때문에 더 느려졌어요.
Kotlin/JS 프로젝트에서 증분 컴파일을 사용하는 것에 대해 어떻게 생각하는지 이 YouTrack 이슈에서 알려주세요.
IR 컴파일러로 최상위 프로퍼티의 지연 초기화가 기본
Kotlin 1.4.30에서 JS IR 컴파일러에서 최상위 프로퍼티의 지연 초기화 프로토타입을 제시했어요. 애플리케이션 시작 시 모든 프로퍼티를 초기화할 필요를 없애서, 지연 초기화는 시작 시간을 줄여요. 측정 결과 실제 Kotlin/JS 애플리케이션에서 약 10%의 속도 향상이 있었어요.
이제 이 메커니즘을 다듬고 제대로 테스트했으므로, IR 컴파일러에서 최상위 프로퍼티의 지연 초기화를 기본으로 만들고 있어요.
// lazy initialization
val a = run {
val result = // intensive computations
println(result)
result
} // run은 변수가 처음 사용될 때 실행된다
어떤 이유로 프로퍼티를 (애플리케이션 시작 시) 즉시 초기화해야 한다면 @EagerInitialization 애노테이션으로 표시하면 돼요.
IR 컴파일러로 프로젝트 모듈별 별도 JS 파일이 기본
이전에는 JS IR 컴파일러가 프로젝트 모듈에 대해 별도 .js 파일을 생성하는 능력을 제공했어요. 이것은 기본 옵션인 프로젝트 전체에 대한 단일 .js 파일의 대안이었어요. 그 파일은 너무 크고 사용하기 불편할 수 있어요. 프로젝트의 함수를 쓰고 싶을 때마다 전체 JS 파일을 의존성으로 포함해야 하기 때문이에요. 여러 파일을 갖는 것은 유연성을 더하고 그런 의존성의 크기를 줄여요. 이 기능은 -Xir-per-module 컴파일러 옵션으로 사용할 수 있었어요.
1.6.20부터 JS IR 컴파일러는 프로젝트 모듈에 대해 별도 .js 파일을 기본으로 생성해요.
프로젝트를 단일 .js 파일로 컴파일하는 것은 이제 다음 Gradle 프로퍼티로 제공돼요:
# gradle.properties
kotlin.js.ir.output.granularity=whole-program // `per-module`이 기본
이전 릴리스에서 실험적 per-module 모드(-Xir-per-module=true 플래그로 사용 가능)는 각 모듈에서 main() 함수를 호출했어요. 이것은 일반적인 단일 .js 모드와 일관되지 않아요. 1.6.20부터 main() 함수는 두 경우 모두 주 모듈에서만 호출돼요. 모듈이 로드될 때 어떤 코드를 실행해야 한다면 @EagerInitialization 애노테이션으로 표시된 최상위 프로퍼티를 사용할 수 있어요. IR 컴파일러로 최상위 프로퍼티의 지연 초기화가 기본을 참고하세요.
Char 클래스 최적화
Char 클래스는 이제 inline 클래스와 유사하게 박싱을 도입하지 않고 Kotlin/JS 컴파일러가 처리해요. 이것은 Kotlin/JS 코드에서 문자 연산을 빠르게 해줘요.
성능 개선 외에도 이것은 Char가 JavaScript로 내보내지는 방식을 바꿔요. 이제 Number로 번역돼요.
내보내기 및 TypeScript 선언 생성 개선
Kotlin 1.6.20은 내보내기 메커니즘(@JsExport 애노테이션)에 여러 수정과 개선을 가져와요. TypeScript 선언(.d.ts) 생성을 포함해서요. 인터페이스와 enum을 내보내는 능력을 추가했고, 이전에 보고된 일부 모서리(corner) 경우에서 내보내기 동작을 수정했어요. 자세한 내용은 YouTrack의 내보내기 개선 목록을 참고하세요.
JavaScript에서 Kotlin 코드 사용에 대해 더 알아보세요.
비동기 테스트에 대한 @AfterTest 보장
Kotlin 1.6.20은 @AfterTest 함수가 Kotlin/JS의 비동기 테스트에서 제대로 동작하게 해줘요. 테스트 함수의 반환 타입이 정적으로 Promise로 해석되면, 컴파일러가 이제 해당 then() 콜백으로 @AfterTest 함수의 실행을 예약해요.
보안 (Security)
Kotlin 1.6.20은 코드의 보안을 개선하는 몇 가지 기능을 도입해요:
klib에서 상대 경로 사용
klib 형식의 라이브러리는 소스 파일의 직렬화된 IR 표현을 포함하며, 적절한 디버그 정보를 생성하기 위한 그 경로도 포함해요. Kotlin 1.6.20 이전에는 저장된 파일 경로가 절대 경로였어요. 라이브러리 작성자는 절대 경로를 공유하고 싶지 않을 수 있으므로, 1.6.20 버전은 대체 옵션과 함께 와요.
klib를 퍼블리시하고 아티팩트에서 소스 파일의 상대 경로만 사용하고 싶다면 이제 -Xklib-relative-path-base 컴파일러 옵션에 소스 파일의 기본 경로 하나 또는 여러 개를 전달할 수 있어요:
tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinCompile::class).configureEach {
// $base는 소스 파일의 기본 경로
kotlinOptions.freeCompilerArgs += "-Xklib-relative-path-base=$base"
}
tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinCompile).configureEach {
kotlinOptions {
// $base는 소스 파일의 기본 경로
freeCompilerArgs += "-Xklib-relative-path-base=$base"
}
}
Kotlin/JS Gradle 프로젝트에서 yarn.lock 유지
Kotlin/JS Gradle 플러그인은 이제 yarn.lock 파일을 유지하는 능력을 제공해요. 추가 Gradle 구성 없이 프로젝트의 npm 의존성 버전을 잠글 수 있게 해줘요. 이 기능은 자동 생성된 kotlin-js-store 디렉터리를 프로젝트 루트에 추가해서 기본 프로젝트 구조에 변화를 가져와요. 그 안에 yarn.lock 파일이 들어 있어요.
kotlin-js-store 디렉터리와 그 내용을 버전 관리 시스템에 커밋할 것을 강력히 권장해요. 잠금 파일을 버전 관리 시스템에 커밋하는 것은 권장되는 관행이에요. 다른 기계의 개발 환경이든 CI/CD 서비스든 모든 기계에서 애플리케이션이 정확히 같은 의존성 트리로 빌드되는 것을 보장하기 때문이에요. 잠금 파일은 또한 새 기계에서 프로젝트를 체크아웃할 때 npm 의존성이 조용히 업데이트되는 것을 막아줘요. 이것은 보안 우려 사항이에요.
Dependabot 같은 도구도 Kotlin/JS 프로젝트의 yarn.lock 파일을 파싱해서, 의존하는 npm 패키지가 손상되면 경고를 제공할 수 있어요.
필요하면 빌드 스크립트에서 디렉터리와 잠금 파일 이름을 모두 바꿀 수 있어요:
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().lockFileDirectory =
project.rootDir.resolve("my-kotlin-js-store")
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().lockFileName = "my-yarn.lock"
}
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin) {
rootProject.extensions.getByType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension).lockFileDirectory =
file("my-kotlin-js-store")
rootProject.extensions.getByType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension).lockFileName = 'my-yarn.lock'
}
기본적으로 --ignore-scripts로 npm 의존성 설치
Kotlin/JS Gradle 플러그인은 이제 npm 의존성 설치 중에 라이프사이클 스크립트의 실행을 기본으로 방지해요. 이 변화는 손상된 npm 패키지의 악성 코드 실행 가능성을 줄이는 것을 목표로 해요.
옛 구성으로 되돌리려면 build.gradle(.kts)에 다음 줄을 추가해서 라이프사이클 스크립트 실행을 명시적으로 켤 수 있어요:
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().ignoreScripts = false
}
rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin) {
rootProject.extensions.getByType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension).ignoreScripts = false
}
Kotlin/JS Gradle 프로젝트의 npm 의존성에 대해 더 알아보세요.
Gradle
Kotlin 1.6.20은 Kotlin Gradle 플러그인에 다음 변화를 가져와요:
- Kotlin 컴파일러 실행 전략을 정의하는 새 프로퍼티 kotlin.compiler.execution.strategy와 compilerExecutionStrategy
- 옵션 kapt.use.worker.api, kotlin.experimental.coroutines, kotlin.coroutines의 deprecation
- kotlin.parallel.tasks.in.project 빌드 옵션 제거
Kotlin 컴파일러 실행 전략을 정의하는 프로퍼티
Kotlin 1.6.20 이전에는 Kotlin 컴파일러 실행 전략을 정의하기 위해 시스템 프로퍼티 -Dkotlin.compiler.execution.strategy를 사용했어요. 이 프로퍼티는 어떤 경우에 불편할 수 있었어요. Kotlin 1.6.20은 같은 이름의 Gradle 프로퍼티 kotlin.compiler.execution.strategy와 컴파일 태스크 프로퍼티 compilerExecutionStrategy를 도입해요.
시스템 프로퍼티는 여전히 동작하지만, 향후 릴리스에서 제거될 거예요.
프로퍼티의 현재 우선순위는 다음과 같아요:
- 태스크 프로퍼티
compilerExecutionStrategy가 시스템 프로퍼티와 Gradle 프로퍼티kotlin.compiler.execution.strategy보다 우선해요. - Gradle 프로퍼티가 시스템 프로퍼티보다 우선해요.
이 프로퍼티들에 할당할 수 있는 세 가지 컴파일러 실행 전략이 있어요:
| 전략 | 설명 |
|---|---|
| daemon | Kotlin 데몬에서 컴파일 |
| in-process | Gradle 데몬의 프로세스에서 컴파일 |
| out-of-process | 별도 프로세스에서 컴파일 |
따라서 kotlin.compiler.execution.strategy 프로퍼티(시스템과 Gradle 둘 다)의 사용 가능한 값은 다음과 같아요:
daemon(기본)in-processout-of-process
gradle.properties에서 Gradle 프로퍼티 kotlin.compiler.execution.strategy를 사용해요:
# gradle.properties
kotlin.compiler.execution.strategy=out-of-process
compilerExecutionStrategy 태스크 프로퍼티의 사용 가능한 값은 다음과 같아요:
org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy.DAEMON(기본)org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy.IN_PROCESSorg.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy.OUT_OF_PROCESS
build.gradle.kts 빌드 스크립트에서 태스크 프로퍼티 compilerExecutionStrategy를 사용해요:
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
// ...
tasks.withType<KotlinCompile>().configureEach {
compilerExecutionStrategy.set(KotlinCompilerExecutionStrategy.IN_PROCESS)
}
이 YouTrack 태스크에 피드백을 남겨 주세요.
kapt와 coroutines를 위한 빌드 옵션의 deprecation
Kotlin 1.6.20에서 프로퍼티들의 deprecation 수준을 바꿨어요:
kapt.use.worker.api로 Kotlin 데몬을 통해 kapt를 실행하는 능력을 deprecated 했어요. 이제 Gradle 출력에 경고를 생성해요. 기본적으로 kapt는 1.3.70 릴리스부터 Gradle workers를 사용해 왔으므로, 이 방법을 유지하는 것을 권장해요. 향후 릴리스에서kapt.use.worker.api옵션을 제거할 거예요.kotlin.experimental.coroutinesGradle DSL 옵션과gradle.properties에서 사용되는kotlin.coroutines프로퍼티를 deprecated 했어요. 그냥 suspending 함수를 사용하거나build.gradle(.kts)파일에 kotlinx.coroutines 의존성을 추가하면 돼요. Coroutines 가이드에서 coroutines에 대해 더 알아보세요.
kotlin.parallel.tasks.in.project 빌드 옵션 제거
Kotlin 1.5.20에서 빌드 옵션 kotlin.parallel.tasks.in.project의 deprecation을 발표했어요. 이 옵션은 Kotlin 1.6.20에서 제거됐어요.
프로젝트에 따라 Kotlin 데몬의 병렬 컴파일은 더 많은 메모리가 필요할 수 있어요. 메모리 소비를 줄이려면 Kotlin 데몬의 힙 크기를 늘리면 돼요.
Kotlin Gradle 플러그인의 현재 지원되는 컴파일러 옵션에 대해 더 알아보세요.