프로그래밍/Kotlin

[KotlinConf2024] Kotlin 2.0의 새로운 피쳐들

Lou Park 2024. 9. 18. 21:31

 

1. Explicit Backing Fields (명시적 백킹 필드)

명시적 백킹 필드가 어떤건지 이해하기 위해서 안드로이드 프로젝트에서 널리쓰이는 패턴의 코드를 살펴보자.

class MyViewModel {
    private val _title = MutableStateFlow<String>("Placeholder")
    val title: StateFlow<String> get() = _title
}

title이라는 상태는 ViewModel에서만 수정할 수 있도록 이렇게 캡슐화 할 수 있는데, Kotlin 2.0에서는 훨씬 더 간결한 형태로 이를 표현할 수 있다.

class MyViewModel {
    val title: StateFlow<String>
        field = MutableStateFlow<String>("Placeholder")
}

다가오는 업데이트에서는 Named explicit backing fields같은 것도 나올 수 있다는 점도 주목할만하다.

class MyViewModel {
    val title: StateFlow<String>
        field mutableTitle = MutableStateFlow<String>("Placeholder")
}

 

2. 연산자와 숫자 변환의 조합 (Combination of operators and Numeric Conversions)

K2 컴파일러는 FIR(Frontend Intermediate Representation)을 도입했는데, 이로서 더 빠른 컴파일과 정확한 오류 메세지를 제공하여 컴파일러와 IDE 성능을 향상시킬 수 있도록 했다. FIR로 인해 눈에 띄게 보이는 변화가 바로 연산자와 숫자 변환의 조합인데, 이 기능은 명시적 타입 변환을 생략하여 계산 절차를 간단하게 한다.

 

아래 코드는 Kotlin 1.X버전에서는 컴파일 오류가 나지만, Kotlin 2.X 버전에서는 정상적으로 작동 한다. Kotlin 1.X 버전이었다면 1대신 1L이라고 명시적으로 Long 타입이라고 선언했어야 할 것이다. 

fun foo(longs: MutableList<Long>) {
    longs[0] += 1
}

 

Kotlin 2.X 버전에서는 문법적 설탕을 제외하면 (desugar) 이렇게 되므로 문제가 없다.

longs.set(0, longs.get(0).plus(1))

 

3. Nullable 연산자 호출의 조합 (Combination of Nullable Operator Calls)

바로위에 설명했던 FIR로 인해 연산이 간편해지는 기능은 Nullable 연산자에서도 해당된다. 아래 두 구문 모두 Kotlin 2.X 버전에서는 문제없이 작동한다.

class Box(val longs: MutableList<Long>)

fun foo(box: Box?) {
    box?.longs[0] += 1 // Kotlin 1.x 버전에서 컴파일 오류
    box?.longs[0] += 1L // Kotlin 1.x 버전에서 컴파일 오류
}

 

4. 더 스마트해진 스마트 캐스팅 (More Smart Casts)

K2에서는 타입 추론이 더욱 좋아졌다.

class Phone {
    fun ring() {
        println("Ringing...")
    }
}

fun makeNoise(device: Any) {
    if (device is Phone) {
        device.ring()
    }
}

 

이 코드에서 device.ring() 부분은 Kotlin 1.X 버전에서 컴파일 오류를 뱉는다. 왜냐하면 지역 변수에는 데이터 흐름에 대한 정보가 포함되지 않기 때문에 스마트 캐스팅이 불가능하다. 따라서 device는 아직 Any일 뿐이기에 ring()을 호출 할 수 없다. 하지만 Kotlin 2.X 버전에서는 이것이 작동한다. (정말 기다리던 기능...)

 

5. || 연산자 뒤의 스마트 캐스트 (Smart Casts After || )

Kotlin 2.X에서는 아래 코드도 동작한다!

interface Action {
    fun execute()
}

interface None : Action
interface Send : Action
interface Receive : Action

fun demo(action: Any) {
    if (action is Send || action is Receive) {
        action.execute()
    }
}

 

6. 인라인 람다의 클로져 안에서의 스마트 캐스트 (Smart Casts inside the Closures of Inline Lambdas)

Kotlin 1.X 버전에서는 아래 코드 Int?를 Int로 스마트 캐스팅하는 것이 불가능하다.

maxIndex는 var(가변 변수)로 선언되어 있다. 가변 변수가 람다 함수 클로저에 의해 캡쳐 되면 스마트 캐스팅은 적용되지 않는다. 왜냐하면 함수가 이 범위를 벗어나서 다른 컨텍스트에서 여러번, 또는 병렬로 실행되어서 변할 수 있는 가능성이 충분하기 때문이다. 따라서 컴파일러는 이런 극단적 상황을 피하기 위해 스마트 캐스팅을 허용하지 않는다.

 

Kotlin 2.X 버전에서는 이 인라인 함수를 암시적으로 *callsInPlace 컨트랙트를 가지는 것으로 취급한다. callsInPlace 컨트랙트는 함수 인자로 전달된 람다가 소유 함수의 호출동안에만 호출된다는 약속이다. 따라서 위에서 컴파일러가 우려하던 범위를 벗어난 곳에서의 변수 변경은 일어나지 않으므로, 컴파일러는 이 람다를 일반적인 for 루프 처럼 다루어서 람다 안의 스마트 캐스팅이 가능하도록 하는 것이다. 

fun indexOfMax(numbers: IntArray): Int? {
    var maxIndex: Int? = null
    numbers.forEachIndexed { i, number ->
        // Kotlin 1.x 버전에서는 Int?에서 Int로 스마트 캐스팅은 불가능.
        // maxIndex가 클로저에 의해 캡쳐된 지역 변수이기 때문에
        if (maxIndex == null || numbers[maxIndex!!] <= number) {
            maxIndex = i
        }
    }
    return maxIndex
}

 

원문: https://medium.com/@omarsahl/kotlin-language-features-from-kotlinconf-2024-whats-new-and-what-s-next-a4668eae5e9d