
이번 게시글에서는 저번 게시글에 이어서 Koin의 core 기능들에 대해서 계속해서 알아보고자 합니다.
1편은 아래 링크에서 확인하실 수 있습니다.
Koin 톺아보기 1편
이번 게시글에서는 Koin에 대해 알아보고자 합니다. Koin이란?Koin은 Kotlin에 특화된 경량화된 의존성 주입 프레임워크입니다. koin은 다음과 같은 2가지의 의존성 주입 방식이 있습니다.Kotlin DSLAnnotat
everyday-develop-myself.tistory.com
Qualifiers
`Qualifiers`는 Koin 모듈에서 동일한 타입의 여러 정의를 구분할 수 있도록 해줍니다.
동일한 인터페이스에 대해 여러 구현체가 있는 경우나, 동일한 타입이지만 서로 다른 설정이 필요할 때 `Qualifiers`를 사용합니다.
// qualifiers가 없다면 - conflict!
val networkModule = module {
single { OkHttpClient.Builder()...build() }
single { OkHttpClient.Builder()...build() } // 어떤 건가요?
}
Named
`namd()`를 사용하여 두 정의를 구별할 수 있습니다.
먼저 아래와 같이 정의합니다.
import org.koin.core.qualifier.named
val networkModule = module {
single(named("encrypted")) {
OkHttpClient.Builder()
.addInterceptor(EncryptionInterceptor())
.build()
}
single(named("logging")) {
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor())
.build()
}
}
그리고 아래와 같이 사용할 수 있습니다.
// In module definitions
val apiModule = module {
single {
ApiService(
encryptedClient = get(named("encrypted")),
loggingClient = get(named("logging"))
)
}
}
// With KoinComponent
class MyService : KoinComponent {
private val encryptedClient: OkHttpClient by inject(named("encrypted"))
}
Annotations의 경우에는 아래와 같이 사용할 수 있습니다.
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
@Single
@Named("encrypted")
class EncryptedHttpClient : OkHttpClient()
@Single
@Named("logging")
class LoggingHttpClient : OkHttpClient()
@Single
class ApiService(
@Named("encrypted") private val encryptedClient: OkHttpClient,
@Named("logging") private val loggingClient: OkHttpClient
)
Type-Safe Qualifiers
타입을 직접 지정하거나, enum을 사용하여 Type-Safe하게 사용할 수도 있습니다.
// Type 지정
single(named<EncryptedClient>()) {
OkHttpClient.Builder()
.addInterceptor(EncryptionInterceptor())
.build()
}
val client: OkHttpClient = get(named<EncryptedClient>())
// enum 사용
single(named(NetworkClient.ENCRYPTED)) {
OkHttpClient.Builder()
.addInterceptor(EncryptionInterceptor())
.build()
}
val client: OkHttpClient = get(named(NetworkClient.ENCRYPTED))
의존성 가져오기
Koin 컨데이너로부터 의존성을 가져오는 방식은 크게 3가지가 있습니다.
1. 생성자 주입 (Constructor Injection) - 가장 추천되는 방식
Koin은 모든 생성자 파라미터를 자동으로 해석합니다.
모듈에 필요한 의존성이 정의되어 있다면, Koin에 의해 자동으로 주입됩니다.
class UserRepository(
private val database: Database,
private val apiClient: ApiClient
)
class UserViewModel(
private val repository: UserRepository
) : ViewModel()
val appModule = module {
single<Database>()
single<ApiClient>()
single<UserRepository>()
viewModel<UserViewModel>()
}
2. 함수 주입 (Function Injection)
Custom한 로직이 필요한 경우에는 함수를 사용하여 인스턴스를 생성하여 주입합니다.
함수 주입은 다음과 같은 경우에 유용합니다.
- 직접 제어할 수 없는 외부 라이브러리로부터 인스턴스를 생성해야 할 때
- 복잡한 초기화 로직이 필요할 때
- 빌더나 DSL을 설정해야 할 때
// Kotlin DSL
fun createHttpClient(dataSource: DataSource): HttpClient {
return HttpClient {
install(ContentNegotiation) { json() }
defaultRequest { url(dataSource.baseUrl) }
}
}
val appModule = module {
single<DataSource>()
single { create(::createHttpClient) }
}
// Annotations
@Module
class NetworkModule {
@Singleton
fun createHttpClient(dataSource: DataSource): HttpClient {
return HttpClient {
install(ContentNegotiation) { json() }
defaultRequest { url(dataSource.baseUrl) }
}
}
}
3. 필드 주입 (Field Injection)
`by inject()`를 통해 지연 주입하거나 `get()`을 사용해 즉시 주입할 수 있습니다.
class MyActivity : AppCompatActivity() {
// Lazy - 최초 접근 시 생성
private val viewModel: UserViewModel by viewModel()
private val service: MyService by inject()
}
class MyActivity : AppCompatActivity() {
// Eager - 즉시 생성
private val service: MyService = get()
}
KoinComponent
`KoinComponent`는 안드로이드 컴포넌트가 아니고, DI는 써야하는데 생성자 주입을 쓸 수 없는 위치에서 사용합니다.
class MyHelper : KoinComponent {
private val service: MyService by inject()
private val database: Database = get()
fun doSomething() {
service.process(database.query())
}
}
다만, `KoinComponent`를 사용하면 DI 컨테이너에 직접 의존하게 되어 프레임워크 결합이 증가하게 됩니다. 그래서 비즈니스 로직 클래스에서는 최대한 사용을 피해야 합니다.
Scopes
`Scopes`는 의존성의 생명주기를 제어합니다.
`Scopes`는 다음과 같은 사용에서 사용합니다.
- Factory보다 오래 지속되지만, Singleton보다는 짧게 유지되는 인스턴스가 필요할 때
- 특정 컨텍스트(Activity, Fragment) 내에서 상태를 공유해야 할 때
- 컨텍스트가 종료될 때 자동으로 정리되길 원할 때
Koin에서 Scope와 관련된 기능은 2단계로 나뉩니다.
1. Scope 정의
먼저 MyActivity 스코프를 정의합니다.
scope<MyActivity> { }
2. binding 정의
다음으로는 정의한 스코프에 종속된 binding을 정의합니다.
scope<MyActivity> {
scoped<MyPresenter>()
}
이제 MyPresenter는 MyActivity 스코프 안에서만 존재하게 됩니다. 동일한 스코프 안에서는 동일한 인스턴스를 공유하며 스코프가 종료되면 해당 인스턴스는 종료됩니다.
Annotation으로는 아래와 같이 사용할 수 있습니다.
@Scope(MyActivityScope::class)
@Scoped
class Presenter(private val repository: UserRepository)
@Scope(MyActivityScope::class)
@Scoped
class Navigator
일반적인 Android 스코프에 대해서는 Scope Archetype Annotations을 사용할 수 있습니다.
// ViewModel 스코프
@ViewModelScope
class UserCache
// Activity 스코프
@ActivityScope
class ActivityPresenter
@ActivityRetainedScope
class RetainedPresenter
// Fragment 스코프
@FragmentScope
class FragmentPresenter
마무리
이렇게 해서 간단하게 Koin에 대해 알아보고 여러 기능에 대하 알아보았습니다. 공식문서를 참고하면 좀 더 다양한 케이스에서 사용할 수 있는 기능들이 존재하니, 시간이 된다면 한번씩 공부해보는 것이 좋겠습니다.
Reference
Qualifiers | Koin
Qualifiers allow you to differentiate between multiple definitions of the same type in your Koin modules.
insert-koin.io
Retrieving Dependencies | Koin
This guide covers how to retrieve dependencies from Koin in different contexts.
insert-koin.io
Scopes | Koin
Scopes control the lifecycle of your dependencies. This guide covers how to define, create, and manage scopes.
insert-koin.io
'Android > Kotlin' 카테고리의 다른 글
| Koin 톺아보기 1편 (0) | 2026.02.09 |
|---|---|
| 코루틴은 왜 경량 스레드라 불리는가? (1) | 2025.12.19 |
| 안드로이드의 MessageQueue 이해하기 (0) | 2025.04.08 |
| [Flow 연산자] 생산자 총정리 (0) | 2025.03.31 |
| 코루틴의 구조적 동시성(Structured Concurrency) (0) | 2025.03.25 |