Kotlin 2.2.20 코드 개선사항

박채빈·2025년 9월 19일
1

KotlinStudy

목록 보기
8/8
post-thumbnail

Kotlin은 지속적으로 발전하고 있으며, 매 업데이트마다 일상적인 안드로이드 개발을 더욱 원활하게 만드는 소소한 품질 개선사항들을 제공합니다. 최신 안정 버전인 Kotlin 2.2.20은 화려한 새 기능들로 가득하지는 않지만, 많은 보일러플레이트 코드를 제거하는 미묘하면서도 중요한 변화들을 포함하고 있습니다.

이 글에서는 Kotlin 2.2.20에서 안드로이드 개발자들에게 관련된 주요 업데이트들을 종합적으로 살펴보겠습니다.

핵심 언어 개선사항

1. suspend 함수의 스마트한 오버로드 해결

suspend 함수와 일반 함수의 오버로드를 모두 가지고 있다면, 일반 람다를 전달할 때 컴파일러가 모호성 오류를 표시하는 것이 얼마나 혼란스러운지 알 것입니다.

기존 문제점

fun log(block: () -> String) { 
    println("non-suspend: ${block()}") 
}

fun log(block: suspend () -> String) { 
    println("suspend: called") 
}

fun test() {
    log { "Hello" }  
    // ❌ Error: ambiguous call between overloads
}

기존에는 이 문제를 해결하기 위해 보기 싫은 타입 캐스팅을 사용해야 했습니다.

log({ "Hello" } as () -> String)

개선된 해결책

새로운 버전의 Kotlin에서는 이 문제가 명확하게 해결됩니다.

fun test() {
    log { "Hello" }           // ✅ non-suspend 버전
    log(suspend { "Hi" })     // ✅ suspend 버전
}

실무 적용 예시

// API 클라이언트에서 자주 사용되는 패턴
fun fetchUserData(onResult: (User) -> Unit) { /* 동기 처리 */ }
fun fetchUserData(onResult: suspend (User) -> Unit) { /* 비동기 처리 */ }

// 이제 명확하게 구분 가능
fetchUserData { user -> updateUI(user) }                    // 동기 콜백
fetchUserData(suspend { user -> 
    delay(100)
    updateUI(user) 
}) // 비동기 콜백

2. 표현식 본문 함수에서 return 사용

표현식 본문(fun foo() = ...)은 짧은 함수에 유용하지만, 기존에는 return을 사용하려고 하면 작동하지 않았습니다.

기존 제한사항

// ❌ 허용되지 않음
fun getUserName(user: User?): String = 
    if (user == null) return "Guest" else user.name

블록 본문으로 전환해야 했습니다:

fun getUserName(user: User?): String {
    return if (user == null) "Guest" else user.name
}

개선된 문법

이제 타입이 명시적으로 지정된 경우 표현식 본문 내에서 return을 사용할 수 있습니다.

fun getUserName(user: User?): String = user?.name ?: return "Guest"

Kotlin은 이제 타입을 명시적으로 지정할 때만 return 문을 광범위하게 허용합니다. 따라서 다음과 같은 코드도 가능합니다:

fun getDisplayNameOrDefault(userId: String?): String =
    getDisplayName(userId ?: return "default")

👉 간결하고, 깔끔하며, 유연합니다.

3. when 표현식의 스마트한 완전성 검사

열거형과 sealed 클래스는 종종 when 표현식으로 감싸집니다. 이전 버전의 Kotlin은 다소 엄격했습니다.

기존 제한사항

enum class Role { ADMIN, MEMBER, GUEST }

fun accessLevel(role: Role): Int {
    if (role == Role.ADMIN) return 100
    
    return when (role) {
        Role.MEMBER -> 10
        Role.GUEST -> 1
        else -> 0   // ❌ 컴파일러가 이것을 강제로 추가하게 함
    }
}

