Dagger 적용기

최근 적용한 dagger에 대해서 이야기합니다

시작하며

DI 좋은건 다들 알지만 하나하나 만들어가자면 손이 너무 많이가는 것도 사실입니다. 그래서 많은 안드로이드 프로젝트에서 Dagger 또는 코틀린을 적용한 프로젝트라면 koin 등을 사용하고 있습니다. 저희는 설계 초기에 Dagger를 적용하기로 결정하고 그에 따른 크고 작은 작업들을 진행해 왔습니다. 환경설정 같은 내용은 다른 글들이 많이 있으니 넘어가고 바로 적용한 과정을 이야기해보겠습니다.

Constructor Injection

dagger에서는 inject 방법으로 3가지를 제시합니다

  • Constructor Injection
  • Method(Setter) Injection
  • Field Injection

여기서 Constructor InjectionField Injection을 주로 사용합니다.

Singleton

기존에는 Dagger 적용이 예정되어 있었기 때문에 Container는 따로 작성하지 않고 편의성 목적으로 많은 인스턴스를 Singleton으로 관리하고 있었습니다.


// 변경전

public class ShopListServiceImpl implements ShopListService {

    private final CommonService commonService;
    
    public ShopListServiceImpl(CommonService commonService) {
        this.commonService = commonService;
    }

    public static ShopListService getInstance() {
        return InstanceHolder.INSTANCE;
    }

    private static final class InstanceHolder {

        private static final ShopListService INSTANCE = new ShopListServiceImpl(CommonServiceImpl.getInstance()));
    }
}

// 변경후

@Singleton
public class ShopListServiceImpl implements ShopListService {

    private final CommonService commonService;
    
    @Inject
    public ShopListServiceImpl(CommonService commonService) {
        this.commonService = commonService;
    }

}

이제 ShopListServiceImpl은 필요한 곳으로 주입될 준비가 되었습니다. 하지만 ShopListServiceImpl과 ShopListService의 연결이 필요합니다. 여기서 Module을 작성합니다.

Module

모듈에서 Provides 또는 Binds 를 사용하여 주입될 클래스 인스턴스를 직접 생성하거나 주입받아 연결합니다. 메소드 내부에서 직접 인스턴스를 생성하지 않고 연결만 하는 경우에는 아래와 같이 Binds를 이용하여 추상화 할 수 있습니다.


@Module
public abstract class ServiceModule {

    // 직접 인스턴스 생성
    @Provides
    @Singleton
    ShopListApi provideShopListService(Retrofit retrofit){
        return retrofit.create(ShopListApi.class);
    };

    // 주입 받아서 연결
    @Binds
    @Singleton
    abstract ShopListService provideShopListService(ShopListServiceImpl shopListService);
}


Component

컴포넌트에서 사용되는 ModuleScope Level , 주입받을 대상 등을 설정합니다.

  • Singleton
  • Activity
  • Fragment

위와 같은 3단계로 구분하여 많이 사용합니다.

Scope Level

Scope Level은 Component Dependency 또는 Subcomponent로 설정합니다. 저희는 dagger-android를 같이 사용하기 때문에 Subcomponent이용하여 Scope Level을 정의합니다.

  • 어플리케이션과 생명주기를 함께하는 component

    주로 singleton으로 관리되던 클래스들을 포함


@Singleton
@Component(modules = {
        AndroidSupportInjectionModule.class,
        ApplicationModule.class,
        ApiModule.class,
        CacheDataModule.class,
        RemoteDataModule.class,
        DataSourceModule.class,
        RepositoryModule.class,
        ServiceModule.class,
        FailoverModule.class,
        DataBaseModule.class,
        AiActivityModule.class
})
public interface ApplicationComponent {
    void inject(BDApplication application);
}


  • Activity와 생명주기를 함께하는 component & module

    dagger가 처음 나왔을때는 각 화면에서 필요한 component 초기화하기 위해서 직접 ApplicationComponent를 참조해야 했지만 지금은 dagger-android를 이용하여 직접 참조 없이 편하게 사용할 수 있습니다.


@Module
public class ShopsPagerActivityModule {

    @Provides
    @ActivityScope
    @ActivityContext
    Context provideContext(ShopListPagerActivity context) {
        return context;
    }

    @Provides
    @ActivityScope
    ShopListPagerContract.Presenter providePresenter(ShopListPagerPresenterImpl presenter) {
        return presenter;
    }

}

Subcomponent의 builder 내부의 메소드를 생략가능한 경우 아래와 같이 ContributesAndroidInjector 이용하여 작성하는 것이 가능합니다.


@Module(subcomponents = AiShopListPagerActivityComponent.class)
public abstract class AiShopListPagerActivityModule {

    @Binds
    @IntoMap
    @ActivityKey(ShopListPagerActivity.class)
    abstract AndroidInjector.Factory<? extends Activity>
    bind ShopListPagerActivityInjectorFactory(AiShopListPagerActivityComponent.Builder builder);
    
    @ActivityScope
    @Subcomponent(modules = {AiShopsFragmentModule.class,ShopsPagerActivityModule.class})
    public interface AiShopsPagerActivityComponent extends AndroidInjector<ShopsPagerActivity> {

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<ShopsPagerActivity> {

        public abstract void activityModule(ShopsPagerActivityModule module);

        @Override
        public void seedInstance(ShopsPagerActivity instance) {
            activityModule(new ShopsPagerActivityModule());
        }
    }

}


@Module
public abstract class AiActivityModule {

    @ActivityScope
    @ContributesAndroidInjector(modules = {
            ShopsPagerActivityModule.class,
            AiShopsFragmentModule.class,
            AiMenuDrawerFragmentModule.class})
    abstract ShopListPagerActivity shopListPagerActivity();
}

준비끝!

이제 사용할 준비가 끝났습니다. 어플리케이션에는 HasActivityInjector를 액티비티에는 HasSupportFragmentInjector(플래그먼트가 있는 경우)를 구현하면 되겠습니다. 구현에는 특별한 작업은 필요하지 않고 DispatchingAndroidInjector를 주입받아 넘겨주기만 하면 됩니다.


@Singleton
public class BDApplication extends Application implements HasActivityInjector {


    @Inject
    DispatchingAndroidInjector<Activity> dispatchingActivityInjector;
    
    @Override
    public void onCreate() {
            DaggerApplicationComponent.builder()
                .applicationModule(new ApplicationModule(this))
                .build()
                inject(this);
    }
    
    @Override
    public AndroidInjector<Activity> activityInjector() {
        return dispatchingActivityInjector;
    }
}


@ActivityScope
public class ShopListPagerActivity extends BaseActivity implements HasSupportFragmentInjector{

    @Inject
    DispatchingAndroidInjector<Fragment> androidInjector;
    
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
    }
    
    @Override
    public AndroidInjector<Fragment> supportFragmentInjector() {
        return androidInjector;
    }
}

끝으로

아직 Dagger를 적용만 했을뿐 Module정리가 온전하게 끝나지 않았습니다. 앞으로 기능별로 모듈을 분리하고 Dagger를 이용하여 Integration Test를 작성할 예정입니다. 적용하기까지 긴 시간이 걸렸지만 천천히 모두 함께 완성했기에 의미가 있는 작업이었다고 생각합니다.

참조

android dagger