[DI] Pure Dependency Injection

ErroredPasta·2022년 6월 11일
0

Dependency Injection

목록 보기
2/6

Pure dependency injection은 어떠한 library의 도움 없이 직접 dependency injection을 하는 것을 말합니다.
Dependency injection을 할 때 클래스 내부에 필요한 dependency의 생성을 외부로 전가하게 됩니다. 클래스의 생성을 계속 외부로 전가하는 것은 불가능하며 언젠가는 생성을 해야합니다.

Composition Root

A Composition Root is a single, logical location in an application
where modules are composed together.

Composition root은 application에서 module이 구성되는 단일의 logical한 장소입니다. 그러므로 필요한 클래스의 생성은 composition root에서 이루어져야 합니다. 그렇게 함으로써 클래스간의 관계를 관리하기 용이해집니다.
Composition root은 application의 entry point에 최대한 가까이에 위치해야합니다.

Entry Point

Entry point는 진입점이라는 의미로 프로그램이 시작되는 지점을 의미합니다. Android에서는 유저가 생성하지 못하는 Application이나 Activity, Fragment같은 component에 DI를 하기위해서 entry point에서 최대한 가까운 곳(onCreate 등)에서 필요한 dependency들을 주입받아야 합니다.
Dagger Hilt에서는 아래와 같은 component들에 대해서 기본적으로 DI를 관리합니다.

예시[EL1]

보통 Activity에서 자신의 상태를 나타내고 business logic을 처리하기 위해 ViewModel을 생성하여야 합니다. 그리고 ViewModel을 생성하기 위해서는 Repository 혹은 Use case가 필요하고 또 Repository를 생성하기위해서는 DataSource가 필요하게 됩니다.
Repository, Use case등은 app 전반에 걸쳐서 사용되므로 AppContainer에 필요한 dependency들을 정의하면 아래와 같습니다.

class AppContainer {

	// [No getter dependency]
    // Container와 함께 생명주기를 같이할 dependency
    // AppContainer는 Application에서 생성되므로
    // singleton으로 처리할 dependency이다.
    private val okHttpClient: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            }).build()
    }

    private val exchangeRateApi: ExchangeRateApi by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ExchangeRateApi::class.java)
    }

    private val ioDispatcher: CoroutineDispatcher get() = Dispatchers.IO

	// [Getter dependency]
    // 생명주기를 관리하지 않을 dependency
    // 필요할 때마다 새로운 객체를 생성한다.
    private val conversionRepository: ConversionRepository
        get() = ConversionRepositoryImpl(
            api = exchangeRateApi,
            ioDispatcher = ioDispatcher
        )

    private val mainViewModelProvider: Provider<MainViewModel>
        get() = Provider { MainViewModel(repository = conversionRepository) }

    val viewModelFactory: ViewModelProvider.Factory
        get() = MainViewModel.Factory(provider = mainViewModelProvider)

    companion object {
        private const val BASE_URL = "https://v6.exchangerate-api.com/"
    }
}

여기에서 getter가 있는 dependency도 있고 없는 dependency도 있는 것을 볼 수 있습니다. Getter를 지정해준 dependency는 필요할 때마다 새롭게 생성되고 없는 dependency는 AppContainer와 함께 수명을 함께하며 계속 재사용됩니다. AppContainer는 Application에서 사용되므로 getter가 없는 dependency는 singleton으로 처리할 것들입니다.

이렇게 AppContainer를 정의하였으면 Application에서 해당 container를 가지도록 해주면 됩니다.

class ConversionApplication : Application() {
    val appContainer: AppContainer by lazy {
        AppContainer()
    }
}

appContainer를 lateinit var로 하여 onCreate()에서 생성해주어도 됩니다. 단, lateinit var로 할 경우 setter를 private하게하여 다른 component에서 변경할 수 없도록 하는 것이 좋습니다.

이제 남은 일은 MainActivity에서 필요한 dependency를 주입받는 것입니다. MainActivity에서 현재 ViewModel을 생성하기 위해 ViewModelProvider.Factory만 필요하여 AppContainer에서 바로 사용하여도 되지만 MainActivity에서 사용할 container를 정의하고 해당 container를 이용하여 dependency를 주입 받아보겠습니다.

MainActivityContainer에서 필요한 dependency중 AppContainer에서 가져올 수 있는 것들은 가져오도록 하면 아래와 같이됩니다.

class MainActivityContainer(
    private val appContainer: AppContainer
) {

    // get from appContainer
    val viewModelFactory: ViewModelProvider.Factory get() = appContainer.viewModelFactory
}

마지막으로 MainActivityContainer를 생성하기 위해서 Factory interface를 정의해주고 AppContainer에서 MainActivityContainer.Factory를 property로 가지도록 하여 MainActivityContainer를 생성할 수 있게 해주면 됩니다.

class MainActivityContainer(
    private val appContainer: AppContainer
) {

	...
	
    // MainActivityContainer를 생성할 Factory interface
    interface Factory {
        fun create(): MainActivityContainer
    }
}
class AppContainer {

	...
    
    val mainActivityContainerFactory = object : MainActivityContainer.Factory {
        override fun create(): MainActivityContainer =
            MainActivityContainer(appContainer = this@AppContainer)
    }
    
	...
}
class MainActivity : AppCompatActivity() {

	...

    private val mainActivityContainer by lazy {
        (application as ConversionApplication).appContainer
            .mainActivityContainerFactory.create()
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	// Composition root
        viewModelFactory = mainActivityContainer.viewModelFactory

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

        ...
    }
    
    ...
}

[1] https://github.com/ErroredPasta/Dependency-Injection/tree/pure-di

Reference

[1] Mark Seemann and Steven van Deursen, Dependency Injection Principles, Practices, and Patterns (n.p.: Manning Publications, 2019), 85.

[2] "Manual dependency injection," Android Developers, last modified Oct 27, 2021, accessed Jun 11, 2022, https://developer.android.com/training/dependency-injection/manual.

profile
Hola, Mundo

0개의 댓글