[Android/DI] Dagger2 - Scope와 dagger.android

2021. 7. 25. 22:14·Android/Dependency Injection
반응형

Dagger의 가장 큰 장점은 계층관계의 오브젝트 그래프를 만들 수 있다는 것 입니다.

이번에 다룰 @Subcomponent와 @Scope를 이용하면 오브젝트 그래프를 이용하여 앱 구성요소의 생명주기 동안 메모리 할당과 해제를 효율적으로 관리할 수 있게 해줍니다.

Application(Singleton) Scope 지정

먼저, Application Scope를 가져야하는 클래스들(Repository, DataSource 등등....)은 앱이 실행되는 동안 인스턴스가 메모리에 있기를 원하기 때문에 Application의 생명주기와 동일한 범위를 따르는 오브젝트 그래프를 만듭니다. 이렇게 하면 그래프는 앱 수명 주기에 연결됩니다.

 

그래프를 생성하는 인터페이스는 @Component로 주석이 지정되므로 ApplicationComponent로 호출할 수 있습니다. Dagger는 종속 관계에 따른(일반적으로 Lifecycle에 관련한 종속관계) 프로젝트의 오브젝트 그래프를 만들 수 있습니다. Dagger가 이렇게 하도록 하려면 인터페이스를 만들고 @Component로 주석을 지정해야 합니다.

 

프로젝트 빌드 시 Dagger는 자동으로 ApplicationComponent 인터페이스의 구현, 즉 DaggerApplicationComponent를 생성합니다. 또한, Dagger는 Annotation Proceesor를 사용하여 최초의 진입점으로 계층 관계로 구성된 종속 항목을 만듭니다.

@Singleton
@Component(
    modules = [ApplicationModule::class,
        ViewModelModule::class,
        NetworkModule::class,
        DataSourceModule::class]
)
interface ApplicationComponent {

    fun userActivityComponentBuilder(): UserActivityComponent.Builder

    fun inject(myApplication: MyApplication)


    @Component.Builder
    interface Builder {
        fun application(@BindsInstance application: Application): Builder
        fun setApplicationModule(applicationModule: ApplicationModule): Builder
        fun setDataSourceModule(dataSourceModule: DataSourceModule): Builder
        fun setNetworkModule(networkModule: NetworkModule): Builder

        fun build(): ApplicationComponent
    }
}
@Module
class DataSourceModule {

    @Singleton
    @Provides
    fun provideUserRemoteDataSource(userRetrofitService: UserRetrofitService): UserRemoteDataSource {
        return UserRemoteDataSourceImpl(userRetrofitService)
    }

    @Singleton
    @Provides
    fun provideUserLocalDataSource(): UserLocalDataSource {
        return UserLocalDataSourceImpl()
    }
}
@Module
abstract class RepositoryModule {

    @Singleton
    @Binds
    abstract fun bindUserRepository(userRepositoryImpl: UserRepositoryImpl): UserRepository
}
@Module(
    includes = [
        RepositoryModule::class],
    subcomponents = [UserActivityComponent::class]
)
class ApplicationModule {

    @Singleton
    @Provides
    fun provideApplicationContext(application: Application): Context {
        return application.applicationContext
    }
}

ApplicationComponent에 속한 Module(현재 예제에서는 ApplicationModule, RepositoryModule, DataSourceModule, NetWorkModule)들에 선언되어있는 객체들에 @Singleton 어노테이션을 통해 객체를 요청할 때 마다 항상 동일한 인스턴스를 주입받을 수 있습니다.

 

만약, 싱글톤이 아닌 매번 새로운 인스턴스를 받고싶다면 @Singleton 어노테이션을 제거해주면 됩니다.

Activity, Fragment Scope 지정

Dagger에서의 권장사항은 앱의 라이프사이클에 따라 Scope를 정의하는 것 입니다.

그러므로, @ActivityScope와 @FragmentScope를 정의하겠습니다.

@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class FragmentScope

일반적으로 앱 구성요소의 생명주기는 Application 생명주기 안에서 Activity 생명주기가 존재하고, Activity안에 Fragment의 생명주기가 존재합니다.