ADMIN이 이미 처리되었음에도 불구하고, 컴파일러는 else를 요구했습니다.

개선된 완전성 검사

이제 Kotlin 2.2.20은 처리된 케이스를 인식하고 불필요한 분기를 건너뛸 수 있게 해줍니다.

fun accessLevel(role: Role): Int {
    if (role == Role.ADMIN) return 100
    
    return when (role) {
        Role.MEMBER -> 10
        Role.GUEST -> 1
    } // ✅ else가 필요없음
}

실무 적용 예시

sealed class ViewState {
    object Loading : ViewState()
    data class Success(val data: String) : ViewState()
    data class Error(val message: String) : ViewState()
}

fun handleState(state: ViewState) {
    if (state is ViewState.Loading) {
        showProgressBar()
        return
    }
    
    when (state) {
        is ViewState.Success -> showData(state.data)
        is ViewState.Error -> showError(state.message)
        // Loading은 이미 처리되었으므로 else 불필요
    }
}

👉 노이즈가 줄어들고, 명확성이 향상됩니다.

4. catch에서 reified 예외 처리

제네릭을 사용한 예외 포착은 기존에 보일러플레이트 is 검사가 필요했습니다.

기존 방식

inline fun <reified E : Throwable> runCatchingOld(block: () -> Unit) {
    try {
        block()
    } catch (e: Throwable) {
        if (e is E) {
            println("Caught: ${e::class.simpleName}")
        }
    }
}

개선된 방식

inline fun <reified E: Throwable> runCatching(block: () -> Unit) {
    try {
        block()
    } catch (e: E) {   // ✅ 이제 작동함
        println("Caught: ${e::class.simpleName}")
    }
}

fun main() {
    runCatching<IllegalArgumentException> {
        throw IllegalArgumentException("Bad arg")
    }
}

실무 적용 예시

inline fun <reified T: Exception> safeApiCall(
    crossinline call: suspend () -> ApiResponse
): ApiResult {
    return try {
        val response = call()
        ApiResult.Success(response)
    } catch (e: T) {
        ApiResult.Error(e.message ?: "Unknown error")
    }
}

// 사용 예시
val result = safeApiCall<NetworkException> {
    userRepository.fetchUserProfile()
}

👉 더 깔끔하고, 안전하며, 보일러플레이트가 적습니다.

Kotlin 계약(Contracts) 고급 개선사항

1. 제네릭 타입 어설션 지원

제네릭 타입에 대한 타입 어설션을 수행하는 계약 작성이 가능해졌습니다.

@OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class)
inline fun <reified T> Result<*>.requireSuccess(): T {
    contract {
        returns() implies (this@requireSuccess is Result.Success<T>)
    }
    require(this is Result.Success<T>)
    return this.data
}

2. returnsNotNull() 함수

특정 조건이 충족될 때 함수가 non-null 값을 반환함을 보장하는 새로운 계약 함수입니다.

@OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class)
fun decode(input: String?): String? {
    contract {
        returnsNotNull() implies (input != null)
    }
    return input?.let { Base64.decode(it) }
}

fun usage(data: String?) {
    val result = decode(data)
    if (data != null) {
        // result는 자동으로 non-null로 스마트 캐스트됨
        println(result.length)
    }
}

3. holdsIn 키워드

람다 내에서 특정 불린 조건이 참이라고 가정할 수 있게 해주는 새로운 키워드입니다.

@OptIn(ExperimentalContracts::class, ExperimentalExtendedContracts::class)
inline fun User?.whenValid(block: (User) -> Unit) {
    contract {
        holdsIn(block, this@whenValid != null)
    }
    if (this != null) {
        block(this) // 블록 내에서 this는 non-null로 스마트 캐스트
    }
}

Kotlin Multiplatform 개선사항

Swift Export 기본 제공

Swift export가 기본적으로 제공되어 iOS 개발자와의 협업이 더욱 용이해졌습니다.

