Clean Software/Dependency Injection

2. Dagger로 DI 적용하기

728x90
반응형

"hello world" 의존성 주입하기 (@Module, @Provides, @Componet)

Hello world 문자열을 제공할 모듈 만들기

@Module은 의존성을 제공하는 클래스에 붙이고,
@Provides는 의존성을 제공하는 메서드에 붙인다.

Dagger는 컴파일타임에 @Module과 @Provides 어노테이션을 읽고 의존성 주입에 필요한 파일들을 생성한다. 

@Module
class MyModule {
    @Provides
    fun provideHelloWorld: String{
       return "Hello World"
    }
}

Hello world 문자열 제공받을 컴포넌트 만들기

@Componet는 참조된 모든 의존성을 제공받을 인터페이스에 붙인다.
이때 인터페이스 내에는 제공할 의존성들을 메서드로 정의해야 한다.

메서드 반환형을 보고 모듈과 관계를 맺으므로 바인드 된 모듈로부터 해당 반환형을 갖는 메서드를 찾지 못하면 컴파일 에러가 발생한다.

Dagger는 컴파일타임에 @Componet를 구현한 클래스를 생성하는데 이때 클래스 이름은 컴포넌트 이름에 Dagger라는 접두어가 붙는다. (DaggerMyComponet) 

@Component(modules = MyModule.class)
interface MyComponent {
    fun getString(): String
}

이제 Dagger에 의해 생성된 클래스(DaggerMyComponet)를 통해 의존성을 제공받는다.

class MainActivity{
...
    val myComponent: MyComponent = DaggerMycomponet.creat()
    System.out.println("result : " + myComponent.getString)

}

 

 

"객체 생성  방법" 의존성 주입하기 (@Inject)

개발자가 클래스의 종속 항목을 선언하고 어노테이션을 사용하여 종속 항목을 충족하는 방법을 지정하기만 하면 Dagger는 빌드 시간에 자동으로 이러한 모든 일을 완료합니다. Dagger는 개발자가 수동으로 작성한 것과 유사한 코드를 생성합니다.

내부적으로 Dagger는 클래스의 인스턴스를 제공하는 방법을 찾기 위해 참조할 수 있는 객체의 그래프를 만듭니다. 그래프의 모든 클래스와 관련하여 Dagger는 필요한 유형의 인스턴스를 가져오는 데 내부적으로 사용하는 팩토리 유형 클래스를 생성합니다.

// @Inject는 Dagger가 종속항목(이 객체의 인스턴스를 생성하는 방법)을 알 수 있도록 합니다.
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

위 코드는 Dagger에게 다음 2가지를 알려줍니다.

@Inject 주석이 달린 생성자를 사용하여 
UserRepository 인스턴스를 만드는 방법을 알려줍니다.
종속 항목은 UserLocalDataSource 및 UserRemoteDataSource임을 알려줍니다

그러나 Dagger는 UserRepository의 인스턴스를 만드는 방법을 알고 있지만 종속 항목을 만드는 방법은 모릅니다.

다음과 같이 종속항목 클래스에도 주석을 지정하면
Dagger는 그 클래스를 만드는 방법을 알게 됩니다.
// @Inject는 Dagger가 종속항목(이 객체들의 인스턴스를 생성하는 방법)을 알 수 있도록 합니다.
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

 

 

Dagger 구성요소(종속성 그래프) (@Component)

Dagger는 종속 항목이 필요할 때 이러한 종속 항목을 가져올 위치를 찾는 데 사용할 수 있는 프로젝트의 종속 항목 그래프를 만들 수 있습니다. Dagger가 이렇게 하도록 하려면 인터페이스를 만들고 @Component로 주석을 지정해야 합니다. Dagger는 수동 종속 항목 삽입 시와 마찬가지로 컨테이너를 만듭니다.

@Component 인터페이스 내에서 필요한 클래스의 인스턴스(즉, UserRepository)를 반환하는 함수를 정의할 수 있습니다. 

@Component는 노출하는 유형을 충족하는 데 필요한
모든 종속 항목이 있는 컨테이너를 생성하도록
Dagger에 지시합니다.

이를 Dagger 구성요소(컴포넌트)라고 합니다.

이 구성요소에는 Dagger가 제공하는 방법을 알고 있는 객체와 객체의 각 종속 항목으로 구성된 그래프가 포함되어 있습니다.

// @Component는 Dagger가 종속성 그래프를 생성하도록 합니다.
@Component
interface ApplicationGraph {
    // 컴포넌트 인터페이스 내 함수의 반환 유형은 컨테이너에서 제공할 수 있는 것입니다.
    fun repository(): UserRepository
}

