Kotlin 1.4.0의 새로운 기능

Kotlin 1.4.0의 새로운 기능

릴리스: 2020년 8월 17일

Kotlin 1.4.0에서는 품질과 성능에 초점을 맞춰 모든 구성 요소에 대한 많은 개선 사항을 제공해요. 아래에서 Kotlin 1.4.0의 가장 중요한 변경 사항 목록을 확인할 수 있어요.

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

출처: What's new in Kotlin 1.4.0

본문

언어 기능과 개선 사항

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

  • Kotlin 인터페이스의 SAM 변환
  • 라이브러리 작성자를 위한 명시적 API 모드
  • 명명된 인자와 위치 인자 섞기
  • 트레일링 콤마(Trailing comma)
  • 호출 가능 참조 개선
  • 루프에 포함된 when 안에서의 breakcontinue

Kotlin 인터페이스의 SAM 변환

Kotlin 1.4.0 이전에는 Java 메서드와 Java 인터페이스를 다룰 때만 SAM(Single Abstract Method) 변환을 적용할 수 있었어요. 이제부터는 Kotlin 인터페이스에도 SAM 변환을 사용할 수 있어요. 이렇게 하려면 Kotlin 인터페이스를 fun 수식어로 명시적으로 함수형 인터페이스로 표시해 주세요.

단일 추상 메서드만 가진 인터페이스가 매개변수로 기대되는 곳에 람다를 인자로 전달하면 SAM 변환이 적용돼요. 이 경우 컴파일러는 람다를 추상 멤버 함수를 구현하는 클래스의 인스턴스로 자동 변환해요.

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 모드를 제공해요. 이 모드에서 컴파일러는 라이브러리의 API를 더 명확하고 일관성 있게 만드는 데 도움이 되는 추가 검사를 수행해요. 라이브러리 공개 API에 노출되는 선언에 대해 다음 요구 사항을 추가해요.

  • 기본 가시성이 선언을 공개 API에 노출하는 경우 가시성 수정자가 필요해요. 이렇게 하면 어떤 선언도 의도치 않게 공개 API에 노출되지 않도록 보장해요.
  • 공개 API에 노출되는 속성과 함수에 명시적 타입 지정이 필요해요. 이렇게 하면 API 사용자들이 사용하는 API 멤버의 타입을 알 수 있게 보장해요.

구성에 따라 이러한 명시적 API는 오류(엄격 모드)나 경고(경고 모드)를 만들 수 있어요. 가독성과 상식을 위해 일부 종류의 선언은 그러한 검사에서 제외돼요.

  • 기본 생성자(primary constructors)
  • data 클래스의 속성
  • 속성 getter와 setter
  • override 메서드

명시적 API 모드는 모듈의 프로덕션 소스만 분석해요.

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

kotlin {
    // for strict mode
    explicitApi()
    // or
    explicitApi = ExplicitApiMode.Strict

    // for warning mode
    explicitApiWarning()
    // or
    explicitApi = ExplicitApiMode.Warning
}
kotlin {
    // for strict mode
    explicitApi()
    // or
    explicitApi = 'strict'

    // for warning mode
    explicitApiWarning()
    // or
    explicitApi = 'warning'
}

명령줄 컴파일러를 사용할 때는 값이 strict 또는 warning-Xexplicit-api 컴파일러 옵션을 추가해서 명시적 API 모드로 전환해 주세요.

-Xexplicit-api={strict|warning}

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

명명된 인자와 위치 인자 섞기

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

모든 인자가 올바른 위치에 있는데 중간에 있는 한 인자에만 이름을 지정하고 싶을 때는 정말 불편했어요. 특히 boolean이나 null 값이 어떤 속성에 속하는지 확실하게 만들고 싶을 때 유용했죠.

Kotlin 1.4에는 그런 제한이 없어요. 이제 위치 인자 세트의 중간에 있는 인자에 이름을 지정할 수 있어요. 게다가 올바른 순서를 유지하기만 하면 위치 인자와 명명된 인자를 원하는 대로 섞을 수 있어요.

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

//Function call with a named argument in the middle
reformat("This is a String!", uppercaseFirstLetter = false , '-')

트레일링 콤마

Kotlin 1.4부터 인자 목록과 매개변수 목록, when 항목, 구조 분해 선언의 구성 요소 같은 열거형에 트레일링 콤마를 추가할 수 있어요. 트레일링 콤마를 사용하면 콤마를 추가하거나 제거하지 않고 새 항목을 추가하고 순서를 변경할 수 있어요.

특히 매개변수나 값에 여러 줄 구문을 사용할 때 유용해요. 트레일링 콤마를 추가하면 매개변수나 값이 있는 줄을 쉽게 바꿀 수 있어요.

