Kotlin Multiplatform 실전 가이드
KMP 구조, Android/iOS/Web 코드 공유, Compose Multiplatform 실전 적용 사례.
한 줄 요약: Kotlin Multiplatform(KMP)은 비즈니스 로직을 한 번 작성하고 Android, iOS, 웹, 데스크톱에서 공유하는 실용적인 접근이다. UI는 각 플랫폼 네이티브를 그대로 쓰거나 Compose Multiplatform으로 통합할 수 있으며, 2024년 1.0 안정화로 프로덕션 도입이 현실적이다.
- Android 앱과 iOS 앱에서 중복 비즈니스 로직을 공유하고 싶은 팀
- React Native나 Flutter 대신 네이티브 성능을 유지하면서 코드 공유를 원하는 경우
- 기존 Android Kotlin 코드베이스를 iOS에 점진적으로 확장하려는 팀
- KMP와 Compose Multiplatform의 범위 차이를 파악하고 싶은 경우
※ Kotlin Multiplatform 1.0 안정 릴리스(2023년 11월) 및 Compose Multiplatform 1.6(2024년) 기준. 공식 문서: jetbrains.com/kotlin-multiplatform
KMP 구조 — 공유 레이어와 플랫폼 레이어
KMP의 핵심 아이디어는 공유할 수 있는 것만 공유하고, 플랫폼 특화 코드는 각 플랫폼에 남긴다는 원칙이다. React Native나 Flutter처럼 UI까지 완전 통합하는 방식과 달리, KMP는 비즈니스 로직 공유에 집중하고 UI는 각 플랫폼의 네이티브 방식을 유지할 수 있다.
KMP 모듈 구조:
commonMain— 모든 플랫폼에서 실행되는 순수 Kotlin 코드. 비즈니스 로직, 데이터 모델, API 클라이언트, Repository 패턴.androidMain— Android 전용 구현. Android SDK, Room, Jetpack 라이브러리.iosMain— iOS 전용 구현. Foundation, UIKit 관련 코드, Swift Interop.jsMain— 웹(Kotlin/JS) 전용 구현.desktopMain— JVM 기반 데스크톱(macOS, Windows, Linux).
expect/actual 메커니즘: 플랫폼마다 다른 구현이 필요한 경우 expect로 인터페이스를 선언하고, 각 플랫폼 소스셋에서 actual로 구현한다. 예: 플랫폼별 로컬 스토리지, 암호화, 날짜 포맷.
KMP 프로젝트 구조 및 expect/actual 예시// build.gradle.kts — KMP 멀티플랫폼 설정 kotlin { androidTarget() iosX64() iosArm64() iosSimulatorArm64() sourceSets { commonMain.dependencies { implementation("io.ktor:ktor-client-core:2.3.7") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") } androidMain.dependencies { implementation("io.ktor:ktor-client-okhttp:2.3.7") } iosMain.dependencies { implementation("io.ktor:ktor-client-darwin:2.3.7") } } } // commonMain/Platform.kt — expect 선언 expect fun getPlatformName(): String expect class PlatformLogger() { fun log(message: String) } // androidMain/Platform.android.kt — actual 구현 actual fun getPlatformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}" actual class PlatformLogger actual constructor() { actual fun log(message: String) = android.util.Log.d("KMP", message) } // iosMain/Platform.ios.kt — actual 구현 actual fun getPlatformName(): String = UIDevice.currentDevice.systemName() actual class PlatformLogger actual constructor() { actual fun log(message: String) = NSLog("%@", message) }
Shared Module 실전 예시 — Repository 패턴
실무에서 가장 많이 공유하는 코드는 네트워크 요청, 데이터 모델, 비즈니스 규칙이다. 아래는 Ktor + Kotlin Serialization을 사용한 공통 API 클라이언트와 Repository 구현이다.
commonMain — 공유 Repository 및 API 클라이언트// commonMain/data/UserRepository.kt import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* import kotlinx.serialization.Serializable @Serializable data class User( val id: Int, val name: String, val email: String ) class UserRepository(private val client: HttpClient) { suspend fun getUser(id: Int): Result<User> { return runCatching { client.get("https://api.example.com/users/$id").body<User>() } } suspend fun getUsers(): Result<List<User>> { return runCatching { client.get("https://api.example.com/users").body<List<User>>() } } } // commonMain/di/HttpClientFactory.kt import io.ktor.client.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* fun createHttpClient(): HttpClient = HttpClient { install(ContentNegotiation) { json() } }
위 코드는 commonMain에 작성되며 Android와 iOS 모두에서 동일하게 동작한다. Android에서는 OkHttp 엔진, iOS에서는 Darwin(NSURLSession) 엔진이 자동으로 선택된다. 비즈니스 로직은 완전히 공유되면서 플랫폼별 네트워크 구현은 KMP가 처리한다.
Compose Multiplatform — UI도 공유하려면
Compose Multiplatform(CMP)은 KMP 위에 UI 공유 레이어를 추가한다. Jetpack Compose 문법으로 작성한 UI를 Android, iOS, 웹(Wasm), 데스크톱에서 공유한다.
KMP vs Compose Multiplatform 차이:
| 항목 | KMP (비즈니스 로직만) | Compose Multiplatform (UI 포함) |
|---|---|---|
| 공유 범위 | 비즈니스 로직, 네트워킹, 데이터 | UI + 비즈니스 로직 전체 |
| iOS UI | SwiftUI / UIKit (네이티브) | Compose로 렌더링 (Skia 엔진) |
| 안정성 | Stable (1.0) | iOS Alpha → Beta 진행 중 |
| 플랫폼 접근성 | 각 플랫폼 접근성 그대로 | Skia 렌더링으로 접근성 제한 |
| 추천 케이스 | 플랫폼 룩앤필 유지 필요 | 내부 도구, B2B, 디자인 통일 우선 |
소비자 대면 앱이라면 각 플랫폼의 네이티브 룩앤필을 유지하는 것이 사용자 경험상 유리하다. 내부 관리 도구, 대시보드, B2B 앱에서는 Compose Multiplatform으로 UI까지 완전히 공유하는 것도 실용적이다.
iOS 통합 — Swift에서 KMP 모듈 사용하기
KMP의 iOS 통합은 Kotlin 코드를 Objective-C 프레임워크(XCFramework)로 빌드해 Swift에서 호출하는 방식이다. Kotlin coroutines는 Swift의 async/await로 변환되며, Kotlin의 sealed class는 Swift에서 enum-like 패턴으로 사용할 수 있다.
iOS — XCFramework 빌드 및 Swift에서 KMP 호출# XCFramework 빌드 (Gradle 태스크) ./gradlew assembleXCFramework # → build/XCFrameworks/release/shared.xcframework 생성 # Xcode에서 shared.xcframework를 Frameworks에 추가 후 // Swift에서 KMP 코드 호출 import shared // KMP 모듈 import class UserViewModel: ObservableObject { @Published var user: User? @Published var errorMessage: String? private let repository = UserRepository(client: createHttpClient()) func loadUser(id: Int) { // Kotlin suspend 함수 → Swift async/await 변환 Task { do { // KotlinInt로 변환 필요 let result = try await repository.getUser(id: Int32(id)) await MainActor.run { self.user = result } } catch { await MainActor.run { self.errorMessage = error.localizedDescription } } } } }
실무 적용 사례와 도입 판단 기준
실제 도입 사례:
- Netflix: 모바일 앱의 일부 비즈니스 로직 공유에 KMP 사용
- Touchlab: 자사 앱과 클라이언트 프로젝트에서 KMP 프로덕션 적용, SKIE 오픈소스 개발
- Philips: 의료기기 데이터 처리 로직 Android/iOS 공유
- Square: SDK 레이어에 KMP 도입
도입하기 좋은 상황:
- Android 팀이 Kotlin에 이미 능숙하고, iOS 네이티브도 유지하고 싶은 경우
- API 클라이언트, 캐싱, 비즈니스 규칙이 두 플랫폼에서 중복 구현된 경우
- 새 기능을 한 팀이 두 플랫폼에 동시 출시해야 하는 경우
주의가 필요한 상황:
- iOS 팀이 Swift 외 코드에 익숙하지 않아 KMP 디버깅이 어려운 경우
- 서드파티 라이브러리를 많이 쓰는데 KMP 버전이 없는 경우 (expect/actual로 래핑 필요)
- iOS 앱의 비중이 매우 낮아 공유 이득이 마이그레이션 비용보다 작은 경우