-
(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 ) { ... }
LoginActivityclass 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) : FactoryAppContainer에 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를 자동화!!!
'안드로이드 학습 > Android 기술면접 대비' 카테고리의 다른 글
(DI - 1편) 의존성 주입이란 (0) 2024.11.16 (DI - 3편) DI 라이브러리 Hilt Annotations (0) 2024.11.16 안드로이드 코루틴 Scope (0) 2024.11.15 안드로이드 코루틴 (Coroutine) (0) 2024.11.08 안드로이드 앱 아키텍처 (App Architecture) (0) 2024.10.17