그러므로, ApplicationComponent의 SubComponent로 UserActivityComponent를 만들어주고, UserActivityComponent로 UserFragmentComponent를 생성해줍니다.

 

또한, SubComponent에는 @Subcomponent.Builder를 통해 UserActivityComponent의 Provision 메소드를 반환할 수 있도록 해줘야합니다.

@ActivityScope
@Subcomponent(modules =[UserActivityModule::class])
interface UserActivityComponent {

    fun inject(activity: UserActivity)
    fun userFragmentBuilder(): UserFragmentComponent.Builder

    @Subcomponent.Builder
    interface Builder {
        fun setModule(module: UserActivityModule):Builder
        @BindsInstance
        fun setActivity(activity: UserActivity): Builder
        fun build(): UserActivityComponent
    }
}
@Module(subcomponents = [UserFragmentComponent::class])
class UserActivityModule {

    @Provides
    @ActivityScope
    fun provideActivityName(): String {
        return UserActivity::class.simpleName ?: "UserActivity"
    }
}
@FragmentScope
@Subcomponent(modules = [UserFragmentModule::class])
interface UserFragmentComponent {

    fun inject(fragment: UserFragment)

    @Subcomponent.Builder
    interface Builder {

        fun setModule(module: UserFragmentModule): Builder
        @BindsInstance
        fun setFragment(fragment: UserFragment): Builder
        fun build(): UserFragmentComponent
    }
}
@Module
class UserFragmentModule {

    @Provides
    @FragmentScope
    fun provideFragmentName(): String {
        return UserFragment::class.simpleName ?: "UserFragment"
    }
}

이렇게 코드를 작성해주면, 아래와 같은 그림의 오브젝트 그래프의 구조가 됩니다.

ApplicationComponent의 오브젝트 그래프

Dagger에서 @Inject를 통한 인스턴스 주입 과정은 SubComponent를 타고 내려와 가장 아래부터 우선 탐색하여 인스턴스를 반환하고 인스턴스가 없으면 상위 컴포넌트들을 순차적으로 올라가 탐색하는 과정을 반복합니다. 이러한 원리를 이용하여 @Scope를 지정하여 인스턴스의 생명주기를 조절할 수 있습니다.

 

위 코드에서, UserActivityComponent위에 @ActivityScope를 등록해주었고, UserActivityModule내의 @ActivityScope를 가진 바인딩 메소드를 지정해주면 UserActivityComponent를 통해 객체를 주입받는 인스턴스들은 해당 Scope동안 항상 같은 객체를 주입받을 수 있습니다.

 

또한, 하위 컴포넌트에서는 상위 컴포넌트에 등록된 객체를 가져올 수 있습니다.

하위 컴포넌트에서 상위 컴포넌트의 상위 Scope로 지정되어있는 객체 또한 마찬가지로 항상 같은 객체로 주입받을 수 있습니다.

(반대로, Scope를 지정해주지 않은 객체는 항상 다른 객체를 주입받습니다.)

 

Fragment에서도 @ActivityScope의 객체를 가져온다면 마찬가지로 항상 동일한 객체를 주입받을 수 있습니다.

그러므로, Activity가 그대로이고 Fragment가 생성, 삭제 된다해도 @FragmentScope로 지정한 객체들은 매번 새로운 객체를 주입받을 수 있습니다.

class MyApplication : Application() {

    private lateinit var appComponent: ApplicationComponent

    override fun onCreate() {
        super.onCreate()
        appComponent = DaggerApplicationComponent.builder().application(this)
            .setApplicationModule(ApplicationModule())
            .setDataSourceModule(DataSourceModule())
            .setNetworkModule(NetworkModule())
            .build()

    }
    fun getAppComponent(): ApplicationComponent = appComponent
}
class UserActivity : AppCompatActivity() {

    private lateinit var binding: ActivityUserBinding
    private lateinit var component: UserActivityComponent

    @Inject
    lateinit var activityName: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        component =
            (applicationContext as MyApplication).getAppComponent().userActivityComponentBuilder()
                .setActivity(this).setModule(
                    UserActivityModule()
                ).build().apply {
                    inject(this@UserActivity)
                }

