수동 종속 항목 삽입 (뷰)

개념 및 Jetpack Compose 구현

Android의 권장 앱 아키텍처는 코드를 클래스로 분할하여 관심사 분리의 이점을 누리길 권장합니다. 관심사 분리는 정의된 단일 책임이 계층 구조의 각 클래스에 있는 원칙입니다. 이로 인해 서로의 종속 항목을 충족하기 위해 연결해야 하는 더 작고 많은 클래스가 생성됩니다.

Android 앱은 일반적으로 여러 클래스로 구성되며 그중 일부는 서로 종속됩니다.
그림 1. Android 앱의 애플리케이션 그래프

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

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

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

이 섹션에서는 실제 Android 앱 시나리오에서 수동 종속 항목 삽입을 적용하는 방법을 다룹니다. 앱에서 종속성 삽입을 어떻게 사용할 수 있는지 반복되는 접근 방식을 안내합니다. 이 접근 방식은 Dagger에서 자동으로 생성하는 것과 아주 유사해지는 지점에 도달할 때까지 계속 개선됩니다. Dagger에 관한 자세한 내용은 Dagger 기본사항을 참고하세요.

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

일반적인 Android 앱의 로그인 흐름을 다룰 때 LoginActivityLoginViewModel에 종속되고 LoginViewModel은 UserRepository에 종속됩니다. 그런 다음 UserRepository 에 종속되고 이는UserLocalDataSourceUserRemoteDataSource 에 종속되며 이는 Retrofit 서비스에 종속됩니다.

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

흐름의 RepositoryDataSource 클래스는 다음과 같습니다.

Kotlin

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

class UserLocalDataSource { ... }
class UserRemoteDataSource(
   private val loginService: LoginRetrofitService
) { ... }

자바

class UserLocalDataSource {
   public UserLocalDataSource() { }
   ...
}

class UserRemoteDataSource {

   private final Retrofit retrofit;

   public UserRemoteDataSource(Retrofit retrofit) {
       this.retrofit = retrofit;
   }

   ...
}

class UserRepository {

   private final UserLocalDataSource userLocalDataSource;
   private final UserRemoteDataSource userRemoteDataSource;

   public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
       this.userLocalDataSource = userLocalDataSource;
       this.userRemoteDataSource = userRemoteDataSource;
   }

   ...
}

LoginActivity는 다음과 같습니다.

Kotlin

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

자바

public class MainActivity extends Activity {

   private LoginViewModel loginViewModel;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       // 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
       Retrofit retrofit = new Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(LoginService.class);

       // Then, satisfy the dependencies of UserRepository
       UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
       UserLocalDataSource localDataSource = new UserLocalDataSource();

       // Now you can create an instance of UserRepository that LoginViewModel needs
       UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

       // Lastly, create an instance of LoginViewModel with userRepository
       loginViewModel = new LoginViewModel(userRepository);
   }
}

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

  1. 상용구 코드가 많습니다. 코드의 다른 부분에서 LoginViewModel의 다른 인스턴스를 만들려면 코드가 중복될 수 있습니다.

  2. 종속 항목은 순서대로 선언해야 합니다. UserRepository를 만들려면 LoginViewModel 전에 인스턴스화해야 합니다.

  3. 객체를 재사용하기가 어렵습니다. 여러 기능에 걸쳐 UserRepository 를 재사용하려면 싱글톤 패턴을 따르게 해야 합니다. 모든 테스트가 동일한 싱글톤 인스턴스를 공유하므로 싱글톤 패턴을 사용하면 테스트가 더 어려워집니다.

컨테이너로 종속 항목 관리

객체 재사용 문제를 해결하려면 종속 항목을 가져오는 데 사용하는 자체 종속 항목 컨테이너 클래스를 만들면 됩니다. 이 컨테이너에서 제공하는 모든 인스턴스는 공개될 수 있습니다. 이 예에서는 UserRepository의 인스턴스만 필요하므로 종속 항목을 비공개로 만들 수 있으며, 제공해야 하는 경우 나중에 공개할 수 있습니다.

Kotlin

// Container of objects shared across the whole app
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)
}

자바

// Container of objects shared across the whole app
public class AppContainer {

   // Since you want to expose userRepository out of the container, you need to satisfy
   // its dependencies as you did before
   private Retrofit retrofit = new Retrofit.Builder()
           .baseUrl("https://example.com")
           .build()
           .create(LoginService.class);

   private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit);
   private UserLocalDataSource localDataSource = new UserLocalDataSource();

   // userRepository is not private; it'll be exposed
   public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);
}

이러한 종속 항목은 전체 애플리케이션에서 사용되므로 모든 활동에서 사용할 수 있는 공통된 위치인 Application 클래스에 배치해야 합니다. AppContainer 인스턴스가 포함된 맞춤 Application 클래스를 만듭니다.

