Kotlin 1.9.0 Release, 제대로 살펴보기

2023. 8. 28. 23:59Spring/Kotlin

본 포스팅은 개발 시 직접적으로 사용하게 될 Kotlin 1.9.0의 업데이트 사항들을 살펴보는 것을 목표로 합니다.

 
 
 
 

Kotlin 1.9.0

지난 2023년 8월 23일, Kotlin 1.9.0 가 릴리즈 되었습니다.
기존의 기능들이 Stable 지원되고, 성능 개선이나 사용성 향상이 되는 등의 다양한 업데이트가 진행되었습니다.
 
참고로, 1.7부터 Alpha로 지원되던 K2 Compiler가 1.9부터 Beta로 지원됩니다.
따라서 Kotlin 2 릴리즈 전 설정 후 사용해보실 수 있습니다.

본 포스팅에서는 Web(Wasm), Multiplatform Support 등은 제외하고,
Kotlin의 Language / Library 측면의 내용만을 정리했습니다.

 
 

🔍  Highlights

본 포스팅에서 다룰 내용이자 목차는 아래와 같습니다.

📌 Enum.entries
📌 Data object
📌 Inline value classes
📌 ..< operator
📌 Stable time API
📌 @Volatile
📌  Regex named capture group
📌  createParentDirectories()
 
 
본 포스팅에서 다루지는 않지만, 참고할 만한 업데이트 사항입니다.
 
✔️ New Kotlin K2 compiler updates
✔️ Preview of the Gradle configuration cache in Kotlin Multiplatform
✔️ Changes to Android target support in Kotlin Multiplatform
✔️ Preview of custom memory allocator in Kotlin/Native
✔️ Library linkage in Kotlin/Native
✔️ Size-related optimizations in Kotlin/Wasm
 
 
 


 
 

Enum.entries

🔗 Kotlin - Enum.entries

 
 
entries는 Enum의 values() 함수를 성능을 고려한 형태입니다.

Kotlin Github 를 확인해보면 Enum.values() 가 아닌 Enum.entries 를 사용해야 할 내용에 대해 확인할 수 있습니다.

 
Enum의 entries 속성은 이미 1.8.20 버전에서 Experimental feature로 소개되었습니다.
1.9.0에서 부터는 Stable 되어 제공됩니다.
 

values()는 계속해서 지원될 예정이지만, entries 사용을 추천합니다. 
IntelliJ는 아래와 같은 경고 메시지를 띄웁니다. 
👉🏻 'Enum.values()' is recommended to be replaced by 'Enum.entries' since 1.9

 
 
 

✔️ values()

Openjdk bugs 과 Openjdk mail 에서 확인할 수 있듯이,
values() 함수는 "design bug in Java" 라고 언급되는 JDK Issue 중 하나입니다.
 

This is essentially an API design bug; because values() returns an array, and arrays are mutable, it must copy the array every time. Otherwise some miscreant could change the contents of this array, and other consumers of `values()` would see wrong data.

 
요약하자면, 다음과 같습니다.
values()는 모든 호출 마다 Mutable Array 복사본을 생성해서 반환하기 때문에, API 본질적 설계 버그입니다.
이는 values()이 반환한 Array 값을 악의적인 의도로 변경하거나 배열을 조작하려는 개발자의 실수로 이어질 수 있습니다.
 
설계 버그인 이유는 Enum의 정의 값들을 반환하는 배열이 Mutable할 이유가 전혀 없죠.
새로운 배열을 계속 생성하니, 불필요하게 메모리를 차지 하기도 합니다.
 
 
 

✔️ values() → entries

1.9부터는 바로 아래와 같이 사용할 수 있습니다.
 

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 }

 

만약, 1.9 버전 미만이라면, 아래와 같이 gradle을 설정한 후, @OptIn Annotation을 붙여서 사용할 수 있습니다.

 
 
Step 1 / 1.9 버전 사용 gradle 설정
 

// kotlin
tasks
    .withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>()
    .configureEach {
        compilerOptions
            .languageVersion
            .set(
                org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9
            )
    }
    