        binding = DataBindingUtil.setContentView<ActivityUserBinding>(this, R.layout.activity_user)
            .apply {
                activity = this@UserActivity
                lifecycleOwner = this@UserActivity
                vm = this@UserActivity.viewModel
            }
    }
}
class UserFragment : Fragment() {

    private lateinit var binding: FragmentUserBinding

    @Inject
    lateinit var fragmentName: String

    override fun onAttach(context: Context) {
        super.onAttach(context)
        (activity as UserActivity).component.userFragmentBuilder().setModule(UserFragmentModule())
            .setFragment(this).build().inject(this@UserFragment)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user, container, false)
        return binding.root
    }

}

각 앱 구성요소에서 멤버인젝션으로 주입하는 예제입니다. (Dagger 관련 부분 제외하고 코드를 지웠습니다.)

 

위 코드처럼, Application, Activity, Fragment에 맞는 Component를 가져와서 inject를 해주면, 각 Scope에 맞게 객체가 주입됩니다.

 

dagger-android 적용

Dagger2의 문제점

- 보일러 플레이트 코드가 너무 많음 (프로젝트가 커질수록 코드 변경(리팩토링)에 대해 취약해짐)

- 매번 Member-injection 메소드를 작성해야함

- Activity 등, 주입받을 곳에서 일일히 Component 객체를 가져와 주입시켜줘야 함

 

이러한 문제점들을 해결하기 위해, Dagger2에서는 dagger-android 패키지를 제공하였습니다.

 

위의 예제 코드에 dagger-android를 적용시켜 보도록 하겠습니다.

 

과정은 아래와 같습니다.

1. UserActivityComponent에 AndroidInjector<T>상속, Factory 메소드에도 마찬가지로 AndroidInjector.Factory<T> 상속

2. ApplicationComponent에 AndroidInjector<T> 상속

3. AndroidInjector들의 멀티바인딩 수행

4. Application에 HasAndroidInjetor상속 및 DispatchingAndroidInjector 주입

5. Activity 혹은 Fragment에서 의존성 주입

 

UserActivityComponent.kt

@ActivityScope
@Subcomponent(modules =[UserActivityModule::class])
interface UserActivityComponent: AndroidInjector<UserActivity> {

    //fun inject(activity: UserActivity)

    @Subcomponent.Factory
    interface Factory: AndroidInjector.Factory<UserActivity> {}


}

UserActivityComponent에 AndroidInjector<UserActivity>를 상속시켜줍니다.

또한, Factory 메소드에도 마찬가지로 AndroidInjector.Factory 상속시켜 줍니다.

 

ActivityBindingModule.kt

@Module
abstract class ActivityBindingModule {

    @Binds
    @IntoMap
    @ClassKey(UserActivity::class)
    abstract fun bindAndroidInjectorFactory(factory: UserActivityComponent.Factory): AndroidInjector.Factory<*>

}

AndroidInjector의 Factory들을 멀티바인딩 하는 메소드를 작성해야 합니다.

AndroidInjector를 통해서 SubComponent들을 Member Injection 을 수행할 수 있도록 해줍니다.

 

Key는 ClassKey 혹은 StringKey를 사용 가능한데 ClassKey를 이용해서 바인딩해주겠습니다.

 

AndroidInjectionModule.kt

@Beta
@Module
public abstract class AndroidInjectionModule {
  @Multibinds
  abstract Map<Class<?>, AndroidInjector.Factory<?>> classKeyedInjectorFactories();

  @Multibinds
  abstract Map<String, AndroidInjector.Factory<?>> stringKeyedInjectorFactories();

  private AndroidInjectionModule() {}
}

 

AndroidInjector.kt

/**
 * Performs members-injection for a concrete subtype of a <a
 * href="https://developer.android.com/guide/components/">core Android type</a> (e.g., {@link
 * android.app.Activity} or {@link android.app.Fragment}).
 *
 * <p>Commonly implemented by {@link dagger.Subcomponent}-annotated types whose {@link
 * dagger.Subcomponent.Factory} extends {@link Factory}.
 *
 * @param <T> a concrete subtype of a core Android type
 * @see AndroidInjection
 * @see DispatchingAndroidInjector
 * @see ContributesAndroidInjector
 */
