ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (DI - 2편) 안드로이드 의존성 수동 주입
    안드로이드 학습/Android 기술면접 대비 2024. 11. 16. 12:53

    (DI - 1편) 의존성 주입이란
    (DI - 2편) 안드로이드 의존성 수동 주입
    (DI - 3편) DI 라이브러리 Hilt

     

    구글에서는 권장하는 아키텍처를 통해 프로젝트를 만들때 관심사를 분리하기를 권장한다. 관심사 분리를 통해 각 클래스가 정의된 하나의 책임을 갖게 하는 것을 권장하는 것 같다. 이렇게 하면 더 많은 작은 클래스와 연결하여 서로의 종속성을 충족해야 한다. 

     

     

    의존성 주입을 사용하면 클래스를 쉽게 연결할 수 있고 테스트를 위해 구현을 교체할 수 있다.  예를 들어 Repository에 종속된 ViewModel을 테스트 할때 가짜 또는 모의 구현과 함께 Repository의 다른 구현을 전달하여 다른 case를 테스트 할 수 있다. 

     

    이번 글에서는 수동 의존성 주입을 학습해보려고 한다.  이 접근 방식은 Dagger에서 자동으로 생성하는 것과 아주 유사해지는 지점에 도달할 때까지 학습한다

     

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

     

    수동 의존성 주입 :

    일반적인 Android 앱의 로그인 흐름을 다룰 때 LoginActivity는 LoginViewModel에 종속되고 LoginViewModel은 UserRepository에 종속됩니다. 그러면 UserRepository는 UserLocalDataSource 및 UserRemoteDataSource는 Retrofit에 종속됩니다. 있습니다.

    LoginActivity는 로그인 흐름의 진입점이며 사용자는 이 Activity와 상호작용합니다. 따라서 LoginActivity는 모든 종속 항목이 있는 LoginViewModel을 만들어야 한다. 

     

    Repository 와 DataSource 클래스의 흐름은 아래와 같다. 

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


    LoginActivity

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

     

    이 방식은 다음과 같은 문제가 있다. 

    • 상용구 코드가 많다. 코드의 다른 부분에서 LoginViewModel의 다른 인스턴스를 만들려면 코드가 중복될 수 있다.
    • 의존성은 순서대로 선언해야 한다. UserRepository를 만들려면 LoginViewModel 전에 인스턴스화해야 한다. 
    • 객체를 재사용하기가 어렵다. 여러 기능에 걸쳐 UserRepository를 재사용하려면 싱글톤 패턴을 따르게 해야 한다. 모든 테스트가 동일한 싱글톤 인스턴스를 공유하므로 싱글톤 패턴을 사용하면 테스트가 더 어려워진다.

    Container로 의존성 관리 : 

    객체 재사용 문제를 해결하려면 의존성을 가져오는 데 사용하는 자체 의존성 Container class를 만들면 된다. 이 컨테이너에서 제공하는 모든 인스턴스는 공개될 수 있습니다.

     

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

     

    AppContainer class

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

     

    이러한 종속 항목은 전체 application에 걸쳐 사용되므로 모든 Activity에서 사용할 수 있는 일반적인 위치인 Application class에 배치해야한다. 

     

    Application class

    class MyApplication : Application() {
    
        // Instance of AppContainer that will be used by all the Activities of the app
        val appContainer = AppContainer()
    }

     

    이제 앱에서 AppContainer의 instance를 가져와서 공유된 UserRepository instance를 획득할 수 있다. 

     

    LoginActivity

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

     

    이런 식으로는 Singleton UserRepository를 얻지 못한다. 대신 모든 Activity에서 공유된 AppContainer가 있고 다른 Class에서 사용할 수 있는 객체 instance를 만든다. 

     

    LoginViewModel이 application의 더 많은 위치에 필요로 하면 한곳에서 Login ViewModel의 인스턴스를 만드는 것이 좋다. LoginViewModel을 Container로 이동하고 새 객체에 팩토리를 제공할 수 있다. 

     

    LoginViewModelFactory

    interface Factory<T> {
        fun create(): T
    }
    
    class LoginViewModelFactory(private val userRepository: UserRepository) : Factory

     

    AppContainer에 LoginViewModelFactory를 포함하여 LoginActivity에서 사용할 수 있다.

    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. 여전히 사용구 코드가 많다. 객체의 재사용 여부에 따라 수동으로 Factory나 paremter를 만들어야 한다. 

    프로젝트에 기능을 더 많이 포함하려고 할때 AppContainer는 복잡해진다. 앱이 커지고 다양한 기능 흐름을 도입하기 시작하면 더 많은 문제를 발생시킬 수 있다. 

    • 흐름이 다양하면 객체가 해당 흐름의 범위에만 있기를 원할 수 있다. 예를 들어 LoginUserData를 만들 때, 다른 사용자의 이전 로그인 흐름의 데이터를 유지하지 않고 모든 새 흐름의 새 인스턴스를 만들고 싶을 수 있다. 그럴 때는 AppContainer 내에 FlowConatainer 객체를 만들면 가능하다. 
    • 흐름에 필요 하지 않은 인스턴스는 삭제

    LoginContainer

    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
    }

     

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

     

    Login 관련 내용을 LoginActivity에서만 활용한다면 AppContainer에 LoginContainer를 만들고 LoginActivity에서만 onCreate에서 할당해주고, onDestroy 될때 null로 바꿔준다. 

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

     

    이렇게 하면 LoginFragment나 다른 LoginActivity안에서 사용하는 Fragment에서 활용가능하다.

     

    결론

    종속 항목 삽입은 확장 및 테스트 가능한 Android 앱을 만드는 데 유용한 기법입니다. 컨테이너를 사용하여 앱의 다양한 부분에서 클래스 인스턴스를 공유하고 한곳에서 팩토리를 사용하는 클래스 인스턴스를 만드세요.

     

    애플리케이션이 커지면 상용구 코드(예: 팩토리)를 많이 작성하게 되고 상용구 코드는 오류가 발생하기 쉽습니다. 컨테이너의 범위와 수명 주기를 직접 관리하여 메모리를 확보하기 위해 더 이상 필요하지 않은 컨테이너를 최적화 및 삭제해야 합니다. 이 작업을 잘못하면 앱에서 미묘한 버그와 메모리 누수가 발생할 수 있습니다.

     

    그래서 Dagger 사용해서 이 process를 자동화!!!

Designed by Tistory.