fun reformat(
    str: String,
    uppercaseFirstLetter: Boolean = true,
    wordSeparator: Character = ' ', //trailing comma
) {
    // ...
}

val colors = listOf(
    "red",
    "green",
    "blue", //trailing comma
)

호출 가능 참조 개선

Kotlin 1.4은 호출 가능 참조를 사용하는 더 많은 경우를 지원해요.

  • 기본값을 가진 매개변수를 포함하는 함수에 대한 참조
  • Unit을 반환하는 함수의 함수 참조
  • 함수의 인자 수에 따라 적응하는 참조
  • 호출 가능 참조의 suspend 변환

기본값을 가진 매개변수를 포함하는 함수에 대한 참조

이제 기본값을 가진 매개변수를 포함하는 함수에 대한 호출 가능 참조를 사용할 수 있어요. 함수 foo에 대한 호출 가능 참조가 인자를 받지 않으면 기본값 0이 사용돼요.

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

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

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

이전에는 apply 또는 foo 함수에 추가 오버로드를 작성해야 했어요.

// some new overload
fun applyInt(func: (Int) -> String): String = func(0)

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

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

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

fun main() {
    foo { returnsInt() } // this was the only way to do it  before 1.4
    foo(::returnsInt) // starting from 1.4, this also works
}

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

이제 가변 인자 수(vararg)를 전달할 때 호출 가능 참조를 함수에 적응시킬 수 있어요. 전달된 인자 목록 끝에 같은 타입의 매개변수를 몇 개든 전달할 수 있어요.

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)
}

호출 가능 참조의 suspend 변환

람다에 대한 suspend 변환에 더해, Kotlin은 이제 버전 1.4.0부터 호출 가능 참조에 대한 suspend 변환도 지원해요.

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

fun test() {
    takeSuspend { call() } // OK before 1.4
    takeSuspend(::call) // In Kotlin 1.4, it also works
}

루프에 포함된 when 표현식 안에서 break와 continue 사용하기

Kotlin 1.3에서는 루프에 포함된 when 표현식 안에서 한정되지 않은(unqualified) breakcontinue를 사용할 수 없었어요. 그 이유는 이 키워드들이 when 표현식에서 가능한 fall-through 동작을 위해 예약되어 있었기 때문이에요.

그래서 루프의 when 표현식 안에서 breakcontinue를 사용하려면 레이블을 붙여야 했는데, 꽤 번거로웠어요.

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를 사용할 수 있어요. 이 키워드는 가장 가까운 포함 루프를 종료하거나 다음 단계로 진행하는 예상대로 동작해요.

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 개발을 단순화할 수 있어요.

  • 새롭고 유연한 Project Wizard
  • 코루틴 디버거(Coroutine Debugger)

새롭고 유연한 Project Wizard

유연한 새 Kotlin Project Wizard를 사용하면 UI 없이는 구성하기 어려울 수 있는 멀티플랫폼 프로젝트를 포함해 다양한 유형의 Kotlin 프로젝트를 쉽게 만들고 구성할 수 있어요.

[IMAGE: Kotlin Project Wizard – Multiplatform project]

새 Kotlin Project Wizard는 간단하면서도 유연해요.

  1. 하려는 것에 따라 프로젝트 템플릿을 선택해요. 앞으로 더 많은 템플릿이 추가될 거예요.
  2. 빌드 시스템(Gradle(Kotlin 또는 Groovy DSL), Maven, IntelliJ IDEA)을 선택해요. Kotlin Project Wizard는 선택한 프로젝트 템플릿에서 지원되는 빌드 시스템만 보여줘요.
  3. 메인 화면에서 바로 프로젝트 구조를 미리 볼 수 있어요.

그러면 프로젝트 생성을 마칠 수 있고, 원하면 다음 화면에서 프로젝트를 구성할 수도 있어요.

  1. 이 프로젝트 템플릿에서 지원되는 모듈과 타깃을 추가/제거할 수 있어요.
  2. 타깃 JVM 버전, 타깃 템플릿, 테스트 프레임워크 같은 모듈과 타깃 설정을 구성해요.

[IMAGE: Kotlin Project Wizard - Configure targets]

앞으로 더 많은 구성 옵션과 템플릿을 추가해서 Kotlin Project Wizard를 훨씬 더 유연하게 만들 계획이에요.

다음 튜토리얼을 따라 하면서 새 Kotlin Project Wizard를 직접 사용해 볼 수 있어요.

  • Kotlin/JVM 기반 콘솔 애플리케이션 만들기
  • React용 Kotlin/JS 애플리케이션 만들기
  • Kotlin/Native 애플리케이션 만들기