// groovy
tasks
    .withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask.class)
    .configureEach {
        compilerOptions.languageVersion =
              org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9
}

 
 
Step 2 /  @OptIn Annotation(ExperimentalStdlibApi)
 

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

 
 
 
 

Data object

🔗 Kotlin - data object

 
Data object는 data class와 동일한 방식의 toString(), equals() 및 hashCode() 를 포함한 형태입니다.
Kotlin 1.8.20에 소개된 Data object 도 마찬가지로 Stable 하게 지원됩니다.
 

이 기능은 데이터 object 선언과 함께 data class을 선언합니다.
때문에, sealed classinterface hierarchy로 구성된 sealed 계층에서 특히 유용합니다. 
 

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
}

 
위 코드에서 EndOfFile을 일반 object 대신 data object로 선언하면
직접 재정의할 필요 없이 toString()을 포함됨을 의미합니다.
data object의 기능은 함께 제공되는 data class 정의와 동일한 형태로 /*대칭*/ 유지됩니다.
 

kotlin에서는 data object와 data class의 기능을 동일하게 가져간다는 말을 '대칭을 유지한다'라고 표현합니다.
원문: This maintains symmetry with the accompanying data class definitions.

 
 
 
 

Inline value classes

🔗 Kotlin - Inline value classes

 
Kotlin 1.9.0 부터, value class 내 secondary constructors의 body를 작성할 수 있습니다.
 

@JvmInline
value class Person(private val fullName: String) {
    init {
        check(fullName.isNotBlank()) {
            "Full name shouldn't be empty"
        }
    }
    
    // Until Kotlin 1.9.0:
    constructor(name: String, lastName: String) : this("$name $lastName")
    
    // Allowed by default since Kotlin 1.9.0:
    constructor(name: String, lastName: String) : this("$name $lastName") {
        check(lastName.isNotBlank()) {
            "Last name shouldn't be empty"
        }
    }
}

 
기존에는 public 기본 구성자에서만 body를 작성할 수 있었는데요.
이로 인해, 기본 값을 캡슐화하거나, 값에 제약을 건 inline class (value class)를 만드는 것이 불가능했습니다.
더 자세한 사항은 Kotlin: KEEP를 참고하세요.
 
 

Kotlin 1.4.30에서 init 블록에 대한 제한을 해제하여 이를 가능하게 했으며,
Kotlin 1.8.2 에서는 보조 생성자의 body를 구현하는 기능의 Preview를 소개했습니다.
 
그리고, 1.9.0 부터는 default를 위 기능을 사용할 수 있게 되었습니다.
실제, Kotlin 1.8.2 버전 이하에서 보조 생성자의 body를 생성하고자 하면 아래와 같은 컴파일 오류를 발생시킵니다.
The feature "value classes secondary constructor with body" is only available since language version 1.9

 
 
 
 
 

Open-ended Operator: ..<

🔗 Kotlin - ..< operator

: open-ended ranges
 
 

..< 연산자는 구간(open-ended ranges) 을 포함하는 미만 범위 연산자입니다. 

Kotlin 1.7.20에 도입되어 1.8.0에서 Stable이 되었습니다.

1.9.0부터는, 개방형 범위에서 작업하기 위한 표준 라이브러리 API도 Stable입니다.
 

쉽게 말해,  ..< 연산자는 less than ()을 표현하는 연산자 입니다.

주의할 점은, less than 은 equal 을 포함하지 않는다는 점입니다.


가령, 아래와 같이 infix 함수를 사용할 때, 범위에 대한 오류를 범하곤 합니다.

 

fun main() {
    for (number in 2 until 10) {
        if (number % 2 == 0) {
            print("$number ")
        }
    }
}

 
출력이 어떻게 될지 예상이 가시나요?
 

2 4 6 8

 

출력은 의 범위이기 때문에 10을 포함하지 않습니다. 

하지만, 영어권이 아닌 곳에서 until 이라는 단어는 포함 여부를 혼동시킬 수 있기 때문에,
아래와 같이 명확하게 표시할 수 있습니다.
 

