Kotlin Multiplatform (KMP) Deep Dive: Cross-Platform 모바일 개발 전략
Kotlin Multiplatform (KMP)를 활용한 크로스플랫폼 모바일 앱 개발 전략을 심층 분석합니다. iOS, Android 앱에서 비즈니스 로직을 공유하고 네이티브 UI를 유지하는 KMP의 아키텍처, 장단점, 실전 팁을 코드 예제와 함께 소개합니다.
Kotlin Multiplatform (KMP) Deep Dive: Cross-Platform 모바일 개발 전략
모바일 앱 개발 시장은 iOS와 Android라는 두 개의 거대한 축을 중심으로 발전해왔습니다. 각 플랫폼의 고유한 언어와 프레임워크로 인해 개발 팀은 동일한 기능을 두 번 구현해야 하는 비효율성에 직면하곤 합니다. 이러한 문제를 해결하기 위해 등장한 다양한 크로스플랫폼 솔루션 중에서도, Kotlin Multiplatform (이하 KMP)는 비즈니스 로직 공유를 통해 네이티브 앱의 장점을 그대로 유지하면서 개발 효율성을 높이는 독특한 접근 방식을 제시합니다. 이 글에서는 KMP의 핵심 개념부터 아키텍처, 장단점, 그리고 실제 프로젝트에 적용할 수 있는 실전 팁과 코드 예제까지 심층적으로 다루어 보겠습니다.
KMP란 무엇인가? Cross-Platform 개발의 새로운 지평
Kotlin Multiplatform은 JetBrains에서 개발한 크로스플랫폼 기술로, 하나의 Kotlin 코드베이스로 iOS, Android, 웹, 데스크톱 등 다양한 플랫폼에서 동작하는 애플리케이션을 만들 수 있게 해줍니다. 특히 모바일 개발 영역에서는 iOS와 Android 앱 간에 비즈니스 로직, 데이터 모델, 네트워크 계층 등을 공유하는 데 특화되어 있습니다.
다른 크로스플랫폼 프레임워크인 React Native나 Flutter가 UI까지 포함한 전체 애플리케이션을 단일 코드베이스로 구현하는 것을 목표로 하는 반면, KMP는 UI는 각 플랫폼의 네이티브 기술(iOS는 SwiftUI/UIKit, Android는 Jetpack Compose/XML View)을 사용하고, 핵심 비즈니스 로직만 Kotlin으로 공유하는 전략을 취합니다. 이는 개발 팀이 각 플랫폼의 최신 UI 트렌드와 성능 최적화를 놓치지 않으면서, 중복 코드를 최소화하고 일관된 로직을 유지할 수 있게 합니다. 결과적으로 KMP는 "Write Once, Run Anywhere"보다는 "Write Once, Run Natively"에 더 가깝다고 할 수 있습니다.
KMP 아키텍처 이해: Shared, iOS, Android 모듈
KMP 프로젝트는 일반적으로 shared 모듈과 각 플랫폼별 모듈(androidApp, iosApp)로 구성됩니다. shared 모듈은 다시 commonMain, androidMain, iosMain과 같은 소스 세트(Source Set)로 나뉘어집니다.
-
commonMain: 플랫폼에 독립적인 코드를 작성하는 공간입니다. 비즈니스 로직, 데이터 모델, 네트워크 인터페이스 정의 등 대부분의 공유 가능한 코드가 여기에 위치합니다. -
androidMain: Android 플랫폼에 특화된 코드를 작성하는 공간입니다.commonMain에서 정의된expect선언에 대한actual구현을 제공하거나, Android API를 직접 사용하는 코드가 들어갑니다. -
iosMain: iOS 플랫폼에 특화된 코드를 작성하는 공간입니다.commonMain에서 정의된expect선언에 대한actual구현을 제공하거나, iOS Framework API를 직접 사용하는 코드가 들어갑니다.
이러한 구조에서 가장 중요한 개념은 expect와 actual 선언입니다. commonMain에서는 특정 기능이 필요함을 expect 키워드를 사용하여 선언하고, 각 플랫폼별 소스 세트(androidMain, iosMain)에서는 이 expect 선언에 대한 실제 구현을 actual 키워드를 사용하여 제공합니다.
// commonMain/kotlin/com/example/Platform.kt
package com.example
expect class Platform() {
val name: String
}
// androidMain/kotlin/com/example/Platform.kt
package com.example
actual class Platform actual constructor() {
actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}
// iosMain/kotlin/com/example/Platform.kt
package com.example
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion()
}
// commonMain/kotlin/com/example/Greeting.kt
package com.example
class Greeting {
private val platform: Platform = Platform()
fun greet(): String {
return "Hello, ${platform.name}!"
}
}
위 예시처럼 Platform 클래스는 commonMain에서 name 속성을 expect하고, androidMain과 iosMain에서 각 플랫폼에 맞는 실제 값을 actual로 제공합니다. Greeting 클래스는 commonMain에 존재하며, Platform의 name을 사용하여 플랫폼에 종속적인 메시지를 생성할 수 있습니다.
KMP의 장점과 단점 비교: React Native, Flutter와 차이점
KMP는 다른 크로스플랫폼 프레임워크와는 다른 철학을 가지고 있기 때문에 명확한 장단점을 가집니다.
KMP의 주요 장점
- 네이티브 UI/UX: KMP는 UI를 공유하지 않으므로, 각 플랫폼의 최신 UI 가이드라인과 디자인 시스템을 완벽하게 따를 수 있습니다. 이는 최고의 사용자 경험과 성능을 보장합니다.
- Kotlin 언어의 장점: 간결하고 안전하며 생산적인 Kotlin 언어를 사용하여 비즈니스 로직을 작성합니다. Kotlin은 이미 Android 개발의 주류 언어이며, JVM 및 LLVM(iOS) 환경에서 모두 뛰어난 성능을 발휘합니다.
- 기존 프로젝트와의 통합 용이성: 기존에 Swift/Objective-C로 개발된 iOS 앱이나 Java/Kotlin으로 개발된 Android 앱에 KMP 모듈을 점진적으로 통합할 수 있습니다. 전체 앱을 한 번에 전환할 필요 없이, 필요한 부분부터 공유 로직을 적용할 수 있다는 큰 장점이 있습니다.
- 최고의 성능: 공유되는 비즈니스 로직은 Kotlin으로 컴파일되어 각 플랫폼의 네이티브 코드와 동일한 수준의 성능을 제공합니다. 런타임 브릿지나 가상 머신 오버헤드가 없습니다.
- 플랫폼별 유연성:
expect/actual메커니즘을 통해 플랫폼별로 다른 구현이 필요할 때 유연하게 대응할 수 있습니다.
KMP의 주요 단점
- UI 개발 중복: 비즈니스 로직은 공유되지만, UI는 여전히 각 플랫폼에서 별도로 구현해야 합니다. 이는 UI 개발에 드는 시간과 리소스가 여전히 이중으로 발생함을 의미합니다. (단, Compose Multiplatform을 통해 UI도 공유하려는 움직임이 활발합니다.)
- 학습 곡선: Kotlin 언어와 멀티플랫폼 Gradle 빌드 시스템에 대한 이해가 필요하며, iOS 개발자에게는 Kotlin 학습이 필요할 수 있습니다.
- 툴링 및 생태계 성숙도: Flutter나 React Native에 비해 KMP는 상대적으로 젊은 기술이며, 툴링 지원이나 서드파티 라이브러리 생태계가 아직은 성장 단계에 있습니다.
크로스플랫폼 프레임워크 비교
| 특징 | Kotlin Multiplatform (KMP) | Flutter | React Native |
|---|---|---|---|
| UI 공유 | X (네이티브 UI 사용) | O (자체 렌더링 엔진) | O (네이티브 컴포넌트 렌더링) |
| 로직 공유 | O (Kotlin) | O (Dart) | O (JavaScript) |
| 언어 | Kotlin | Dart | JavaScript |
| 성능 | 네이티브 수준 | 고성능 (Skia 엔진) | 네이티브에 근접 |
| 네이티브 접근 | expect/actual로 직접 | 플랫폼 채널 | 브릿지 |
| 장점 | 네이티브 UI/UX, 점진적 도입 | 단일 코드베이스, 빠른 개발 | 웹 개발자 친숙, 큰 생태계 |
| 단점 | UI 개발 중복 | Dart 학습, 앱 크기 | 브릿지 오버헤드, 디버깅 |
KMP 프로젝트 시작하기: 기본적인 설정과 구조
새로운 KMP 프로젝트를 시작하는 가장 쉬운 방법은 Android Studio의 New Project Wizard를 이용하거나, JetBrains에서 제공하는 Kotlin Multiplatform Wizard 웹사이트를 활용하는 것입니다.
기본적인 KMP 프로젝트 구조는 다음과 같습니다.
MyKmpApp/
├── gradle/
├── .gradle/
├── .idea/
├── build.gradle.kts
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
├── androidApp/ // Android 애플리케이션 모듈 (네이티브 UI)
│ ├── src/
│ ├── build.gradle.kts
│ └── ...
├── iosApp/ // iOS 애플리케이션 모듈 (네이티브 UI)
│ ├── MyKmpApp.xcodeproj/
│ ├── MyKmpApp/
│ └── ...
└── shared/ // KMP 공유 로직 모듈
├── src/
│ ├── androidMain/ // Android 플랫폼별 구현
│ │ └── kotlin/
│ ├── commonMain/ // 공유 로직
│ │ └── kotlin/
│ └── iosMain/ // iOS 플랫폼별 구현
│ └── kotlin/
└── build.gradle.kts
shared/build.gradle.kts 파일은 KMP 모듈의 핵심 빌드 설정을 정의합니다.
// shared/build.gradle.kts
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods") // iOS 통합을 위해 필요
kotlin("plugin.serialization") // kotlinx.serialization 사용 시
id("com.android.library")
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
// iOS 프레임워크 생성 설정
val iosTarget = targets.getByName<KotlinNativeTarget>("iosArm64")
iosTarget.binaries.framework {
baseName = "shared" // 생성될 프레임워크 이름
isStatic = true // 정적 라이브러리로 빌드
}
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")
// Ktor HTTP Client
implementation("io.ktor:ktor-client-core:2.3.6")
}
}
val androidMain by getting {
dependencies {
// Ktor Android Engine
implementation("io.ktor:ktor-client-android:2.3.6")
}
}
val iosMain by getting {
dependencies {
// Ktor iOS Engine
implementation("io.ktor:ktor-client-ios:2.3.6")
}
}
}
}
android {
namespace = "com.example.shared"
compileSdk = 34
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
이 설정은 shared 모듈을 Android 라이브러리와 iOS 프레임워크로 빌드하도록 구성합니다. kotlinx-coroutines-core, kotlinx-serialization-json, ktor-client-core와 같은 주요 라이브러리들은 commonMain에서 의존성을 추가하여 모든 플랫폼에서 공유될 수 있습니다. ktor-client-android와 ktor-client-ios는 각 플랫폼에 특화된 HTTP 엔진 구현이므로 androidMain과 iosMain에 추가됩니다.
실전 KMP 개발 팁: 데이터 공유와 플랫폼 통합
KMP 프로젝트에서 효율적으로 개발하기 위한 몇 가지 실전 팁을 소개합니다.
1. 데이터 직렬화 (Serialization)
네트워크 통신이나 로컬 저장소에 데이터를 저장할 때 객체를 직렬화/역직렬화하는 것은 필수적입니다. kotlinx.serialization 라이브러리는 KMP 환경에서 완벽하게 동작하며, JSON, ProtoBuf 등 다양한 포맷을 지원합니다.
// commonMain/kotlin/com/example/model/User.kt
package com.example.model
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: String,
val name: String,
val email: String
)
// commonMain/kotlin/com/example/api/ApiClient.kt
package com.example.api
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import com.example.model.User
class ApiClient {
private val client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}
suspend fun getUsers(): List<User> {
return client.get("https://api.example.com/users").body()
}
suspend fun getUser(id: String): User {
return client.get("https://api.example.com/users/$id").body()
}
}
@Serializable 어노테이션을 사용하여 데이터 클래스를 직렬화 가능하게 만들고, HttpClient에 ContentNegotiation 플러그인을 설치하여 JSON 직렬화를 처리할 수 있습니다.
2. 비동기 처리 (Coroutines)
Kotlin Coroutines는 KMP에서 비동기 작업을 처리하는 표준 방식입니다. kotlinx-coroutines-core는 commonMain에서 사용 가능하며, iOS에서는 Swift의 Concurrency 모델과도 잘 통합될 수 있습니다.
// commonMain/kotlin/com/example/repository/UserRepository.kt
package com.example.repository
import com.example.api.ApiClient
import com.example.model.User
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class UserRepository(private val apiClient: ApiClient) {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users
private val scope = CoroutineScope(Dispatchers.Default)
fun fetchUsers() {
scope.launch {
try {
_users.value = apiClient.getUsers()
} catch (e: Exception) {
// 에러 처리
println("Error fetching users: ${e.message}")
}
}
}
}
CoroutineScope와 launch를 사용하여 비동기 작업을 시작하고, StateFlow를 통해 데이터 변경 사항을 UI에 알릴 수 있습니다.
3. 의존성 주입 (Dependency Injection)
KMP에서 의존성 주입은 조금 더 복잡할 수 있습니다. Koin이나 Dagger Hilt(Android), Swinject(iOS) 등 각 플랫폼별로 선호하는 DI 프레임워크가 다를 수 있기 때문입니다. 가장 간단한 방법은 공유 모듈에서는 필요한 의존성을 생성자 주입 방식으로 받고, 각 플랫폼 앱에서 실제 인스턴스를 생성하여 전달하는 것입니다.
// commonMain/kotlin/com/example/AppModule.kt
package com.example
import com.example.api.ApiClient
import com.example.repository.UserRepository
// 간단한 의존성 컨테이너 (DI 프레임워크를 사용하지 않을 경우)
object AppModule {
val apiClient: ApiClient by lazy { ApiClient() }
val userRepository: UserRepository by lazy { UserRepository(apiClient) }
}
이러한 방식으로 AppModule 객체를 통해 필요한 의존성을 제공하고, 각 플랫폼 앱에서 이 AppModule을 호출하여 공유 로직에 접근할 수 있습니다.
iOS와 Android 앱에 KMP 모듈 통합하기
KMP의 shared 모듈은 Android에서는 일반적인 Gradle 라이브러리 모듈로, iOS에서는 .framework (또는 .xcframework) 파일로 빌드됩니다.
Android 앱 통합
Android 앱에서는 androidApp/build.gradle.kts 파일에 shared 모듈의 의존성을 추가하기만 하면 됩니다.
// androidApp/build.gradle.kts
dependencies {
implementation(project(":shared")) // KMP shared 모듈 의존성 추가
// ... 다른 Android 의존성들
}
이후 Android 코드에서 shared 모듈의 Kotlin 클래스를 마치 로컬 Kotlin 라이브러리처럼 사용할 수 있습니다.
// androidApp/src/main/java/com/example/android/MainActivity.kt
package com.example.android
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import com.example.Greeting // KMP shared 모듈의 Greeting 클래스 임포트
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text(text = Greeting().greet())
}
}
}
iOS 앱 통합
iOS에서는 shared 모듈이 .framework 또는 .xcframework로 빌드되며, 이를 Xcode 프로젝트에 통합해야 합니다. 가장 일반적인 방법은 CocoaPods를 사용하는 것입니다.
shared/build.gradle.kts에 kotlin("native.cocoapods") 플러그인을 추가하고, cocoapods 블록을 구성합니다.
// shared/build.gradle.kts (일부 발췌)
kotlin {
// ...
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
ios.deploymentTarget = "14.1" // iOS 배포 타겟
framework {
baseName = "shared"
isStatic = true
}
podfile = project.file("../iosApp/Podfile") // iOS 앱의 Podfile 경로
}
}
iosApp/Podfile에 shared 모듈을 pod으로 추가합니다.
# iosApp/Podfile
# ...
target 'iosApp' do
# ...
pod 'shared', :path => '../shared' # shared 모듈을 로컬 pod으로 추가
end
# ...
이후 iosApp 디렉토리에서 pod install을 실행하면 shared 프레임워크가 Xcode 프로젝트에 통합됩니다. Swift 코드에서는 import shared를 통해 Kotlin 코드를 호출할 수 있습니다.
// iosApp/iosApp/ContentView.swift
import SwiftUI
import shared // KMP shared 모듈 임포트
struct ContentView: View {
var body: some View {
Text(Greeting().greet()) // KMP shared 모듈의 Greeting 클래스 사용
}
}
KMP의 미래: Compose Multiplatform과 함께하는 Full Stack 크로스플랫폼
KMP는 오랫동안 비즈니스 로직 공유에 집중해왔지만, 최근에는 UI까지 공유하려는 움직임이 활발합니다. JetBrains에서 개발 중인 Compose Multiplatform은 Android의 Jetpack Compose를 기반으로 하여 iOS, 데스크톱, 웹 등 다양한 플랫폼에서 선언형 UI를 공유할 수 있게 해줍니다.
Compose Multiplatform이 성숙해지면, 개발 팀은 KMP를 통해 비즈니스 로직뿐만 아니라 UI까지 단일 Kotlin 코드베이스로 작성하여 진정한 의미의 Full Stack 크로스플랫폼 개발을 실현할 수 있을 것입니다. 이는 KMP의 활용 범위를 크게 확장시키고, 모바일 개발의 미래를 더욱 흥미롭게 만들 잠재력을 가지고 있습니다.
마무리
Kotlin Multiplatform은 모바일 개발 팀에게 iOS와 Android 앱 간의 비즈니스 로직을 효율적으로 공유하면서도, 각 플랫폼의 네이티브 UI/UX를 완벽하게 유지할 수 있는 강력한 도구를 제공합니다. 기존 네이티브 프로젝트에 점진적으로 도입할 수 있는 유연성과 Kotlin 언어의 생산성 및 성능은 KMP를 매력적인 선택지로 만듭니다. 비록 UI 개발 중복이라는 과제가 남아 있지만, Compose Multiplatform과 같은 기술의 발전은 KMP의 미래를 더욱 밝게 비추고 있습니다. KMP를 통해 더 적은 노력으로 더 나은 모바일 경험을 제공할 수 있기를 기대합니다.
관련 게시글
Kotlin Multiplatform으로 Cross-Platform 모바일 개발 가속화하기
Kotlin Multiplatform (KMP)을 활용한 크로스플랫폼 모바일 앱 개발 전략을 심층적으로 다룹니다. iOS와 Android에서 코드 재사용을 극대화하고, 네이티브 UI와 성능을 유지하는 방법을 알아보세요.
Kotlin Multiplatform (KMP): 크로스플랫폼 모바일 개발 심층 가이드
Kotlin Multiplatform (KMP)을 활용한 크로스플랫폼 모바일 앱 개발 가이드입니다. React Native, Flutter와 비교하며 KMP의 장점, 아키텍처, 실전 팁 및 코드 예제를 상세히 다룹니다.
SwiftUI 입문 가이드: Cross-Platform 개발자를 위한 iOS 앱 여정 시작하기
React Native, Flutter 개발자도 쉽게 시작할 수 있는 SwiftUI 입문 가이드! 선언형 UI, 상태 관리, 핵심 컴포넌트, 그리고 실전 To-Do 앱 예제까지, iOS 모바일 앱 개발의 첫걸음을 떼어보세요.