Developing Myself Everyday
Published 2026. 2. 12. 20:57
Koin 톺아보기 2편 Android/Kotlin

 

 

이번 게시글에서는 저번 게시글에 이어서 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

 

 

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!