@Beta
public interface AndroidInjector<T> {

  /** Injects the members of {@code instance}. */
  void inject(T instance);

  /**
   * Creates {@link AndroidInjector}s for a concrete subtype of a core Android type.
   *
   * @param <T> the concrete type to be injected
   */
  interface Factory<T> {
    /**
     * Creates an {@link AndroidInjector} for {@code instance}. This should be the same instance
     * that will be passed to {@link #inject(Object)}.
     */
    AndroidInjector<T> create(@BindsInstance T instance);
  }

  /**
   * An adapter that lets the common {@link dagger.Subcomponent.Builder} pattern implement {@link
   * Factory}.
   *
   * @param <T> the concrete type to be injected
   * @deprecated Prefer {@link Factory} now that components can have {@link dagger.Component.Factory
   *     factories} instead of builders
   */
  @Deprecated
  abstract class Builder<T> implements AndroidInjector.Factory<T> {
    @Override
    public final AndroidInjector<T> create(T instance) {
      seedInstance(instance);
      return build();
    }

    /**
     * Provides {@code instance} to be used in the binding graph of the built {@link
     * AndroidInjector}. By default, this is used as a {@link BindsInstance} method, but it may be
     * overridden to provide any modules which need a reference to the activity.
     *
     * <p>This should be the same instance that will be passed to {@link #inject(Object)}.
     */
    @BindsInstance
    public abstract void seedInstance(T instance);

    /** Returns a newly-constructed {@link AndroidInjector}. */
    public abstract AndroidInjector<T> build();
  }
}

 

ApplicationComponent.kt

@Singleton
@Component(
    modules = [AndroidInjectionModule::class,
        ApplicationModule::class,
        ActivityInjectorModule::class,
        ViewModelModule::class,
        NetworkModule::class,
        ActivityBindingModule::class,
        DataSourceModule::class]
)
interface ApplicationComponent: AndroidInjector<MyApplication> { // member-injection 생략 ( inject() 생략 가능하게 해줌)
    
    //fun userActivityComponentBuilder(): UserActivityComponent.Builder

    //fun inject(myApplication: MyApplication)

    @Component.Factory
    interface Factory : AndroidInjector.Factory<MyApplication> { // MyApplication 인스턴스를 그래프에 바인딩하고 컴포넌트를 반환하는 create()메소드를 포함시켜줌

    }
}

@Module에 AndroidInjectorModule과 ActivityInjectorModule을 추가해줍니다.

 

그리고 ApplicationComponent에서 AndroidInjector<T>를 상속해주고, Factory 인터페이스에 AndroidInjector.Factory를 상속해줍니다.

 

-> AndroidInjector를 통해 오브젝트 그래프에 있는 컴포넌트들의 Member Injection 메소드들을 추가시켜줍니다.

또한, AndroidInjector의 Factory 메소드를 상속함으로서 MyApplication 인스턴스를 오브젝트 그래프에 바인딩 시켜주고 모듈을 포함한 AndroidInjector<MyApplication>의 create()메소드를 반환해줍니다.

 

MyApplication.kt

class MyApplication : Application(), HasAndroidInjector {

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>

    override fun onCreate() {
        super.onCreate()
        DaggerApplicationComponent.factory().create(this).inject(this)
    }

    override fun androidInjector(): AndroidInjector<Any> {
        return dispatchingAndroidInjector
    }
}

Application에 HasAndroidInjector 인터페이스를 상속해주고 DispatchingAndroidInjector를 @Inject를 통해 주입시켜줍니다.

그러면, DispatchingAndroidInjector를 통해서 Android의 구성요소(Activity, Fragment 등등)에 바인딩 되어있는 AndroidInjector를 통해서 Member Injection을 수행할 수 있도록 해줍니다.

 

UserActivity.kt

class UserActivity : AppCompatActivity(), HasAndroidInjector {

