Skip to content

Kotlin 1.4.0의 새로운 기능

출시일: 2020년 8월 17일

Kotlin 1.4.0에서는 품질과 성능에 집중하여 모든 컴포넌트에 걸쳐 다양한 개선 사항을 제공합니다. 아래에서 Kotlin 1.4.0의 가장 중요한 변경 사항 목록을 확인할 수 있습니다.

Kotlin 출시 주기에 대한 정보는 Kotlin 출시 프로세스를 참조하세요.

언어 기능 및 개선 사항

Kotlin 1.4.0에는 다음과 같은 다양한 언어 기능과 개선 사항이 포함되어 있습니다.

Kotlin 인터페이스를 위한 SAM 변환

Kotlin 1.4.0 이전에는 Kotlin에서 Java 메서드 및 Java 인터페이스를 사용할 때만 SAM(Single Abstract Method) 변환을 적용할 수 있었습니다. 이제부터는 Kotlin 인터페이스에 대해서도 SAM 변환을 사용할 수 있습니다. 이를 위해 Kotlin 인터페이스를 fun 수정자를 사용하여 명시적으로 함수형(functional)으로 표시해야 합니다.

단 하나의 추상 메서드만 가진 인터페이스가 파라미터로 예상되는 곳에 람다를 인자로 전달하면 SAM 변환이 적용됩니다. 이 경우 컴파일러는 람다를 해당 추상 멤버 함수를 구현하는 클래스의 인스턴스로 자동 변환합니다.

kotlin
fun interface IntPredicate {
    fun accept(i: Int): Boolean
}

val isEven = IntPredicate { it % 2 == 0 }

fun main() { 
    println("Is 7 even? - ${isEven.accept(7)}")
}

Kotlin 함수형 인터페이스와 SAM 변환에 대해 자세히 알아보기.

라이브러리 작성자를 위한 명시적 API 모드

Kotlin 컴파일러는 라이브러리 작성자를 위한 _명시적 API 모드(explicit API mode)_를 제공합니다. 이 모드에서 컴파일러는 라이브러리의 API를 더 명확하고 일관되게 만드는 데 도움이 되는 추가 검사를 수행합니다. 라이브러리의 공개(public) API에 노출되는 선언에 대해 다음과 같은 요구 사항을 추가합니다.

  • 기본 가시성이 공개 API에 노출되는 경우, 선언에 가시성 수정자가 필수적으로 요구됩니다. 이는 의도치 않게 선언이 공개 API에 노출되지 않도록 보장합니다.
  • 공개 API에 노출되는 프로퍼티와 함수에 대해 명시적인 타입 지정이 요구됩니다. 이는 API 사용자가 사용하는 멤버의 타입을 확실히 알 수 있도록 보장합니다.

설정에 따라 이러한 명시적 API는 에러(strict 모드) 또는 경고(warning 모드)를 발생시킬 수 있습니다. 가독성과 상식적인 수준에서 다음과 같은 종류의 선언은 이러한 검사에서 제외됩니다.

  • 주 생성자(primary constructors)
  • 데이터 클래스의 프로퍼티
  • 프로퍼티의 게터(getter) 및 세터(setter)
  • override 메서드

명시적 API 모드는 모듈의 프로덕션 소스만 분석합니다.

Gradle 빌드 스크립트에서 명시적 API 모드로 모듈을 컴파일하려면 다음 라인을 추가하세요.

kotlin
kotlin {    
    // strict 모드 사용 시
    explicitApi() 
    // 또는
    explicitApi = ExplicitApiMode.Strict
    
    // warning 모드 사용 시
    explicitApiWarning()
    // 또는
    explicitApi = ExplicitApiMode.Warning
}
groovy
kotlin {    
    // strict 모드 사용 시
    explicitApi() 
    // 또는
    explicitApi = 'strict'
    
    // warning 모드 사용 시
    explicitApiWarning()
    // 또는
    explicitApi = 'warning'
}

명령줄 컴파일러를 사용하는 경우, -Xexplicit-api 컴파일러 옵션에 strict 또는 warning 값을 추가하여 명시적 API 모드로 전환할 수 있습니다.

bash
-Xexplicit-api={strict|warning}

명시적 API 모드에 대한 더 자세한 내용은 KEEP에서 확인하세요.

이름이 지정된 인자와 위치 인자 혼합 사용

Kotlin 1.3에서는 이름이 지정된 인자(named arguments)로 함수를 호출할 때, 이름이 없는 모든 인자(위치 인자)를 첫 번째 이름이 지정된 인자 앞에 배치해야 했습니다. 예를 들어 f(1, y = 2)는 호출할 수 있었지만 f(x = 1, 2)는 호출할 수 없었습니다.

모든 인자가 올바른 위치에 있지만 중간에 있는 인자 하나만 이름을 지정하고 싶을 때 이는 매우 번거로운 일이었습니다. 특히 불리언(boolean) 값이나 null 값이 어떤 속성에 해당하는지 명확히 밝히고 싶을 때 매우 유용합니다.

Kotlin 1.4에서는 이러한 제한이 사라졌습니다. 이제 위치 인자들 중간에서도 인자의 이름을 지정할 수 있습니다. 또한 위치 인자와 이름이 지정된 인자가 올바른 순서를 유지하는 한 원하는 방식으로 혼합해서 사용할 수 있습니다.

kotlin
fun reformat(
    str: String,
    uppercaseFirstLetter: Boolean = true,
    wordSeparator: Char = ' '
) {
    // ...
}

// 중간에 이름이 지정된 인자를 포함한 함수 호출
reformat("This is a String!", uppercaseFirstLetter = false , '-')

트레일링 쉼표(Trailing comma)

Kotlin 1.4부터는 인자 및 파라미터 리스트, when 엔트리, 구조 분해 선언의 컴포넌트와 같은 열거형 항목에 트레일링 쉼표(마지막 쉼표)를 추가할 수 있습니다. 트레일링 쉼표를 사용하면 쉼표를 추가하거나 제거하지 않고도 새로운 항목을 추가하거나 순서를 변경할 수 있습니다.

이는 특히 파라미터나 값을 여러 줄에 걸쳐 작성할 때 유용합니다. 트레일링 쉼표를 추가해 두면 파라미터나 값이 있는 라인을 쉽게 바꿀 수 있습니다.

kotlin
fun reformat(
    str: String,
    uppercaseFirstLetter: Boolean = true,
    wordSeparator: Character = ' ', // 트레일링 쉼표
) {
    // ...
}
kotlin
val colors = listOf(
    "red",
    "green",
    "blue", // 트레일링 쉼표
)

Callable 참조 개선

Kotlin 1.4는 Callable 참조를 사용할 수 있는 더 많은 케이스를 지원합니다.

  • 기본값을 가진 파라미터를 포함하는 함수에 대한 참조
  • Unit을 반환하는 함수에서의 함수 참조
  • 함수의 인자 개수에 따라 적응하는 참조
  • Callable 참조에 대한 Suspend 변환

기본값을 가진 파라미터를 포함하는 함수에 대한 참조

이제 기본값을 가진 파라미터를 포함하는 함수에 대해 Callable 참조를 사용할 수 있습니다. 함수 foo에 대한 Callable 참조가 인자를 받지 않으면 기본값 0이 사용됩니다.

kotlin
fun foo(i: Int = 0): String = "$i!"

fun apply(func: () -> String): String = func()

fun main() {
    println(apply(::foo))
}

이전에는 apply 또는 foo 함수에 대해 추가적인 오버로드를 작성해야 했습니다.

kotlin
// 새로운 오버로드 예시
fun applyInt(func: (Int) -> String): String = func(0)

Unit을 반환하는 함수에서의 함수 참조

Kotlin 1.4에서는 어떤 타입이든 반환하는 함수에 대한 Callable 참조를 Unit을 반환하는 함수에서 사용할 수 있습니다. Kotlin 1.4 이전에는 이 경우 람다 인자만 사용할 수 있었습니다. 이제는 람다 인자와 Callable 참조를 모두 사용할 수 있습니다.

kotlin
fun foo(f: () -> Unit) { }
fun returnsInt(): Int = 42

fun main() {
    foo { returnsInt() } // 1.4 이전에는 이 방식만 가능했습니다
    foo(::returnsInt) // 1.4부터는 이 방식도 작동합니다
}

함수의 인자 개수에 따라 적응하는 참조

이제 가변 인자(vararg)를 전달할 때 함수에 대한 Callable 참조를 적응시킬 수 있습니다. 전달된 인자 리스트의 마지막에 동일한 타입의 파라미터를 원하는 개수만큼 전달할 수 있습니다.

kotlin
fun foo(x: Int, vararg y: String) {}

fun use0(f: (Int) -> Unit) {}
fun use1(f: (Int, String) -> Unit) {}
fun use2(f: (Int, String, String) -> Unit) {}

