Koin은 Kotlin 언어에 맞춰 설계된 의존성 주입(Dependency Injection, DI) 라이브러리.
Spring 같은 무거운 프레임워크 없이도, 간단한 DSL을 통해 객체 생성과 주입을 관리할 수 있도록 도와준다.
KMP에서 현재 기준 Hilt는 지원을 안하지만 Koin 라이브러리로 의존성 주입을 할 수 있다고 알고 있다.
때문에 Koin 라이브러리를 이용한 의존성 주입 방법을 알아보자.
참고로 유투브영상을 참고했다. 아래 링크를 남깁니다.
https://www.youtube.com/watch?v=TAKZy3uQTdE&list=PLQkwcJG4YTCS55alEYv3J8CD4BXhqLUuk&index=5
먼저 라이브러리를 추가하면 된다.
[versions]
koin="3.6.0-Beta4"
koinComposeMultiplatform = "1.2.0-Beta4"
navigationCompose = "2.8.0-alpha02"
lifecycleViewModel="2.8.2"
[libraries]
lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleViewModel"}
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinComposeMultiplatform" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeMultiplatform" }
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
//...
/composeApp/build.gradle.kt
kotlin {
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
//여기 추가
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
//여기 추가
api(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.lifecycle.viewmodel)
implementation(libs.navigation.compose)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}
/composeApp/commonMain/kotlin/yourpackage/dependencies/DbClient
expect class DbClient
---
/composeApp/commonMain/kotlin/yourpackage/dependencies/MyRepository
interface MyRepository {
fun helloWorld(): String
}
class MyRepositoryImpl(
private val dbClient: DbClient
): MyRepository {
override fun helloWorld(): String {
return "Hello World!"
}
}
---
/composeApp/commonMain/kotlin/yourpackage/dependencies/MyViewModel
class MyViewModel(
private val repository: MyRepository
): ViewModel() {
fun getHelloWorldString(): String {
return repository.helloWorld()
}
}
위 3개의 데이터를 의존성 주입하기
/composeApp/commonMain/kotlin/yourpackage/di/Module/kt
//android, ios에서 따로 의존성 주입하기 위해 expect 지정
expect val platformModule: Module
//공통 모듈에서 의존성 주입할 것(MyRepository)
val sharedModule = module {
singleOf(::MyRepositoryImpl).bind<MyRepository>()
viewModelOf(::MyViewModel)
}
먼저 사용되는 expect가 2개가 있다.(DbClient, platformModule)
DbClient는 한 예로 각 앱의 데이터소스를 접근하는 것(예로 Android의 SharedPreferences, Room 같은)은 IOS와 Android 방식이 다르니 expert를 한 것이고, 이 DbClient의 정보를 의존성 주입을 하기 위해 platformModule도 expert를 진행한 것이다.
이제 이 expert를 각 모듈에 actual 키워드를 사용해 구현하자. expert를 사용하면 빨간 줄이 뜰텐데 create missed actuals ... 클릭하면 원하는 모듈에 actual 추가가 가능하다.

아래와 같이 어디에 Actual을 할지 선택하는 창이 뜨는데, 여기서 androidMain하고 nativeMain을 선택했다. 이유는 모든 곳에 actual을 할 시 IOS 앱 실행시 중복 선언이 된다는 오류가 발생했었다.


nativeMain모듈은 공통 Native 계층으로 Android는 제외하고 Kotlin/Native 타겟들이(iOS, macOS, Linux, Windows 등) 모두 공유하는 코드가 들어가는 곳이기 때문에 선택했다.
이후 두 정보를 actual한 구현체는 아래와 같다.
/nativeMain/kotlin/yourpackage/dependencies/DbClient.native.kt
actual class DbClient
/nativeMain/kotlin/yourpackage/di/Modules.native.kt
actual val platformModule: Module = module {
//위에 native의 DbClient 의존성 주입
singleOf(::DbClient)
}
--
//Android
/AndroidMain/kotlin/yourPackage/dependencies/DbClient.android.kt
actual class DbClient(
private val context: Context
) {
}
/AndroidMain/kotlin/yourPackage/di/Modules.android.kt
actual val platformModule: Module = module {
//위에 DbClient.android.kt에서 가져온 DbClient 의존성 주입
singleOf(::DbClient)
}
사실 둘의 차이점은 없어보이지만 다른 점으로는 안드로이드에서는 DataStore나 Room을 만들기 위해서는 context가 필요하기 때문에 생성자에 추가했다.
commonMain 모듈에 di 패키지에 initKon.kt 파일 생성
fun initKoin(config: KoinAppDeclaration? = null) {
startKoin {
config?.invoke(this)
modules(
sharedModule,
platformModule
)
}
}
파라미터에 KoinAppDeclaration 객체를 사용한 이유는 추후에 안드로이드 쪽에서 context 정보를 넘겨줘야하기 때문이다.
참고로 KoinAppDeclaration은 고차함수다.