fun main() {
    for (number in 2..<10) {
        if (number % 2 == 0) {
            print("$number ")    // 2 4 6 8
        }
    }
}

 
 
 
 
 

Stable Time API

🔗 Kotlin - ..< operator

 
기존 시간 API에는 measureTimeMillis와 measureNanoTime 함수가 존재합니다.
하지만, 이 두 함수는 사용하기에 직관적이지 않습니다.
 
둘 다 다른 단위로 시간을 측정하는 것은 맞지만,
measureTimeMillis는 실제 세계 시간을 나타내는 Wall Clock 을 사용하지만,
measureNanoTime은 컴퓨터가 직접 계산하는 Monotonic Clock 를 사용합니다.
 
이 문제를 해결하기 위해 1.9.0은 아래의 기능을 포함하는 새로운 시간 API를 제공합니다.
 
- 원하는 시간 단위로 Monotonic Time Source (Clock) 를 사용해서 코드 실행 시간 측정 가능
- 특정 시간의 표시
- 두 시 사이의 차이를 비교
- 특정 시간 이후 시간이 얼마나 흘렀는지 확인
- 현재 시간이 특정 시점을 지나갔는지 확인
 
하나씩 확인해보겠습니다.
 

✔️ Measure execution time 

: 코드 실행 시간 측정하기

 
코드 실행 시간 측정을 위해서 아래의 두 함수를 사용할 수 있습니다.
 
#1.
inline fun measureTime
: 코드 실행 시간 측정
 
#2. 
inline fun measureTimedValue
: 코드 실행 시간 측정 + 결과를 반환 시간 측정
 

기본적으로 두 기능 모두 Monotonic Time Source를 사용합니다.
만약, 실제 세계 시간을 사용하고 싶다면, System.nanoTime()를 사용할 수 있습니다.

Android 라면, SystemClock.enapsedRealTimeNanos()를 사용할 수 있습니다.

 

tailrec fun ack(m: Int, n: Int): Int = when {
    m == 0 -> n + 1
    n == 0 -> ack(m - 1, 1)
    else -> ack(m - 1, ack(m, n - 1))
}

fun computeAck(m: Int, n: Int) {
    var res = 0
    val t = measureTime {
        res = ack(m, n)
    }
    println("ack($m, $n) = ${res}")
    println("duration: ${t.inWholeNanoseconds / 1e6} ms")
}

fun main() {
    println("Hello from Kotlin!\n")
    computeAck(3, 10)
}

 
위 코드를 실행 시키면, 아래와 같은 결과를 확인할 수 있습니다.
 

Hello from Kotlin!

ack(3, 10) = 8189
duration: 77.754667 ms

 
 

✔️ Measure differences in time

 
특정 순간을 시간으로 표시하려면 TimeSource 인터페이스와 MarkNow() 함수를 사용하여 TimeMark를 생성합니다. 
 

동일한 TimeSource 내의 TimeMarks의 차이를 감산 연산자(-)를 사용해서 측정할 수 있습니다.

혹은, ② TimeMarks 간 비교를 할 수도 있습니다.
 

import kotlin.time.*

fun main() {
    val timeSource = TimeSource.Monotonic
    val mark1 = timeSource.markNow()
    Thread.sleep(500) // 0.5초 Sleep
    val mark2 = timeSource.markNow()

    repeat(4) { n ->
        val mark3 = timeSource.markNow()
        val elapsed1 = mark3 - mark1	// ①
        val elapsed2 = mark3 - mark2	// ①

        println("Measurement 1.${n + 1}: elapsed1=$elapsed1, elapsed2=$elapsed2, diff=${elapsed1 - elapsed2}")
    }
    
    println(mark2 > mark1) // ② true
}

 
 
 
 

✔️ Check if a time has passed

특정 기한이 지났거나 특정 시간에 도달했는지 확인하려면,

extension 함수인 hasPassNow() 혹은 hasNotPassNow() 를 사용할 수 있습니다.

 

import kotlin.time.*
import kotlin.time.Duration.Companion.seconds

