Android의 권장 앱 아키텍처는 코드를 클래스로 분할하여 관심사 분리의 이점을 누리길 권장합니다. 관심사 분리는 정의된 단일 책임이 계층 구조의 각 클래스에 있는 원칙입니다. 이로 인해 서로의 종속 항목을 충족하기 위해 연결해야 하는 더 작고 많은 클래스가 생성됩니다.
클래스 간 종속성은 그래프로 표시할 수 있고 그래프에서 각 클래스는 종속된 클래스에 연결됩니다. 모든 클래스와 서로의 종속성을 표시하면 애플리케이션 그래프 가 구성됩니다. 그림 1은 애플리케이션 그래프의 추상적 개념을 보여줍니다. 클래스 A (ViewModel)가 클래스 B (Repository)에 종속되면 A에서 B로 향하는 선이 종속 항목을 나타냅니다.
종속 항목 삽입을 사용하면 클래스를 쉽게 연결할 수 있고 테스트를 위해 구현을 교체할 수 있습니다. 예를 들어 저장소에 종속된 ViewModel을 테스트할 때 가짜 또는 모의 구현과 함께 Repository의 다른 구현을 전달하여 다른 사례를 테스트할 수 있습니다.
수동 종속 항목 삽입의 기본사항
이 섹션에서는 실제 Android 앱 시나리오에서 수동 종속 항목 삽입을 적용하는 방법을 다룹니다. 앱에서 종속성 삽입을 어떻게 사용할 수 있는지 반복되는 접근 방식을 안내합니다. 이 접근 방식은 Dagger에서 자동으로 생성하는 것과 아주 유사해지는 지점에 도달할 때까지 계속 개선됩니다. Dagger에 관한 자세한 내용은 Dagger 기본사항을 참고하세요.
흐름 은 앱에서 기능에 상응하는 화면 그룹이라고 간주합니다. 로그인, 등록, 결제가 모두 흐름의 예입니다.
일반적인 Android 앱의 로그인 흐름을 다룰 때 LoginActivity는 LoginViewModel에 종속되고 LoginViewModel은 UserRepository에 종속됩니다. 그런 다음
UserRepository 에 종속되고 이는UserLocalDataSource 및
UserRemoteDataSource 에 종속되며 이는 Retrofit 서비스에 종속됩니다.
LoginActivity 는 로그인 흐름의 진입점이며 사용자는 이 활동과 상호작용합니다. 따라서 LoginActivity는 모든 종속 항목과 함께 LoginViewModel을 만들어야 합니다.
흐름의 Repository 및 DataSource 클래스는 다음과 같습니다.
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);
}
}
이 접근 방식은 다음과 같은 문제가 있습니다.
상용구 코드가 많습니다. 코드의 다른 부분에서
LoginViewModel의 다른 인스턴스를 만들려면 코드가 중복될 수 있습니다.종속 항목은 순서대로 선언해야 합니다.
UserRepository를 만들려면LoginViewModel전에 인스턴스화해야 합니다.객체를 재사용하기가 어렵습니다. 여러 기능에 걸쳐
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);
}
}
AppContainer에 LoginViewModelFactory를 포함하여 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();
}
}
이 접근 방식은 이전 방식보다 좋지만 여전히 다음과 같은 문제를 고려해야 합니다.
AppContainer를 직접 관리하여 모든 종속 항목의 인스턴스를 수동으로 만들어야 합니다.- 여전히 상용구 코드가 많습니다. 객체의 재사용 여부에 따라 수동으로 팩토리나 매개변수를 만들어야 합니다.
애플리케이션 흐름에서 종속 항목 관리
프로젝트에 기능을 더 많이 포함하려 할 때 AppContainer는 복잡해집니다. 앱이 커지고 다양한 기능 흐름을 도입하기 시작하면 더 많은 문제가 발생합니다.
- 흐름이 다양하면 객체가 해당 흐름의 범위에만 있기를 원할 수 있습니다. 예를 들어
LoginUserData(로그인 흐름에서만 사용되는 사용자 이름과 비밀번호로 구성될 수 있는)를 만들 때, 다른 사용자의 이전 로그인 흐름의 데이터를 유지하지 않고 개발자는 모든 새 흐름에 새 인스턴스를 원합니다. 다음 코드 예에서와 같이AppContainer내부에FlowContainer객체를 만들어 이 작업을 완수할 수 있습니다. - 애플리케이션 그래프와 흐름 컨테이너를 최적화하는 것도 어려울 수 있습니다. 흐름에 따라 필요하지 않은 인스턴스를 삭제해야 합니다.
활동 하나 (LoginActivity)와 여러 프래그먼트 (LoginUsernameFragment, LoginPasswordFragment)로 구성된 로그인 흐름을 가정해보겠습니다. 이러한 뷰는 다음과 같이 하려고 합니다.
- 로그인 흐름이 완료될 때까지 공유해야 하는 동일한
LoginUserData인스턴스에 액세스합니다. - 흐름이 다시 시작되면
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)에서 독립적이므로 이 컨테이너의 수명 주기를 관리하는 것은 활동입니다. LoginActivity 는 onCreate에서 인스턴스를 만들고 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 인스턴스를 사용할 수 있습니다.
이 경우 뷰 수명 주기 로직을 처리하므로 수명 주기 관찰 을 사용하는 것이 좋습니다.