본문 바로가기

안드로이드

[Android] Dagger2 주요 개념 총 정리

개요

Dagger2에서 사용하는 주요 annotation을 총 정리하는 포스트이다.

 

@Inject

  • 대거가 해당 타입을 어떻게 생성하는지 알 수 있게 한다.
  • 특정 클래스 내부에서 해당 객체를 주입한다.
class Repository @Inject constructor(val userManager: UserManager) {
    ...
}



class MainActivity : AppCompatActivity(){
   @Inject
   lateinit var repository: Repository
}

 

 

@Component

Component는 인터페이스로 대거가 컴파일 타임에 그래프를 생성하는데 필요한 정보를 제공한다.

Component의 메소드의 종류는 두 가지이다.

  1. Provision Method: 매개변수가 없고, 모듈이 제공하는 객체를 return 타입으로 갖는다. 컴포넌트 객체를 생성한 곳에서 해당 메소드로 객체를 획득할 수 있다.
  2. Member-Injection Method: 의존성을 주입시킬 객체를 메소드의 파라미터로 넘겨 해당 객체가 필요로 하는 의존성을 주입받을 수 있다.
@Component(modules=[CoffeeModule.class])
interface MyComponent{
    //Provision Method
    fun makeCoffee() : Coffee
    
    //Member-Injection Method
    fun inject(mainActivity: MainActivity)
}

 

@Module

Module은 의존성 주입에 필요한 객체들을 관리한다.

@Module
object CoffeeModule{
    @Provides
    fun provideCoffeeBean() : CoffeeBean{
        return CoffeeBean()
    }
    
    @Provides
    fun provideCoffee(coffeeBean:CoffeeBean) : Coffee{
    	return CoffeeBean(coffeeBean)
    }
}

 

@Qualifier

때로는 자료형만으로 의존성을 식별하기에 어려울 때도 있다. 예를 들어 같은 타입을 반환하는 서로 다른 객체를 다루어야 할 경우 대거가 어떤 의존성을 주입할 지 결정하지 못할 것이다.

class SharedPreferencesStorage @Inject constructor(name: String, context: Context) : Storage {

    private val sharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE)

    ...
}

@Module
class StorageModule {

    @Provides
    fun provideRegistrationStorage(context: Context): Storage {
        return SharedPreferencesStorage("registration", context)
    }

    @Provides
    fun provideLoginStorage(context: Context): Storage {
        return SharedPreferencesStorage("login", context)
    }
}

 

이 경우 Qualifier를 통해 주입할 종속성을 구분할 수 있다.

 

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class RegistrationStorage

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class LoginStorage

@Module
class StorageModule {

    @RegistrationStorage
    @Provides
    fun provideRegistrationStorage(context: Context): Storage {
        return SharedPreferencesStorage("registration", context)
    }

    @LoginStorage
    @Provides
    fun provideLoginStorage(context: Context): Storage {
        return SharedPreferencesStorage("login", context)
    }
}

 

Qualifer로 종속성을 골라 주입하기

// 메서드에서 주입하는 방식
class ClassDependingOnStorage(@RegistrationStorage private val storage: Storage) { ... } 

// 필드 인젝션 방식
class ClassDependingOnStorage {

    @Inject
    @field:RegistrationStorage lateinit var storage: Storage
}

 

 

@Provides vs @Binds

Storage를 상속받는 SharedPreferencesStorage를 어떻게 주입할 수 있을까? 방법은 두 가지이다.

interface Storage {
  ...
}

class SharedPreferencesStorage @Inject constructor() : Storage { ... }

 

@Provides

Provides는 Module에서 외부에서 생성한 라이브러리나 개발자가 정의한 객체를 주입하기 위해 사용한다. 아래 경우에서 SharedPreferencesStorage의 생성방법을 @Inject를 통해 알려주었기 때문에 이를 그대로 반환한다.

@Module
class StorageModule {
     @Provides
     fun provideStorage(storage: SharedPreferencesStorage): Storage {
         return storage 
     }
 }

 

@Binds

Binds 역시 Module에서 Provides와 비슷한 기능을 수행한다. 다만 Binds는 내부적으로 훨씬 더 적은 코드를 생성하므로 더 효율적인 의존성 주입 방법이다.

@Module
abstract class StorageModule {
    @Binds
    abstract fun bindStorage(storage: SharedPreferencesStorage): Storage 
}

