DI 프레임워크로 Hilt를 사용한 경험이 있다. 3. Dagger와 Hilt에서 지원하는 Annotation들이 어떤게 있는지 알고있다. • @Binds, @Provides, @HiltAndroidApp, @AndroidEntryPoint, .. 4. ViewModel이 생성되는 과정을 알고있다.
의존성은 수정하기 어렵게 만들기 때문에 필요한 의존성만 유지하면서 변경을 방해하는 의존성을 제거하는게 객체지향 설계의 핵심 • 추상화 의존을 통해 컴파일 타임 의존성과 런타임 의존성을 분리해 컨텍스트 독립성을 확보해야 한다. 의존성 주입(DI)은 외부 객체의 의존성을 제공하는 방법이며, 외부 객체에 대한 생성과 사용에 대한 관심을 분리하는 것에 의의가 있다.
Team { override val name: String = "Android팀" } class KmpTeam : Team { override val name: String = "KMP팀" } class MashUp( private val team: Team, ) { fun printTeam() { Log.d("MashUp", "팀이름: ${team.name}") } } MashUp이 의존하는 Team에 대한 추상화 Session 예시) 매쉬업 안드팀을 KMP팀으로 교체하기
val team: Team, ) { fun printTeam() { Log.d("MashUp", "팀이름: ${team.name}") } } fun main() { val team = AndroidTeam() val mashUp = MashUp(team) mashUp.printTeam() } • 결국 어디선가 객체를 생성해서 수동 주입 해야함 • Hilt는 주입할 객체를 생성하기 위해 @HiltAndroidApp 어노테이션을 사용 • 컴파일 타임에 필요한 의존성을 만들어놓고 Application#onCreate시점에 의존성 그래프를 초기화함 DI 프레임워크를 사용하는 이유다!
KSP)를 기반으로 동작 • 중간 여러 단계를 거쳐 generated code가 생성됨 • Metro는 Kotlin Compiler에 의해 동작, IR과정을 거쳐 .class파일 생성 (KAPT나 KSP가 없어도 됨) Hilt (KSP) Metro Dagger, Hilt와 유사한 내용도 많으니 어떤게 비슷한지 잘 비교해봅시다 !
의존성에 따라 컴파일 타임에 구성 : Compiler 내장함수를 통해 구성하게 됨 (createGraph) • Dagger에서는 @Component와 동일한 역할 : @Module에서 생성된 의존성을 주입하기 위한 브릿지 인터페이스 역할 @DependencyGraph interface AppGraph { val app: WeatherApp } fun main() { val app = createGraph<AppGraph>.app }
의존성에 따라 컴파일 타임에 구성 : Compiler 내장함수를 통해 구성하게 됨 (createGraph) • Dagger에서는 @Component와 동일한 역할 : @Module에서 생성된 의존성을 주입하기 위한 브릿지 인터페이스 역할 • 그래프에 초기 입력 값이 필요한 경우 Factory 선언 가능 : 이 때는 createGraphFactory를 통해 그래프 구성 @DependencyGraph interface AppGraph { val app: WeatherApp @DependencyGraph.Factory fun interface Factory { fun create(@Provides useMetric: Boolean): AppGraph } } fun main() { val app = createGraphFactory<AppGraph.Factory>() .create(useMetric = true) .app }
의존성에 따라 컴파일 타임에 구성 : Compiler 내장함수를 통해 구성하게 됨 (createGraph) • Dagger에서는 @Component와 동일한 역할 : @Module에서 생성된 의존성을 주입하기 위한 브릿지 인터페이스 역할 • 그래프에 초기 입력 값이 필요한 경우 Factory 선언 가능 : 이 때는 createGraphFactory를 통해 그래프 구성 • 입력 값 매개변수에는 @Provides나 @Includes가 있어야 함 @DependencyGraph interface AppGraph { val app: WeatherApp @DependencyGraph.Factory fun interface Factory { fun create(@Provides useMetric: Boolean): AppGraph } } fun main() { val app = createGraphFactory<AppGraph.Factory>() .create(useMetric = true) .app }
담는 컨테이너 제공 • enum을 제외한 모든 클래스 유형에 대해 바인딩 가능함 @DependencyGraph interface AppGraph { val app: WeatherApp @DependencyGraph.Factory fun interface Factory { fun create(@Includes subGraph: ForecastGraph): AppGraph } @DependencyGraph interface ForecastGraph { val forecast: String @Provides fun provideForecast(): String = "Sunshine!" } }
• 슈퍼 타입에 정의하면 여러 그래프에서 재사용할 수 있음 ◦ 단, override 형태의 바인딩 제공은 compile error! interface CacheProvider { @Provides fun provideCache(): Cache = Cache() } @DependencyGraph interface AppGraph : CacheProvider { val app: WeatherApp // DO NOT // override fun provideCache(): Cache = FakeCache() }
• 슈퍼 타입에 정의하면 여러 그래프에서 재사용할 수 있음 ◦ 단, override 형태의 바인딩 제공은 compile error! • 동일 타입의 여러 바인딩을 제공할 경우 @Qualifier 사용 가능 ◦ 간편하게 @Named를 통해 Type Key로 활용 가능 @Qualifier annotation class RealCache() interface CacheProvider { @RealCache @Provides fun provideRealCache(): Cache = RealCache() @Named("TestableCache") @Provides fun provideTestableCache(): Cache = TestableCache() } @DependencyGraph interface AppGraph : CacheProvider { val app: WeatherApp }
• 여러 생성자에 대한 바인딩이 필요할 경우 클래스 바인딩 형태로 사용 가능 @Inject class HttpClient(private val cache: Cache) // ========================================== class HttpClient( private val cache: Cache, private val timeout: Duration, ) { @Inject constructor(cache: Cache): this(cache, 30.seconds) }
런타임 의존성이 필요한 경우 사용 • 모든 바인딩이 반드시 Graph상에 필요한게 아닌, 런타임에 동적으로 제공해 인스턴스를 제공할 수 있음 @AssistedInject class HttpClient( @Assisted val timeout: Duration, val cache: Cache ) { @AssistedFactory fun interface Factory { fun create(timeout: Duration): HttpClient } } @Inject class ApiClient(httpClientFactory: HttpClient.Factory) { private val httpClient = httpClientFactory.create(30.seconds) }
Factory { fun create(@Provides team: Team): AppGraph } } fun main() { val mashUp = createGraphFactory<AppGraph.Factory>() .create(team = KMPTeam()) .mashUp mashUp.printTeam() } interface Team { val name: String } class AndroidTeam : Team { override val name: String get() = "Android Team" } class KMPTeam : Team { override val name: String get() = "KMP Team" } @Inject class MashUp(private val team: Team) { fun printTeam() { Log.d("MashUp", "팀이름 : ${team.name}") } } 🤔 그렇다면 KMP 팀은 어떻게 구성될 수 있나? Session
동안 하나의 인스턴스만 생성되도록 보장하는 마커 어노테이션 • @SingleIn : Metro의 표준 스코프 어노테이션 @Target(AnnotationTarget.ANNOTATION_CLASS) public annotation class Scope @Scope public annotation class SingleIn(val scope: KClass<*>)
함 ◦ 범위가 지정되지 않은 그래프가 범위가 지정된 바인딩에 접근하는 것은 ERROR ◦ 범위가 일치하지 않는 바인딩에 접근하는 것도 ERROR @DependencyGraph interface AppGraph { // This is an error! val exampleClass: ExampleClass } @SingleIn(AppScope::class) @Inject class ExampleClass // ================================ @SingleIn(AppScope::class) @DependencyGraph interface AppGraph { // This is an error! val exampleClass: ExampleClass } @SingleIn(UserScope::class) @Inject class ExampleClass
interface AppGraph { // ... } • @DependencyGraph.scope를 지정하면 지정된 스코프 내에서의 하나의 인스턴스만 제공하는 것으로 간주됨 • @SingleIn과 함께 지정하는 것은 중복 @SingleIn(AppScope::class) • Dagger에서 사용하는 @Singleton과 동일한 의미를 가짐 ◦ 동일하게 Double-Check와 synchronized를 통해 인스턴스 일관성 확보 @Singleton
Factory { fun create(@Provides team: Team): AppGraph } } @Inject @SingleIn(AppScope::class) class MashUp(private val team: Team) { fun printTeam() { Log.d("MashUp", "팀이름 : ${team.name}") } } 🤔 그렇다면 KMP 팀은 어떻게 구성될 수 있나? Session
때 사용 • 추상 함수 또는 확장 함수 형태로 바인딩 제공 가능 interface RepositoryGraph { @Binds fun bindRepository(impl: RepositoryImpl): Repository @Binds val RepositoryImpl.bind: Repository } @Inject class UseCase(val repository: Repository)
때 사용 • 추상 함수 또는 확장 함수 형태로 바인딩 제공 가능 • 멀티 모듈 환경에서 자동으로 바인딩을 탐색/제공하기 위해서는 @ContributesBinding 사용 // :data @ContributesBinding(DataScope::class) interface RepositoryGraph { @Binds fun bindRepository(impl: RepositoryImpl): Repository @Binds val RepositoryImpl.bind: Repository } // :domain @Inject class UseCase(val repository: Repository)
• @Binds를 통해 Cache 타입을 자동으로 검색해서 바인딩을 제공할 수 있음 @DependencyGraph(scope = AppScope::class) interface AppGraph { private val cache: Cache @Binds val CacheImpl.bind: Cache } @Inject class CacheImpl(..): Cache
형태로 스코프에 제공하는 데 사용 @DependencyGraph(scope = AppScope::class) interface AppGraph { val cache: Cache } @ContributesBinding(AppScope::class) @Inject class CacheImpl(..): Cache
형태로 스코프에 제공하는 데 사용 • 상위 타입이 여러 개인 경우 @Contributes.binding을 통해 명시적으로 정의할 수 있음 @DependencyGraph(scope = AppScope::class) interface AppGraph { val cache: Cache } @ContributesBinding( scope = AppScope::class, binding = binding<Cache>(), ) @Inject class CacheImpl(..): Cache, Closable
처리 시점에 최종 병합하여 바인딩을 제공하는데 사용 • 멀티 모듈 프로젝트에서 각 모듈별로 독립된 그래프를 생성할 수 있음 @ContributesTo(AppScope::class) interface NetworkProviders { @Provides fun provideHttpClient(): HttpClient }
처리 시점에 최종 병합하여 바인딩을 제공하는데 사용 • 멀티 모듈 프로젝트에서 각 모듈별로 독립된 그래프를 생성할 수 있음 • 여러 스코프에 동일하게 제공하는 것도 가능 @ContributesTo(AppScope::class) interface NetworkProviders { @Provides fun provideHttpClient(): HttpClient } @ContributesTo(AppScope::class) @ContributesTo(LoggedInScope::class) interface NetworkProviders { @Provides fun provideHttpClient(): HttpClient }