안드로이드 앱 아키텍처에 따라 수동으로 의존성 주입하기
Android 앱의 로그인 흐름을 다룰 때 LoginActivity는 LoginViewModel에 종속되고 LoginViewModel은 UserRepository에 종속됩니다. 그러면 UserRepository는 UserLocalDataSource와 UserRemoteDataSource에 종속되고 UserLocalDataSource와 UserRemoteDataSource는 Retrofit 서비스에 종속됩니다.
따라서 다음과 같이 구현해야 합니다.
Repository.kt
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource) { ... }
DataSource.kt
class UserLocalDataSource { ... }
class UserRemoteDataSource(private val loginService: LoginRetrofitService) { ... }
LoginActivity.kt
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// LoginViewModel의 종속성을 충족하려면 모든 종속성의 종속성도 재귀적으로 충족해야 합니다.
// 먼저 UserRemoteDataSource의 의존성인 Retrofit을 생성합니다.
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
// 그런 다음 UserRepository의 종속성을 충족합니다.
val remoteDataSource = UserRemoteDataSource(retrofit)
val localDataSource = UserLocalDataSource()
// 이제 LoginViewModel에 필요한 UserRepository 인스턴스를 만들 수 있습니다.
val userRepository = UserRepository(localDataSource, remoteDataSource)
// 마지막으로 userRepository를 사용하여 LoginViewModel의 인스턴스를 만듭니다.
loginViewModel = LoginViewModel(userRepository)
}
}
LoginActivity는 로그인 흐름의 진입점이며 사용자는 이 Activity와 상호작용합니다. 따라서 LoginActivity는 모든 종속 항목이 있는 LoginViewModel을 만들어야 합니다.
이 접근 방식은 다음과 같은 문제가 있습니다.
- 상용구 코드가 많습니다. 코드의 다른 부분에서 LoginViewModel의 다른 인스턴스를 만들려면 코드가 중복될 수 있습니다.
- 종속 항목은 순서대로 선언해야 합니다. UserRepository를 만들려면 LoginViewModel 전에 인스턴스화해야 합니다.
- 객체를 재사용하기가 어렵습니다. 여러 기능에 걸쳐 UserRepository를 재사용하려면 싱글톤 패턴을 따르게 해야 합니다. 모든 테스트가 동일한 싱글톤 인스턴스를 공유하므로 싱글톤 패턴을 사용하면 테스트가 더 어려워집니다.
factory 패턴 및 container 이용하여 의존성 주입하기
객체 재사용 문제를 해결하려면 종속 항목을 가져오는 데 사용하는 자체 종속 항목 컨테이너 클래스를 만들면 됩니다.
이 컨테이너에서 제공하는 모든 인스턴스는 public될 수 있습니다.
다음 예에서는 UserRepository의 인스턴스만 필요하므로 나중에 제공해야 하는 경우 public하는 옵션으로 종속 항목을 private할 수 있습니다.
// 전체 앱에서 공유되는 객체 컨테이너
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는 private이 아닙니다. 노출되어야만 합니다.
val userRepository = UserRepository(localDataSource, remoteDataSource)
}
이러한 종속 항목은 전체 애플리케이션에 걸쳐 사용되므로 모든 Activity에서 사용할 수 있는 일반적인 위치, 즉 Application 클래스에 배치해야 합니다. AppContainer 인스턴스가 포함된 맞춤 Application 클래스를 만듭니다.
// AndroidManifest.xml 파일에 지정해야 하는 맞춤 애플리케이션 클래스
class MyApplication : Application() {
// 앱의 모든 Activity에서 사용할 AppContainer의 인스턴스
val appContainer = AppContainer()
}
이제 애플리케이션에서 AppContainer의 인스턴스를 가져와서 공유 UserRepository 인스턴스를 획득할 수 있습니다.
class LoginActivity: Activity() {
private lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Application의 AppContainer 인스턴스에서 userRepository 가져오기
val appContainer = (application as MyApplication).appContainer
loginViewModel = LoginViewModel(appContainer.userRepository)
}
}
이런 식으로는 싱글톤 UserRepository를 얻지 못합니다. 대신 그래프의 객체가 포함된 모든 활동에서 공유된 AppContainer가 있고 다른 클래스에서 사용할 수 있는 이러한 객체의 인스턴스를 만듭니다.
만약 LoginViewModel이 애플리케이션의 더 많은 위치에 필요하면 한곳에서 LoginViewModel의 인스턴스를 만드는 것이 좋습니다. 만든 LoginViewModel을 컨테이너로 이동하고 그 유형의 새 객체에 팩토리를 제공할 수 있습니다.
LoginViewModelFactory의 코드는 다음과 같습니다.
// interface Factory<T> { fun create(): T}유형의 객체를 생성하는 함수가 있는 Factory 인터페이스 정의
// LoginViewModel은 UserRepository에 의존하므로 LoginViewModel의 인스턴스를 생성하기 위해서는 파라미터로 전달하는 UserRepository의 인스턴스가 필요합니다.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory {
override fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}
AppContainer에 LoginViewModelFactory를 포함하여 LoginActivity에서 사용하게 할 수 있습니다.
// AppContainer는 이제 LoginViewModelFactory와 함께 LoginViewModel의 인스턴스를 제공할 수 있습니다.
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)
// AppContainer의 애플리케이션 인스턴스에서 LoginViewModelFactory를 가져와 새 LoginViewModel 인스턴스를 만듭니다.
val appContainer = (application as MyApplication).appContainer
loginViewModel = appContainer.loginViewModelFactory.create()
}
}
이 접근 방식은 이전 방식보다 좋지만 여전히 다음과 같은 문제를 고려해야 합니다.
- AppContainer를 직접 관리하여 모든 종속 항목의 인스턴스를 수동으로 만들어야 합니다.
- 여전히 상용구 코드가 많습니다. 객체의 재사용 여부에 따라 수동으로 팩토리나 매개변수를 만들어야 합니다.
application 흐름에서 사용하여 의존성 주입하기
프로젝트에 기능을 더 많이 포함하려 할 때 AppContainer는 복잡해집니다. 앱이 커지고 다양한 기능 흐름을 도입하기 시작하면 더 많은 문제가 발생합니다.
- 흐름이 다양하면 객체가 해당 흐름의 범위에만 있기를 원할 수 있습니다. 예를 들어 (로그인 흐름에서만 사용되는 사용자 이름과 비밀번호로 구성될 수 있는) LoginUserData를 만들 때, 다른 사용자의 이전 로그인 흐름의 데이터를 유지하지 않고 모든 새 흐름에 새 인스턴스를 만들고 싶을 수 있습니다. 다음 코드 예와 같이 AppContainer 내에 FlowContainer 객체를 만들면 가능합니다.
- 애플리케이션 그래프와 흐름 컨테이너를 최적화하는 것도 어려울 수 있습니다. 흐름에 따라 필요하지 않은 인스턴스를 삭제해야 합니다.
활동 하나(LoginActivity)와 여러 프래그먼트(LoginUsernameFragment, LoginPasswordFragment)로 구성된 로그인 흐름을 가정해보겠습니다. 이러한 뷰는 다음과 같이 하려고 합니다.
- 로그인 흐름이 완료될 때까지 공유해야 하는 동일한 LoginUserData 인스턴스에 액세스합니다.
- 흐름이 다시 시작되면 LoginUserData의 새 인스턴스를 만듭니다.
로그인 흐름 컨테이너로 이 작업을 완수할 수 있습니다. 이 컨테이너는 로그인 흐름이 시작될 때 만들어지고 흐름이 끝날 때 메모리에서 삭제되어야 합니다.
코드 예에 LoginContainer를 추가해보겠습니다. 앱에서 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()
}
}
LoginActivity와 마찬가지로 로그인 프래그먼트는 AppContainer에서 LoginContainer에 액세스하여 공유 LoginUserData 인스턴스를 사용할 수 있습니다.
이 경우 뷰 수명 주기 로직을 처리하므로 수명 주기 관찰을 사용하는 것이 좋습니다.
종속 항목 삽입은 확장 및 테스트 가능한 Android 앱을 만드는 데 유용한 기법입니다.
컨테이너를 사용하여 앱의 다양한 부분에서 클래스 인스턴스를 공유하고 한곳에서 팩토리를 사용하는 클래스 인스턴스를 만드세요.
그러나 애플리케이션이 커지면 상용구 코드(예: 팩토리)를 많이 작성하게 되고 상용구 코드는 오류가 발생하기 쉽습니다. 컨테이너의 범위와 수명 주기를 직접 관리하여 메모리를 확보하기 위해 더 이상 필요하지 않은 컨테이너를 최적화 및 삭제해야 합니다. 이 작업을 잘못하면 앱에서 미묘한 버그와 메모리 누수가 발생할 수 있습니다.
'Clean Software > Dependency Injection' 카테고리의 다른 글
3. Android 에서 Dagger로 DI 적용하기 (0) | 2023.04.27 |
---|---|
2. Dagger로 DI 적용하기 (0) | 2023.04.27 |
의존성 주입 (목차) (0) | 2023.04.27 |
의존성 주입 방법 (생성자 주입, 서비스 로케이터, Dagger) (0) | 2021.05.17 |