fun test() {
    use0(::foo) 
    use1(::foo) 
    use2(::foo) 
}

Callable 참조에 대한 Suspend 변환

람다에 대한 Suspend 변환 외에도, Kotlin은 버전 1.4.0부터 Callable 참조에 대한 Suspend 변환을 지원합니다.

kotlin
fun call() {}
fun takeSuspend(f: suspend () -> Unit) {}

fun test() {
    takeSuspend { call() } // 1.4 이전에도 가능
    takeSuspend(::call) // Kotlin 1.4부터는 이 방식도 작동
}

루프 내 when 문 내부에서 break 및 continue 사용

Kotlin 1.3에서는 루프에 포함된 when 표현식 내부에서 레이블이 지정되지 않은(unqualified) breakcontinue를 사용할 수 없었습니다. 그 이유는 이러한 키워드들이 when 표현식에서의 잠재적인 fall-through 동작을 위해 예약되어 있었기 때문입니다.

이 때문에 루프 내 when 문에서 breakcontinue를 사용하려면 레이블(label)을 지정해야 했으며, 이는 다소 번거로웠습니다.

kotlin
fun test(xs: List<Int>) {
    LOOP@for (x in xs) {
        when (x) {
            2 -> continue@LOOP
            17 -> break@LOOP
            else -> println(x)
        }
    }
}

Kotlin 1.4에서는 루프 내 when 표현식 내부에서 레이블 없이 breakcontinue를 사용할 수 있습니다. 이들은 가장 가까운 바깥쪽 루프를 종료하거나 다음 단계로 진행하는 예상대로의 동작을 수행합니다.

kotlin
fun test(xs: List<Int>) {
    for (x in xs) {
        when (x) {
            2 -> continue
            17 -> break
            else -> println(x)
        }
    }
}

when 내부의 fall-through 동작은 추후 디자인 대상입니다.

IDE의 새로운 도구들

Kotlin 1.4와 함께 IntelliJ IDEA의 새로운 도구들을 사용하여 Kotlin 개발을 간소화할 수 있습니다.

새로운 유연한 프로젝트 마법사

새로운 유연한 Kotlin 프로젝트 마법사(Project Wizard)를 사용하면 UI 없이 설정하기 어려울 수 있는 멀티플랫폼 프로젝트를 포함하여 다양한 유형의 Kotlin 프로젝트를 쉽게 생성하고 구성할 수 있습니다.

Kotlin 프로젝트 마법사 – 멀티플랫폼 프로젝트

새로운 Kotlin 프로젝트 마법사는 간단하면서도 유연합니다.

  1. 수행하려는 작업에 따라 프로젝트 템플릿을 선택합니다. 앞으로 더 많은 템플릿이 추가될 예정입니다.
  2. Gradle (Kotlin 또는 Groovy DSL), Maven 또는 IntelliJ IDEA 중 빌드 시스템을 선택합니다. Kotlin 프로젝트 마법사는 선택한 프로젝트 템플릿에서 지원되는 빌드 시스템만 표시합니다.
  3. 메인 화면에서 직접 프로젝트 구조를 미리 확인합니다.

그런 다음 프로젝트 생성을 완료하거나, 옵션으로 다음 화면에서 프로젝트를 구성할 수 있습니다.

  1. 이 프로젝트 템플릿에서 지원되는 모듈 및 타겟을 추가/제거합니다.
  2. 타겟 JVM 버전, 타겟 템플릿 및 테스트 프레임워크와 같은 모듈 및 타겟 설정을 구성합니다.

Kotlin 프로젝트 마법사 - 타겟 구성

앞으로 더 많은 구성 옵션과 템플릿을 추가하여 Kotlin 프로젝트 마법사를 더욱 유연하게 만들 계획입니다.

다음 튜토리얼을 통해 새로운 Kotlin 프로젝트 마법사를 체험해 볼 수 있습니다.

코루틴 디버거

이미 많은 사람이 비동기 프로그래밍을 위해 코루틴(coroutines)을 사용하고 있습니다. 하지만 Kotlin 1.4 이전에는 코루틴을 디버깅하는 것이 매우 힘들었습니다. 코루틴이 스레드 사이를 이동하기 때문에 특정 코루틴이 무엇을 하고 있는지 이해하고 컨텍스트를 확인하기가 어려웠습니다. 어떤 경우에는 중단점(breakpoint) 위로 단계를 추적하는 것이 제대로 작동하지 않았습니다. 결과적으로 코루틴을 사용하는 코드를 디버깅하기 위해 로깅이나 직관에 의존해야 했습니다.

Kotlin 1.4에서는 Kotlin 플러그인과 함께 제공되는 새로운 기능을 통해 코루틴 디버깅이 훨씬 편리해졌습니다.

디버깅은 kotlinx-coroutines-core 버전 1.3.8 이상에서 작동합니다.

Debug Tool Window에 이제 새로운 Coroutines 탭이 포함됩니다. 이 탭에서는 현재 실행 중인 코루틴과 일시 중단된 코루틴에 대한 정보를 찾을 수 있습니다. 코루틴은 실행 중인 디스패처별로 그룹화됩니다.

코루틴 디버깅

이제 다음이 가능합니다.

  • 각 코루틴의 상태를 쉽게 확인합니다.
  • 실행 중인 코루틴과 일시 중단된 코루틴 모두에 대해 지역 변수 및 캡처된 변수의 값을 확인합니다.
  • 전체 코루틴 생성 스택과 코루틴 내부의 호출 스택을 확인합니다. 스택에는 일반적인 디버깅 중에 손실될 수 있는 변수 값을 포함한 모든 프레임이 포함됩니다.

각 코루틴의 상태와 스택을 포함하는 전체 보고서가 필요한 경우, Coroutines 탭 안에서 우클릭한 후 Get Coroutines Dump를 클릭하세요. 현재 코루틴 덤프는 다소 단순하지만, 향후 버전의 Kotlin에서는 더 읽기 쉽고 유용하게 개선할 예정입니다.

코루틴 덤프

코루틴 디버깅에 대한 자세한 내용은 이 블로그 포스트IntelliJ IDEA 문서에서 확인할 수 있습니다.

새로운 컴파일러

새로운 Kotlin 컴파일러는 매우 빨라질 것입니다. 지원되는 모든 플랫폼을 통합하고 컴파일러 확장을 위한 API를 제공할 것입니다. 이는 장기 프로젝트이며, Kotlin 1.4.0에서 이미 몇 가지 단계를 완료했습니다.

더 강력해진 새로운 타입 추론 알고리즘

Kotlin 1.4는 더 강력해진 새로운 타입 추론 알고리즘을 사용합니다. 이 새로운 알고리즘은 이미 Kotlin 1.3에서 컴파일러 옵션을 지정하여 사용해 볼 수 있었으며, 이제는 기본으로 사용됩니다. 새로운 알고리즘에서 해결된 전체 이슈 목록은 YouTrack에서 확인할 수 있습니다. 가장 눈에 띄는 개선 사항은 다음과 같습니다.

타입이 자동으로 추론되는 케이스 확대

새로운 추론 알고리즘은 이전 알고리즘에서 타입을 명시적으로 지정해야 했던 많은 경우에 대해 타입을 추론합니다. 예를 들어, 다음 예제에서 람다 파라미터 it의 타입은 String?으로 올바르게 추론됩니다.

kotlin
val rulesMap: Map<String, (String?) -> Boolean> = mapOf(
    "weak" to { it != null },
    "medium" to { !it.isNullOrBlank() },
    "strong" to { it != null && "^[a-zA-Z0-9]+$".toRegex().matches(it) }
)

fun main() {
    println(rulesMap.getValue("weak")("abc!"))
    println(rulesMap.getValue("strong")("abc"))
    println(rulesMap.getValue("strong")("abc!"))
}

Kotlin 1.3에서는 이를 작동시키기 위해 명시적인 람다 파라미터를 도입하거나 to를 명시적인 제네릭 인자가 있는 Pair 생성자로 교체해야 했습니다.

람다의 마지막 표현식에 대한 스마트 캐스트

Kotlin 1.3에서는 예상되는 타입을 명시하지 않으면 람다 내부의 마지막 표현식이 스마트 캐스트 되지 않았습니다. 따라서 다음 예제에서 Kotlin 1.3은 result 변수의 타입을 String?으로 추론합니다.

kotlin
val result = run {
    var str = currentValue()
    if (str == null) {
        str = "test"
    }
    str // Kotlin 컴파일러는 여기서 str이 null이 아님을 압니다
}
// 'result'의 타입은 Kotlin 1.3에서 String?이고 Kotlin 1.4에서 String입니다

Kotlin 1.4에서는 새로운 추론 알고리즘 덕분에 람다 내부의 마지막 표현식이 스마트 캐스트 되며, 이 더 정확한 타입이 결과 람다 타입을 추론하는 데 사용됩니다. 따라서 result 변수의 타입은 String이 됩니다.

