Kotlin Multiplatform — 하나의 코드베이스로 JVM, JS, Native를 타겟팅하는 방법
"안드로이드, iOS, 웹, 서버에서 같은 비즈니스 로직을 각각 다시 작성하고 있다면, 하나의 코드베이스로 모든 플랫폼을 지원할 수는 없을까요?"
Kotlin Multiplatform(KMP)은 하나의 코틀린 코드베이스로 JVM, JavaScript, iOS(Native), WebAssembly 등 여러 플랫폼을 타겟팅하는 기술입니다. "한 번 작성하면 어디서든 실행"(Write Once, Run Anywhere)이 아니라 "핵심 로직을 공유하고, 플랫폼별로 필요한 부분만 따로 구현"하는 접근입니다.
KMP의 핵심 아이디어
KMP는 코드를 세 영역으로 나눕니다.
┌─────────────────────────────────┐
│ commonMain │ ← 플랫폼 무관한 순수 코틀린
│ (비즈니스 로직, 데이터 모델) │
├─────────┬───────────┬───────────┤
│ jvmMain │ iosMain │ jsMain │ ← 플랫폼별 구현
│ (JVM) │ (iOS) │ (JS) │
└─────────┴───────────┴───────────┘
- commonMain: 모든 플랫폼에서 공유하는 코드 (순수 코틀린)
- 플랫폼 소스셋: 각 플랫폼에 특화된 구현 (파일 시스템, 네트워크, UI 등)
프로젝트 구조
기본적인 KMP 프로젝트 구조입니다.
my-kmp-project/
├── build.gradle.kts
├── shared/
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/kotlin/ ← 공유 코드
│ ├── commonTest/kotlin/ ← 공유 테스트
│ ├── jvmMain/kotlin/ ← JVM 전용
│ ├── iosMain/kotlin/ ← iOS 전용
│ ├── jsMain/kotlin/ ← JS 전용
│ └── ...
├── androidApp/ ← Android 앱
├── iosApp/ ← iOS 앱 (Xcode 프로젝트)
└── webApp/ ← 웹 앱
build.gradle.kts 설정
plugins {
kotlin("multiplatform") version "1.9.0"
}
kotlin {
// 타겟 플랫폼 선언
jvm()
iosArm64()
iosSimulatorArm64()
js(IR) {
browser()
nodejs()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("io.ktor:ktor-client-core:2.3.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.0")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.0")
}
}
val jsMain by getting {
dependencies {
implementation("io.ktor:ktor-client-js:2.3.0")
}
}
}
}
expect/actual — 플랫폼별 구현 연결
공통 코드에서 플랫폼별 구현이 필요한 부분을 expect로 선언하고, 각 플랫폼에서 actual로 구현합니다.
함수
// commonMain — 선언만
expect fun platformName(): String
// jvmMain
actual fun platformName(): String = "JVM ${System.getProperty("java.version")}"
// iosMain
actual fun platformName(): String = "iOS ${UIDevice.currentDevice.systemVersion}"
// jsMain
actual fun platformName(): String = "JavaScript"
클래스
// commonMain
expect class UUID {
fun toHexString(): String
}
// jvmMain
actual class UUID(private val uuid: java.util.UUID) {
actual fun toHexString(): String = uuid.toString()
companion object {
fun randomUUID() = UUID(java.util.UUID.randomUUID())
}
}
// iosMain
actual class UUID(private val uuid: platform.Foundation.NSUUID) {
actual fun toHexString(): String = uuid.UUIDString()
companion object {
fun randomUUID() = UUID(platform.Foundation.NSUUID())
}
}
typealias를 활용한 간단한 매핑
플랫폼 타입이 이미 같은 인터페이스를 제공하는 경우 typealias로 연결할 수 있습니다.
// commonMain
expect class AtomicInt(value: Int) {
fun get(): Int
fun set(value: Int)
fun incrementAndGet(): Int
}
// jvmMain — 기존 클래스를 그대로 사용
actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger
공유 가능한 코드 범위
KMP에서 무엇을 공유할 수 있는지 정리합니다.
공유하기 좋은 것
- 데이터 모델: data class, 직렬화/역직렬화
- 비즈니스 로직: 유효성 검증, 계산, 상태 관리
- 네트워크 클라이언트: Ktor를 사용한 API 호출
- 로컬 저장소: SQLDelight를 사용한 DB 접근
- 유틸리티: 날짜 처리, 문자열 변환 등
플랫폼별로 구현해야 하는 것
- UI: 각 플랫폼의 네이티브 UI 또는 Compose Multiplatform
- 플랫폼 API: 카메라, GPS, 푸시 알림 등
- 파일 시스템: 플랫폼별 경로와 접근 방식
- 스레딩: 플랫폼별 동시성 모델 (코루틴으로 추상화 가능)
실전 예제 — API 클라이언트 공유
// commonMain
class ApiClient(private val httpClient: HttpClient) {
suspend fun getUser(id: Long): UserDto {
return httpClient.get("https://api.example.com/users/$id").body()
}
suspend fun createUser(request: CreateUserRequest): UserDto {
return httpClient.post("https://api.example.com/users") {
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
}
@Serializable
data class UserDto(
val id: Long,
val name: String,
val email: String
)
@Serializable
data class CreateUserRequest(
val name: String,
val email: String
)
// jvmMain — OkHttp 엔진
fun createApiClient(): ApiClient {
val httpClient = HttpClient(OkHttp) {
install(ContentNegotiation) { json() }
}
return ApiClient(httpClient)
}
// iosMain — Darwin 엔진
fun createApiClient(): ApiClient {
val httpClient = HttpClient(Darwin) {
install(ContentNegotiation) { json() }
}
return ApiClient(httpClient)
}
비즈니스 로직과 데이터 모델은 commonMain에 한 번만 작성하고, HTTP 엔진만 플랫폼별로 교체합니다.
Compose Multiplatform
JetBrains가 Google의 Jetpack Compose를 기반으로 멀티플랫폼 UI 프레임워크를 만들었습니다.
// commonMain에서 UI 작성
@Composable
fun App() {
MaterialTheme {
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("클릭 횟수: $count", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("클릭")
}
}
}
}
이 코드가 Android, iOS, Desktop(JVM), Web에서 모두 동작합니다.
지원 플랫폼별 상태 (2025년 기준)
| 플랫폼 | 상태 | 비고 |
|---|---|---|
| Android | Stable | Jetpack Compose 그대로 |
| iOS | Beta | UIKit 위에 렌더링 |
| Desktop (JVM) | Stable | Swing/AWT 위에 렌더링 |
| Web (Wasm) | Alpha | WebAssembly 기반 |
KMP 도입 시 고려할 점
장점
- 비즈니스 로직을 한 번만 작성하고 테스트합니다
- 플랫폼 간 데이터 모델이 항상 동기화됩니다
- 코틀린 생태계(코루틴, 직렬화, Ktor 등)를 그대로 활용합니다
주의할 점
- iOS 팀이 코틀린에 익숙하지 않으면 협업 비용이 발생합니다
- Kotlin/Native의 메모리 모델과 스레딩이 JVM과 다릅니다
- 빌드 시간이 단일 플랫폼 프로젝트보다 길 수 있습니다
- 일부 라이브러리가 KMP를 지원하지 않을 수 있습니다
KMP 지원 주요 라이브러리
| 카테고리 | 라이브러리 |
|---|---|
| 네트워크 | Ktor |
| 직렬화 | kotlinx.serialization |
| 비동기 | kotlinx.coroutines |
| 데이터베이스 | SQLDelight |
| DI | Koin, Kodein |
| 날짜/시간 | kotlinx-datetime |
| 이미지 로딩 | Coil (Compose Multiplatform) |
정리
- KMP는 "모든 것을 공유"가 아니라 "공유할 수 있는 것을 공유"하는 실용적 접근입니다
- expect/actual로 공통 인터페이스와 플랫폼별 구현을 연결합니다
- commonMain에 비즈니스 로직, 데이터 모델, API 클라이언트를 작성하고, 플랫폼별 소스셋에서 UI와 네이티브 API를 구현합니다
- Compose Multiplatform으로 UI까지 공유할 수 있으며, Android와 Desktop은 Stable, iOS는 Beta 상태입니다
- Ktor, kotlinx.serialization, SQLDelight 등 KMP 지원 라이브러리 생태계가 점점 풍부해지고 있습니다
댓글 로딩 중...