어노테이션을 이용한 정의
Koin 어노테이션을 사용하면 일반적인 Koin DSL과 동일한 종류의 정의를 어노테이션으로 선언할 수 있습니다. 클래스에 필요한 어노테이션을 태깅하기만 하면, 모든 것이 자동으로 생성됩니다!
예를 들어, DSL 선언인 single { MyComponent(get()) }와 동일한 작업은 다음과 같이 @Single을 태깅하는 것만으로 처리할 수 있습니다:
@Single
class MyComponent(val myDependency : MyDependency)Koin 어노테이션은 Koin DSL과 동일한 시맨틱(semantics)을 유지합니다. 다음 정의들을 사용하여 컴포넌트를 선언할 수 있습니다:
@Single- 싱글톤 인스턴스 (DSL의single { }로 선언됨)@Factory- 팩토리 인스턴스. 인스턴스가 필요할 때마다 매번 새로 생성됩니다. (DSL의factory { }로 선언됨)@KoinViewModel- 안드로이드 ViewModel 인스턴스 (DSL의viewModel { }로 선언됨)@KoinWorker- 안드로이드 Worker Workmanager 인스턴스 (DSL의worker { }로 선언됨)
스코프(Scope)에 대해서는 스코프 선언하기 섹션을 확인하세요.
최상위 함수 어노테이션(Annotated Top-Level Functions)
어노테이션은 클래스뿐만 아니라 최상위 함수(top-level functions)에서도 작동합니다. 이는 외부 라이브러리나 빌더 패턴에서 인스턴스를 제공할 때 유용합니다. 최상위 함수는 클래스와 마찬가지로 @ComponentScan에 의해 감지됩니다:
import org.koin.core.annotation.Singleton
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named
// Room 데이터베이스 인스턴스 제공
@Singleton
fun provideDatabase(context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "my-db").build()
// JSON 직렬화 도구 제공
@Singleton
fun provideJson(): Json = Json { ignoreUnknownKeys = true }
// HTTP 클라이언트 제공
@Singleton
@Named("api")
fun provideHttpClient(json: Json): HttpClient = HttpClient { install(ContentNegotiation) { json(json) } }파라미터는 DI 컨테이너에서 자동으로 해결(resolve)됩니다. 한정자(Qualifier, @Named, 커스텀 @Qualifier)는 함수와 파라미터 모두에 적용할 수 있습니다.
모듈 함수 (제공자 함수)
@Module 클래스 내부에서 @Singleton, @Factory 등의 어노테이션이 붙은 함수는 Dagger/Hilt의 @Provides와 유사한 제공자 함수(provider functions) 역할을 합니다:
import org.koin.core.annotation.Module
import org.koin.core.annotation.Singleton
@Module
internal object DatabaseModule {
@Singleton
fun providesDatabase(context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "my-db").build()
}
@Module(includes = [DatabaseModule::class])
class DaosModule {
@Singleton
fun providesTopicDao(database: AppDatabase): TopicDao = database.topicDao()
@Singleton
fun providesNewsDao(database: AppDatabase): NewsResourceDao = database.newsResourceDao()
}이는 직접 어노테이션을 추가할 수 없는 외부 라이브러리(Room, Retrofit, OkHttp 등)를 래핑할 때 사용하는 패턴입니다.
커스텀 한정자 어노테이션 (Custom Qualifier Annotations)
@Named 외에도 @Qualifier를 사용하여 파라미터가 있는 커스텀 한정자 어노테이션을 만들 수 있습니다:
import org.koin.core.annotation.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Dispatcher(val niaDispatcher: NiaDispatchers)
enum class NiaDispatchers { Default, IO }제공자 함수와 주입 지점(injection points)에서 이를 사용합니다:
import org.koin.core.annotation.Module
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Singleton
@Module
@Configuration
class DispatchersModule {
@Singleton
@Dispatcher(NiaDispatchers.IO)
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
@Singleton
@Dispatcher(NiaDispatchers.Default)
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@Singleton
fun providesApplicationScope(
@Dispatcher(NiaDispatchers.Default) dispatcher: CoroutineDispatcher,
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
}커스텀 한정자는 컴파일 타임에 검증됩니다. 제공자의 한정자와 주입 지점의 한정자가 일치하지 않으면 빌드 에러가 발생합니다.
Kotlin 멀티플랫폼을 위한 ViewModel
@KoinViewModel 어노테이션은 통합된 koin-core-viewmodel API를 사용하여 ViewModel을 생성하며, Kotlin 멀티플랫폼 호환성을 제공합니다.
@KoinViewModel
class UserViewModel(val repository: UserRepository) : ViewModel()이는 안드로이드와 Compose 멀티플랫폼 모두와 호환되는 viewModel 정의를 생성합니다.
자동 또는 특정 바인딩
컴포넌트를 선언할 때, 감지된 모든 "바인딩"(연관된 상위 타입)이 이미 준비됩니다. 예를 들어, 다음과 같은 정의가 있다면:
@Single
class MyComponent(val myDependency : MyDependency) : MyInterfaceKoin은 MyComponent 컴포넌트가 MyInterface에도 연결되어 있음을 선언합니다. DSL로는 single { MyComponent(get()) } bind MyInterface::class와 동일합니다.
Koin이 자동으로 감지하도록 하는 대신, binds 어노테이션 파라미터를 사용하여 실제로 바인딩하려는 타입을 직접 지정할 수도 있습니다:
@Single(binds = [MyBoundType::class])Nullable 의존성
컴포넌트가 nullable 의존성을 사용하더라도 자동으로 처리되니 걱정하지 마세요. 정의 어노테이션을 그대로 사용하면 Koin이 어떻게 처리할지 판단합니다:
@Single
class MyComponent(val myDependency : MyDependency?)생성된 DSL은 single { MyComponent(getOrNull()) }과 동일합니다.
참고: 이는 주입된 파라미터(injected Parameters)와 프로퍼티(properties)에도 적용됩니다.
@Named를 사용한 한정자(Qualifier)
동일한 타입에 대한 여러 정의를 구분하기 위해 @Named 어노테이션을 사용하여 정의에 "이름"(한정자라고도 함)을 추가할 수 있습니다:
@Single
@Named("InMemoryLogger")
class LoggerInMemoryDataSource : LoggerDataSource
@Single
@Named("DatabaseLogger")
class LoggerLocalDataSource(private val logDao: LogDao) : LoggerDataSource의존성을 해결(resolve)할 때는 named 함수와 함께 한정자를 사용하면 됩니다:
val logger: LoggerDataSource by inject(named("InMemoryLogger"))커스텀 한정자 어노테이션을 만드는 것도 가능합니다. 이전 예제를 사용하면 다음과 같습니다:
@Named
annotation class InMemoryLogger
@Named
annotation class DatabaseLogger
@Single
@InMemoryLogger
class LoggerInMemoryDataSource : LoggerDataSource
@Single
@DatabaseLogger
class LoggerLocalDataSource(private val logDao: LogDao) : LoggerDataSourceval logger: LoggerDataSource by inject(named<InMemoryLogger>())@InjectedParam을 사용한 주입된 파라미터
생성자 멤버를 "주입된 파라미터(injected parameter)"로 태깅할 수 있습니다. 이는 의존성 해결을 요청할 때 해당 의존성이 그래프에 전달됨을 의미합니다.
예를 들어:
@Single
class MyComponent(@InjectedParam val myDependency : MyDependency)그런 다음 MyComponent를 호출할 때 MyDependency의 인스턴스를 전달할 수 있습니다:
val m = MyDependency()
// MyDependency를 전달하며 MyComponent 해결
koin.get<MyComponent> { parametersOf(m) }생성된 DSL은 single { params -> MyComponent(params.get()) }과 동일합니다.
지연 의존성 주입 - Lazy<T>
Koin은 지연 의존성을 자동으로 감지하고 해결할 수 있습니다. 예를 들어, 여기서는 LoggerDataSource 정의를 지연해서 해결하고자 합니다. 다음과 같이 Kotlin의 Lazy 타입을 사용하기만 하면 됩니다:
@Single
class LoggerInMemoryDataSource : LoggerDataSource
@Single
class LoggerAggregator(val lazyLogger : Lazy<LoggerDataSource>)내부적으로는 get() 대신 inject()를 사용하는 DSL을 생성합니다:
single { LoggerAggregator(inject()) }의존성 리스트 주입 - List<T>
Koin은 의존성 리스트를 자동으로 감지하고 해결할 수 있습니다. 예를 들어, 여기서는 모든 LoggerDataSource 정의를 해결하고자 합니다. 다음과 같이 List Kotlin 타입을 사용하기만 하면 됩니다:
@Single
@Named("InMemoryLogger")
class LoggerInMemoryDataSource : LoggerDataSource
@Single
@Named("DatabaseLogger")
class LoggerLocalDataSource(private val logDao: LogDao) : LoggerDataSource
@Single
class LoggerAggregator(val datasource : List<LoggerDataSource>)내부적으로는 getAll() 함수를 사용하는 DSL을 생성합니다:
single { LoggerAggregator(getAll()) }@Property를 사용한 프로퍼티
정의에서 Koin 프로퍼티를 해결하려면 생성자 멤버에 @Property를 태깅하면 됩니다. 어노테이션에 전달된 값을 통해 Koin 프로퍼티를 해결하게 됩니다:
@Factory
public class ComponentWithProps(
@Property("id") public val id : String
)생성된 DSL은 factory { ComponentWithProps(getProperty("id")) }와 동일합니다.
@PropertyValue - 기본값이 있는 프로퍼티 (1.4부터)
Koin 어노테이션은 @PropertyValue 어노테이션을 통해 코드에서 직접 프로퍼티의 기본값을 정의할 수 있는 기능을 제공합니다. 예제를 살펴보겠습니다:
@Factory
public class ComponentWithProps(
@Property("id") public val id : String
){
public companion object {
@PropertyValue("id")
public const val DEFAULT_ID : String = "_empty_id"
}
}생성된 DSL은 factory { ComponentWithProps(getProperty("id", ComponentWithProps.DEFAULT_ID)) }와 동일합니다.
JSR-330 호환 어노테이션
Koin 어노테이션은 koin-jsr330 모듈을 통해 JSR-330(Jakarta Inject) 호환 어노테이션을 제공합니다. 이 어노테이션들은 Hilt, Dagger 또는 Guice와 같은 다른 JSR-330 호환 프레임워크에서 마이그레이션하는 개발자들에게 특히 유용합니다.
설정
프로젝트에 koin-jsr330 의존성을 추가하세요:
dependencies {
implementation "io.insert-koin:koin-jsr330:$koin_version"
}사용 가능한 JSR-330 어노테이션
@Singleton (jakarta.inject.Singleton)
JSR-330 표준 싱글톤 어노테이션으로, Koin의 @Single과 동일합니다:
import jakarta.inject.Singleton
@Singleton
class DatabaseService이는 Koin에서 싱글톤 인스턴스라는 @Single과 동일한 결과를 생성합니다.
@Named (jakarta.inject.Named)
문자열 기반 한정자를 위한 JSR-330 표준 한정자 어노테이션입니다:
import jakarta.inject.Named
import jakarta.inject.Singleton
@Singleton
@Named("inMemory")
class InMemoryCache : Cache
@Singleton
@Named("redis")
class RedisCache : Cache@Inject (jakarta.inject.Inject)
JSR-330 표준 주입 어노테이션입니다. Koin 어노테이션은 명시적인 생성자 마킹이 필요하지 않지만, JSR-330 호환성을 위해 @Inject를 사용할 수 있습니다:
import jakarta.inject.Inject
import jakarta.inject.Singleton
@Singleton
class UserService @Inject constructor(
private val repository: UserRepository
)@Qualifier (jakarta.inject.Qualifier)
커스텀 한정자 어노테이션을 만들기 위한 메타 어노테이션입니다:
import jakarta.inject.Qualifier
@Qualifier
annotation class Database
@Qualifier
annotation class Cache
@Singleton
@Database
class DatabaseConfig
@Singleton
@Cache
class CacheConfig@Scope (jakarta.inject.Scope)
커스텀 스코프 어노테이션을 만들기 위한 메타 어노테이션입니다:
import jakarta.inject.Scope
@Scope
annotation class RequestScoped
// Koin의 스코프 시스템과 함께 사용
@Scope(name = "request")
@RequestScoped
class RequestProcessor혼합 사용
동일한 프로젝트에서 JSR-330 어노테이션과 Koin 어노테이션을 자유롭게 혼합하여 사용할 수 있습니다:
// JSR-330 스타일
@Singleton
@Named("primary")
class PrimaryDatabase : Database
// Koin 스타일
@Single
@Named("secondary")
class SecondaryDatabase : Database
// 동일 클래스 내 혼합 사용
@Factory
class DatabaseManager @Inject constructor(
@Named("primary") private val primary: Database,
@Named("secondary") private val secondary: Database
)프레임워크 마이그레이션의 이점
JSR-330 어노테이션을 사용하면 프레임워크 마이그레이션 시 다음과 같은 몇 가지 장점이 있습니다:
- 익숙한 API: Hilt, Dagger 또는 Guice를 사용하던 개발자들이 익숙한 어노테이션을 사용할 수 있습니다.
- 점진적 마이그레이션: 기존의 JSR-330 어노테이션이 적용된 코드를 최소한의 수정으로 그대로 사용할 수 있습니다.
- 표준 준수: JSR-330을 따르면 의존성 주입 표준과의 호환성을 보장합니다.
- 팀 온보딩: 다른 DI 프레임워크에 익숙한 팀원들이 더 쉽게 적응할 수 있습니다.
INFO
Koin의 JSR-330 어노테이션은 해당 Koin 어노테이션과 동일한 기본 DSL을 생성합니다. JSR-330과 Koin 어노테이션 중 무엇을 선택할지는 순전히 스타일의 문제이며, 팀의 선호도나 마이그레이션 요구 사항에 따라 결정하면 됩니다.
