Android에서 dependency injection을 할 때 많이 사용하는 framework중 하나로 Dagger2가 있습니다. Dagger2는 Square에서 개발하여 현재 Google에서 유지보수를 하는 library로 compile time에 코드 생성을 통해 dependency injection을 이루어냅니다.
Dagger2에서 가장 중요한 두가지는 Module
과 Component
입니다.
우선 Dagger2를 사용하기 위해서 module의 build.gradle
에 아래와 같이 dependency를 추가해주어야 합니다.
plugins {
id 'kotlin-kapt'
}
dependencies {
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
}
Annotates a class that contributes to the object graph.
Dagger의 Module은 dependency를 가지고 있는 클래스로 dependency들을 이용하여 object graph를 구성합니다.
Module은 dependency를 제공하는 방법을 알고있거나 dependency를 특정 type으로 bind하는 방법을 알고있어야 합니다.
Annotates methods of a module to create a provider method binding.
@Provides
annotation은 Module내에서 dependency를 생성하는 method에 적용시켜야합니다. 필요시 다른 dependency를 parameter로 받아서 사용할 수 있습니다.
@Module
object NetworkModule {
...
// Function body에 ExchangeRateApi type의
// dependency를 어떻게 생성할지 정의
// 생성시 OkHttpClient가 필요하므로 parameter로 받음
// Parameter로 받을 OkHttpClient instance 또한
// Dagger에서 어떻게 제공할지 알고 있어야함
@Provides
@Singleton
fun provideExchangeRateApi(
okHttpClient: OkHttpClient
): ExchangeRateApi = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ExchangeRateApi::class.java)
}
Annotates abstract methods of a Module that delegate bindings.
@Binds
annotation은 parameter로 받은 dependency를 어떤 type으로 binding할지 정의한 abstract method에 적용시켜야 합니다.
@Module
interface RepositoryModule {
// ConversionRepositoryImpl을 ConversionRepository로 bind
@Binds
fun bindConversionRepository(repositoryImpl: ConversionRepositoryImpl): ConversionRepository
}
Annotates an interface or abstract class for which a fully-formed, dependency-injected implementation is to be generated from a set of modules().
Component
는 Dagger2가 Module
을 통해서 어떻게 dependency들을 주입할지 구현할 때 사용할 interface 혹은 abstract class입니다.
우선 @Component
를 interface에 적용하고 Component
에 필요한 dependency
들을 가진 Module들을 modules
로 지정해주어야 합니다. 그 다음, method로 어떤 dependency들을 제공할지 정의하면 됩니다.
@Component(
// 어떤 module들을 사용할지
modules = [
NetworkModule::class,
DispatcherModule::class,
ViewModelFactoryModule::class,
RepositoryModule::class
]
)
interface AppComponent {
// 어떤 dependency를 제공할지
fun provideViewModelFactory(): ViewModelProvider.Factory
}
위와같이 Component를 정의하고 build를 하게되면 Dagger prefix가 붙은 클래스가 자동으로 생성되어 사용할 수 있게 됩니다.
class ConversionApplication : Application() {
val appComponent: AppComponent by lazy {
// AppComponent앞에 Dagger가 붙은 DaggerAppComponent
DaggerAppComponent.create()
}
}
이제 생성한 AppComponent를 이용하여 필요한 dependency를 주입받을 수 있습니다.
class MainActivity : AppCompatActivity() {
private lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<MainViewModel> { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
// ViewModelProvider.Factory를 주입받음
viewModelFactory =
(application as ConversionApplication).appComponent.provideViewModelFactory()
super.onCreate(savedInstanceState)
setContentView(binding.root)
...
}
}
이전의 Pure DI에서 MainActivityContainer가 AppContainer에서 필요한 dependency를 가져왔던 것 처럼 Component에서 dependency를 제공하기 위해서 다른 Component에서 필요한 dependency를 가져와야 하는 경우도 존재합니다. 이를 가능하도록 하려면 Subcomponent
를 이용하여야 합니다.
A subcomponent that inherits the bindings from a parent Component or Subcomponent.
Subcomponent
는 부모 Component(혹은 Subcomponent)로부터 dependency를 상속받아서 사용할 수 있는 Component입니다.
Subcomponent는 Component와 마찬가지로 interface나 abstract class에 @Subcomponent
annotation을 적용하고 해당 Subcomponent에서 필요한 Module이 있다면 modules
로 지정해주면 됩니다.
@Subcomponent
interface MainActivityComponent {
fun provideViewModelFactory(): ViewModelProvider.Factory
}
여기에서는 ViewModelProvider.Factory를 AppComponent에서 가져올 것이므로 modules에 ViewModelFactoryModule
을 추가하지 않았습니다.
그리고 Subcomponent를 사용하기 위해서는 부모 Component가 Subcomponent를 어떻게 생성할지 알아야 합니다.
이를 위해서 Subcomponent 내부에 @Subcomponent.Factory
혹은 @Subcomponent.Builder
annotation을 적용한 interface를 통해 Subcomponent를 생성하는 method를 정의하고 해당 Factory 혹은 Builder를 부모 Component에서 가져올 수 있도록 하여야 합니다.
@Subcomponent
interface MainActivityComponent {
// MainActivityComponent를 생성할 때 사용할 Factory
@Subcomponent.Factory
interface Factory {
fun create(): MainActivityComponent
}
fun provideViewModelFactory(): ViewModelProvider.Factory
}
@Singleton
@Component(
modules = [
NetworkModule::class,
DispatcherModule::class,
ViewModelFactoryModule::class,
RepositoryModule::class
]
)
interface AppComponent {
// 부모 Component인 AppComponent에서
// MainActivityComponent를 생성할 때 사용할
// MainActivityComponent.Factory를 가져올 수 있도록 한다.
fun getMainActivityComponentFactory(): MainActivityComponent.Factory
}
마지막으로 부모 Component에서 사용하는 Module에 어떤 Subcomponent를 가지는지 명시해주면 됩니다.
이를 위한 새로운 Module을 생성하여도 되지만 ViewModelProvider.Factory가 MainActivity에서 사용되므로 ViewModelFactoryModule
에 subcomponents
로 MainActivityComponent를 명시해주었습니다.
// 모듈이 AppComponent에서 사용되고
// AppComponent가 MainActivityComponent를 Subcomponent로 가진다.
@Module(subcomponents = [MainActivityComponent::class])
interface ViewModelFactoryModule {
@Binds
fun bindMainViewModelFactory(factory: MainViewModel.Factory): ViewModelProvider.Factory
}
이제 MainActivity에서 MainActivityComponent를 생성하여 원하는 dependency를 주입받을 수 있게 됩니다.
class MainActivity : AppCompatActivity() {
private lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<MainViewModel> { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
// MainActivityComponent에서 dependency를 주입받음
viewModelFactory = (application as ConversionApplication).appComponent
.getMainActivityComponentFactory()
.create()
.provideViewModelFactory()
super.onCreate(savedInstanceState)
setContentView(binding.root)
...
}
}
Identifies injectable constructors, methods, and fields.
이제 Module과 Component를 이용하여 필요한 dependency를 주입받을 수 있게 되었습니다.
하지만 주입받을 dependency가 많을 경우 Component에 provide하는 method를 정의하는 것과 method를 호출하여 dependency들을 property로 지정하는 것이 번거로울 수 있습니다.
@Inject
를 이용하여 필요한 dependency들을 Dagger가 자동으로 주입하도록 할 수 있습니다.
@Subcomponent
interface MainActivityComponent {
@Subcomponent.Factory
interface Factory {
fun create(): MainActivityComponent
}
// MainActivity를 parameter로 받아서
// @Inject가 적용된 dependency를 주입
fun inject(activity: MainActivity)
}
우선 Component에 DI를 할 클래스를 parameter로 받는 method를 정의해주어야 합니다. MainActivityComponent의 경우 MainActivity에 DI를 할 것이므로 위와 같이 정의를 해주었습니다.
class MainActivity : AppCompatActivity() {
// 주입 받을 dependency에 @Inject annotation을 적용
// Dagger에서 코드 생성으로 주입할 수 있도록 public
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<MainViewModel> { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
(application as ConversionApplication).appComponent
.getMainActivityComponentFactory()
.create()
.inject(this)
super.onCreate(savedInstanceState)
setContentView(binding.root)
...
}
}
이제 MainActivityComponent에서 주입받아야 하는 dependency에 @Inject
annotation을 적용해주고 inject method를 호출하기만 하면 됩니다.
[1] https://github.com/ErroredPasta/Dependency-Injection
[1] "Dagger," Dagger, last modified n.d., accessed Jun 15, 2022, https://dagger.dev/dev-guide/.
[2] "Using Dagger in Android apps," Android Developers, last modified Dec 03, 2021, accessed Jun 15, 2022, https://developer.android.com/training/dependency-injection/dagger-android.