주요 특징:

  • 모듈별 지원: 각 Kotlin 모듈이 별도의 Swift 모듈로 내보내짐
  • 패키지 보존: Kotlin 패키지가 Swift에서 명시적으로 보존됨
  • 개선된 null 처리: 기본 타입의 null 가능성이 직접 변환됨
  • 오버로드 지원: Kotlin의 오버로드된 함수를 Swift에서 모호성 없이 호출 가능

웹 타겟용 공유 소스셋

js와 wasmJs 타겟용 새로운 공유 소스셋이 추가되어 Compose Multiplatform 웹 애플리케이션 개발이 더욱 효율적이 되었습니다.

kotlin {
    js()
    wasmJs()
    // 기본 계층 구조 활성화로 webMain과 webTest 소스셋 사용 가능
    applyDefaultHierarchyTemplate()
}

안정적인 크로스 플랫폼 컴파일

이제 어떤 호스트에서든 Kotlin 라이브러리용 .klib 아티팩트를 생성할 수 있어 Apple 타겟이 더 이상 Mac 머신을 필수로 요구하지 않습니다.

성능 및 빌드 개선사항

1. Kotlin/JVM: invokedynamic을 이용한 when 표현식 최적화

when 표현식을 invokedynamic으로 컴파일하여 더 작은 바이트코드 생성이 가능해졌습니다.

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xuse-invokedynamic-for-when")
    }
}

2. Kotlin/Native 최적화

  • 스택 카나리 지원: 바이너리에서 스택 오버플로우 보호 기능 추가
  • 바이너리 크기 감소: 릴리즈 빌드에서 더 작은 바이너리 크기 제공
  • 개선된 디버거 객체 요약: LLDB와 GDB에서 더 명확한 객체 정보 표시
# gradle.properties에 추가
kotlin.native.binary.stackProtector=basic
kotlin.native.binary.smallBinary=true

3. Kotlin/JS & Kotlin/Wasm 개선

  • BigInt 지원: Long 값이 JavaScript BigInt로 컴파일되어 64비트 정수 처리 개선
  • 향상된 예외 처리: JavaScript 상호 운용에서 예외 정보가 더 상세하게 표시
  • 브라우저 디버깅: 추가 설정 없이 브라우저에서 바로 디버깅 가능

4. 빌드 시스템 개선

  • 스마트한 증분 컴파일: 인라인 람다 변경 시 모듈 간 올바른 재컴파일 트리거
  • Gradle 빌드 리포트: Kotlin/Native 작업용 성능 메트릭 추가
  • Maven Kotlin 데몬: kotlin-maven-plugin에서 기본적으로 사용

IDE 지원 및 개발 환경

플러그인 번들링

Kotlin 2.2.20을 지원하는 Kotlin 플러그인은 최신 버전의 IntelliJ IDEA와 Android Studio에 번들로 제공됩니다. 별도의 플러그인 업데이트 없이 바로 최신 기능을 사용할 수 있습니다.

업데이트 방법

// build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.android") version "2.2.20"
}
// build.gradle
plugins {
    id 'org.jetbrains.kotlin.android' version '2.2.20'
}

표준 라이브러리 개선사항

1. Atomic 타입용 업데이트 함수

일반적인 atomic 타입들을 위한 새로운 업데이트 함수들이 추가되었습니다.

@OptIn(ExperimentalAtomicApi::class)
import kotlin.concurrent.*

val counter = AtomicInt(0)

// 다양한 업데이트 패턴
counter.update { it * 2 }                    // 새 값으로 업데이트
val oldValue = counter.fetchAndUpdate { it + 1 }  // 이전 값 반환 후 업데이트  
val newValue = counter.updateAndFetch { it - 1 }  // 업데이트 후 새 값 반환

2. 배열 copyOf() 오버로드

배열을 더 크게 만들고 새 요소를 초기화 람다로 채울 수 있는 기능이 추가되었습니다.

