Kotlin 1.9 변경점

until 연산자에 대한 특수 구문 도입

1.8 이하에서는 아래와 같이 until연산자를 사용하여, 0부터 9까지의 범위를 표현할 수 있습니다.

    for (i in 0 until 10) { ... }

1.9 부터는 ..< 연산자로 동일한 기능을 수행할 수 있습니다.

    for (i in 0 ..< 10) { ... }

Jetbrains 설명에 따르면, Kotlin의 범위 구문이 구체적이지 않다는 의견을 받아왔고, 이를 해결하기 위해 ..< 연산자를 도입하였다고 합니다. UX연구 결과에 따르면, ..<until에 비해 20~30% 정도 개발자의 실수를 줄이는 효과가 있다고 합니다.

data object 도입

class에 대하여 toString(). equals(), hashCode()를 자동으로 처리하여 주는 data class와 달리, object는 이러한 기능을 제공하지 않습니다. 하지만, 1.9 부터는 data object를 도입하여 data class와 처럼 3가지의 함수를 자동 처리하여 줍니다.

    sealed interface ReadResult
    data class Number(val number: Int) : ReadResult
    data class Text(val text: String) : ReadResult
    data object EndOfFile : ReadResult

    fun main() {
        println(Number(7)) // Number(number=7)
        println(EndOfFile) // EndOfFile
    }