Kotlin 1.3에서는 이러한 케이스를 작동시키기 위해 명시적인 캐스트(!! 또는 as String과 같은 타입 캐스트)를 추가해야 하는 경우가 많았으나, 이제 이러한 캐스트는 불필요해졌습니다.

Callable 참조에 대한 스마트 캐스트

Kotlin 1.3에서는 스마트 캐스트 된 타입의 멤버 참조에 접근할 수 없었습니다. 이제 Kotlin 1.4에서는 가능합니다.

kotlin
import kotlin.reflect.KFunction

sealed class Animal
class Cat : Animal() {
    fun meow() {
        println("meow")
    }
}

class Dog : Animal() {
    fun woof() {
        println("woof")
    }
}

fun perform(animal: Animal) {
    val kFunction: KFunction<*> = when (animal) {
        is Cat -> animal::meow
        is Dog -> animal::woof
    }
    kFunction.call()
}

fun main() {
    perform(Cat())
}

animal 변수가 특정 타입 CatDog로 스마트 캐스트 된 후 서로 다른 멤버 참조 animal::meowanimal::woof를 사용할 수 있습니다. 타입 체크 후 서브타입에 해당하는 멤버 참조에 접근할 수 있습니다.

위임된 프로퍼티에 대한 더 나은 추론

위임된 프로퍼티의 타입은 by 키워드 뒤에 오는 위임 표현식을 분석할 때 고려되지 않았습니다. 예를 들어 다음 코드는 이전에는 컴파일되지 않았지만, 이제 컴파일러는 oldnew 파라미터의 타입을 String?으로 올바르게 추론합니다.

kotlin
import kotlin.properties.Delegates

fun main() {
    var prop: String? by Delegates.observable(null) { p, old, new ->
        println("$old$new")
    }
    prop = "abc"
    prop = "xyz"
}

서로 다른 인자를 가진 Java 인터페이스에 대한 SAM 변환

Kotlin은 처음부터 Java 인터페이스에 대한 SAM 변환을 지원해 왔지만, 기존 Java 라이브러리를 사용할 때 가끔 번거로운 지원되지 않는 케이스가 하나 있었습니다. 두 개의 SAM 인터페이스를 파라미터로 받는 Java 메서드를 호출할 때, 두 인자 모두 람다이거나 일반 객체여야 했습니다. 한 인자는 람다로, 다른 인자는 객체로 전달할 수 없었습니다.

새로운 알고리즘은 이 이슈를 수정하여 어떤 경우에도 SAM 인터페이스 대신 람다를 전달할 수 있게 되었으며, 이는 자연스럽게 기대되는 방식입니다.

java
// FILE: A.java
public class A {
    public static void foo(Runnable r1, Runnable r2) {}
}
kotlin
// FILE: test.kt
fun test(r1: Runnable) {
    A.foo(r1) {}  // Kotlin 1.4에서 작동합니다
}

Kotlin에서의 Java SAM 인터페이스

Kotlin 1.4에서는 Kotlin에서 Java SAM 인터페이스를 사용하고 이에 SAM 변환을 적용할 수 있습니다.

kotlin
import java.lang.Runnable

fun foo(r: Runnable) {}

fun test() { 
    foo { } // OK
}

Kotlin 1.3에서는 SAM 변환을 수행하기 위해 위의 foo 함수를 Java 코드로 선언해야 했습니다.

통합 백엔드 및 확장성

Kotlin에는 실행 파일을 생성하는 세 가지 백엔드인 Kotlin/JVM, Kotlin/JS, Kotlin/Native가 있습니다. Kotlin/JVM과 Kotlin/JS는 서로 독립적으로 개발되었기 때문에 공유하는 코드가 많지 않았습니다. Kotlin/Native는 Kotlin 코드를 위한 중간 표현(IR)을 기반으로 구축된 새로운 인프라를 바탕으로 합니다.

이제 Kotlin/JVM과 Kotlin/JS를 동일한 IR로 마이그레이션하고 있습니다. 결과적으로 세 백엔드 모두 많은 로직을 공유하고 통합된 파이프라인을 갖게 됩니다. 이를 통해 대부분의 기능, 최적화 및 버그 수정을 모든 플랫폼에 대해 단 한 번만 구현할 수 있게 됩니다. 새로운 IR 기반 백엔드는 현재 Alpha 단계에 있습니다.

공통 백엔드 인프라는 멀티플랫폼 컴파일러 확장을 위한 문을 열어줍니다. 파이프라인에 플러그인하여 모든 플랫폼에서 자동으로 작동하는 맞춤형 처리 및 변환을 추가할 수 있게 될 것입니다.

현재 Alpha 단계인 새로운 JVM IRJS IR 백엔드를 사용해 보고 의견을 공유해 주시길 권장합니다.

Kotlin/JVM

Kotlin 1.4.0에는 다음과 같은 다양한 JVM 전용 개선 사항이 포함되어 있습니다.

새로운 JVM IR 백엔드

Kotlin/JS와 함께 Kotlin/JVM을 통합 IR 백엔드로 마이그레이션하고 있으며, 이를 통해 대부분의 기능과 버그 수정을 모든 플랫폼에 대해 한 번에 구현할 수 있습니다. 또한 모든 플랫폼에서 작동하는 멀티플랫폼 확장을 만들어 이 혜택을 누릴 수 있습니다.

Kotlin 1.4.0은 아직 이러한 확장을 위한 공개 API를 제공하지 않지만, 이미 새로운 백엔드를 사용하여 컴파일러 플러그인을 구축하고 있는 Jetpack Compose를 포함한 파트너들과 긴밀히 협력하고 있습니다.

현재 Alpha 단계인 새로운 Kotlin/JVM 백엔드를 사용해 보고 이슈 트래커에 이슈나 기능 요청을 제출해 주시기 바랍니다. 이는 컴파일러 파이프라인을 통합하고 Jetpack Compose와 같은 컴파일러 확장을 Kotlin 커뮤니티에 더 빨리 제공하는 데 도움이 됩니다.

새로운 JVM IR 백엔드를 활성화하려면 Gradle 빌드 스크립트에 추가 컴파일러 옵션을 지정하세요.

kotlin
kotlinOptions.useIR = true

Jetpack Compose를 활성화하면 kotlinOptions에 컴파일러 옵션을 지정하지 않아도 자동으로 새로운 JVM 백엔드가 선택됩니다.

명령줄 컴파일러를 사용하는 경우 -Xuse-ir 컴파일러 옵션을 추가하세요.

새로운 JVM IR 백엔드로 컴파일된 코드는 새로운 백엔드를 활성화한 경우에만 사용할 수 있습니다. 그렇지 않으면 에러가 발생합니다. 이를 고려하여 라이브러리 작성자가 프로덕션 환경에서 새로운 백엔드로 전환하는 것은 권장하지 않습니다.

기본 메서드 생성을 위한 새로운 모드

Kotlin 코드를 JVM 1.8 이상 타겟으로 컴파일할 때, Kotlin 인터페이스의 비추상 메서드를 Java의 default 메서드로 컴파일할 수 있었습니다. 이를 위해 해당 메서드를 표시하는 @JvmDefault 어노테이션과 이 어노테이션의 처리를 활성화하는 -Xjvm-default 컴파일러 옵션을 포함하는 메커니즘이 있었습니다.

1.4.0에서는 기본 메서드 생성을 위한 새로운 모드를 추가했습니다. -Xjvm-default=all은 Kotlin 인터페이스의 모든 비추상 메서드를 Java default 메서드로 컴파일합니다. default 없이 컴파일된 인터페이스를 사용하는 코드와의 호환성을 위해 all-compatibility 모드도 추가했습니다.

Java 상호 운용성에서의 기본 메서드에 대한 자세한 내용은 상호 운용성 문서이 블로그 포스트를 참조하세요.

null 체크를 위한 통합된 예외 타입

Kotlin 1.4.0부터 모든 런타임 null 체크는 KotlinNullPointerException, IllegalStateException, IllegalArgumentException, TypeCastException 대신 java.lang.NullPointerException을 던집니다. 이는 !! 연산자, 메서드 서문의 파라미터 null 체크, 플랫폼 타입 표현식 null 체크, null이 불가능한 타입으로의 as 연산자에 적용됩니다. lateinit null 체크와 checkNotNull 또는 requireNotNull과 같은 명시적인 라이브러리 함수 호출에는 적용되지 않습니다.

이 변경 사항은 Kotlin 컴파일러나 Android R8 최적화 도구와 같은 다양한 종류의 바이트코드 처리 도구에 의해 수행될 수 있는 null 체크 최적화의 가능성을 높입니다.

개발자 관점에서는 크게 변하는 것이 없습니다. Kotlin 코드는 이전과 동일한 에러 메시지로 예외를 던질 것입니다. 예외의 타입은 변경되지만 전달되는 정보는 동일하게 유지됩니다.