코루틴 디버거

많은 사람들이 이미 비동기 프로그래밍에 코루틴을 사용해요. 하지만 디버깅에 관해서는 Kotlin 1.4 이전에 코루틴을 다루는 것이 정말 고통스러울 수 있었어요. 코루틴이 스레드 사이를 오가면서 특정 코루틴이 무엇을 하고 있는지 이해하고 그 컨텍스트를 확인하는 것이 어려웠죠. 어떤 경우에는 중단점에 대한 스텝 추적이 그냥 동작하지 않았어요. 결과적으로 코루틴을 사용하는 코드를 디버깅하려면 로깅이나 머릿속 계산에 의존해야 했어요.

Kotlin 1.4에서는 Kotlin 플러그인과 함께 제공되는 새 기능 덕분에 코루틴 디버깅이 훨씬 편리해졌어요.

디버깅은 kotlinx-coroutines-core 1.3.8 이상 버전에서 동작해요.

Debug 도구 창에 이제 새 Coroutines 탭이 있어요. 이 탭에서 현재 실행 중인 코루틴과 일시 중단된 코루틴에 대한 정보를 모두 찾을 수 있어요. 코루틴은 실행 중인 디스패처별로 그룹화돼요.

[IMAGE: Debugging coroutines]

이제 다음을 할 수 있어요.

  • 각 코루틴의 상태를 쉽게 확인할 수 있어요.
  • 실행 중인 코루틴과 일시 중단된 코루틴 모두의 지역 변수와 캡처된 변수 값을 볼 수 있어요.
  • 전체 코루틴 생성 스택과 코루틴 내부의 호출 스택을 볼 수 있어요. 스택은 표준 디버깅 중에 유실되었을 모든 프레임을 변수 값과 함께 포함해요.

각 코루틴의 상태와 스택을 포함한 전체 보고서가 필요하다면 Coroutines 탭 안에서 오른쪽 클릭한 다음 Get Coroutines Dump를 클릭해 주세요. 현재 코루틴 덤프는 꽤 단순하지만, 향후 Kotlin 버전에서 더 읽기 쉽고 유용하게 만들 계획이에요.

[IMAGE: Coroutines Dump]

코루틴 디버깅에 대해 이 블로그 게시물IntelliJ IDEA 문서에서 자세히 알아보세요.

새로운 컴파일러

새 Kotlin 컴파일러는 정말 빠를 것이고, 지원되는 모든 플랫폼을 통합하고 컴파일러 확장을 위한 API를 제공할 거예요. 이것은 장기 프로젝트이며, Kotlin 1.4.0에서 이미 여러 단계를 완료했어요.

  • 새롭고 더 강력한 타입 추론 알고리즘이 기본으로 활성화되었어요.
  • 새로운 JVM 및 JS IR 백엔드. 안정화되면 기본이 될 예정이에요.

새롭고 더 강력한 타입 추론 알고리즘

Kotlin 1.4는 새롭고 더 강력한 타입 추론 알고리즘을 사용해요. 이 새 알고리즘은 Kotlin 1.3에서 컴파일러 옵션을 지정해서 사용해 볼 수 있었는데, 이제 기본으로 사용돼요. 새 알고리즘에서 수정된 이슈의 전체 목록은 YouTrack에서 확인할 수 있어요. 가장 눈에 띄는 개선 사항 몇 가지는 다음과 같아요.

  • 타입이 자동으로 추론되는 더 많은 경우
  • 람다의 마지막 표현식에 대한 스마트 캐스트
  • 호출 가능 참조에 대한 스마트 캐스트
  • 위임 속성에 대한 더 나은 추론
  • 다른 인자를 가진 Java 인터페이스의 SAM 변환
  • Kotlin의 Java SAM 인터페이스

타입이 자동으로 추론되는 더 많은 경우

새 추론 알고리즘은 이전 알고리즘이 명시적으로 지정하도록 요구했던 많은 경우에 타입을 추론해요. 예를 들어 다음 예시에서 람다 매개변수 it의 타입이 String?으로 올바르게 추론돼요.

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?으로 추론해요.

val result = run {
    var str = currentValue()
    if (str == null) {
        str = "test"
    }
    str // the Kotlin compiler knows that str is not null here
}
// The type of 'result' is String? in Kotlin 1.3 and String in Kotlin 1.4

Kotlin 1.4에서는 새 추론 알고리즘 덕분에 람다 안의 마지막 표현식이 스마트 캐스트되고, 이 새롭고 더 정확한 타입이 결과 람다 타입을 추론하는 데 사용돼요. 그래서 result 변수의 타입이 String이 돼요.

