본문 바로가기

안드로이드

[Android] 3. 구글 코드랩 Dagger를 이용한 리팩토링 - Registration Flow

개요

해당 게시글은 구글 코드랩 문서를 번역한 게시글입니다.

 

@Module, @Binds, @BindsInstance annotation

대거에게 Storage를 어떻게 제공해야 하는지 알릴 수 있을까? Storage는 기존 클래스와 다르게 인터페이스이며 따라서 직접 객체화할 수 없다. 따라서 대거에게 UserManager에서 사용할 Storage의 구현체(구상 클래스)를 알려야 한다. 이 경우는 SharedPreferencesStorage이다.

 

그러기 위해서는 대거 모듈을 활용해야한다. 대거 모듈이란 @Module이 붙은 클래스를 의미한다.

 

컴포넌트와 유사하게, 대거 모듈은 대거에게 특정 타입의 객체를 어떻게 제공하는지 알려준다. 의존성(객체)들은 @Provides와 @Binds로 정의된다.

 

따라서 해당 모듈은 Storage에 대한 정보를 갖고 있어야 한다.

app/src/main/java/com/example/android/dagger/di/StorageModule.kt

// 대거에게 대거 모듈임을 알린다
@Module
class StorageModule {

}

 

 

 

@Binds annotation

@Binds를 사용하여 인터페이스를 제공할 때 사용해야하는 구현체를 대거에게 알릴 수 있다. @Binds는 추상 함수에만 붙을 수 있음에 유의해야한다. 추상 함수의 반환 타입은 우리가 원하는 인터페이스(Storage)이다. 해당 구현은 추상 함수의 매개변수를 추가하며 더 자세한 구현 타입을 결정지을 수 있다 (i.e. SharedPreferencesStorage).

 

StorageModule.kt

// 대거에게 대거 모듈임을 알림.
// @Binds가 존재하기 때문에 StorageModule이 추상 클래스
@Module
abstract class StorageModule {

    // Storage 타입이 필요로 할 때 Dagger가 SharedPreferencesStorage를 제공하게 한다.
    @Binds
    abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}

 

아래 내용을 알아두어야 한다.

  • provideStorage는 임의의 메소드 명이다. 대거는 이를 개의치 않아 아무거나 가능할 수 있다. 대거가 유의하는 내용은 리턴 타입과 매개변수이다.
  • StorageModule은 추상 클래스이다. @Binds에 의해 provideStorage가 추상 메소드이기 때문이다.

 

지금까지 대거에게 Storage가 호출되면 어떤 객체를 반환할 지 결정하는 과정을 마쳤다(i.e. SharedPreferencesStorages). 아직 대거는 SharedPreferencesStorage를 어떻게 생성하는지 모른다. 따라서, SharedPreferecesStorage의 생성자에 @Inject 처리를 수행한다.

 

SharedPreferencesStorage.kt

// @Injec는 대거가 해당 인스턴스를 어떻게 제공해야 하는지 알려준다.
class SharedPreferencesStorage @Inject constructor(context: Context) : Storage { ... }

 

요약. 인퍼페이스로 매개변수로 공급 받는 다면, 모듈에서 우리가 생성을 원하는 타입을 매개변수로 받고 해당 인터페이스를 반환하는 추상 메소드를 @Binds 처리하여 정의한다. 또한, 해당 매개변수 클래스에 @Inject 처리한다.

 

 

다음으로 ApplicationGraph는 StorageModule에 대한 정보를 알아야한다. 그러기 위해 AppComponent에 모듈을 지정해준다.

AppComponent.kt

// StorageModule에 대한 정보를 추가한 컴포넌트 정의
@Component(modules = [StorageModule::class])
interface AppComponent {
    
    // 이 컴포넌트에 의해 주입을 원하는 클래스
    fun inject(activity: RegistrationActivity)
}

이 방법을 통해 AppComponent는 StorageModule의 정보에 접근할 수 있게 된다. 좀 더 복잡한 앱에서는 네트워킹과 관련된  NetworkModule을 가질 수도 있다.

 

여튼 지금까지 과정 후에 다시 빌드를 거치면 대거가 Context를 못찾겠다는 에러를 발생시킨다.

 

@BindsInstance annotation

어떻게 대거에게 Context를 제공하는 방법을 알릴 수 있을까? Context는 안드로이드 시스템에 의해 제공되는 객체이다. 그러므로 그래프 외부에 존재한다. 따라서 Context는 이미 그래프의 객체를 생성할 때 사용가능하므로 단순히 전달만 해주면 된다.

 

전달하는 방법은 컴포넌트 팩토리와 @BindsInstance를 사용하는 것이다.

 

AppComponent.kt

@Component(modules = [StorageModule::class])
interface AppComponent {

    //  AppComponent를 생성하기 위한 팩토리
    @Component.Factory
    interface Factory {
        // @BindsInstance를 통해 Context를 그래프에서 사용할 수 있게 된다.
        fun create(@BindsInstance context: Context): AppComponent
    }

    fun inject(activity: RegistrationActivity)
}

@Component.Factory로 처리된 인터페이스르 선언했다. 내부에는 @BindsInstance 처리된 Context를 매개변수로 받는Component를 반환하는 create 메소드를 정의했다.

 

@BindsInstance는 대거에게 해당 그래프를 추가해야하며 Context가 필요할 때 마다 해당 인스턴스를 제공해야 한다고 알려준다.

 

요약. 그래프 외부에서 생성되는 객체는 @BindsInstance를 사용한다.

 

프로젝트 빌드 성공

위까지 과정을 완료하면 그래프가 성공적으로 생성되어 프로젝트 빌드에 성공한다. Annotation 처리로 자동으로 생성된 클래스는 Dagger{ComponentName}으로 생성된다. 현재까지 생성된 그래프의 상태는 다음과 같다.

AppComponet는 StorageModule의 정보를 포함하고 어떤 Storage 객체를 생생해야 하는지 알고 있다. Storage는 Context에 대한 의존성을 가지며 그래프를 생성할 때 제공하므로 Storage의 모든 의존성은 해결된다.

 

Context는 AppComponent의 팩토리에 의해 전달된다. 그러므로 언제든 같은 Context 객체를 제공받을 수 있다. 이런 경우에 하얀 점으로 다이어그램에서 표시한다.

 

RegistrationActivity의 경우 그래프에 접근하여 @Inject 처리된 RegistrationViewModel에 대한 의존성을 주입받을 수 있다.

 

AppComponent는 RegistrationActivity에 대한 의존성을 충족시키이 위해 RegistrationViewModel을 생성해야 하고, 이 과정에서 UserMnager를 충족하기 위해 UserManager를 만들어야 한다. UserManager는 Storage에 의한 종속성이 존재하지만 이미 그래프에 정의되어 다른 것은 필요로 하지 않는다.