Kotlin

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {

   // Instance of AppContainer that will be used by all the Activities of the app
   val appContainer = AppContainer()
}

자바

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
public class MyApplication extends Application {

   // Instance of AppContainer that will be used by all the Activities of the app
   public AppContainer appContainer = new AppContainer();
}

이제 애플리케이션에서 AppContainer의 인스턴스를 가져와서 공유 UserRepository 인스턴스를 획득할 수 있습니다.

Kotlin

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

자바

public class MainActivity extends Activity {

   private LoginViewModel loginViewModel;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       // Gets userRepository from the instance of AppContainer in Application
       AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
       loginViewModel = new LoginViewModel(appContainer.userRepository);
   }
}

이런 식으로는 싱글톤 UserRepository를 얻지 못합니다. 대신 그래프의 객체가 포함된 모든 활동에서 공유된 AppContainer가 있고 다른 클래스에서 사용할 수 있는 이러한 객체의 인스턴스를 만듭니다.

애플리케이션의 여러 위치에서 LoginViewModel이 필요한 경우 LoginViewModel의 인스턴스를 만드는 중앙 집중식 위치를 두는 것이 좋습니다. LoginViewModel의 생성을 컨테이너로 이동하고 팩토리로 해당 유형의 새 객체를 제공할 수 있습니다. LoginViewModelFactory의 코드는 다음과 같습니다.

Kotlin

// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
   fun create(): T
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory<LoginViewModel> {
   override fun create(): LoginViewModel {
       return LoginViewModel(userRepository)
   }
}

자바

// Definition of a Factory interface with a function to create objects of a type
public interface Factory<T> {
   T create();
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory implements Factory<LoginViewModel> {

   private final UserRepository userRepository;

   public LoginViewModelFactory(UserRepository userRepository) {
       this.userRepository = userRepository;
   }

   @Override
   public LoginViewModel create() {
       return new LoginViewModel(userRepository);
   }
}

AppContainerLoginViewModelFactory를 포함하여 LoginActivity에서 사용하게 할 수 있습니다.

Kotlin

// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
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()
   }
}

자바

// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory
public class AppContainer {
   ...

   public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

   public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository);
}

public class MainActivity extends Activity {

   private LoginViewModel loginViewModel;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       // Gets LoginViewModelFactory from the application instance of AppContainer
       // to create a new LoginViewModel instance
       AppContainer appContainer = ((MyApplication) getApplication()).appContainer;
       loginViewModel = appContainer.loginViewModelFactory.create();
   }
}

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

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

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

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

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

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

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

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

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

Kotlin

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
}

자바

// Container with Login-specific dependencies
class LoginContainer {

   private final UserRepository userRepository;

   public LoginContainer(UserRepository userRepository) {
       this.userRepository = userRepository;
       loginViewModelFactory = new LoginViewModelFactory(userRepository);
   }

   public LoginUserData loginData = new LoginUserData();

   public LoginViewModelFactory loginViewModelFactory;
}

// AppContainer contains LoginContainer now
public class AppContainer {
   ...
   public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource);

   // LoginContainer will be null when the user is NOT in the login flow
   public LoginContainer loginContainer;
}

흐름 관련 컨테이너가 있으면 컨테이너 인스턴스를 언제 만들고 삭제할지 판단해야 합니다. 로그인 흐름이 활동 (LoginActivity)에서 독립적이므로 이 컨테이너의 수명 주기를 관리하는 것은 활동입니다. LoginActivityonCreate에서 인스턴스를 만들고 onDestroy에서 삭제할 수 있습니다.

Kotlin

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

자바

public class LoginActivity extends Activity {

   private LoginViewModel loginViewModel;
   private LoginData loginData;
   private AppContainer appContainer;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       appContainer = ((MyApplication) getApplication()).appContainer;

       // Login flow has started. Populate loginContainer in AppContainer
       appContainer.loginContainer = new LoginContainer(appContainer.userRepository);

       loginViewModel = appContainer.loginContainer.loginViewModelFactory.create();
       loginData = appContainer.loginContainer.loginData;
   }

   @Override
   protected void onDestroy() {
       // Login flow is finishing
       // Removing the instance of loginContainer in the AppContainer
       appContainer.loginContainer = null;

       super.onDestroy();
   }
}

LoginActivity와 마찬가지로 로그인 프래그먼트는 AppContainer에서 LoginContainer에 액세스하여 공유 LoginUserData 인스턴스를 사용할 수 있습니다.

이 경우 뷰 수명 주기 로직을 처리하므로 수명 주기 관찰 을 사용하는 것이 좋습니다.