Kotlin 1.3에서는 이런 경우가 동작하도록 명시적 캐스트( !! 또는 as String 같은 타입 캐스트)를 추가해야 하는 경우가 많았는데, 이제 그런 캐스트가 불필요해졌어요.

호출 가능 참조에 대한 스마트 캐스트

Kotlin 1.3에서는 스마트 캐스트된 타입의 멤버 참조에 접근할 수 없었어요. 이제 Kotlin 1.4에서는 할 수 있어요.

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?으로 올바르게 추론해요.

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 인터페이스 대신 전달할 수 있어요.

// FILE: A.java
public class A {
    public static void foo(Runnable r1, Runnable r2) {}
}

// FILE: test.kt
fun test(r1: Runnable) {
    A.foo(r1) {}  // Works in Kotlin 1.4
}

Kotlin의 Java SAM 인터페이스

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

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 IR 및 JS IR 백엔드를 사용해 보고 피드백을 공유해 주시길 권장해요.

Kotlin/JVM

Kotlin 1.4.0은 다음과 같은 JVM 관련 개선 사항을 포함해요.

  • 새로운 JVM IR 백엔드
  • 인터페이스에서 기본 메서드를 생성하는 새로운 모드
  • null 검사를 위한 통합 예외 타입
  • JVM 바이트코드의 타입 어노테이션

새로운 JVM IR 백엔드

Kotlin/JS와 함께 Kotlin/JVM도 통합 IR 백엔드로 마이그레이션하고 있어요. 이렇게 하면 대부분의 기능과 버그 수정을 모든 플랫폼에 대해 한 번만 구현할 수 있어요. 모든 플랫폼에서 동작하는 멀티플랫폼 확장을 만들어서 이점을 누릴 수도 있어요.

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

현재 Alpha 상태인 새 Kotlin/JVM 백엔드를 사용해 보고, 이슈와 기능 요청을 이슈 트래커에 등록해 주시길 권장해요. 이렇게 하면 컴파일러 파이프라인을 통합하고 Jetpack Compose 같은 컴파일러 확장을 Kotlin 커뮤니티에 더 빨리 가져올 수 있어요.

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

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) 투영

다음 예시에서 String 타입의 @Foo 어노테이션은 바이트코드로 생성된 다음 라이브러리 코드에서 사용될 수 있어요.

@Target(AnnotationTarget.TYPE)
annotation class Foo

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

Kotlin/JS

JS 플랫폼에서 Kotlin 1.4.0은 다음 개선 사항을 제공해요.

  • 새로운 Gradle DSL
  • 새로운 JS IR 백엔드

새로운 Gradle DSL

kotlin.js Gradle 플러그인에는 조정된 Gradle DSL이 함께 제공되어, 여러 새로운 구성 옵션을 제공하고 kotlin-multiplatform 플러그인이 사용하는 DSL에 더 밀접하게 정렬돼요. 가장 영향력 있는 변경 사항 중 일부는 다음과 같아요.

  • binaries.executable()을 통한 실행 파일 생성의 명시적 토글. Kotlin/JS 실행과 그 환경에 대해 여기에서 자세히 알아보세요.
  • cssSupport를 통한 Gradle 구성 내에서 webpack의 CSS 및 style 로더 구성. CSS와 style 로더 사용에 대해 여기에서 자세히 알아보세요.
  • 필수 버전 번호나 semver 버전 범위와 devNpm, optionalNpm, peerNpm을 통한 development, peer, optional npm 의존성 지원을 포함한 npm 의존성 관리 개선. Gradle에서 직접 npm 패키지의 의존성 관리에 대해 여기에서 자세히 알아보세요.
  • Kotlin 외부 선언 생성기인 Dukat에 대한 더 강력한 통합. 외부 선언을 이제 빌드 시점에 생성하거나 Gradle 태스크로 수동 생성할 수 있어요.

새로운 JS IR 백엔드

현재 Alpha 안정성을 가진 Kotlin/JS용 IR 백엔드는 특히 죽은 코드 제거를 통한 생성 코드 크기와 JavaScript·TypeScript와의 개선된 상호 운용에 초점을 맞춘 Kotlin/JS 타깃 특정 기능을 제공해요.

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

