Development/Android

Koin? 한번 알아볼까요?

Jamie 2026. 4. 1. 13:43
반응형

안녕하세요. Jamie입니다.

 

안드로이드 개발에서 의존성 주입(DI)은 아키텍쳐를 구현하는데 있어 필수입니다.

 

기존에는 ServiceLocator를 만들어서 수동으로 주입하고, Dagger, Hilt에 이어 Koin이 현재는 주목받고 있는데요,

현재 시점에서 Koin이 왜 다시 주목받는지, 그리고 Hilt와의 비교에 대해서 포스팅 해보도록 하겠습니다.

Koin이란?

Koin, 그리고 항상 같이 비교가 되는 Hilt는 기본적으로 DI(Dependency Injection) 라이브러리라고 합니다.

근데, 사실 Koin은 "의존성 주입"이라 부르지만, 기술적으로는 DSL(Domain Specific Language) 기반의 Service Locator 패턴에 가깝습니다.

 

하지만 사용 방식은 DI와 동일합니다.

이 둘의 핵심적인 차이는 "의존성을 누가 주입해 주는가?"와 "의존성을 언제 찾는가?"에 있습니다.

 

 

1. 개념적 차이: 주는 쪽 vs 찾는 쪽

구분 DI (Hilt / Dagger) Service Locator (Koin)

핵심 개념 외부(컨테이너)에서 객체에 의존성을 직접 꽂아줌. 객체가 필요할 때 중앙 저장소(Locator)에 의존성을 요청함.
제어권 제어의 역전(IoC)이 완벽하게 일어남. 객체가 직접 Locator를 호출하므로 제어권이 일부 남아있음.
컴파일 시점 컴파일 타임에 의존성 그래프를 검증함 (안정성 ↑). 런타임에 의존성을 찾음. 없으면 런타임 에러 (Crash)

 

 

2. 코드로 비교해보겠습니다.

1) Hilt: 의존성 주입 (DI)

Hilt는 어노테이션 프로세서를 통해 컴파일 시점에 주입 코드를 생성합니다. 객체 스스로가 "누가 나한테 줄지" 고민할 필요가 없습니다.

 

// Hilt는 생성자에 @Inject만 붙이면 외부에서 알아서 넣어줍니다.
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository 
) : ViewModel() {
    // repository는 이미 '주입'되어 있는 상태입니다.
}

 

UserViewModel은 UserRepository가 어디서 오는지 전혀 모릅니다. 그냥 누군가 줄 것이라고 믿고 기다릴 뿐입니다.

 

 

2) Koin: 서비스 로케이터 (Service Locator)

Koin은 DSL을 통해 미리 등록된 객체들을 get()이나 by inject()를 통해 찾아오는 방식입니다.

 

// Koin 모듈 정의
val appModule = module {
    single { UserRepository() }
    viewModel { UserViewModel(get()) } // get()이 저장소에서 객체를 찾아옴
}

// ViewModel 내부
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    // ...
}

// Activity에서의 사용
class UserActivity : AppCompatActivity() {
    // "Koin아, 나 이거 필요해. 하나 찾아다 줘(inject)." 라고 요청함
    private val viewModel: UserViewModel by inject() 
}

 

get() 함수는 사실 "중앙 서비스 저장소에서 해당 타입의 객체를 찾아서 반환하라"는 명령입니다.

즉, 컴파일러가 미리 연결해두는 게 아니라, 런타임에 "나 이거 줘!"라고 요청해서 받아오는 방식입니다.

 

 

3) 왜 Koin은 Service Locator인가?

Koin이 Service Locator인 기술적 이유는 다음과 같습니다.

  1. 런타임 해석: Koin은 앱이 실행될 때 module 내부의 람다식을 실행하며 객체를 생성합니다. 만약 get()으로 요청한 객체가 모듈에 등록되어 있지 않다면, 컴파일 에러가 아니라 앱이 실행 중에 죽습니다(Runtime Exception).
  2. 명시적 호출: by inject()나 get()을 호출하는 행위 자체가 “Service Locator에게 의존성을 찾아달라고 요청"하는 행위입니다.

 

 

자 이정도로 비교할 수 있고 이제 Koin의 구조에 대해 알아보겠습니다.

 

 

3. Koin의 핵심 구성 요소

  • Module: 의존성들을 정의하는 컨테이너 (module { ... })
  • Single: 앱 전체에서 하나만 존재하는 싱글톤 객체 생성 (single { ... })
  • Factory: 주입될 때마다 매번 새로운 객체 생성 (factory { ... })
  • ViewModel: Jetpack ViewModel 전용 주입 (viewModel { ... })
  • get(): 정의된 모듈 내에서 필요한 의존성을 자동으로 찾아줌 (컴포넌트 간 연결)

 

4. Koin 예제코드

이 예제는 Data -> Repository -> ViewModel -> UI로 이어지는 코드 예제에 Koin을 적용했습니다.

1. 의존성 설정 (libs.versions.toml)

[versions]
koin = "4.0.0"

[libraries]
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }

 

2. 도메인 및 데이터 레이어

 

// 모델 데이터 클래스 선언
data class UserProfile(
	val name: String, 
	val email: String
)

// 레포지토리 선언
interface UserRepository {
    suspend fun getUser(): UserProfile
}

// 레포지토리 구현부
class UserRepositoryImpl : UserRepository {
    override suspend fun getUser(): UserProfile {
        return UserProfile("Jamie", "jamie@android.com")
    }
}

 

3. Koin 모듈 정의 (DSL)

import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module

val appModule = module {
    // 싱글톤으로 Repository 주입
    single<UserRepository> { UserRepositoryImpl() }

    // ViewModel 주입 (get()이 자동으로 UserRepository를 찾아줌)
    viewModel { MainViewModel(get()) }
}

 

4. ViewModel 구현

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class MainViewModel(private val repository: UserRepository) : ViewModel() {
    private val _user = MutableStateFlow<UserProfile?>(null)
    val user = _user.asStateFlow()

    fun fetchUser() {
        viewModelScope.launch {
            _user.value = repository.getUser()
        }
    }
}

 

5. Application 클래스 설정

import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin

class MyKoinApp : Application() {
    override fun onCreate() {
        super.onCreate()

        startKoin {
            androidContext(this@MyKoinApp) // Koin에 Context 주입
            modules(appModule) // 정의한 Koin 모듈 등록
        }
    }
}

 

6. UI에서 사용하기

import androidx.compose.runtime.*
import org.koin.androidx.compose.koinViewModel

@Composable
fun UserProfileScreen(
		viewModel: MainViewModel = koinViewModel()
) {
    val user by viewModel.user.collectAsState()

    LaunchedEffect(Unit) {
        viewModel.fetchUser()
    }

    Column(modifier = Modifier.padding(16.dp)) {
        user?.let {
            Text(text = "이름: ${it.name}", style = MaterialTheme.typography.h4)
            Text(text = "이메일: ${it.email}")
        } ?: Text("로딩 중...")
    }
}

 

 

정리

  1. single { … } : 앱이 살아있는 동안 하나만 만들어서 돌려씁니다.
  2. get() : Koin이 관리하는 객체에서 알아서 찾아서 가져옵니다
  3. koinViewModel() : Compose 내부에서 ViewModel을 가져오는 방법입니다.
  4. 나중에 프로젝트가 커지면 dataModule , domainModule , uiModule 으로 나눠서 관리하면 성능도 좋고 코드도 깔끔해집니다.
반응형