JVM 바이트코드의 타입 어노테이션

Kotlin은 이제 JVM 바이트코드(타겟 버전 1.8+)에 타입 어노테이션을 생성할 수 있으므로 런타임에 Java 리플렉션에서 사용할 수 있습니다. 바이트코드에 타입 어노테이션을 생성하려면 다음 단계를 따르세요.

  1. 선언한 어노테이션이 적절한 어노테이션 타겟(Java의 ElementType.TYPE_USE 또는 Kotlin의 AnnotationTarget.TYPE)과 유지 정책(AnnotationRetention.RUNTIME)을 가지고 있는지 확인합니다.
  2. 어노테이션 클래스 선언을 JVM 바이트코드 타겟 버전 1.8+로 컴파일합니다. -jvm-target=1.8 컴파일러 옵션으로 지정할 수 있습니다.
  3. 해당 어노테이션을 사용하는 코드를 JVM 바이트코드 타겟 버전 1.8+(-jvm-target=1.8)로 컴파일하고 -Xemit-jvm-type-annotations 컴파일러 옵션을 추가합니다.

표준 라이브러리는 타겟 버전 1.6으로 컴파일되기 때문에 현재 표준 라이브러리의 타입 어노테이션은 바이트코드에 생성되지 않습니다.

현재는 다음과 같은 기본 케이스만 지원됩니다.

  • 메서드 파라미터, 메서드 반환 타입 및 프로퍼티 타입의 타입 어노테이션
  • Smth<@Ann Foo>, Array<@Ann Foo>와 같은 타입 인자의 불변 프로젝션(Invariant projections)

다음 예제에서 String 타입의 @Foo 어노테이션은 바이트코드에 생성되어 라이브러리 코드에서 사용할 수 있습니다.

kotlin
@Target(AnnotationTarget.TYPE)
annotation class Foo

class A {
    fun foo(): @Foo String = "OK"
}

Kotlin/JS

JS 플랫폼에서 Kotlin 1.4.0은 다음과 같은 개선 사항을 제공합니다.

새로운 Gradle DSL

kotlin.js Gradle 플러그인에는 조정된 Gradle DSL이 함께 제공됩니다. 이 DSL은 여러 새로운 구성 옵션을 제공하며 kotlin-multiplatform 플러그인에서 사용되는 DSL과 더 밀접하게 정렬됩니다. 가장 영향력 있는 변경 사항은 다음과 같습니다.

새로운 JS IR 백엔드

현재 Alpha 단계인 Kotlin/JS용 IR 백엔드는 데드 코드 제거(DCE)를 통한 생성된 코드 크기 최적화, JavaScript 및 TypeScript와의 상호 운용성 개선 등 Kotlin/JS 타겟에 특화된 몇 가지 새로운 기능을 제공합니다.

Kotlin/JS IR 백엔드를 활성화하려면 gradle.propertieskotlin.js.compiler=ir 키를 설정하거나 Gradle 빌드 스크립트의 js 함수에 IR 컴파일러 타입을 전달하세요.

groovy
kotlin {
    js(IR) { // 또는: LEGACY, BOTH
        // ...
    }
    binaries.executable()
}

새로운 백엔드를 구성하는 방법에 대한 자세한 내용은 Kotlin/JS IR 컴파일러 문서를 확인하세요.

새로운 @JsExport 어노테이션과 Kotlin 코드에서 TypeScript 정의 생성 기능을 통해 Kotlin/JS IR 컴파일러 백엔드는 JavaScript 및 TypeScript 상호 운용성을 개선합니다. 또한 Kotlin/JS 코드를 기존 도구와 더 쉽게 통합하고 하이브리드 애플리케이션을 생성하며 멀티플랫폼 프로젝트에서 코드 공유 기능을 활용할 수 있게 해줍니다.

Kotlin/JS IR 컴파일러 백엔드에서 사용 가능한 기능에 대해 자세히 알아보기.

Kotlin/Native

1.4.0에서 Kotlin/Native는 다음과 같은 상당수의 새로운 기능과 개선 사항을 얻었습니다.

Swift 및 Objective-C에서 Kotlin의 suspend 함수 지원

1.4.0에서는 Swift 및 Objective-C에서 suspend 함수에 대한 기본 지원을 추가했습니다. 이제 Kotlin 모듈을 Apple 프레임워크로 컴파일할 때, suspend 함수는 콜백이 있는 함수(Swift/Objective-C 용어로는 completionHandler)로 사용할 수 있습니다. 생성된 프레임워크의 헤더에 이러한 함수가 있으면 Swift 또는 Objective-C 코드에서 이를 호출하고 심지어 오버라이드할 수도 있습니다.

예를 들어 다음과 같은 Kotlin 함수를 작성하면:

kotlin
suspend fun queryData(id: Int): String = ...

...Swift에서 다음과 같이 호출할 수 있습니다.

swift
queryData(id: 17) { result, error in
   if let e = error {
       print("ERROR: \(e)")
   } else {
       print(result!)
   }
}

Swift 및 Objective-C에서 suspend 함수 사용에 대해 자세히 알아보기.

Objective-C 제네릭 지원 기본 활성화

이전 버전의 Kotlin은 Objective-C 상호 운용성에서 제네릭에 대한 실험적 지원을 제공했습니다. 1.4.0부터 Kotlin/Native는 기본적으로 Kotlin 코드에서 제네릭이 포함된 Apple 프레임워크를 생성합니다. 경우에 따라 이는 Kotlin 프레임워크를 호출하는 기존 Objective-C 또는 Swift 코드를 깨뜨릴 수 있습니다. 제네릭 없이 프레임워크 헤더를 작성하려면 -Xno-objc-generics 컴파일러 옵션을 추가하세요.

kotlin
kotlin {
    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
        binaries.all {
            freeCompilerArgs += "-Xno-objc-generics"
        }
    }
}

Objective-C 상호 운용성 문서에 나열된 모든 세부 사항과 제한 사항은 여전히 유효합니다.

Objective-C/Swift 상호 운용성에서의 예외 처리

1.4.0에서는 예외가 번역되는 방식과 관련하여 Kotlin에서 생성된 Swift API를 약간 변경했습니다. Kotlin과 Swift 사이에는 에러 처리에 근본적인 차이가 있습니다. 모든 Kotlin 예외는 언체크(unchecked) 예외인 반면, Swift에는 체크(checked) 에러만 있습니다. 따라서 Swift 코드가 예상되는 예외를 인지하게 하려면 Kotlin 함수에 잠재적인 예외 클래스 리스트를 지정하는 @Throws 어노테이션을 표시해야 합니다.

Swift 또는 Objective-C 프레임워크로 컴파일할 때, @Throws 어노테이션을 가지고 있거나 상속받은 함수는 Objective-C에서는 NSError*를 생성하는 메서드로, Swift에서는 throws 메서드로 표현됩니다.

이전에는 RuntimeExceptionError 이외의 모든 예외가 NSError로 전파되었습니다. 이제 이 동작이 변경됩니다. 이제 NSError@Throws 어노테이션의 파라미터로 지정된 클래스(또는 그 서브클래스)의 인스턴스인 예외에 대해서만 던져집니다. Swift/Objective-C에 도달하는 다른 Kotlin 예외는 처리되지 않은 것으로 간주되어 프로그램 종료를 유발합니다.

Apple 타겟에서 릴리스용 .dSYM 기본 생성

1.4.0부터 Kotlin/Native 컴파일러는 Darwin 플랫폼의 릴리스 바이너리에 대해 기본적으로 디버그 심볼 파일(.dSYM)을 생성합니다. 이는 -Xadd-light-debug=disable 컴파일러 옵션으로 비활성화할 수 있습니다. 다른 플랫폼에서 이 옵션은 기본적으로 비활성화되어 있습니다. Gradle에서 이 옵션을 토글하려면 다음을 사용하세요.

kotlin
kotlin {
    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
        binaries.all {
            freeCompilerArgs += "-Xadd-light-debug={enable|disable}"
        }
    }
}

크래시 리포트 심볼화에 대해 자세히 알아보기.

성능 개선

