오늘도 더 나은 코드를 작성하였습니까?

수동 종속성 삽입 본문

DI/Dagger2

수동 종속성 삽입

hik14 2020. 11. 30. 16:13

 

클래스 간 종속성은 그래프로 표시할 수 있고 그래프에서 각 클래스는 종속된 클래스에 연결됩니다. 모든 클래스와 서로의 종속성을 표시하면 애플리케이션 그래프가 구성됩니다.  위 그림은 애플리케이션 그래프의 추상적 개념을 보여줍니다. 클래스 A(ViewModel)가 클래스 B(Repository)에 종속하면 A에서 B까지 가리키는 선이 종속성을 나타냅니다.

 

종속성 삽입을 사용하면 클래스를 쉽게 연결할 수 있고 테스트를 위해 구현을 교체할 수 있습니다. 예를 들어 저장소에 종속된 ViewModel을 테스트할 때 fake나 mock과 함께 Repository의 다른 구현을 전달하여 다른 사례를 테스트할 수 있습니다.

 

수동 종속성 삽입의 기본사항

 

흐름은 앱에서 기능에 상응하는 화면 그룹이라고 간주합니다. 로그인, 등록, 결제가 모두 흐름의 예입니다.

 

일반적인 Android 앱의 로그인 흐름을 다룰 때 

LoginActivity LoginViewModel에 종속하고

LoginViewModel은 UserRepository에 종속하고

UserRepository UserLocalDataSource UserRemoteDataSource에 종속하고

UserLocalDataSource와 UserRemoteDataSource는 Retrofit 서비스에 종속합니다.

 

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를 재사용하려면 싱글톤 패턴으로 만들어된다.  그렇게 되면, 모든 테스트가 동일한 싱글톤 인스턴스를 공유하므로 싱글톤 패턴을 사용하면 테스트가 더 어려워집니다.

컨테이너로 종속성 관리

객체 재사용 문제를 해결하려면 종속 항목을 가져오는 데 사용하는 자체적으로  종속 항목 컨테이너 클래스를 만들어보자.

이 컨테이너에서 제공하는 모든 인스턴스는 공개될 수 있습니다.

다음 예에서는 UserRepository의 인스턴스만 필요하므로 나중에 제공해야 하는 경우 공개하는 옵션으로 종속 항목을 비공개할 수 있습니다.

// 앱 전반에 걸쳐 공유 객체의 컨테이너 클래스. 
    class AppContainer {

        //userRepository 외부 노출전 사전에 필요한 종속성을 먼저 삽입한다. 
        private val retrofit = Retrofit.Builder()
                                .baseUrl("https://example.com")
                                .build()
                                .create(LoginService::class.java)

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

        // userRepository 는 외부에 노출시켜야되기 때문에 공개한다. 
        val userRepository = UserRepository(localDataSource, remoteDataSource)
    }
    

 

이러한 종속 항목(UserRepository)은 전체 애플리케이션에 걸쳐 사용되므로 모든 Activity에서 사용할 수 있는 일반적인 위치,

즉 애플리케이션 클래스에 배치해야 합니다. AppContainer 인스턴스가 포함된 맞춤 애플리케이션 클래스를 만듭니다.

 

    // 커스텀 application 객체를 커스텀 합니다. 
    class MyApplication : Application() {

        // 앱의 모든 activity/fragment에 사용될 컨테이너 클래스 생성
        val appContainer = AppContainer()
    }

 

참고: AppContainer는 Application 클래스에 배치된 앱 전체에서 공유하는 고유한 인스턴스가 있는 일반적인 클래스일 뿐이고, AppContainer는 싱글톤 패턴으로 만들어져 있지 않다.

(Kotlin에서는 object가 아니고 자바에서는 일반적인 Singleton.getInstance() 메서드로 액세스되지 않습니다)

 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)
        }
    }

싱글톤 UserRepository를 얻을수 없습니다. (AppContainer는 싱글톤 패턴을 따르지 않기 때문에)

대신에, AppContainer는  Activity(그래프의 객체를 포함하고 다른 클래스가 사용할 수있는 객체의 인스턴스를 만드는)에 공유된다.

 

LoginViewModel이 애플리케이션의 더 많은 위치에 필요하면, 한 곳에서 LoginViewModel의 인스턴스를 생성하는 것이 좋다.

LoginViewModel의 생성을  컨테이너로 이동시키고  팩토리(ViewModelFactory)를 제공할 수 있습니다.

    // 팩토리 인터페이스를 정의
    interface Factory {
        fun create(): T
    }

    // 뷰모델 팩토리
    // 뷰모델은 레포지터리에 의존성을 가지고 있기 때문에, 뷰모델을 생성하기 위해서 팩토리의 생성사 파라미터로
    // 레포지터리 객체를 받아야 한다.  
    class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
        override fun create(): LoginViewModel {
            return LoginViewModel(userRepository)
        }
    }