fun main() {
    val timeSource = TimeSource.Monotonic
    val mark1 = timeSource.markNow()
    
    val fiveSeconds: Duration = 5.seconds
    val mark2 = mark1 + fiveSeconds

    println(mark2.hasPassedNow()) // false - 5초 안지남
    Thread.sleep(6000) // 6초 대기    
    println(mark2.hasPassedNow()) // true  - 5초 지남

}

 
 
 
 
 
 

@Volatile

Java에서와 동일하게, Kotlin에서 @Volatile 를 Stable 하게 지원합니다.

var 속성에 주석을 붙이면 '읽기'와 '쓰기'가 atomic하게 되고,
'쓰기'는 항상 다른 스레드도 확인할 수 있게끔 표시visible 됩니다.

writes are always made visible to other threads

 

1.8.20 에서 JVM과 Kotlin/Native 모두 사용 가능한 Kotlin.concurrent.Volatile Preview 로 도입했으며,

1.9.0 에서 kotlin.concurrent.VolatileStable로 지원합니다.

1.8.20 이전에는 JVM에서만 kotlin.jvm.Volatile 주석을 사용할 수 있었습니다.
다른 플랫폼에서 사용한 경우 무시되어 오류가 발생했습니다.

 

멀티 플랫폼 프로젝트에서 kotlin.jvm.Volatile 을 사용하고 있었다면,

kotlin.concurrent.Volatile 로 마이그레이션하는 것을 추천합니다.

 
 
 

 

Regex named capture group

1.9.0 이전에도 정규 표현식을 사용할 때, 캡처 그룹 이름을 통해 문자열을 추출하기 위한 기능이 존재합니다.
하지만, 이를 지원하는 특정 함수가 없었는데요.

실제로, stackoverflow에서 이를 구현하는 코드들을 논의한 것을 확인할 수 있습니다.

 
1.9.0에는 정규 표현식 matching에 대한 그룹 이름을 기반으로 문자열을 추출할 수 있는 기능을 지원합니다.
즉, 그룹 이름을 가져오기 위해 따로 코드를 작성하지 않고,
Kotlin이 지원하는 형식 common function 을 사용해서 손쉽고 안정적으로 추출할 수 있다는 의미입니다.
 

가령, 아래와 같은 그룹 city, state, areaCode 를 추출하는 정규식을 확인해보겠습니다.

 

fun main() {
    val regex = """\b(?<city>[A-Za-z\s]+),\s(?<state>[A-Z]{2}):\s(?<areaCode>[0-9]{3})\b""".toRegex()
    val input = "Coordinates: Austin, TX: 123"

    val match = regex.find(input)!!
    println(match.groups["city"]?.value)
    // Austin
    println(match.groups["state"]?.value)
    // TX
    println(match.groups["areaCode"]?.value)
    // 123
}

 
각 이름을 통해 문자열을 추출한 결과를 확인할 수 있습니다.
 
 
 
 
 
 

createParentDirectories()

 
상위 디렉터리를 생성하기 위한 새 경로 Utility가 추가되었습니다.
가령, 새 파일을 만드는 데 부모 디렉터리가 없다면, 필요한 모든 부모 디렉터리를 생성합니다. 
 
실제로 부모 디렉터리 생성을 위해 파일 경로를 제공하면, 먼저 부모 디렉터리가 존재하는지 확인합니다.

만약 존재하지 않으면 부모 디렉터리가 생성됩니다.

createParentDirectorys()는 파일을 복사할 때 특히 유용한데,

가령, 아래와 같이 copyToRecursive() 함수와 함께 사용할 수 있습니다.

 

sourcePath.copyToRecursively(
   destinationPath.createParentDirectories(),
   followLinks = false
)

 
 
 
 
지금까지 다룬 코드 상의 업데이트가 아니더라도, 다양한 기능들이 업데이트 되었습니다. 
다른 업데이트 사항들을 확인하면, 해당 언어에 대한 이해에 큰 도움이 되기 때문에 한 번쯤 읽어보시는 것을 추천드립니다.
 
 
 
 

| References |

What's new in 1.9.0
What's new in 1.8.20
Kotlinlang Docs
Kotlinlang API :latest-jvm-stdlib