Kotlin/Native는 개발 프로세스와 실행 속도를 모두 높이는 여러 성능 개선 사항을 적용받았습니다. 다음은 몇 가지 예시입니다.

  • 객체 할당 속도를 높이기 위해 이제 시스템 할당자 대신 mimalloc 메모리 할당자를 대안으로 제공합니다. mimalloc은 일부 벤치마크에서 최대 2배 더 빠르게 작동합니다. 현재 Kotlin/Native에서 mimalloc 사용은 실험적이며, -Xallocator=mimalloc 컴파일러 옵션을 사용하여 전환할 수 있습니다.

  • C 상호 운용성 라이브러리가 구축되는 방식을 재작업했습니다. 새로운 도구를 통해 Kotlin/Native는 이전보다 최대 4배 빠르게 상호 운용성 라이브러리를 생성하며, 결과물(artifact)의 크기는 이전의 25%에서 30% 수준으로 줄어듭니다.

  • GC 최적화로 인해 전반적인 런타임 성능이 개선되었습니다. 이 개선 사항은 특히 수명이 긴 객체가 많은 프로젝트에서 두드러질 것입니다. HashMapHashSet 컬렉션은 이제 불필요한 박싱을 피함으로써 더 빠르게 작동합니다.

  • 1.3.70에서 Kotlin/Native 컴파일 성능을 개선하기 위한 두 가지 새로운 기능을 도입했습니다. 프로젝트 의존성 캐싱 및 Gradle 데몬에서 컴파일러 실행이 그것입니다. 그 이후로 수많은 이슈를 수정하고 이러한 기능의 전반적인 안정성을 개선했습니다.

CocoaPods 의존성 관리 간소화

이전에는 프로젝트를 의존성 관리자인 CocoaPods와 통합하면 프로젝트의 iOS, macOS, watchOS 또는 tvOS 부분을 멀티플랫폼 프로젝트의 다른 부분과 분리하여 Xcode에서만 빌드할 수 있었습니다. 다른 부분들은 IntelliJ IDEA에서 빌드할 수 있었습니다.

게다가 CocoaPods에 저장된 Objective-C 라이브러리(Pod 라이브러리)에 대한 의존성을 추가할 때마다 IntelliJ IDEA에서 Xcode로 전환하여 pod install을 호출하고 거기서 Xcode 빌드를 실행해야 했습니다.

이제는 코드 작업(코드 하이라이팅, 자동 완성 등)에 제공되는 이점을 누리면서 IntelliJ IDEA에서 직접 Pod 의존성을 관리할 수 있습니다. 또한 Xcode로 전환할 필요 없이 Gradle을 사용하여 전체 Kotlin 프로젝트를 빌드할 수 있습니다. 즉, Swift/Objective-C 코드를 작성하거나 시뮬레이터나 디바이스에서 애플리케이션을 실행해야 할 때만 Xcode로 이동하면 됩니다.

이제 로컬에 저장된 Pod 라이브러리와도 작업할 수 있습니다.

필요에 따라 다음 사이에 의존성을 추가할 수 있습니다.

  • Kotlin 프로젝트와 원격 CocoaPods 저장소에 저장되었거나 머신에 로컬로 저장된 Pod 라이브러리
  • Kotlin Pod (CocoaPods 의존성으로 사용되는 Kotlin 프로젝트)와 하나 이상의 타겟이 있는 Xcode 프로젝트

초기 구성을 완료하고 cocoapods에 새로운 의존성을 추가할 때 IntelliJ IDEA에서 프로젝트를 다시 임포트하기만 하면 됩니다. 새로운 의존성이 자동으로 추가됩니다. 추가 단계는 필요하지 않습니다.

의존성을 추가하는 방법 알아보기.

Kotlin 멀티플랫폼

멀티플랫폼 프로젝트 지원은 Alpha 단계에 있습니다. 향후 호환되지 않게 변경될 수 있으며 수동 마이그레이션이 필요할 수 있습니다. YouTrack을 통한 피드백을 환영합니다.

Kotlin 멀티플랫폼은 네이티브 프로그래밍의 유연성과 이점을 유지하면서 다양한 플랫폼을 위해 동일한 코드를 작성하고 유지 관리하는 데 드는 시간을 줄여줍니다. 멀티플랫폼 기능과 개선 사항에 계속해서 노력을 기울이고 있습니다.

멀티플랫폼 프로젝트에는 Gradle 6.0 이상이 필요합니다.

계층적 프로젝트 구조를 통한 여러 타겟 간 코드 공유

새로운 계층적 프로젝트 구조 지원을 통해 멀티플랫폼 프로젝트 내의 여러 플랫폼 간에 코드를 공유할 수 있습니다.

이전에는 멀티플랫폼 프로젝트에 추가된 모든 코드를 하나의 타겟으로 제한되어 다른 플랫폼에서 재사용할 수 없는 플랫폼 전용 소스 세트에 배치하거나, 프로젝트의 모든 플랫폼에서 공유되는 commonMain 또는 commonTest와 같은 공통 소스 세트에 배치해야 했습니다. 공통 소스 세트에서는 플랫폼별 actual 구현이 필요한 expect 선언을 사용해야만 플랫폼별 API를 호출할 수 있었습니다.

이는 모든 플랫폼에서 코드를 공유하는 것은 쉽게 만들었지만, 일부 타겟 사이에서만 공유하는 것, 특히 공통 로직과 서드파티 API를 많이 재사용할 수 있는 유사한 타겟들 사이에서의 공유는 쉽지 않았습니다.

예를 들어 iOS를 타겟으로 하는 일반적인 멀티플랫폼 프로젝트에는 두 개의 iOS 관련 타겟이 있습니다. 하나는 iOS ARM64 디바이스용이고 다른 하나는 x64 시뮬레이터용입니다. 이들은 별도의 플랫폼 전용 소스 세트를 가지지만, 실제로는 디바이스와 시뮬레이터용으로 서로 다른 코드가 필요한 경우가 드물며 의존성도 매우 비슷합니다. 따라서 iOS 전용 코드는 이들 사이에서 공유될 수 있습니다.

분명히 이러한 설정에서는 iOS 디바이스와 시뮬레이터 모두에 공통적인 API를 직접 호출할 수 있는 Kotlin/Native 코드를 포함하는 두 iOS 타겟을 위한 공유 소스 세트를 갖는 것이 바람직할 것입니다.

iOS 타겟을 위한 코드 공유

이제 각 소스 세트를 사용하는 타겟에 따라 사용 가능한 API와 언어 기능을 추론하고 조정하는 계층적 프로젝트 구조 지원을 통해 이를 수행할 수 있습니다.

일반적인 타겟 조합의 경우 타겟 숏컷(shortcut)을 사용하여 계층 구조를 생성할 수 있습니다. 예를 들어 ios() 숏컷을 사용하여 위에서 보여준 공유 소스 세트와 두 개의 iOS 타겟을 생성할 수 있습니다.

kotlin
kotlin {
    ios() // iOS 디바이스 및 시뮬레이터 타겟; iosMain 및 iosTest 소스 세트
}

다른 타겟 조합의 경우, dependsOn 관계로 소스 세트를 연결하여 수동으로 계층 구조를 생성하세요.

계층적 구조

kotlin
kotlin{
    sourceSets {
        val desktopMain by creating {
            dependsOn(commonMain)
        }
        val linuxX64Main by getting {
            dependsOn(desktopMain)
        }
        val mingwX64Main by getting {
            dependsOn(desktopMain)
        }
        val macosX64Main by getting {
            dependsOn(desktopMain)
        }
    }
}
groovy
kotlin {
    sourceSets {
        desktopMain {
            dependsOn(commonMain)
        }
        linuxX64Main {
            dependsOn(desktopMain)
        }
        mingwX64Main {
            dependsOn(desktopMain)
        }
        macosX64Main {
            dependsOn(desktopMain)
        }
    }
}

계층적 프로젝트 구조 덕분에 라이브러리는 타겟의 서브셋에 대한 공통 API를 제공할 수도 있습니다. 라이브러리에서 코드 공유에 대해 자세히 알아보세요.

계층적 구조에서 네이티브 라이브러리 활용

여러 네이티브 타겟 간에 공유되는 소스 세트에서 Foundation, UIKit, POSIX와 같은 플랫폼 의존적인 라이브러리를 사용할 수 있습니다. 이를 통해 플랫폼 전용 의존성에 제한받지 않고 더 많은 네이티브 코드를 공유할 수 있습니다.

추가 단계는 필요하지 않으며 모든 것이 자동으로 이루어집니다. IntelliJ IDEA는 공유 코드에서 사용할 수 있는 공통 선언을 감지하도록 도와줄 것입니다.

플랫폼 의존 라이브러리 사용에 대해 자세히 알아보기.

의존성을 단 한 번만 지정

이제부터는 동일한 라이브러리의 다양한 변형을 공유 소스 세트와 플랫폼별 소스 세트에서 각각 지정하는 대신, 공유 소스 세트에서 의존성을 단 한 번만 지정하면 됩니다.

kotlin
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
            }
        }
    }
}
groovy
kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
            }
        }
    }
}

-common, -native 등과 같이 플랫폼을 지정하는 접미사가 붙은 kotlinx 라이브러리 아티팩트 이름은 더 이상 지원되지 않으므로 사용하지 마세요. 대신 위의 예시처럼 라이브러리 기본 아티팩트 이름인 kotlinx-coroutines-core를 사용하세요.

단, 이 변경 사항은 현재 다음에는 적용되지 않습니다.

  • stdlib 라이브러리 – Kotlin 1.4.0부터 stdlib 의존성은 자동으로 추가됩니다.
  • kotlin.test 라이브러리 – 여전히 test-commontest-annotations-common을 사용해야 합니다. 이 의존성들은 나중에 다뤄질 예정입니다.