위의 bindStorage 메소드의 매개변수인 SharedPreferencesStorage는 @Inject를 통해 대거에게 생성 방법을 알려주었기 때문에 해당 객체를 생성하여 Storage로 바인딩하여 반환하게 된다.

 

 

 

Scope

특정 컴포넌트에 Scope를 지정하고 이와 생명주기를 같이할 클래스(타입)에 Scope을 지정할 수 있다. Scope에 지정되면 해당 컴포넌트 생명주기 동안은 항상 같은 객체를 반환하는 것을 보장한다. 다른 말로는 Scope이 없는 객체는 항상 새로운 객체를 주입하게 된다. 

 

Scoping rules

  1. type에 Scope를 붙일 때에는 오직 같은 Scope이 붙은 Component에 의해서만 사용 가능하다.
  2. Component에 Scope를 지정하면 Scope이 없는 type이나 동일한 Scope이 붙은 type만 제공할 수 있다.
  3. Subcomponent는 상위 Component에서 사용 중인 annotation을 사용할 수 없다.

 

@Singleton

대표적인 Scope로 객체를 단 한번만 생성하여 해당 객체가 필요한 곳에 같은 객체를 주입한다.

@Component(modules =[AModul.class])
@Singleton
interface MyComponent {
    ...
}
//Module에서 지정
@Provides
@Singleton
String provideName(){
    return "Charles";
}

//Class에 지정
@Singleton
public class Ctype {
    ...
    @Inject
    public Ctype(A a, B b){
        ...
    }
}

 

Custom Scope

직접 Scope를 생성할 수도 있다. 한 생명 주기 내에서 더 작은 생명주기를 만들어내야할 때 아래와 같이 Scope를 만들 수 있다. 

@Scope
@MustBeDocumented
@Retention(RetentionPolicy.RUNTIME)
annotation class ActivityScope

 

 

@Subcomponent

Subcomponent는 상위 Component를 상속하는 Component로 상위 Component의 객체는 Subcomponent에 의해서도 제공이 가능하다. 보통 Subcomponent에 Scope를 사용하여 특정 Component 내부의 객체는 해당 Scope 동안 같은 객체를 제공하도록 보장한다.

 

@ActivityScope
@Subcomponent
interface RegistrationComponent { ... }


@ActivityScope
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    ...
}
class RegistrationActivity : AppCompatActivity() {
    lateinit var registrationComponent: RegistrationComponent

    override fun onCreate(savedInstanceState: Bundle?) {
        registrationComponent = (application as MyApplication).appComponent.registrationComponent().create() 
        registrationComponent.inject(this)

        super.onCreate(savedInstanceState)
        ...
    }
    ...
}

Acitivty에 RegistrationComponent를 생성하여 주입하였다. RegistrationComponent는 해당 Acitivity와 생명주기를 같이하게 되고, RegistrationViewModel도 생명주기를 같이하여 같은 객체 주입을 보장받는다. 왜냐하면 RegistrationComponent에 @ActivityScope이 붙었고 RegistrationViewModel 또한 그렇기 때문이다.

 

 

@ContributesAndroidInjector

기존에 Subcomponent를 직접 생성하여 inject하는 보일러 플레이트를 줄이기 위한 annotation이다.

만약 다른 Activity, Fragment에서도 Storage Module 관련 클래스를 주입 받고 싶다면, 각각에 대해 MainComponent를 생성하고 inject 메소드를 호출하는 코드를 반복해야될 것이다.

 

 

사용 예시

@Module
abstract class ActivityBuilder{
    @ContributesAndroidInjector(modules = MainActivityModule.class)
    abstract MainActivity mainActivity();
    
    @ActivityScope
    @ContributesAndroidInjector(modules = RegistrationActivityModule.class)
    abstract RegistrationActivity registrationActivity();
}

위는 각각 MainActivity, RegistrationActivity 관련 Subcomponent를 생성한다. 그리고 RegistrationActivity 관련 Subcomponent는 ActivityScope로 지정되어 RegistrationModule 내의 @Provides 등에 @ActivityScope 등을 통해 생명주기를 같이 하여 항상 같은 객체를 반환하는 것을 보장할 수 있다. 

 

각 추상 메소드는 내부적으로 아래와 같은 코드를 만들어낸다고 생각하면 된다.

@Subcomponent
@PerActivity
public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> {
    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<YourActivity> {}
}