[DI] Dagger2 - Scope, Qualifier, Provider, Lazy

ErroredPasta·2022년 6월 19일
0

Dependency Injection

목록 보기
4/6

Scope

Identifies scope annotations. A scope annotation applies to a class containing an injectable constructor and governs how the injector reuses instances of the type.

Dependency injection을 할 때, 필요할 때 마다 생성하는 것이 아니라 동일한 dependency를 주입해주어야 할 때가 있습니다. 그런 경우에 Scope를 정의하여 동일한 Component객체내에서 같은 객체를 재사용 하도록 할 수 있습니다.

@Scope annotation은 직접 Component나 dependency에 적용하는 것이 아니라 사용자가 원하는 Scope annotation 클래스를 정의할 때 사용하는 것입니다.

// ActivityComponent에서 사용할 Scope annotation class
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

위와 같이 Scope를 정의하였으면 적용해야할 dependency와 Component에 적용을 해주어야 합니다.

@Module
object ActivityModule {
	
    // Dependency를 ActivityScope가 적용된 Component에서
    // 단일 객체를 재사용하도록 지정
    @Provides
    @ActivityScope
    fun provideMyDependency(): MyDependency = MyDependency()
}
// Component에서도 ActivityScope를 적용
@Subcomponent(modules = [ActivityModule::class])
@ActivityScope
interface MainActivityComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): MainActivityComponent
    }

    fun inject(activity: MainActivity)
}

이렇게 MainActivityComponent와 적용이 필요한 dependency에 ActivityScope를 적용하면 MyDependency 객체는 동인한 MainActivityComponent객체에서 provide할 때 동일한 객체를 재사용하여 제공하게 됩니다.

Qualifier

Identifies qualifier annotations.

하나의 type에 대해 다른 객체를 주입해야 하는 경우가 발생할 수 있습니다. 하지만 하나의 type에 대해 두가지 이상의 dependency를 provide하는 방법이 존재할 경우 bound multiple times에러가 발생하게 됩니다.

예를 들어 Coroutine의 dispatcher를 provide하는 module이 아래와 같이 정의되어 있다고 가정해봅시다.

@Module
object DispatcherModule {

    @Provides
    fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

    @Provides
    fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}

하나는 IO dispatcher를 provide하고 하나는 Default dispatcher를 provide합니다. 그러나 두 dispatcher의 type은 같아서 빌드시 에러가 나게 됩니다.

error: [Dagger/DuplicateBindings] kotlinx.coroutines.CoroutineDispatcher is bound multiple times

동일한 type에 대해 dependency를 구분지어주기 위해서 Qualifier annotation 클래스를 정의해주어야 합니다.

// IO dispatcher를 구분하기 위한 qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher

이제 정의해준 qualifier를 provide와 주입받을 곳에 적용해주면 됩니다.

@Module
object DispatcherModule {

    @Provides
    @IoDispatcher
    fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

    @Provides
    fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}
class ConversionRepositoryImpl @Inject constructor(
    private val api: ExchangeRateApi,
    // IO dispatcher를 주입받음
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : ConversionRepository {
	...
}

위의 repository에서 @IoDispatcher annotation을 제거하게 되면 IO dispatcher가 아니라 Default dispatcher를 주입받게 됩니다.

Provider와 Lazy

T라는 type의 dependency를 주입받을 때 T라는 type으로만 주입받을 수 있는 것이 아니라 Provider<T> 혹은 Lazy<T>로도 주입받을 수 있습니다.

Provider<T>

Provides instances of T.

Provider<T>는 type T에 대해서 다수의 객체를 생성할 때 사용됩니다. 대표적으로 factory에서 생성할 객체마다 다른 dependency를 주입해야 할 때 사용할 수 있습니다.

class Factory @Inject constructor(
    private val provider: Provider<MainViewModel>
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return provider.get() as T
    }
}

위는 MainViewModel을 생성할 때 사용할 MainViewModel.Factory입니다. 여기서 Factory가 Provider<MainViewModel>을 주입받아서 새로운 MainViewModel을 생성할 때 사용합니다.

Provider는 무작정 새로운 instance를 생성하는 것이 아니라 Scope에 맞게 생성합니다.

@Module
object ActivityModule {
	
    // Dependency를 ActivityScope가 적용된 Component에서
    // 단일 객체를 재사용하도록 지정
    @Provides
    @ActivityScope // @ActivityScope는 MainActivityComponent에도 적용
    fun provideMyDependency(): MyDependency = MyDependency()
}
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var myDependencyProvider: Provider<MyDependency>

    @FlowPreview
    override fun onCreate(savedInstanceState: Bundle?) {
        (application as ConversionApplication).appComponent
            .getMainActivityComponentFactory()
            .create(this)
            .inject(this)

        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        // 아래의 둘은 같은 객체를 갖게 된다.
        myDependencyProvider.get()
        myDependencyProvider.get()
    }
}

위의 예제에서 myDependencyProvider는 get()을 호출하였을 때 같은 객체를 return하게 됩니다. 그 이유는 MyDependency와 MainActivityComponent에 @ActivityScope가 적용이 되었으므로 동일한 Component 객체내에서 같은 MyDependency를 반환하기 때문입니다.

만약 MyDependency에 아무런 Scope가 적용되지 않았다면 get()을 호출할 때 마다 새로운 객체를 생성해서 return하게 됩니다.

Lazy<T>

A handle to a lazily-computed value. Each Lazy computes its value on the first call to get() and remembers that same value for all subsequent calls to get().

Lazy<T>Kotlin의 lazy와 비슷하게 type T의 객체를 필요할 때 생성하기 위해서 사용됩니다. Provider와 마찬가지로 get()을 이용하여 객체를 가져올 수 있습니다.

Provider와 차이점

Lazy와 Provider의 차이점은 Provider의 경우 get()을 호출할 때 마다 Scope에 맞게 새로운 객체를 생성합니다. 하지만 Lazy의 경우 처음 한번만 객체를 생성한 이후에는 생성한 객체를 memory에 cache하고 해당 객체를 return합니다.

Reference

[1] "Using Scopes," Android Developers, last modified n.d., accessed Jun 19, 2022, https://developer.android.com/codelabs/android-dagger#8.

[2] "@Provides annotation and Qualifiers," Android Developers, last modified n.d., accessed Jun 19, 2022, https://developer.android.com/codelabs/android-dagger#14.

profile
Hola, Mundo

0개의 댓글