    private lateinit var binding: ActivityUserBinding

    @Inject
    lateinit var activityName: String
    
    @Inject
    lateinit var androidInjector: DispatchingAndroidInjector<Any>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        AndroidInjection.inject(this)
        binding = DataBindingUtil.setContentView<ActivityUserBinding>(this, R.layout.activity_user)
            .apply {
                activity = this@UserActivity
                lifecycleOwner = this@UserActivity
                vm = this@UserActivity.viewModel
            }
    }

    override fun androidInjector(): AndroidInjector<Any> {
        return androidInjector
    }

}

 

@ContributesAndroidInjector 어노테이션 활용

ApplicationComponent의 AndroidInjector.Factory에 추가적인 메소드가 없을 경우 @ContributesAndroidInjector를 이용하면 DispatchingAndroidInjector를 이용하지 않아도 되므로 보일러플레이트를 더 줄일 수 있습니다.

 

 

@Module
abstract class ActivityBindingModule {

    @ActivityScope
    @ContributesAndroidInjector(modules = [UserActivityModule::class])
    abstract fun userActivity(): UserActivity
}
class MyApplication : DaggerApplication() {

    override fun onCreate() {
        super.onCreate()
        DaggerApplicationComponent.factory().create(this).inject(this)
    }
    
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerApplicationComponent.factory().create(this)
    }
}
class UserActivity : AppCompatActivity() {

    private lateinit var binding: ActivityUserBinding

    @Inject
    lateinit var activityName: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        AndroidInjection.inject(this)
        binding = DataBindingUtil.setContentView<ActivityUserBinding>(this, R.layout.activity_user)
            .apply {
                activity = this@UserActivity
                lifecycleOwner = this@UserActivity
                vm = this@UserActivity.viewModel
            }
    }
}

 

반응형
'Android/Dependency Injection' 카테고리의 다른 글
  • [Android/DI] Dagger2 - MultiBinding 주입하기
  • [Android/DI] Dagger2 - Qualifier 어노테이션
  • [Android/DI] Dagger2 - Lazy Injection vs Provider Injection
  • [Android/DI] Dagger2 - 소개 및 Module과 Component
seunghwaan
seunghwaan
공부한 내용을 정리하는 개발 기록 블로그
  • seunghwaan
    SH's Devlog
    seunghwaan
  • 전체
    오늘
    어제
    • 분류 전체보기 (144)
      • Android (62)
        • Basic (17)
        • Kotlin(Java) (14)
        • UI & Animation (1)
        • Compose (2)
        • Coroutines (1)
        • Dependency Injection (6)
        • RxJava (8)
        • BLE (3)
        • TDD (2)
        • JetPack (1)
        • NextStep (4)
        • Error Log (3)
      • Flutter (14)
        • Basic (5)
        • Dart (1)
        • State Management (2)
        • Widgets (4)
        • Error and Tips (2)
      • CS(Computer Science) (18)
        • Network (4)
        • Database (10)
        • Design Pattern (1)
        • Computer Architecture (3)
        • Operating System (0)
      • iOS (8)
        • Basic (0)
        • Swift (8)
      • Cloud (6)
        • AWS (6)
      • Web Frontend (0)
        • JavaScript(TS) (0)
        • React (0)
      • DevOps (25)
        • GIT (4)
        • CI CD (8)
        • Linux (4)
        • Docker (9)
        • Error Log (0)
      • 코딩테스트 (10)
        • DB (6)
        • 알고리즘 (4)
      • Backend (1)
        • Spring (1)
      • Mac Tip (0)
      • Temporary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    database
    Kotlin
    gradle
    상태 관리
    AWS
    Algorithm
    Computer Science
    error
    MySQL
    시작하세요! 도커
    컴퓨터공학
    네트워크
    Swift
    Network
    FLUTTER
    IOS
    RxJava
    CI
    Jenkins
    Android
    Dagger
    cognito
    di
    cs
    Dependency Injection
    docker
    cd
    Linux
    CICD
    BLE
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
seunghwaan
[Android/DI] Dagger2 - Scope와 dagger.android
상단으로

티스토리툴바