Clean Software/Dependency Injection

1. Android에서 수동으로 DI 적용하기

728x90
반응형

안드로이드 앱 아키텍처에 따라 수동으로 의존성 주입하기

 

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을 만들어야 합니다.

 

이 접근 방식은 다음과 같은 문제가 있습니다.

  1. 상용구 코드가 많습니다. 코드의 다른 부분에서 LoginViewModel의 다른 인스턴스를 만들려면 코드가 중복될 수 있습니다.
  2. 종속 항목은 순서대로 선언해야 합니다. UserRepository를 만들려면 LoginViewModel 전에 인스턴스화해야 합니다.
  3. 객체를 재사용하기가 어렵습니다. 여러 기능에 걸쳐 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()
    }
}

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

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

application 흐름에서 사용하여 의존성 주입하기

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

  1. 흐름이 다양하면 객체가 해당 흐름의 범위에만 있기를 원할 수 있습니다. 예를 들어 (로그인 흐름에서만 사용되는 사용자 이름과 비밀번호로 구성될 수 있는) LoginUserData를 만들 때, 다른 사용자의 이전 로그인 흐름의 데이터를 유지하지 않고 모든 새 흐름에 새 인스턴스를 만들고 싶을 수 있습니다. 다음 코드 예와 같이 AppContainer 내에 FlowContainer 객체를 만들면 가능합니다.
  2. 애플리케이션 그래프와 흐름 컨테이너를 최적화하는 것도 어려울 수 있습니다. 흐름에 따라 필요하지 않은 인스턴스를 삭제해야 합니다.

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

  1. 로그인 흐름이 완료될 때까지 공유해야 하는 동일한 LoginUserData 인스턴스에 액세스합니다.
  2. 흐름이 다시 시작되면 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 앱을 만드는 데 유용한 기법입니다.

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

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

728x90
반응형