Kotlin은 지속적으로 발전하고 있으며, 매 업데이트마다 일상적인 안드로이드 개발을 더욱 원활하게 만드는 소소한 품질 개선사항들을 제공합니다. 최신 안정 버전인 Kotlin 2.2.20은 화려한 새 기능들로 가득하지는 않지만, 많은 보일러플레이트 코드를 제거하는 미묘하면서도 중요한 변화들을 포함하고 있습니다.
이 글에서는 Kotlin 2.2.20에서 안드로이드 개발자들에게 관련된 주요 업데이트들을 종합적으로 살펴보겠습니다.
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)
}) // 비동기 콜백
표현식 본문(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")
👉 간결하고, 깔끔하며, 유연합니다.
열거형과 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 불필요
}
}
👉 노이즈가 줄어들고, 명확성이 향상됩니다.
제네릭을 사용한 예외 포착은 기존에 보일러플레이트 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()
}
👉 더 깔끔하고, 안전하며, 보일러플레이트가 적습니다.
제네릭 타입에 대한 타입 어설션을 수행하는 계약 작성이 가능해졌습니다.
@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
}
특정 조건이 충족될 때 함수가 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)
}
}
람다 내에서 특정 불린 조건이 참이라고 가정할 수 있게 해주는 새로운 키워드입니다.
@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로 스마트 캐스트
}
}
Swift export가 기본적으로 제공되어 iOS 개발자와의 협업이 더욱 용이해졌습니다.
주요 특징:
js와 wasmJs 타겟용 새로운 공유 소스셋이 추가되어 Compose Multiplatform 웹 애플리케이션 개발이 더욱 효율적이 되었습니다.
kotlin {
js()
wasmJs()
// 기본 계층 구조 활성화로 webMain과 webTest 소스셋 사용 가능
applyDefaultHierarchyTemplate()
}
이제 어떤 호스트에서든 Kotlin 라이브러리용 .klib
아티팩트를 생성할 수 있어 Apple 타겟이 더 이상 Mac 머신을 필수로 요구하지 않습니다.
when 표현식을 invokedynamic
으로 컴파일하여 더 작은 바이트코드 생성이 가능해졌습니다.
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xuse-invokedynamic-for-when")
}
}
# gradle.properties에 추가
kotlin.native.binary.stackProtector=basic
kotlin.native.binary.smallBinary=true
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'
}
일반적인 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 } // 업데이트 후 새 값 반환
배열을 더 크게 만들고 새 요소를 초기화 람다로 채울 수 있는 기능이 추가되었습니다.
@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> 유지
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)
}
}
미리보기 기능 활성화를 통해 사용할 수 있는 기능들:
kapt.use.k2
속성이 deprecated됨macosX64
, iosX64
등)이 지원 계층 2로 강등일부 기능들은 성능 비용을 수반할 수 있습니다:
smallBinary
옵션은 런타임 성능에 영향을 줄 수 있지만 바이너리 크기와 빌드 시간을 개선Kotlin 2.2.20은 혁신적인 새 기능보다는 개발자 경험의 지속적인 개선에 중점을 둔 릴리즈입니다. 특히 suspend 함수 오버로드 해결 개선은 코루틴을 많이 사용하는 안드로이드 개발자들에게 매우 유용한 변화입니다.
이러한 미묘한 개선사항들이 누적되어 더욱 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있게 도와줍니다. IDE 통합 개선, Multiplatform 지원 확대, 성능 최적화 등 전방위적인 개선이 이루어져 안드로이드 개발뿐만 아니라 전체 Kotlin 생태계의 발전을 가속화하고 있습니다.
향후 Kotlin 2.3.0에서는 더 많은 언어 기능 개선이 예정되어 있으니, 미리보기 기능을 통해 미리 경험해보는 것을 권장합니다.