특정 플랫폼에 대해서만 의존성이 필요한 경우, 여전히 -jvm 또는 -js와 같은 접미사가 붙은 표준 및 kotlinx 라이브러리의 플랫폼 전용 변형(예: kotlinx-coroutines-core-jvm)을 사용할 수 있습니다.

의존성 구성에 대해 자세히 알아보기.

Gradle 프로젝트 개선 사항

Kotlin 멀티플랫폼, Kotlin/JVM, Kotlin/Native, Kotlin/JS에 특화된 Gradle 프로젝트 기능 및 개선 사항 외에도 모든 Kotlin Gradle 프로젝트에 적용되는 몇 가지 변경 사항이 있습니다.

표준 라이브러리 의존성 기본 추가

이제 멀티플랫폼 프로젝트를 포함한 모든 Kotlin Gradle 프로젝트에서 stdlib 라이브러리에 대한 의존성을 선언할 필요가 없습니다. 의존성이 기본으로 추가됩니다.

자동으로 추가되는 표준 라이브러리는 Kotlin Gradle 플러그인과 동일한 버전입니다.

플랫폼별 소스 세트의 경우 해당 플랫폼 전용 변형 라이브러리가 사용되며, 나머지는 공통 표준 라이브러리가 추가됩니다. Kotlin Gradle 플러그인은 Gradle 빌드 스크립트의 kotlinOptions.jvmTarget 컴파일러 옵션에 따라 적절한 JVM 표준 라이브러리를 선택합니다.

기본 동작을 변경하는 방법 알아보기.

Kotlin 프로젝트를 위한 최소 Gradle 버전

Kotlin 프로젝트의 새로운 기능을 즐기려면 Gradle을 최신 버전으로 업데이트하세요. 멀티플랫폼 프로젝트에는 Gradle 6.0 이상이 필요하며, 다른 Kotlin 프로젝트는 Gradle 5.4 이상에서 작동합니다.

IDE에서 *.gradle.kts 지원 개선

1.4.0에서는 Gradle Kotlin DSL 스크립트(*.gradle.kts 파일)에 대한 IDE 지원을 계속해서 개선했습니다. 새로운 버전의 변경 사항은 다음과 같습니다.

  • 성능 향상을 위한 스크립트 구성의 명시적 로드. 이전에는 빌드 스크립트에 가한 변경 사항이 백그라운드에서 자동으로 로드되었습니다. 성능을 개선하기 위해 1.4.0에서는 빌드 스크립트 구성의 자동 로드를 비활성화했습니다. 이제 IDE는 명시적으로 적용할 때만 변경 사항을 로드합니다.

    Gradle 6.0 이전 버전에서는 에디터에서 Load Configuration을 클릭하여 스크립트 구성을 수동으로 로드해야 합니다.

    *.gradle.kts – Load Configuration

    Gradle 6.0 이상에서는 Load Gradle Changes를 클릭하거나 Gradle 프로젝트를 다시 임포트하여 변경 사항을 명시적으로 적용할 수 있습니다.

    IntelliJ IDEA 2020.1 및 Gradle 6.0 이상에서는 전체 프로젝트를 업데이트하지 않고 스크립트 구성의 변경 사항만 로드하는 Load Script Configurations 액션을 하나 더 추가했습니다. 이는 전체 프로젝트를 다시 임포트하는 것보다 훨씬 짧은 시간이 소요됩니다.

    *.gradle.kts – Load Script Changes 및 Load Gradle Changes

    새로 생성된 스크립트나 새로운 Kotlin 플러그인으로 프로젝트를 처음 열 때도 Load Script Configurations를 수행해야 합니다.

    Gradle 6.0 이상에서는 이전 구현에서 개별적으로 로드되었던 것과 달리 이제 모든 스크립트를 한 번에 로드할 수 있습니다. 각 요청마다 Gradle 구성 단계가 실행되어야 하므로 대규모 Gradle 프로젝트에서는 리소스를 많이 소모할 수 있었습니다.

    현재 이러한 로드는 build.gradle.ktssettings.gradle.kts 파일로 제한됩니다(관련 이슈에 투표해 주세요). init.gradle.kts 또는 적용된 스크립트 플러그인에 대한 하이라이팅을 활성화하려면 이전 메커니즘인 단독 스크립트에 추가(adding them to standalone scripts) 기능을 사용하세요. 해당 스크립트에 대한 구성은 필요할 때 별도로 로드됩니다. 이러한 스크립트에 대해 자동 리로드를 활성화할 수도 있습니다.

    *.gradle.kts – 단독 스크립트에 추가

  • 에러 보고 개선. 이전에는 Gradle 데몬의 에러를 별도의 로그 파일에서만 볼 수 있었습니다. 이제 Gradle 데몬은 에러에 대한 모든 정보를 직접 반환하고 이를 Build 도구 창에 표시합니다. 이를 통해 시간과 노력을 모두 아낄 수 있습니다.

표준 라이브러리

1.4.0 표준 라이브러리의 가장 중요한 변경 사항 목록은 다음과 같습니다.

공통 예외 처리 API

다음 API 요소들이 공통 라이브러리로 이동되었습니다.

  • 이 Throwable에 대한 상세 설명과 스택 트레이스를 반환하는 Throwable.stackTraceToString() 확장 함수, 그리고 이 설명을 표준 에러 출력에 프린트하는 Throwable.printStackTrace().
  • 예외를 전달하기 위해 억제된 예외를 지정할 수 있게 해주는 Throwable.addSuppressed() 함수와 모든 억제된 예외 리스트를 반환하는 Throwable.suppressedExceptions 프로퍼티.
  • 함수가 플랫폼 메서드(JVM 또는 Native)로 컴파일될 때 체크될 예외 타입들을 나열하는 @Throws 어노테이션.

배열 및 컬렉션을 위한 새로운 함수

컬렉션

1.4.0에서 표준 라이브러리에는 컬렉션 작업을 위한 여러 유용한 함수가 포함되어 있습니다.

  • setOfNotNull(): 제공된 인자 중 null이 아닌 항목들로만 구성된 세트를 만듭니다.

    kotlin
    fun main() {
        val set = setOfNotNull(null, 1, 2, 0, null)
        println(set)
    }
  • Sequence를 위한 shuffled().

    kotlin
    fun main() {
        val numbers = (0 until 50).asSequence()
        val result = numbers.map { it * 2 }.shuffled().take(5)
        println(result.toList()) // 100 미만의 무작위 짝수 5개
    }
  • onEach()flatMap()에 대응하는 *Indexed() 함수들. 컬렉션 요소에 적용되는 작업에 요소의 인덱스가 파라미터로 제공됩니다.

    kotlin
    fun main() {
        listOf("a", "b", "c", "d").onEachIndexed {
            index, item -> println(index.toString() + ":" + item)
        }
    
       val list = listOf("hello", "kot", "lin", "world")
              val kotlin = list.flatMapIndexed { index, item ->
                  if (index in 1..2) item.toList() else emptyList() 
              }
              println(kotlin)
    }
  • randomOrNull(), reduceOrNull(), reduceIndexedOrNull()*OrNull() 대응 함수들. 이들은 빈 컬렉션에 대해 null을 반환합니다.

    kotlin
    fun main() {
         val empty = emptyList<Int>()
         empty.reduceOrNull { a, b -> a + b }
         // empty.reduce { a, b -> a + b } // Exception: Empty collection can't be reduced.
    }
  • runningFold(), 동의어인 scan(), 그리고 runningReduce()fold()reduce()와 유사하게 컬렉션 요소에 순차적으로 작업을 적용합니다. 차이점은 이 새로운 함수들이 모든 중간 결과의 시퀀스를 반환한다는 것입니다.

    kotlin
    fun main() {
        val numbers = mutableListOf(0, 1, 2, 3, 4, 5)
        val runningReduceSum = numbers.runningReduce { sum, item -> sum + item }
        val runningFoldSum = numbers.runningFold(10) { sum, item -> sum + item }
        println(runningReduceSum.toString())
        println(runningFoldSum.toString())
    }
  • sumOf()는 선택자(selector) 함수를 받아 컬렉션의 모든 요소에 대한 해당 값의 합계를 반환합니다. sumOf()Int, Long, Double, UInt, ULong 타입의 합계를 생성할 수 있습니다. JVM에서는 BigIntegerBigDecimal도 사용할 수 있습니다.

    kotlin
    data class OrderItem(val name: String, val price: Double, val count: Int)
    
    fun main() {
        val order = listOf<OrderItem>(
            OrderItem("Cake", price = 10.0, count = 1),
            OrderItem("Coffee", price = 2.5, count = 3),
            OrderItem("Tea", price = 1.5, count = 2))
    
        val total = order.sumOf { it.price * it.count } // Double
        val count = order.sumOf { it.count } // Int
        println("You've ordered $count items that cost $total in total")
    }
  • min()max() 함수는 Kotlin 컬렉션 API 전체에서 사용되는 명명 규칙에 맞게 minOrNull()maxOrNull()로 이름이 변경되었습니다. 함수 이름의 *OrNull 접미사는 수신자 컬렉션이 비어 있으면 null을 반환함을 의미합니다. minBy(), maxBy(), minWith(), maxWith()에도 동일하게 적용되며, 1.4에서는 *OrNull() 동의어를 가집니다.

  • 새로운 minOf()maxOf() 확장 함수는 컬렉션 항목에 대한 지정된 선택자 함수의 최솟값과 최댓값을 반환합니다.

    kotlin
    data class OrderItem(val name: String, val price: Double, val count: Int)
    
    fun main() {
        val order = listOf<OrderItem>(
            OrderItem("Cake", price = 10.0, count = 1),
            OrderItem("Coffee", price = 2.5, count = 3),
            OrderItem("Tea", price = 1.5, count = 2))
        val highestPrice = order.maxOf { it.price }
        println("The most expensive item in the order costs $highestPrice")
    }

    Comparator를 인자로 받는 minOfWith()maxOfWith(), 그리고 빈 컬렉션에 대해 null을 반환하는 네 가지 함수 모두의 *OrNull() 버전도 있습니다.

  • flatMapflatMapTo에 대한 새로운 오버로드를 통해 수신자 타입과 일치하지 않는 반환 타입의 변환을 사용할 수 있습니다.

    • Iterable, Array, Map에서 Sequence로의 변환
    • Sequence에서 Iterable로의 변환
    kotlin
    fun main() {
        val list = listOf("kot", "lin")
        val lettersList = list.flatMap { it.asSequence() }
        val lettersSeq = list.asSequence().flatMap { it.toList() }    
        println(lettersList)
        println(lettersSeq.toList())
    }
  • 가변 리스트에서 요소를 제거하기 위한 숏컷인 removeFirst(), removeLast()와 이들의 *orNull() 대응 함수.