kotlin {
    js(IR) { // or: 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에서 suspend 함수 지원
  • Objective-C 제네릭 기본 지원
  • Objective-C/Swift 상호 운용의 예외 처리
  • Apple 타깃에서 릴리스 .dSYM 기본 생성
  • 성능 개선
  • CocoaPods 의존성 관리 단순화

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

1.4.0에서 우리는 Swift와 Objective-C에서 suspend 함수에 대한 기본 지원을 추가해요. 이제 Kotlin 모듈을 Apple 프레임워크로 컴파일하면 suspend 함수가 콜백(completionHandler, Swift/Objective-C 용어)을 가진 함수로 사용 가능해요. 생성된 프레임워크 헤더에 그런 함수가 있으면 Swift나 Objective-C 코드에서 호출하고 심지어 재정의할 수도 있어요.

예를 들어 이 Kotlin 함수를 작성하면:

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

...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 {
    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 {
    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배 빠르게 상호 운용 라이브러리를 생성하고, 아티팩트 크기는 이전의 25%에서 30%가 돼요.
  • GC 최적화 덕분에 전반적인 런타임 성능이 개선되었어요. 이 개선은 수명이 긴 객체가 많은 프로젝트에서 특히 두드러져요. HashMapHashSet 컬렉션은 이제 불필요한 박싱을 피해서 더 빠르게 동작해요.
  • 1.3.70에서 프로젝트 의존성 캐싱과 Gradle 데몬에서 컴파일러 실행이라는 Kotlin/Native 컴파일 성능을 개선하는 두 가지 새 기능을 도입했어요. 그 이후로 많은 문제를 수정했고 이 기능들의 전반적인 안정성을 개선했어요.

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에서 프로젝트를 다시 import하기만 하면 돼요. 새 의존성이 자동으로 추가돼요. 추가 단계가 필요 없어요.

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

Kotlin Multiplatform

멀티플랫폼 프로젝트 지원은 Alpha 단계예요. 향후 비호환적으로 변경되고 수동 마이그레이션이 필요할 수 있어요. 이에 대한 피드백은 YouTrack에 남겨 주시면 감사하겠어요.

Kotlin Multiplatform은 네이티브 프로그래밍의 유연성과 이점을 유지하면서도 서로 다른 플랫폼을 위해 같은 코드를 작성하고 유지 관리하는 시간을 줄여줘요. 우리는 멀티플랫폼 기능과 개선에 계속 투자하고 있어요.

  • 계층적 프로젝트 구조로 여러 타깃에서 코드 공유
  • 계층적 구조에서 네이티브 라이브러리 활용
  • kotlinx 의존성 한 번만 지정

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

계층적 프로젝트 구조로 여러 타깃에서 코드 공유

새로운 계층적 프로젝트 구조 지원으로 멀티플랫폼 프로젝트의 여러 플랫폼 사이에서 코드를 공유할 수 있어요.

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

이것은 모든 플랫폼에서 코드를 공유하기 쉽게 만들었지만, 특히 공통 로직과 타사 API를 많이 재사용할 수 있는 비슷한 일부 타깃 사이에서만 공유하는 것은 그렇게 쉽지 않았어요.

예를 들어 iOS를 타깃으로 하는 전형적인 멀티플랫폼 프로젝트에는 두 개의 iOS 관련 타깃이 있어요. 하나는 iOS ARM64 기기용이고 다른 하나는 x64 시뮬레이터용이에요. 그것들은 별도의 플랫폼 특정 소스 세트를 가지지만, 실제로는 기기와 시뮬레이터에 다른 코드가 필요할 경우가 드물고 의존성도 많이 비슷해요. 그래서 iOS 특정 코드를 둘 사이에서 공유할 수 있었어요.

분명히 이 설정에서는 두 iOS 타깃을 위한 공유 소스 세트를 가지는 것이 바람직한데, iOS 기기와 시뮬레이터 모두에 공통인 API를 여전히 직접 호출할 수 있는 Kotlin/Native 코드가 필요해요.

[IMAGE: Code shared for iOS targets]

이제 계층적 프로젝트 구조 지원으로 이 작업을 할 수 있는데, 이 구조는 어떤 타깃이 소비하는지에 따라 각 소스 세트에서 사용 가능한 API와 언어 기능을 추론하고 적응시켜요.

공통 타깃 조합의 경우 타깃 단축키로 계층 구조를 만들 수 있어요. 예를 들어 ios() 단축키로 두 iOS 타깃과 위에 표시된 공유 소스 세트를 만들 수 있어요.

kotlin {
    ios() // iOS device and simulator targets; iosMain and iosTest source sets
}

다른 타깃 조합의 경우 dependsOn 관계로 소스 세트를 연결해서 계층 구조를 수동으로 만들어 주세요.

[IMAGE: Hierarchical structure]

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)
        }
    }
}
kotlin {
    sourceSets {
        desktopMain {
            dependsOn(commonMain)
        }
        linuxX64Main {
            dependsOn(desktopMain)
        }
        mingwX64Main {
            dependsOn(desktopMain)
        }
        macosX64Main {
            dependsOn(desktopMain)
        }
    }
}

