본문 바로가기

안드로이드

안드로이드 수동 종속성 삽입

반응형

기능을 모듈로 분할하여 개발을 확장하는 방법에 대해 소개하고 있는 글입니다.

수동 종속성 삽입

https://developer.android.com/training/dependency-injection/manual

 

수동 종속성 삽입  |  Android 개발자  |  Android Developers

수동 종속성 삽입 Android의 권장 앱 아키텍처는 코드를 클래스로 분할하여 관심사 분리의 이점을 누리길 권장합니다. 관심사 분리는 정의된 단일 책임이 계층 구조의 각 클래스에 있는 원칙입니

developer.android.com

일반적인 안드로이드 앱 로그인 플로우 대해서 설명하며 다루겠습니다.

    class UserRepository(
        private val localDataSource: UserLocalDataSource,
        private val remoteDataSource: UserRemoteDataSource
    ) { ... }

    class UserLocalDataSource { ... }
    class UserRemoteDataSource(
        private val loginService: LoginRetrofitService
    ) { ... }
    class LoginActivity: Activity() {

        private lateinit var loginViewModel: LoginViewModel

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            // In order to satisfy the dependencies of LoginViewModel, you have to also
            // satisfy the dependencies of all of its dependencies recursively.
            // First, create retrofit which is the dependency of UserRemoteDataSource
            val retrofit = Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService::class.java)

            // Then, satisfy the dependencies of UserRepository
            val remoteDataSource = UserRemoteDataSource(retrofit)
            val localDataSource = UserLocalDataSource()

            // Now you can create an instance of UserRepository that LoginViewModel needs
            val userRepository = UserRepository(localDataSource, remoteDataSource)

            // Lastly, create an instance of LoginViewModel with userRepository
            loginViewModel = LoginViewModel(userRepository)
        }
    }

위 방식의 문제점은 아래와 같습니다.

1. 상용구 코드가 많아 다른 부분에서 LoginViewModel 의 다른 인스턴스를 만들려면 코드가 중복될 수 있습니다.
2. 종속성은 순서대로 선언해야하기 때문에 UserRepository를 만들기전에 LoginViewModel을 인스턴스화 해야한다.
3. 객체를 재사용하기 어렵습니다. UserRepository를 재사용하려면 싱글톤 패턴을 따라야합니다. 

컨테이너로 종속성 관리

객체 재사용 문제를 해결하려면 종속 항목을 가져오는 데 사용하는 자체 종속 항목 컨테이너 클래스를 만들면 됩니다. 이 컨테이너에서 제공하는 모든 인스턴스는 공개될 수 있습니다.

    // Container of objects shared across the whole app
    class AppContainer {

        // Since you want to expose userRepository out of the container, you need to satisfy
        // its dependencies as you did before
        private val retrofit = Retrofit.Builder()
                                .baseUrl("https://example.com")
                                .build()
                                .create(LoginService::class.java)

        private val remoteDataSource = UserRemoteDataSource(retrofit)
        private val localDataSource = UserLocalDataSource()

        // userRepository is not private; it'll be exposed
        val userRepository = UserRepository(localDataSource, remoteDataSource)
    }

이러한 종속 항목은 전체 애플리케이션에 걸쳐 사용되므로 모든 활동에서 사용할 수 있는 일반적인 위치, 애플리케이션 클래스에 배치해야 합니다. 

    // Custom Application class that needs to be specified
    // in the AndroidManifest.xml file
    class MyApplication : Application() {

        // Instance of AppContainer that will be used by all the Activities of the app
        val appContainer = AppContainer() // 싱글톤이 아님!
    }
    class LoginActivity: Activity() {

        private lateinit var loginViewModel: LoginViewModel

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            // Gets userRepository from the instance of AppContainer in Application
            val appContainer = (application as MyApplication).appContainer
            loginViewModel = LoginViewModel(appContainer.userRepository)
        }
    }

만일 로그인 뷰모델이 애플리케이션의 많은 위치에서 필요하면 한 곳에서 LoginViewModel의 인스턴스를 만드는 것이 좋습니다. LoginViewModel을 컨테이너로 이동하고 그 유형의 새 객체에 팩토리를 제공할 수 있습니다.

    // Definition of a Factory interface with a function to create objects of a type
    interface Factory {
        fun create(): T
    }

    // Factory for LoginViewModel.
    // Since LoginViewModel depends on UserRepository, in order to create instances of
    // LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
    class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
        override fun create(): LoginViewModel {
            return LoginViewModel(userRepository)
        }
    }
    // AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
    class AppContainer {
        ...
        val userRepository = UserRepository(localDataSource, remoteDataSource)

        val loginViewModelFactory = LoginViewModelFactory(userRepository)
    }

    class LoginActivity: Activity() {

        private lateinit var loginViewModel: LoginViewModel

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            // Gets LoginViewModelFactory from the application instance of AppContainer
            // to create a new LoginViewModel instance
            val appContainer = (application as MyApplication).appContainer
            loginViewModel = appContainer.loginViewModelFactory.create()
        }
    }

이 전보다는 개선이 되었지만 아직 아래와 같은 문제가 남았습니다.

1. AppContainer를 직접 관리하여 모든 종속 항목의 인스턴스를 수동으로 만들어야 합니다.
2. 상용 코드가 많습니다. 객체를 재사용할지에 따라 수동으로 팩토리나 매개변수를 만들어야 합니다.

애플리케이션 플로우에서 종속성 관리