// AppContainer는 팩토리를 이용해 뷰모델을 제공한다.
    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)

            // applicatin 객체의 appcontainer로 부터 팩토리 얻어서 로그인 뷰모델 객체생성. 
            val appContainer = (application as MyApplication).appContainer
            loginViewModel = appContainer.loginViewModelFactory.create()
        }
    }

이전 방식보다 좋지만 여전히 다음과 같은 문제를 고려해야 합니다

 

  1. AppContainer를 직접 관리하여 모든 종속 항목의 인스턴스를 수동으로 생성
  2.  객체를 재사용할지에 따라 수동으로 팩토리나 매개변수를 생성

 

애플리케이션 흐름에서 종속성 관리

 

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

앱이 커지고 다양한 기능 흐름을 도입하기 시작하면 더 많은 문제가 발생합니다.

 

흐름이 다양하면 객체가 흐름의 범위에만 있기를 원할 수 있습니다. 예를 들어 로그인 흐름에서만 사용되는 사용자 이름과 비밀번호로 구성될 수 있는 LoginUserData를 만들 때 개발자는 다른 사용자의 이전 로그인 흐름의 데이터를 유지하지 않으려고 합니다.

개발자는 모든 새 흐름에 새 인스턴스를 원합니다. 다음 코드 예와 같이 AppContainer 내에 FlowContainer 객체를 만들면 가능합니다.

 

애플리케이션 그래프와 FlowContainer 를 최적화하는 것도 어려울 수 있습니다. 흐름에 따라 필요하지 않은 인스턴스를 삭제해야 합니다.

 

LoginActivity와 여러 프래그먼트(LoginUsernameFragment, LoginPasswordFragment)로 구성된 로그인 흐름을 가정해보겠습니다. 이러한 뷰는 다음과 같이 하려고 합니다.

  1. 로그인 흐름이 완료될 때까지 공유해야 하는 동일한 LoginUserData 인스턴스에 액세스합니다.
  2. 흐름이 다시 시작되면 LoginUserData의 새 인스턴스를 만듭니다.

로그인 흐름 컨테이너로 이 작업을 완수할 수 있습니다. 이 컨테이너는 로그인 흐름이 시작될 때 만들어지고 흐름이 끝날 때 메모리에서 삭제되어야 합니다.

 

앱에서 LoginContainer 인스턴스를 여러 개 만들 수 있으려면 싱글톤으로 만들지 말고 로그인 흐름에 필요한 AppContainer의 종속 항목이 있는 클래스로 만듭니다.

 

  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
    }

 

흐름 관련 컨테이너가 있으면 컨테이너 인스턴스를 언제 만들고 삭제할지 판단해야 합니다.

로그인 흐름이 LoginActivity에 포함되어야 해서,  로그인 컨테이너의 수명 주기를 관리하는 것은 Activity입니다. 

LoginActivity onCreate()에서 인스턴스를 만들고 onDestroy()에서 삭제할 수 있습니다.

 

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()
        }
    }

결론

종속성 삽입은 확장 및 테스트 가능한 Android 앱을 만드는 데 유용한 기법입니다.

컨테이너를 사용하여 앱의 다양한 부분에서 클래스 인스턴스를 공유하고 한 곳에서 팩토리를 사용하는 클래스 인스턴스를 만드세요.

 

애플리케이션이 커지면 상용구 코드(예: 팩토리)를 많이 작성하게 되고 상용구 코드는 오류가 발생하기 쉽습니다.

 

컨테이너의 범위와 수명 주기를 직접 관리하여 메모리를 확보하기 위해 더 이상 필요하지 않은 컨테이너를 최적화 및 삭제해야 합니다. 이 작업을 잘못하면 앱에서 미묘한 버그와 메모리 누수가 발생할 수 있습니다.

 

다음 페이지에서는 Dagger를 사용하여 이 프로세스를 자동화하고, 자동화하지 않고 수동으로 작성했을 때와 동일한 코드를 생성할 수 있는 방법을 알아봅니다.

'DI > Dagger2' 카테고리의 다른 글

Android app에서 Dagger 사용  (0) 2021.05.03
Dagger Basic  (0) 2020.11.30
Android의 종속 항목 삽입  (0) 2020.11.24
Dagger2 (안드로이드 의존성 주입 프레임워크)란?  (0) 2020.08.04
의존성 주입이란?  (0) 2020.08.04