계층적 프로젝트 구조 덕분에 라이브러리도 타깃의 부분 집합을 위한 공통 API를 제공할 수 있어요. 라이브러리에서 코드 공유에 대해 자세히 알아보세요.

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

여러 네이티브 타깃 사이에서 공유되는 소스 세트에서 Foundation, UIKit, POSIX 같은 플랫폼 종속 라이브러리를 사용할 수 있어요. 이것은 플랫폼 특정 의존성에 제한되지 않고 더 많은 네이티브 코드를 공유하는 데 도움을 줘요.

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

플랫폼 종속 라이브러리 사용에 대해 자세히 알아보세요.

의존성 한 번만 지정

이제부터는 같은 라이브러리의 다른 변형에 대한 의존성을 공유 소스 세트와 플랫폼 특정 소스 세트에서 각각 지정하는 대신, 공유 소스 세트에서 의존성을 한 번만 지정해야 해요.

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

-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 Multiplatform, Kotlin/JVM, Kotlin/Native, Kotlin/JS에 특정한 Gradle 프로젝트 기능과 개선 외에, 모든 Kotlin Gradle 프로젝트에 적용되는 몇 가지 변경 사항이 있어요.

  • 표준 라이브러리 의존성이 이제 기본으로 추가됨
  • Kotlin 프로젝트에 최신 Gradle 버전 필요
  • IDE에서 Kotlin Gradle DSL 지원 개선

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

멀티플랫폼 프로젝트를 포함한 어떤 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을 클릭해서 스크립트 구성을 수동으로 로드해야 해요.

[IMAGE: *.gradle.kts – Load Configuration]

Gradle 6.0 이상에서는 Load Gradle Changes를 클릭하거나 Gradle 프로젝트를 다시 import해서 변경 사항을 명시적으로 적용할 수 있어요.

IntelliJ IDEA 2020.1에서 Gradle 6.0 이상과 함께 Load Script Configurations라는 동작 하나를 더 추가했어요. 이것은 전체 프로젝트를 업데이트하지 않고 스크립트 구성에 대한 변경 사항을 로드해요. 전체 프로젝트를 다시 import하는 것보다 훨씬 시간이 덜 걸려요.

[IMAGE: *.gradle.kts – Load Script Changes and Load Gradle Changes]

새로 만든 스크립트나 새 Kotlin 플러그인으로 프로젝트를 처음 열 때도 Load Script Configurations를 해야 해요.

Gradle 6.0 이상에서는 이전 구현처럼 각각 개별적으로 로드되는 대신 모든 스크립트를 한 번에 로드할 수 있어요. 각 요청이 Gradle 구성 단계 실행을 요구하므로, 큰 Gradle 프로젝트에서는 리소스 집약적일 수 있어요.

현재 그러한 로딩은 build.gradle.ktssettings.gradle.kts 파일로 제한돼요(관련 이슈에 투표해 주세요). init.gradle.kts나 적용된 스크립트 플러그인에 강조 표시를 활성화하려면 이전 메커니즘 사용해서 standalone 스크립트에 추가해 주세요. 그 스크립트에 대한 구성은 필요할 때 별도로 로드돼요. 그런 스크립트에 대해 자동 다시 로드를 활성화할 수도 있어요.

[IMAGE: *.gradle.kts – Add to standalone scripts]

  • 더 나은 오류 보고. 이전에는 Gradle Daemon의 오류를 별도의 로그 파일에서만 볼 수 있었어요. 이제 Gradle Daemon은 오류에 대한 모든 정보를 직접 반환하고 Build 도구 창에 표시해요. 시간과 노력을 모두 절약해 줘요.

표준 라이브러리

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

  • 공통 예외 처리 API
  • 배열과 컬렉션을 위한 새 함수
  • 문자열 조작 함수
  • 비트 연산
  • 위임 속성 개선
  • KType에서 Java Type으로 변환
  • Kotlin reflection을 위한 Proguard 구성
  • 기존 API 개선
  • stdlib 아티팩트의 module-info 설명자
  • Deprecation
  • deprecated 실험적 코루틴 제외

공통 예외 처리 API

다음 API 요소가 공통 라이브러리로 이동했어요.

  • 스택 트레이스와 함께 이 throwable의 자세한 설명을 반환하는 Throwable.stackTraceToString() 확장 함수와, 이 설명을 표준 오류 출력에 출력하는 Throwable.printStackTrace().
  • 예외를 전달하기 위해 억제된 예외를 지정할 수 있게 하는 Throwable.addSuppressed() 함수와, 억제된 모든 예외의 목록을 반환하는 Throwable.suppressedExceptions 속성.
  • 함수가 플랫폼 메서드(JVM 또는 네이티브 플랫폼)로 컴파일될 때 검사될 예외 타입을 나열하는 @Throws 어노테이션.