배열

다양한 컨테이너 타입 작업 시 일관된 경험을 제공하기 위해 배열을 위한 새로운 함수도 추가했습니다.

  • shuffle(): 배열 요소를 무작위 순서로 배치합니다.
  • onEach(): 각 배열 요소에 지정된 작업을 수행하고 배열 자체를 반환합니다.
  • associateWith()associateWithTo(): 배열 요소를 키로 하는 맵을 빌드합니다.
  • 배열 하위 범위(subrange)를 위한 reverse(): 하위 범위에 있는 요소의 순서를 뒤집습니다.
  • 배열 하위 범위를 위한 sortDescending(): 하위 범위에 있는 요소를 내림차순으로 정렬합니다.
  • 배열 하위 범위를 위한 sort()sortWith()가 이제 공통 라이브러리에서 사용 가능합니다.
kotlin
fun main() {
    var language = ""
    val letters = arrayOf("k", "o", "t", "l", "i", "n")
    val fileExt = letters.onEach { language += it }
       .filterNot { it in "aeuio" }.take(2)
       .joinToString(prefix = ".", separator = "")
    println(language) // "kotlin"
    println(fileExt) // ".kt"

    letters.shuffle()
    letters.reverse(0, 3)
    letters.sortDescending(2, 5)
    println(letters.contentToString()) // [k, o, t, l, i, n]
}

또한 CharArray/ByteArrayString 간의 변환을 위한 새로운 함수가 있습니다.

  • ByteArray.decodeToString()String.encodeToByteArray()
  • CharArray.concatToString()String.toCharArray()
kotlin
fun main() {
	val str = "kotlin"
    val array = str.toCharArray()
    println(array.concatToString())
}

ArrayDeque

양방향 큐(double-ended queue)의 구현인 ArrayDeque 클래스도 추가했습니다. 양방향 큐를 사용하면 큐의 시작과 끝 모두에서 분할 상환 상수 시간(amortized constant time) 내에 요소를 추가하거나 제거할 수 있습니다. 코드에서 큐나 스택이 필요할 때 기본적으로 양방향 큐를 사용할 수 있습니다.

kotlin
fun main() {
    val deque = ArrayDeque(listOf(1, 2, 3))

    deque.addFirst(0)
    deque.addLast(4)
    println(deque) // [0, 1, 2, 3, 4]

    println(deque.first()) // 0
    println(deque.last()) // 4

    deque.removeFirst()
    deque.removeLast()
    println(deque) // [1, 2, 3]
}

ArrayDeque 구현은 내부적으로 크기 조정이 가능한 배열을 사용합니다. 순환 버퍼인 Array에 내용을 저장하고 Array가 가득 찼을 때만 크기를 조정합니다.

문자열 조작을 위한 함수

1.4.0의 표준 라이브러리에는 문자열 조작 API에 대한 여러 개선 사항이 포함되어 있습니다.

  • StringBuilderset(), setRange(), deleteAt(), deleteRange(), appendRange() 등 유용한 새로운 확장 함수가 추가되었습니다.

    kotlin
        fun main() {
            val sb = StringBuilder("Bye Kotlin 1.3.72")
            sb.deleteRange(0, 3)
            sb.insertRange(0, "Hello", 0 ,5)
            sb.set(15, '4')
            sb.setRange(17, 19, "0")
            print(sb.toString())
        }
  • StringBuilder의 일부 기존 함수들을 공통 라이브러리에서 사용할 수 있습니다. 그중에는 append(), insert(), substring(), setLength() 등이 있습니다.

  • 새로운 함수 Appendable.appendLine()StringBuilder.appendLine()이 공통 라이브러리에 추가되었습니다. 이들은 해당 클래스의 JVM 전용 appendln() 함수를 대체합니다.

    kotlin
    fun main() {
        println(buildString {
            appendLine("Hello,")
            appendLine("world")
        })
    }

비트 연산

비트 조작을 위한 새로운 함수들입니다.

  • countOneBits()
  • countLeadingZeroBits()
  • countTrailingZeroBits()
  • takeHighestOneBit()
  • takeLowestOneBit()
  • rotateLeft()rotateRight() (실험적)
kotlin
fun main() {
    val number = "1010000".toInt(radix = 2)
    println(number.countOneBits())
    println(number.countTrailingZeroBits())
    println(number.takeHighestOneBit().toString(2))
}

위임된 프로퍼티 개선

1.4.0에서는 Kotlin의 위임된 프로퍼티(delegated properties) 사용 경험을 개선하기 위해 새로운 기능을 추가했습니다.

  • 이제 프로퍼티를 다른 프로퍼티에 위임할 수 있습니다.
  • 새로운 인터페이스 PropertyDelegateProvider를 통해 단일 선언으로 위임 공급자(delegate provider)를 생성할 수 있습니다.
  • ReadWriteProperty가 이제 ReadOnlyProperty를 확장하므로, 읽기 전용 프로퍼티에 두 인터페이스를 모두 사용할 수 있습니다.

새로운 API 외에도 결과 바이트코드 크기를 줄이는 몇 가지 최적화를 수행했습니다. 이러한 최적화는 이 블로그 포스트에 설명되어 있습니다.

위임된 프로퍼티에 대해 자세히 알아보기.

KType에서 Java Type으로 변환

표준 라이브러리의 새로운 확장 프로퍼티 KType.javaType(현재 실험적)을 사용하면 전체 kotlin-reflect 의존성을 사용하지 않고도 Kotlin 타입에서 java.lang.reflect.Type을 얻을 수 있습니다.

kotlin
import kotlin.reflect.javaType
import kotlin.reflect.typeOf

@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> accessReifiedTypeArg() {
   val kType = typeOf<T>()
   println("Kotlin type: $kType")
   println("Java type: ${kType.javaType}")
}

@OptIn(ExperimentalStdlibApi::class)
fun main() {
   accessReifiedTypeArg<String>()
   // Kotlin type: kotlin.String
   // Java type: class java.lang.String
  
   accessReifiedTypeArg<List<String>>()
   // Kotlin type: kotlin.collections.List<kotlin.String>
   // Java type: java.util.List<java.lang.String>
}

Kotlin 리플렉션을 위한 Proguard 구성

1.4.0부터 kotlin-reflect.jar에 Kotlin 리플렉션을 위한 Proguard/R8 구성을 내장했습니다. 이를 통해 R8 또는 Proguard를 사용하는 대부분의 Android 프로젝트는 추가 구성 없이 kotlin-reflect와 함께 작동할 수 있습니다. 더 이상 kotlin-reflect 내부를 위한 Proguard 규칙을 복사해서 붙여넣을 필요가 없습니다. 하지만 리플렉션을 사용할 모든 API를 명시적으로 나열해야 한다는 점에 유의하세요.