프로젝트에 기능을 더 많이 포함하려 할 때 AppContainer는 복잡해집니다. 

1. 플로우가 다양하면 객체가 플로우의 범위에만 있기를 원할 수 있습니다. 예를 들어 로그인 플로우에만 사용되는 사용자 이름과 비밀번호로 구성되는 LoginUserData를 만들 때 개발자는 이전 로그인 흐름(다른 사용자)의 데이터를 유지하지 않으려고 합니다. 개발자는 모든 새 플로우에서 새 인스턴스를 원합니다. AppContainer 내에 FlowContainer 객체를 만들면 가능합니다.

2. 애플리케이션 그래프와 Flow 컨테이너를 최적화하는 것도 어려울 수 있습니다. 플로우에 따라 필요하지 않는 인스턴트는 삭제해야 합니다.

[ LoginActivity, LoginIsernameFragment, LoginPasswordFragment] 로 구성된 로그인 플로우를 가정해보겠습니다.

1. 로그인 플로우가 완료될 때 까지 동일한 LoginUserData 인스턴스에 액세스
2. 플로우가 다시 시작되면 LoginUserData의 새 인스턴스를 만듦.

    class LoginContainer(val userRepository: UserRepository) {

        val loginData = LoginUserData()

        val loginViewModelFactory = LoginViewModelFactory(userRepository)
    }

    // AppContainer contains LoginContainer now
    class AppContainer {
        ...
        val userRepository = UserRepository(localDataSource, remoteDataSource)

        // LoginContainer will be null when the user is NOT in the login flow
        var loginContainer: LoginContainer? = null
    }

플로우 관련 컨테이너가 있으면 컨테이너 인스턴스를 언제 생성/삭제 해야할지를 판단해야 합니다. 로그인 Flow가 LoginActivity에서 독립적이므로 이 컨테이너의 수명 주기를 관리하는 것은 Activity 입니다.

    class LoginActivity: Activity() {

        private lateinit var loginViewModel: LoginViewModel
        private lateinit var loginData: LoginUserData
        private lateinit var appContainer: AppContainer

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            appContainer = (application as MyApplication).appContainer

            // Login flow has started. Populate loginContainer in AppContainer
            appContainer.loginContainer = LoginContainer(appContainer.userRepository)

            loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
            loginData = appContainer.loginContainer.loginData
        }

        override fun onDestroy() {
            // Login flow is finishing
            // Removing the instance of loginContainer in the AppContainer
            appContainer.loginContainer = null
            super.onDestroy()
        }
    }

LoginActivity 와 마찬가지로 로그인 프래그먼트는 AppContiner 에서 LoginConainer를 엑세스하여 LoginUserData 인스턴스를 사용할 수 있다.

이 경우 뷰 수명 주기 로직을 사용하므로 수명 주기 관찰을 사용하는 것이 좋습니다. 

애플리케이션이 커지면서 상용구 코드가 많아지고 이런 상용구 코드는 오류가 발생하기 쉽습니다. 컨테이너 범위와 수명 주기를 직접 관리하여 최적화해야합니다. 이걸 잘 못하면 앱에서 버그와 메모리 누수가 발생할 수 있습니다.

Dagger를 사용하여 이 프로세스를 자동화할 수 있습니다.

다중 모듈 앱의 Hilt

Hilt 코드를 생성하려면 Hilt를 사용하는 모든 Gradle 모듈에 엑세스해야 합니다. Application 클래스를 컴파일하는 Gradle 모듈에는 전이 종속 항목에 모든 Hilt 모듈과 생성자 삽입 클래스가 있어야 합니다.

다중 모듈 프로겍트가 일반 Gradle 모듈로 구성되어있다면 Hilt를 사용한 종속 항목 삽입을 하여 Hilt를 사용할 수 있습니다. 그러나 동적 기능 모듈(DFM)이 포함된 앱에서는 그렇지 않습니다.

 

 

https://medium.com/mobile-app-development-publication/dagger-hilt-on-multi-module-android-app-26815c427fb

 

Dagger Hilt on Multi-Module Android App

Enable Scalable Development with Dagger Hilt on Multi-Module

medium.com

https://developer.android.com/training/dependency-injection/hilt-multi-module

 

다중 모듈 앱의 Hilt  |  Android 개발자  |  Android Developers

다중 모듈 앱의 Hilt Hilt 코드를 생성하려면 Hilt를 사용하는 모든 Gradle 모듈에 액세스해야 합니다. Application 클래스를 컴파일하는 Gradle 모듈에는 전이 종속 항목에 모든 Hilt 모듈과 생성자 삽입 클

developer.android.com

https://developer.android.com/training/dependency-injection/hilt-android

 

Hilt를 사용한 종속 항목 삽입  |  Android 개발자  |  Android Developers

Hilt를 사용한 종속 항목 삽입 Hilt는 프로젝트에서 수동 종속 항목 삽입을 실행하는 상용구를 줄이는 Android용 종속 항목 삽입 라이브러리입니다. 수동 종속 항목 삽입을 실행하려면 모든 클래스

developer.android.com

 

반응형

'안드로이드' 카테고리의 다른 글

Android, Compose 왜 생겼을까?  (0) 2022.07.29
AttributeSet  (0) 2022.07.09
Compose: 튜토리얼 2  (0) 2022.06.04
Android Compose 튜토리얼 1  (0) 2022.04.20
서비스  (0) 2022.03.30