배열과 컬렉션을 위한 새 함수

컬렉션

1.4.0에서 표준 라이브러리는 컬렉션 작업을 위한 여러 유용한 함수를 포함해요.

  • 제공된 인자 중 null이 아닌 항목을 모두 포함하는 세트를 만드는 setOfNotNull().
fun main() {
    val set = setOfNotNull(null, 1, 2, 0, null)
    println(set)
}
  • 시퀀스를 위한 shuffled().
fun main() {
    val numbers = (0 until 50).asSequence()
    val result = numbers.map { it * 2 }.shuffled().take(5)
    println(result.toList()) //five random even numbers below 100
}
  • onEach()flatMap()을 위한 *Indexed() 대응 함수. 컬렉션 요소에 적용하는 연산이 요소 인덱스를 매개변수로 가져요.
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을 반환해요.
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()처럼 컬렉션 요소에 주어진 연산을 순차적으로 적용해요. 차이점은 이 새 함수들은 중간 결과의 전체 시퀀스를 반환한다는 거예요.
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()는 선택자 함수를 받아 컬렉션의 모든 요소에 대한 그 값의 합을 반환해요. sumOf()Int, Long, Double, UInt, ULong 타입의 합을 만들 수 있어요. JVM에서는 BigIntegerBigDecimal도 사용할 수 있어요.
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() 확장 함수는 컬렉션 항목에 대한 주어진 선택자 함수의 최소값과 최대값을 반환해요.

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로의 변환
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()는 이제 공통 라이브러리에서 사용할 수 있어요.
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()
fun main() {
    val str = "kotlin"
    val array = str.toCharArray()
    println(array.concatToString())
}

ArrayDeque

또한 ArrayDeque 클래스(이중 끝 큐, double-ended queue의 구현)를 추가했어요. 이중 끝 큐는 분할 상환 상수 시간에 큐의 앞이나 뒤에서 요소를 추가하거나 제거할 수 있게 해 줘요. 코드에서 큐나 스택이 필요할 때 기본으로 이중 끝 큐를 사용할 수 있어요.

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에 대한 여러 개선 사항이 포함돼요.

  • StringBuilder에 유용한 새 확장 함수가 있어요. set(), setRange(), deleteAt(), deleteRange(), appendRange() 등이에요.
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() 함수를 대체해요.
fun main() {
    println(buildString {
        appendLine("Hello,")
        appendLine("world")
    })
}

비트 연산

비트 조작을 위한 새 함수:

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

위임 속성 개선

1.4.0에서 Kotlin의 위임 속성 경험을 개선하기 위한 새 기능을 추가했어요.

  • 이제 속성을 다른 속성에 위임할 수 있어요.
  • 새 인터페이스 PropertyDelegateProvider는 단일 선언에서 대리자 공급자를 만들 수 있게 해 줘요.
  • ReadWriteProperty가 이제 ReadOnlyProperty를 확장하므로 읽기 전용 속성에 둘 다 사용할 수 있어요.

새 API 외에도 결과 바이트코드 크기를 줄이는 몇 가지 최적화를 만들었어요. 이 최적화는 이 블로그 게시물에 설명되어 있어요.

위임 속성에 대해 자세히 알아보세요.

KType에서 Java Type으로 변환

stdlib의 새 확장 속성 KType.javaType(현재 실험적)은 전체 kotlin-reflect 의존성을 사용하지 않고 Kotlin 타입에서 java.lang.reflect.Type을 얻는 데 도움을 줘요.

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 reflection을 위한 Proguard 구성

1.4.0부터 Kotlin Reflection용 Proguard/R8 구성을 kotlin-reflect.jar에 내장했어요. 이제 대부분의 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 이상을 사용해야 해요.

Deprecation

Double과 Float의 toShort()와 toByte()

좁은 값 범위와 더 작은 변수 크기 때문에 예상치 못한 결과를 초래할 수 있어서 DoubleFloattoShort()toByte() 함수를 deprecated 처리했어요.

부동 소수점 숫자를 ByteShort로 변환하려면 두 단계 변환을 사용해 주세요. 먼저 Int로 변환한 다음 다시 타깃 타입으로 변환해요.

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

IEEE 754 표준 동등성을 사용해서 일부 모서리 경우에 전체 순서 동등성과 모순되기 때문에 FloatArrayDoubleArraycontains(), indexOf(), lastIndexOf() 확장 함수를 deprecated 처리했어요. 자세한 내용은 이 이슈를 참고해 주세요.

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

빈 컬렉션에서 null을 반환한다는 동작을 더 제대로 반영하는 minOrNull()maxOrNull()을 위해 min()max() 컬렉션 함수를 deprecated 처리했어요. 자세한 내용은 이 이슈를 참고해 주세요.