프로젝트 빌드 시 Dagger는 자동으로 ApplicationGraph 인터페이스의 구현, 즉 DaggerApplicationGraph를 생성합니다.

Dagger는 주석 프로세서를 사용하여 하나의 진입점(UserRepository 인스턴스 가져오기)으로 세 클래스(UserRepository, UserLocalDatasourceUserRemoteDataSource) 간의 관계로 구성된 종속 항목 그래프를 만듭니다.

다음과 같이 사용할 수 있습니다.

// 애플리케이션 그래프의 인스턴스 생성
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// 애플리케이션 그래프에서 UserRepository 인스턴스를 가져옵니다.
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)

때로 컨테이너에 종속 항목의 고유한 인스턴스가 있어야 합니다. 이는 다음과 같이 여러 가지 이유 때문일 수 있습니다.

  1. 동일한 LoginUserData를 사용하는 로그인 흐름의 여러 ViewModel 객체와 같이 동일한 인스턴스를 공유하기 위해 이 유형을 종속 항목으로 보유하는 다른 유형을 원합니다.
  2. 객체는 생성하는 데 비용이 많이 듭니다. 따라서 인스턴스를 종속 항목으로 선언할 때마다 새 인스턴스를 생성하지는 않으려고 합니다(예: JSON 파서).

이 예에서는 UserRepository를 요청할 때마다 항상 동일한 인스턴스를 얻도록 그래프에서 사용 가능한 고유한 UserRepository 인스턴스를 가지려고 할 수 있습니다. 이는 더 복잡한 애플리케이션 그래프가 있는 실제 애플리케이션에서 UserRepository에 따라 여러 ViewModel 객체를 가질 수 있고 UserRepository를 제공해야 할 때마다 UserLocalDataSource  UserRemoteDataSource의 새 인스턴스를 생성하지는 않으려고 하므로 이 예에서 유용합니다.

수동 종속 항목 삽입에서는 동일한 UserRepository 인스턴스를 ViewModel 클래스의 생성자에 전달함으로써 이 작업을 완료합니다.

 

그러나 Dagger에서는 코드를 수동으로 작성하지 않으므로 동일한 인스턴스를 사용하려는 것을 Dagger에 알려야 합니다.

이를 위해서는 범위 주석을 사용하면 됩니다.

 

Dagger로 범위 지정 (@Singleton, @Scope)

범위 주석을 사용하여 객체의 전체 기간을 구성요소의 전체 기간으로 제한할 수 있습니다. 즉, 유형을 제공해야 할 때마다 종속 항목의 동일한 인스턴스를 사용합니다.

ApplicationGraph의 저장소를 요청할 때 UserRepository의 고유한 인스턴스를 가지려면 @Component 인터페이스 및 UserRepository에 동일한 범위 주석을 사용합니다.

각 컴포넌트는 범위를 지정할 수 있는데
이를 통해 컴포넌트 인스턴스는
의존성의 제공방법에 대한 동일성을 보장받을 수 있게 됩니다. 

쉽게 말해 하나의 인스턴스만 만들어서 참조하는 싱글턴 패턴과 비슷한 개념이지만,
애플리케이션의 생명주기와 달리 생명주기를 따로 관리할 수 있다는 점에서 차이가 있다고 볼 수 있습니다.

예를 들어 안드로이드에서
애플리케이션, 액티비티, 프래그먼트 인스턴스에 대한
범위 지정을 다르게 관리함으로써
오브젝트 그래프의 생성과 소멸을 각자 관리할 수 있게 됩니다.

다음과 같이 Dagger에서 사용하는 javax.inject 패키지와 함께 이미 제공된 @Singleton 주석을 사용할 수 있습니다.

// @Component 인터페이스의 범위 주석은 이 주석(예: @Singleton)으로 주석이 달린 클래스가 그래프의 수명에 바인딩되어 있으므로 유형이 요청될 때마다 해당 유형의 동일한 인스턴스가 제공됨을 Dagger에 알립니다.
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}
// @Singleton 범위(예: ApplicationGraph)를 사용하여 이 클래스의 범위를 Component로 지정합니다.
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

 

또는 맞춤 범위 주석을 만들어 사용할 수 있습니다. 다음과 같이 범위 주석을 만들 수 있습니다.

// 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 인터페이스에 주석을 지정하는 데 사용된 것과 동일한 범위가 객체에 제공됩니다. 따라서 applicationGraph.repository()를 호출할 때마다 동일한(고유한) UserRepository 인스턴스를 얻습니다.

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

 

 

 

 

 

728x90
반응형