안드로이드에서 koin을 사용하려면 Application에 등록을 해야한다. 그리고 android에서 DbClient에 생성자에 context 파라미터를 넣어놓았는데 이를 가져오기 위해서는 Application에 있는 context를 가져와야 한다.
/AndroidMain/kotlin/yourPackage/MyApplication.kt
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
//아까 말했던 KoinAppDeclaration가 고차함수이므로 DSL처럼 작성 가능
initKoin {
//여기서 context 정보 추가
androidContext(this@MyApplication)
}
}
}
//이후 Mainifest.xml 파일에 name 속성으로 해당 Application 등록하기
/iosMain/kotlin/yourpackage/MainViewController.kt
fun MainViewController() = ComposeUIViewController(
//코틀린 initKoin() 함수 추가하기
configure = {
initKoin()
}
) {
App()
}
이제 기본적인 의존성 주입은 완료 되었다.
이제 화면을 구성하면서 의존성이 제대로 주입되었고 주입된 데이터가 화면에 정상적으로 출력이 되는지 확인해보자.
/commonMain/kotlin/yourpackage/App.kt
OptIn(KoinExperimentalAPI::class)
@Composable
@Preview
fun App() {
MaterialTheme {
KoinContext {
NavHost(
navController = rememberNavController(),
startDestination = "home"
) {
composable(route = "home") {
//koinViewModel로 MyViewModel를 가져올 수 있다.
val viewModel = koinViewModel<MyViewModel>()
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
//이 getHelloWorldString()은 MyRepository에서 사용한 함수 -> Hello World! 반환이 제대로 이루어지면 정상적으로 의존성 주입이 완료된 것.
text = viewModel.getHelloWorldString()
)
}
}
}
}
}
}
굳 잘뜬다.
만약 의존성을 지우면 어떻게 될까?
예시로 MyRepository 의존성 주입을 주석처리해보자
/commonMain/kotlin/yourpackage/di/Modules.kt
val sharedModule = module {
//이거 주석
//singleOf(::MyRepositoryImpl).bind<MyRepository>()
viewModelOf(::MyViewModel)
}
다시 실행하면 아래와 같은 오류가 발생. MyRepository 정의를 찾을 수가 없다고 뜬다. 의존성 주입이 제대로 되어진 것을 볼 수 있다.

후.. IOS 테스트 과정에서 앱이 제대로 실행이 되질 않았다. 어떤 오류가 떴는지 보자.

이게 무슨 오류인지 찾아보니까 IOS에 androidx 라이브러리가 있다고 생긴 오류라고 GPT가 얘기를 하면서 다른 라이브러리를 받는 것이 좋다고 언급을 했다.
사실 영상 자체가 1년 전이다 보니까 현재 KMP가 계속 발전하는 만큼 라이브러리도 수시로 바뀌는 듯 하다.
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
api(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
//이 두개 라이브러리 삭제
// implementation(libs.lifecycle.viewmodel)
// implementation(libs.navigation.compose)
// ✅ JetBrains MPP 라이프사이클
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1")
implementation("org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.1")
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.1")
// ✅ JetBrains MPP 네비게이션
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.9.0")
}
찾아보니까 org.jetbrains.androidx로 시작하는 라이브러리를 추가하면 해결된다고 해서 이를 적용하고 실행을 진행하니 위에 있던 문제는 조금 지워졌지만 아래의 문제가 발생하는 것을 볼 수 있다.

비슷한 문제 같아서 라이브러리 문제인 것이라 생각을 했는데 이와 관련된 문제를 찾아서 아래 링크를 통해 확인해보니 gradle.properties 속성을 추가하면 해결할 수 있다고 해서 이를 적용했다.
https://github.com/InsertKoinIO/koin/issues/2175
gradle.properties파일
#이거 추가
kotlin.native.cacheKind=none
이렇게 하니까 정상적으로 앱이 실행되는 것을 볼 수 있다... 개고생 ㄷㄷ
옛날 영상의 KMP를 참고하다보니 이래저래 오류가 몇개 발생했지만 그래도 어찌저찌 의존성 주입은 잘 된 것 같다. 꾸준히 학습하면서 KMP에 대해 계속 공부할 예정이다.