deprecated 실험적 코루틴 제외

kotlin.coroutines.experimental API는 1.3.0에서 kotlin.coroutines를 위해 deprecated 되었어요. 1.4.0에서는 kotlin.coroutines.experimental을 표준 라이브러리에서 제거해서 deprecation 주기를 완료해요. 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로 마이그레이션할 때 일부 코드를 다시 작성해야 해요. 이를 돕기 위해 Kotlin Serialization Guide( kotlinx.serialization에 대한 전체 문서 세트)도 제공해요. 가장 중요한 기능을 사용하는 과정을 안내하고, 직면할 수 있는 문제를 해결하는 데 도움을 줄 거예요.

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

스크립팅과 REPL

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

  • 새 의존성 해석 API
  • 새 REPL API
  • 컴파일된 스크립트 캐시
  • 아티팩트 이름 변경

Kotlin 스크립팅에 더 익숙해지도록 예제가 있는 프로젝트를 준비했어요. 표준 스크립트(*.main.kts)의 예시와 Kotlin Scripting API 및 커스텀 스크립트 정의 사용 예시를 포함해요. 사용해 보고 이슈 트래커로 피드백을 공유해 주세요.

새 의존성 해석 API

1.4.0에서 외부 의존성(Maven 아티팩트 같은)을 해석하는 새 API와 그 구현을 도입했어요. 이 API는 새 아티팩트 kotlin-scripting-dependencieskotlin-scripting-dependencies-maven에 게시돼요. kotlin-script-util 라이브러리의 이전 의존성 해석 기능은 이제 deprecated 되었어요.

새 REPL API

새 실험적 REPL API는 이제 Kotlin Scripting API의 일부예요. 게시된 아티팩트에 여러 구현도 있고, 일부는 코드 완료 같은 고급 기능을 가져요. 우리는 Kotlin Jupyter 커널에서 이 API를 사용하며, 이제 자신만의 커스텀 셸과 REPL에서 시도해 볼 수 있어요.

컴파일된 스크립트 캐시

Kotlin Scripting API는 이제 컴파일된 스크립트 캐시를 구현할 수 있는 기능을 제공해서, 변경되지 않은 스크립트의 후속 실행을 크게 가속화해요. 기본 고급 스크립트 구현인 kotlin-main-kts에는 이미 자체 캐시가 있어요.

아티팩트 이름 변경

아티팩트 이름에 대한 혼동을 피하기 위해 kotlin-scripting-jsr223-embeddablekotlin-scripting-jvm-host-embeddable을 각각 kotlin-scripting-jsr223kotlin-scripting-jvm-host로 이름을 바꿨어요. 이 아티팩트들은 번들된 타사 라이브러리를 shading해서 사용 충돌을 피하는 kotlin-compiler-embeddable 아티팩트에 의존해요. 이 이름 변경으로 스크립팅 아티팩트의 기본을 (일반적으로 더 안전한) kotlin-compiler-embeddable 사용으로 만들고 있어요. 어떤 이유로든 shaded되지 않은 kotlin-compiler에 의존하는 아티팩트가 필요하다면 -unshaded 접미사가 있는 아티팩트 버전(예: kotlin-scripting-jsr223-unshaded)을 사용해 주세요. 이 이름 변경은 직접 사용하도록 되어 있는 스크립팅 아티팩트에만 영향을 주고, 다른 아티팩트의 이름은 변경되지 않는다는 점을 참고해 주세요.

Kotlin 1.4.0으로 마이그레이션

Kotlin 플러그인의 마이그레이션 도구는 이전 버전의 Kotlin에서 1.4.0으로 프로젝트를 마이그레이션하는 데 도움을 줘요.

Kotlin 버전을 1.4.0으로 변경하고 Gradle 또는 Maven 프로젝트를 다시 import하기만 하면 돼요. IDE가 마이그레이션에 대해 물어볼 거예요.

동의하면 코드를 검사하고 1.4.0에서 동작하지 않거나 권장되지 않는 모든 것에 대한 수정을 제안하는 마이그레이션 코드 검사를 실행할 거예요.

[IMAGE: Run migration]

코드 검사는 심각도 수준이 다르므로, 어떤 제안을 받아들이고 어떤 것을 무시할지 결정하는 데 도움이 돼요.

[IMAGE: Migration inspections]

Kotlin 1.4.0은 기능 릴리스이므로 언어에 비호환적인 변경을 가져올 수 있어요. 그러한 변경의 자세한 목록은 Kotlin 1.4 호환성 가이드에서 찾을 수 있어요.

더 알아보기