@OptIn(ExperimentalStdlibApi::class)

val originalArray = arrayOf("a", "b", "c")
val expandedArray = originalArray.copyOf(5) { index -> "new$index" }
// 결과: ["a", "b", "c", "new3", "new4"]

// 제네릭 배열에서 nullable 결과 문제 해결
val genericArray: Array<String> = arrayOf("x", "y")
val expanded: Array<String> = genericArray.copyOf(4) { "default" }
// 기존에는 Array<String?>가 되었지만 이제 Array<String> 유지

3. Kotlin/JS 리플렉션 개선

KClass.isInterface 속성이 추가되어 클래스 참조가 Kotlin 인터페이스를 나타내는지 확인 가능합니다.

@OptIn(ExperimentalStdlibApi::class)
interface MyInterface
class MyClass

fun checkTypes() {
    println(MyInterface::class.isInterface) // true
    println(MyClass::class.isInterface)     // false
}

향후 언어 기능 미리보기

Kotlin 2.2.20에서는 Kotlin 2.3.0에 포함될 예정인 기능들을 미리 체험해볼 수 있습니다:

kotlin {
    compilerOptions {
        languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3)
    }
}

미리보기 기능 활성화를 통해 사용할 수 있는 기능들:

  • 표현식 본문에서 명시적 반환 타입과 함께 return 문 사용
  • when 표현식의 데이터 플로우 기반 완전성 검사
  • reified Throwable catch 지원
  • 향상된 Kotlin 계약

호환성 및 주요 변경사항

중요한 변경사항

  1. kapt 컴파일러 플러그인이 기본적으로 K2 컴파일러를 사용
  2. kapt.use.k2 속성이 deprecated됨
  3. Apple의 x86_64 타겟들(macosX64, iosX64 등)이 지원 계층 2로 강등

성능 고려사항

일부 기능들은 성능 비용을 수반할 수 있습니다:

  • 스택 카나리는 일부 경우 성능에 영향을 줄 수 있음
  • smallBinary 옵션은 런타임 성능에 영향을 줄 수 있지만 바이너리 크기와 빌드 시간을 개선

실무 적용 권장사항

  1. suspend 오버로드 활용: API 설계 시 동기/비동기 버전을 명확하게 구분하여 제공
  2. 표현식 본문: 짧은 함수에서 early return 패턴을 더 자주 활용
  3. 완전성 검사: sealed class와 enum에서 불필요한 else 제거로 코드 간소화
  4. reified 예외: 제네릭 예외 처리에서 타입 안전성 향상
  5. Multiplatform 프로젝트: Swift export를 활용한 iOS 네이티브 개발자와의 협업 강화
  6. 성능 최적화:
    • Kotlin/Native의 개선된 바이너리 크기 최적화 활용
    • invokedynamic when 표현식으로 바이트코드 크기 감소
    • 증분 컴파일 개선으로 개발 속도 향상
  7. 계약 활용: 복잡한 타입 관계에서 스마트 캐스트 개선을 위한 계약 사용

마무리

Kotlin 2.2.20은 혁신적인 새 기능보다는 개발자 경험의 지속적인 개선에 중점을 둔 릴리즈입니다. 특히 suspend 함수 오버로드 해결 개선은 코루틴을 많이 사용하는 안드로이드 개발자들에게 매우 유용한 변화입니다.

이러한 미묘한 개선사항들이 누적되어 더욱 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있게 도와줍니다. IDE 통합 개선, Multiplatform 지원 확대, 성능 최적화 등 전방위적인 개선이 이루어져 안드로이드 개발뿐만 아니라 전체 Kotlin 생태계의 발전을 가속화하고 있습니다.

향후 Kotlin 2.3.0에서는 더 많은 언어 기능 개선이 예정되어 있으니, 미리보기 기능을 통해 미리 경험해보는 것을 권장합니다.

참고 자료

profile
안드로이드 개발자

0개의 댓글