Enum.values()를 대체하기 위해 Enum.entries 도입

  • 기존 Enum.values()의 문제점
    • values()를 사용하면 모든 enum 항목을 Array로 만들어 반환합니다.
    • 이때 항상 새로운 Array의 instance를 할당하기 때문에 메모리 성능 문제가 일으킵니다.
    • Array를 반환하기 때문에, 임의적으로 외부에서 Array의 값을 변경할 수 있는 문제도 있습니다.
    • 참고
      • (https://mail.openjdk.org/pipermail/compiler-dev/2018-July/012242.html)
      • (https://dzone.com/articles/memory-hogging-enumvalues-method)

위의 문제점을 개선한 entries가 도입되었습니다.

  • Array가 아닌 List<E>를 상속한 타입인 EnumEntries를 반환합니다.
  • 함수가 아닌 속성으로, 미리 할당되어 있기 때문에 메모리 문제가 개선됩니다.
    enum class Color(val colorName: String, val rgb: String) {
        RED("Red", "#FF0000"),
        ORANGE("Orange", "#FF7F00"),
        YELLOW("Yellow", "#FFFF00")
    }

    fun findByRgb(rgb: String): Color? = Color.entries.find { it.rgb == rgb }

value class에서 보조 생성자 선언 가능

1.8 까지는 value class에서 보조 생성자를 선언할 수 없었으나, 1.9 부터는 가능해졌습니다.

    @JvmInline
    value class Person(private val fullName: String) {
        // 1.4.30 부터 도입
        init {
            check(fullName.isNotBlank()) {
                "Full name shouldn't be empty"
            }
        }

        // 1.9 부터 도입
        constructor(name: String, lastName: String) : this("$name $lastName") {
            check(lastName.isNotBlank()) {
                "Last name shouldn't be empty"
            }
        }
    }

    fun main() {
        val name1 = Person("Kotlin", "Mascot")
        val name2 = Person("Kodee")

        name1.greet() // Hello, Kotlin Mascot
        println(name2.length) // 5
    }

버전 2 미리보기

버전이 1.9 이후 바로 2.0으로 넘어갑니다.

K2 컴파일러 안정화

2022년 6월에 1.7 버전에서 알파 버전으로 공개되었던 새로운 Kotlin 컴파일러(K2)가 2.0 버전에서 안정화됩니다.

1.5 KLOC/s : 1초당 1,500줄의 코드를 처리함을 의미

특징

  • 약 2배 이상의 빠른 컴파일 속도
  • 신 기능
    • Static extensions
    • Colletion literals
    • Name-based destructuring
    • Context receivers
    • Explicit fields

새로운 기능은 아직 정확히 어떠한 버전에서 도입될지 정해지지 않은 상태입니다.

Static extensions

(https://github.com/Kotlin/KEEP/blob/statics/proposals/statics.md#kotlin-statics-and-static-extensions)

static 키워드가 도입됩니다.

static members/extensions/objects 개념이 새롭게 등장하게 됩니다.

static interface, overrides 등장

    static interface Parseable<T> {
        fun parse(s: String): T // static interface with `parse` function
    }

    class Color(val rgb: Int) : Parseable<Color> {
        static {
            override fun parse(s: String): Color { /* impl */ }
        }
    }

    fun main() {
        val parser: Parseable<Color> = Color.static
        val color = parser.parse("red")
        println(color is Parseable<Color>) // false
    }

static objects

    static object Namespace {
        val property = 42 // static property
        fun doSomething() { // static function
            property // OK: Can refer to static property in the same scope
            this // ERROR: Static objects have no instance
        }
    }

    fun main() {
        Namespace.doSomething() // OK
        val x = Namespace // ERROR: Cannot reference static object as value
        val y: Namespace? = null // ERROR: Cannot reference static object as type
    }

static extensions

예를 들어, CsvFile 이라는 클래스에 대하여 open(fileName: String) 정적 확장 함수를 만들고자 한다면, CsvFile클래스에 Companion object가 선언된 경우에만 fun CsvFile.Companion.open(fileName: String) 으로 확장 함수를 선언할 수 있습니다.

2.X 버전 부터는 static도 확장이 가능해져, fun CsvFile.static.open(fileName: String)로 Companion object가 없더라도 확장 함수 선언이 가능해집니다.

최종 사용 구문은 section vs modifier 두 가지 중에서 현재 논의 중입니다.

각각 장단점을 가지고 있는데, 현재 Section이 더 많은 찬성을 받고 있습니다.

  • Section
    • 장점 : Companion object에서 마이그레이션 하기에 용이합니다.
    • 단점 : 다른 언어들에서 주로 사용되는 구문과 달라 Kotlin 첫 사용자에게 학습장벽이 될 수 있습니다.
  • Modifier
    • 장점 : 다른 언어들에서 주로 사용되는 구문과 유사하여 Kotlin 첫 사용자에게 학습장벽이 낮습니다.
    • 단점 : 모든 코드 줄을 바꿔야 하기 때문에 Companion object에서 마이그레이션 하기에 어려움이 있습니다.
    class C {
        // Static section syntax
        static {
            val property = println("initialized")
        }

        // Static modifier syntax
        static val property = println("initialized")
    }

Collection literals

[1, 2, 3]과 같은 방식으로 Collection을 선언할 수 있게 됩니다. 다른 주요 언어 에서는 이미 가능한 방식이죠. 찾아보니 5년 전에 제안된 개선안 인데 좀 늦게 적용되는 경향이 있습니다.

    val arr: Array<String> = ["a", "b", "c"]
    val set: Set<Boolean> = [true, false, true] 
    val list: List<Int> = [1, 2, 3]
    val map: Map<String, Int> = ["one": 1, "two": 2, "three": 3]

Name-based destructuring

아래의 경우 처럼, firstName과 lastName의 배치 순서가 바뀌어도 현재는 그대로 컴파일 되는데 이는 로직에 문제를 일으키게 됩니다.

이 문제를 해결한다고 하는데, 아직 추가 내용은 없습니다.

    data class Person(
        val firstName: String,
        val lastName: String
    )

    fun main() {
        val person = Person("Kotlin", "Mascot")

        val (firstName, lastName) = person
        val (lastName, firstName) = person
    }

Context receivers

Context Receivers라는 기능이 추가됩니다.

특정 context을 통해서만 사용 가능한 함수나 속성을 선언할 수 있게 해줍니다. 이를 통해 코드의 모듈성과 구조화가 향상시킬 수 있을 것 같습니다.

사용 방법

context(...)를 붙여주면 됩니다.

Context receivers를 사용할 때의 차이점을 같은 동작을 하는 코드를 통해 비교하면 아래와 같습니다.

    fun doSomething(scope: CoroutineScope) {
        scope.launch {
            // CoroutineScope 내에서 작동하는 코드
        }
    }

    val scope = CoroutineScope(Dispatchers.Default)
    doSomething(scope)
    context(CoroutineScope)
    fun doSomething() {
        launch {
            // CoroutineScope 내에서 작동하는 코드
        }
    }

    val scope = CoroutineScope(Dispatchers.Default)
    scope.doSomething()

특징

  • 코드 간결성: context를 사용하면, context를 명시적으로 전달할 필요가 없어 코드가 더 간결해집니다.
  • 다중 context 지원: 하나의 함수가 여러 context에서 동작할 수 있으므로, 더 다양한 사용 사례를 지원할 수 있습니다.

Explicit fields

    private val _applicationState = MutableStateFlow(State())
    val applicationState: StateFlow<State>
        get() = _applicationState

현재 변수를 클래스 내부에서 사용하기 위해서 private으로 선언한 후, 외부에서 읽기 전용으로 쓰도록 하기 위해 추가로 public 변수를 선언하는 것이 일반적입니다.

어쩔 수 없는 선언 방식이지만 코드 길이가 2배가 되는 문제가 있습니다.

차후에는 아래와 같이 간편하게 사용할 수 있게 됩니다. 코드가 간결해지고, 가독성이 향상될 것으로 기대됩니다.

    val applicationState: StateFlow<State>
        field = MutableStateFlow(State())