Kotlin Multiplatform
Kotlin Multiplatform
코틀린 멀티플랫폼(Kotlin Multiplatform, KMP)은 JetBrains에서 개발한 프레임워크로, 여러 플랫폼 간에 코드를 공유하면서도 플랫폼별 네이티브 API에 대한 접근성을 유지할 수 있도록 설계되었습니다. 플랫폼 자체를 완전히 추상화하는 크로스플랫폼(cross-platform) 프레임워크와 달리, KMP는 공유 코드를 플랫폼 네이티브 바이너리로 직접 컴파일합니다. 안드로이드에서는 JVM 바이트코드를, iOS에서는 네이티브 바이너리를, 웹 타겟에서는 JavaScript를 각각 생성하는 방식입니다. 이러한 접근 방식 덕분에 네이티브 수준의 성능을 그대로 유지하면서도 핵심 비즈니스 로직을 플랫폼 간에 효율적으로 공유할 수 있습니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
expect/actual메커니즘을 활용한 플랫폼별 구현 방식을 설명할 수 있습니다.- KMP가 공유 코드를 플랫폼 네이티브 출력물로 컴파일하는 과정을 이해할 수 있습니다.
- 공유 코드와 플랫폼별 코드를 조직화하는 소스 셋(source set)의 역할을 파악할 수 있습니다.
- 성능과 네이티브 API 접근 측면에서 KMP와 다른 크로스플랫폼 대안을 비교할 수 있습니다.
- KMP 프로젝트에서 의존성 주입(dependency injection)과 아키텍처 패턴이 어떻게 적용되는지 설명할 수 있습니다.
소스 셋과 프로젝트 구조
KMP 프로젝트는 계층 구조를 이루는 소스 셋으로 코드를 구성합니다. commonMain 소스 셋에는 모든 플랫폼에서 공유되는 코드가 포함됩니다. androidMain, iosMain, jsMain과 같은 플랫폼별 소스 셋은 commonMain을 확장하며, 플랫폼 API에 의존하는 구현을 제공합니다. 즉, 공통 로직은 한 곳에서 관리하고, 플랫폼에 특화된 동작만 각 소스 셋에서 별도로 구현하는 구조입니다.
// commonMain/src/commonMain/kotlin/Platform.kt
expect fun platformName(): String
// androidMain/src/androidMain/kotlin/Platform.android.kt
actual fun platformName(): String = "Android ${Build.VERSION.SDK_INT}"
// iosMain/src/iosMain/kotlin/Platform.ios.kt
actual fun platformName(): String = UIDevice.currentDevice.systemName()
Gradle 플러그인은 빌드 시점에 소스 셋 계층 구조를 해석하고, 각 타겟을 그에 맞는 actual 선언과 함께 컴파일합니다. commonMain의 코드는 코틀린 표준 라이브러리와 멀티플랫폼 라이브러리에서 제공하는 API만 참조할 수 있습니다. 플랫폼에 특화된 API에 접근하려면 반드시 expect/actual 메커니즘을 사용하거나, 해당 플랫폼 소스 셋에 직접 코드를 작성해야 합니다. 이 제약 덕분에 공유 코드의 이식성(portability)이 컴파일 타임에 보장됩니다.
Expect와 Actual 메커니즘
expect/actual 패턴은 KMP에서 플랫폼 간 차이를 처리하는 핵심 메커니즘입니다. expect 선언은 공통 코드에서 함수, 클래스, 프로퍼티의 시그니처만 정의하고 구현은 포함하지 않습니다. 각 플랫폼 소스 셋에서 이에 대응하는 actual 선언을 통해 실제 구현을 제공하는 방식입니다. 이는 마치 인터페이스와 구현체의 관계와 유사하지만, 컴파일 타임에 플랫폼별로 완전히 해석되어 런타임 오버헤드가 전혀 발생하지 않는다는 점에서 차이가 있습니다.
// commonMain
expect class HttpClient() {
suspend fun get(url: String): String
}
// androidMain
actual class HttpClient actual constructor() {
actual suspend fun get(url: String): String {
return URL(url).readText()
}
}
// iosMain
actual class HttpClient actual constructor() {
actual suspend fun get(url: String): String {
return NSURLSession.shared.dataTaskForUrl(url)
}
}