기존 API 개선

  • 여러 함수가 이제 null 수신자에서도 작동합니다. 예를 들면 다음과 같습니다.

    • 문자열의 toBoolean()
    • 배열의 contentEquals(), contentHashcode(), contentToString()
  • DoubleFloatNaN, NEGATIVE_INFINITY, POSITIVE_INFINITY가 이제 const로 정의되어 어노테이션 인자로 사용할 수 있습니다.

  • DoubleFloat의 새로운 상수 SIZE_BITSSIZE_BYTES는 해당 타입의 인스턴스를 바이너리 형태로 표현하는 데 사용되는 비트와 바이트 수를 포함합니다.

  • maxOf()minOf() 최상위 함수가 가변 인자(vararg)를 받을 수 있습니다.

stdlib 아티팩트를 위한 module-info 디스크립터

Kotlin 1.4.0은 기본 표준 라이브러리 아티팩트에 module-info.java 모듈 정보를 추가합니다. 이를 통해 앱에 필요한 플랫폼 모듈만 포함하는 맞춤형 Java 런타임 이미지를 생성하는 jlink 도구와 함께 사용할 수 있습니다. 이전에도 Kotlin 표준 라이브러리 아티팩트와 함께 jlink를 사용할 수 있었으나, 이를 위해 별도의 아티팩트("modular" 분류자가 있는 아티팩트)를 사용해야 했고 전체 설정이 간단하지 않았습니다. Android의 경우, module-info가 있는 jar 파일을 올바르게 처리할 수 있는 Android Gradle 플러그인 버전 3.2 이상을 사용하고 있는지 확인하세요.

지원 중단(Deprecations)

Double 및 Float의 toShort() 및 toByte()

DoubleFloattoShort()toByte() 함수는 좁은 값 범위와 더 작은 변수 크기로 인해 예상치 못한 결과를 초래할 수 있어 지원이 중단되었습니다.

부동 소수점 숫자를 Byte 또는 Short로 변환하려면 2단계 변환을 사용하세요. 먼저 Int로 변환한 다음 대상 타입으로 다시 변환하세요.

부동 소수점 배열에서의 contains(), indexOf(), lastIndexOf()

FloatArrayDoubleArraycontains(), indexOf(), lastIndexOf() 확장 함수는 일부 경계 케이스에서 전순서 동등성(total order equality)과 상충되는 IEEE 754 표준 동등성을 사용하기 때문에 지원이 중단되었습니다. 자세한 내용은 이 이슈를 참조하세요.

min() 및 max() 컬렉션 함수

빈 컬렉션에 대해 null을 반환하는 동작을 더 적절하게 반영하기 위해 min()max() 컬렉션 함수를 minOrNull()maxOrNull()로 대체하고 지원을 중단했습니다. 자세한 내용은 이 이슈를 참조하세요.

지원 중단된 실험적 코루틴 제외

kotlin.coroutines.experimental API는 1.3.0에서 kotlin.coroutines를 위해 지원이 중단되었습니다. 1.4.0에서는 표준 라이브러리에서 kotlin.coroutines.experimental을 제거함으로써 지원 중단 사이클을 완료합니다. 여전히 JVM에서 이를 사용하는 사람들을 위해 모든 실험적 코루틴 API가 포함된 호환성 아티팩트 kotlin-coroutines-experimental-compat.jar를 제공했습니다. 이를 Maven에 게시했으며 표준 라이브러리와 함께 Kotlin 배포판에 포함했습니다.

안정적인 JSON 직렬화

Kotlin 1.4.0과 함께 kotlinx.serialization의 첫 번째 안정 버전인 1.0.0-RC를 출시합니다. 이제 kotlinx-serialization-core(이전 명칭 kotlinx-serialization-runtime)의 JSON 직렬화 API가 안정적임을 선언합니다. 다른 직렬화 형식용 라이브러리는 코어 라이브러리의 일부 고급 부분과 함께 실험적 단계로 유지됩니다.

JSON 직렬화 API를 더 일관되고 사용하기 쉽게 대폭 재작업했습니다. 이제부터 JSON 직렬화 API를 하위 호환성을 유지하는 방식으로 계속 개발할 것입니다. 단, 이전 버전을 사용했다면 1.0.0-RC로 마이그레이션할 때 일부 코드를 다시 작성해야 합니다. 이를 돕기 위해 kotlinx.serialization에 대한 완전한 문서 세트인 Kotlin 직렬화 가이드도 제공합니다. 이 가이드는 가장 중요한 기능을 사용하는 과정을 안내하고 직면할 수 있는 모든 문제를 해결하는 데 도움을 줄 것입니다.

참고: kotlinx-serialization 1.0.0-RC는 Kotlin 컴파일러 1.4에서만 작동합니다. 이전 컴파일러 버전과는 호환되지 않습니다.

스크립팅 및 REPL

1.4.0에서 Kotlin의 스크립팅은 다른 업데이트와 함께 여러 기능 및 성능 개선의 혜택을 받았습니다. 주요 변경 사항은 다음과 같습니다.

Kotlin의 스크립팅에 더 익숙해질 수 있도록 예제 프로젝트를 준비했습니다. 여기에는 표준 스크립트(*.main.kts) 예제와 Kotlin 스크립팅 API 및 맞춤형 스크립트 정의 사용 예제가 포함되어 있습니다. 사용해 보시고 이슈 트래커를 통해 의견을 공유해 주세요.

새로운 의존성 해결 API

1.4.0에서는 외부 의존성(예: Maven 아티팩트)을 해결하기 위한 새로운 API와 그 구현을 도입했습니다. 이 API는 새로운 아티팩트인 kotlin-scripting-dependencieskotlin-scripting-dependencies-maven으로 게시됩니다. kotlin-script-util 라이브러리의 이전 의존성 해결 기능은 이제 지원이 중단되었습니다.

새로운 REPL API

새로운 실험적 REPL API가 이제 Kotlin 스크립팅 API의 일부가 되었습니다. 게시된 아티팩트에는 이를 구현한 여러 구현체가 있으며, 일부는 코드 완성(code completion)과 같은 고급 기능을 갖추고 있습니다. 우리는 이 API를 Kotlin Jupyter 커널에서 사용하고 있으며, 이제 여러분의 맞춤형 셸과 REPL에서도 시도해 볼 수 있습니다.

컴파일된 스크립트 캐시

Kotlin 스크립팅 API는 이제 컴파일된 스크립트 캐시를 구현하는 기능을 제공하여, 변경되지 않은 스크립트의 후속 실행 속도를 대폭 높입니다. 기본 고급 스크립트 구현인 kotlin-main-kts에는 이미 자체 캐시가 포함되어 있습니다.

아티팩트 이름 변경

아티팩트 이름의 혼동을 피하기 위해 kotlin-scripting-jsr223-embeddablekotlin-scripting-jvm-host-embeddable을 각각 kotlin-scripting-jsr223kotlin-scripting-jvm-host로 변경했습니다. 이 아티팩트들은 사용 충돌을 피하기 위해 번들로 제공되는 서드파티 라이브러리를 숨긴(shade) kotlin-compiler-embeddable 아티팩트에 의존합니다. 이번 이름 변경을 통해 (일반적으로 더 안전한) kotlin-compiler-embeddable의 사용을 스크립팅 아티팩트의 기본값으로 만듭니다. 어떤 이유로든 숨겨지지 않은(unshaded) kotlin-compiler에 의존하는 아티팩트가 필요한 경우, kotlin-scripting-jsr223-unshaded와 같이 -unshaded 접미사가 붙은 아티팩트 버전을 사용하세요. 이 이름 변경은 직접 사용될 것으로 예상되는 스크립팅 아티팩트에만 영향을 미치며, 다른 아티팩트의 이름은 변경되지 않습니다.

Kotlin 1.4.0으로 마이그레이션하기

Kotlin 플러그인의 마이그레이션 도구는 프로젝트를 이전 버전의 Kotlin에서 1.4.0으로 마이그레이션하는 것을 도와줍니다.

Kotlin 버전을 1.4.0으로 변경하고 Gradle 또는 Maven 프로젝트를 다시 임포트하기만 하면 됩니다. 그러면 IDE가 마이그레이션에 대해 물어볼 것입니다.

동의하면 마이그레이션 코드 검사(code inspection)가 실행되어 코드를 확인하고 작동하지 않거나 1.4.0에서 권장되지 않는 부분에 대해 수정을 제안합니다.

마이그레이션 실행

코드 검사에는 다양한 심각도 수준(severity levels)이 있어, 어떤 제안을 수용하고 어떤 제안을 무시할지 결정하는 데 도움을 줍니다.

마이그레이션 검사

Kotlin 1.4.0은 기능 릴리스(feature release)이므로 언어에 호환되지 않는 변경 사항이 포함될 수 있습니다. 이러한 변경 사항의 상세 리스트는 Kotlin 1.4 호환성 가이드에서 확인하세요.