블로그명..?
[Dagger2] Dagger2 기초 본문
Dagger란?
수동적으로 의존성 주입을 가진 프로젝트를 구성하려고 하면 프로젝트의 사이즈가 커지고 복잡성이 올라가는 문제가 발생할 수 있는데, 대거를 사용함으로써 코드 복잡성과 프로젝트의 스케일을 획기적으로 제한하는 것이 가능하다.
대거는 클래스에 어노테이션을 붙여주는 것만으로 사용자가 직접 손으로 썼어야할 코드들을 컴파일 타임에 자동으로 생성해준다.
Dagger의 이점
- 다음과 같은 요소들로 보일러플레이트 코드를 줄여준다
- 수동 DI 섹션에서 수동으로 구현한 AppContainer 코드를 생성한다.
- application graph에서 사용가능한 클래스의는 Factory를 생성한다. 이를 통해 내부적으로 의존성을 충족해준다.
- Scope를 설정하여 재사용할 것인지 새로 생성할 것인지 지정해줄 수 있다.
- Dagger SubContainer를 사용하여 특정 흐름의 컨테이너를 만들어 줘서 더 이상 필요하지 않은 객체는 메모리에서 해제함으로써 앱 성능을 향상시킨다.
간단한 Dagger 사용 예 : 팩토리 생성
UserRepository 클래스의 간단한 팩토리를 생성해보자.
USerRepository 클래스를 아래와 같이 정의한다.
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
@Inject 어노테이션을 UserRepository의 생성자에 추가함으로써 Dagger에 UserRepository를 생성하는 방법을 알려준다.
// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
위 코드에서 다음 사항들을 Dagger에게 알려준다.
- @Inject를 생성자에 붙임으로써 UserRepository의 인스턴스를 알려준다.
- UserRepository의 종속 항목이 UserLocalDataSource와 UserRemoteDateSource에 있는 것을 알려준다.
이제 Dagger는 UserRepository의 인스턴스를 생성하는 방법을 알고 있다.
하지만 아직 UserRepository의 종속 항목들을 생성하는 방법은 모른다.
이는 다른 클래스들에 어노테이션을 달아주는 것으로 Dagger에게 종속 항목들의 생성 방법을 알려줄 수 있다.
// @Inject lets Dagger know how to create instances of these objects
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }
Dagger 컴포넌트
Dagger는 프로젝트에 종속성 그래프를 생성할 수 있다.
이에 따라 프로젝트는 종속 항목이 어디에 위치하는지 어떤 종속 항목을 필요로 하는지 알 수 있다.
Dagger가 이를 할 수 있게 하려면, @Component 어노테이션이 달린 인터페이스를 생성해야 한다.
Dagger는 수동 의존성 주입을 했을 때와 같은 container를 생성한다.
@Component 인터페이스 안에는 필요한 클래스들을 return 해주는 함수들을 정의할 수 있다.
@Component는 주입하기에 필요한 모든 인스턴스를 가지고 있는 container를 생성하도록 Dagger에게 지시한다.
이를 Dagger 컴포넌트라고 부른다.
Dagger 컴포넌트는 Dagger가 각각의 종속성을 위해 제공해야하는 객체들로 구성된 Graph를 포함한다.
// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
// The return type of functions inside the component interface is
// what can be provided from the container
fun repository(): UserRepository
}
프로젝트 빌드 시, 위 ApplicationGraph라는 컴포넌트 인터페이스의 구현체를 자동으로 생성한다.
(클래스 명은 DaggerApplicationGraph 처럼 클래스명 앞에 Dagger가 붙는다.)
어노테이션 프로세서를 사용함으로써, Dagger는 하나의 UserRepository 인스턴스를 가져오는 것만으로 다음 세 가지 클래스들의 관계로 구성된 종속성 Graph를 생성한다.
(UserRepository, UserLocalDatasource, UserRemoteDataSource)
이는 다음과 같이 사용할 수 있다.
// Create an instance of the application graph
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// Grab an instance of UserRepository from the application graph
val userRepository: UserRepository = applicationGraph.repository()
Dagger는 매번 요청될 때 마다 UserRepository의 새로운 인스턴스를 만든다.
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()
assert(userRepository != userRepository2)
가끔, 유일한 다음과 같은 몇가지 이유로, 유일한 종속성 인스턴스를 필요로 할 수도 있다.
- Login 기능 흐름에 포함된 다른 타입의 여러 ViewModel 객체에서 동일한 LoginUserData를 필요로 하는 경우 처럼, 다른 타입들이 동일한 타입의 종속성 항목을 포함하는 경우
- 객체의 생성 비용이 높은데, 하위 종속 항목의 새로운 인스턴스를 매번 생성하기를 원하지 않는 경우(예를 들면, JSON parser)
예를 들어, UserRepository에서 매번 요청할 때, 유일한 UserRepository를 사용하고 싶다면, 그렇게 할 수 있다.
좀 더 복잡한 application graph를 가지고 있는 실제 어플리케이션을 개발할 때, UserRepository에 의존하는 다양한 ViewModel을 가지고 있고, UserRepository가 요청될 때 마다 UserLocalDataSource와 UserRemoteDataSource의 새로운 인스턴스를 생성하고 싶지 않은 경우에 훨씬 유용하다.
수동 DI를 사용할 때, UserRepository의 동일한 인스턴스를 VIewModel 클래스의 생성자에 전달하는 것으로 이를 구현했다면, Dagger에서는 수동으로 코드를 입력하지 않아도, Scope 어노테이션을 사용하는 것으로 Dagger에게 동일한 인스턴스를 사용하도록 지시할 수 있다.
Dagger Scope
해당 컴포넌트의 라이프 사이클을 제한하기 위해 Scope를 사용할 수 있다.
이 것은 매번 타입에 인스턴스 제공이 필요할 때 동일한 종속항목의 인스턴스가 사용된다는 것을 뜻한다.
ApplicationGraph에 저장소를 요청할 때 UserRepository에서 고유한 인스턴스를 가지게 하기 위해서는 UserRepository에 @Component 어노테이션과 함께 @Singleton 어노테이션을 사용하면 된다. 이는 Dagger가 사용하는 javax.inject 패키지에 이미 포함되어 있다.
// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are bound to the life of the graph and so
// the same instance of that type is provided every time the type is requested.
@Singleton
@Component
interface ApplicationGraph {
fun repository(): UserRepository
}
// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
위와 같이 Singleton을 사용하여 유일한 인스턴스를 가지는 ApplicationGraph를 만들 수 있다.
또한, 커스텀 Scope 어노테이션을 만들 수도 있다.
// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope
이는 다음과 같이 사용할 수 있다.
@MyCustomScope
@Component
interface ApplicationGraph {
fun repository(): UserRepository
}
@MyCustomScope
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val service: UserService
) { ... }
두 가지 경우에서, 객체는 @Component 인터페이스에서 동일한 Scope로 제공된다.
그러므로, applicationGraph.repository()를 호출할 때 마다, 동일한 UserRepository의 인스턴스를 얻을 수 있다.
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()
assert(userRepository == userRepository2)
결론
Dagger 사용 시의 이점과, 간단한 예제를 통해 Dagger가 어떻게 동작하는지 알아 보았다.
다음에는 Android 앱에서 어떻게 적용할 수 있는지 알아보자
'Android > DI' 카테고리의 다른 글
[DI, 의존성 주입] 기본 개념 (0) | 2021.07.21 |
---|