DaggerAutoInject – Contributing to AndroidInjection with Annotation

도입

Dagger 를 사용하기 위해서는 주입할 의존성을 @Provides 나 @Binds 를 통하여 제공하는 것 말고도, 주입될 대상을 제공해야 하는데, Members-injection methods (dagger/api/latest/dagger/Component.html) 를 사용하거나 각 안드로이드 구성요소 (Activity, Service) 들에 대한 별도의 Subcomponent 를 만들고 @IntoSet 어노테이션을 이용해 DispatchingAndroidInjector 의 injectorFactories 에 해당 Subcomponent 를 추가해야 합니다.

두 가지 방법, Members-injection methods 와 Subcomponet 가 공통점을 가지고 있다면 빠르게 개발 해야 하는 입장에서는 상당히 고역이란 점입니다.

Members-injection methods 는 Component 내부에 직접 적어주긴 하나 작성해야 하는 코드 양이 적어 괜찮다고 볼 수 있습니다. 다음 코드에서 void inject ~ 코드가 하나의 Members-injection methods 를 나타냅니다.

public interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(MainApplication application);
        AppComponent build();
    }
 
    void inject(MainApplication mainApp);
    void inject(MenuView menuView);
}

Subcomponent 로 오게 되면 작성해야 하는 코드는 매우 많아집니다.

  • MainActivity 에 대한 Subcomponent 를 만드는데, 이 Subcomponent 는 AndroidInjector<MainActivity> 를 상속하고 있어야 하고, abstract 클래스인 Builder 를 구현해야 함
  • 만든 MainActivitySubCompoent.Builder 를 @Binds, @IntoMap, @Activitykey 등의 메서드로 제공하는데, 내부적으로 Map<Class<?>, Provider<Activity> > 를 가지고 있어, 필요한 때에 Provider를 제공해야 함
  • 위 두 가지를 모두 포함한 클래스를 만들고, 해당 클래스를 @Module로 설정한 다음 Module 의 파라미터로 MainActivitySubcomponet 를 제공해야 함.

위 세 가지를 모두 반영한 것이 아래 코드입니다.

package com.github.windsekirun.daggerautoinject;

import android.app.Activity;
import com.github.windsekirun.daggerautoinject.sample.MainActivity;
import dagger.Binds;
import dagger.Module;
import dagger.Subcomponent;
import dagger.android.ActivityKey;
import dagger.android.AndroidInjector;
import dagger.multibindings.IntoMap;

@Module(subcomponents = ActivityModule_Contribute_MainActivity.MainActivitySubcomponent.class)
public abstract class ActivityModule_Contribute_MainActivity {
  private ActivityModule_Contribute_MainActivity() {}

  @Binds
  @IntoMap
  @ActivityKey(MainActivity.class)
  abstract AndroidInjector.Factory<? extends Activity> bindAndroidInjectorFactory(
      MainActivitySubcomponent.Builder builder);

  @Subcomponent
  public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> {
    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<MainActivity> {}
  }
}

아무리 Dependency Injection 가 좋다고 해도, 도입에 있어 조금 겁이 날 수 있는 부분이라 생각합니다.

그래서 Dagger-Android 모듈에서는 @ContributesAndroidInjector 어노테이션을 제공하는데, 하나의 전체 모듈을 만들고 @ContributesAndroidInjector 어노테이션을 부착한 메서드를 각 안드로이드 구성요소당 하나씩 만들어 주면 나머지 세 가지 코드에 대해서는 자동으로 생성하는 기능을 가지고 있습니다.

package com.github.windsekirun.daggerautoinject;

import com.github.windsekirun.daggerautoinject.sample.MainActivity;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;

@Module
public abstract class ActivityModule {
  @ContributesAndroidInjector
  abstract MainActivity contribute_MainActivity();
}

하지만, 이와 같은 @ContributeAndroidInjector 에도 개선할 점은 있습니다. 앱에 Activity 가 여러 개 있다면, Activity 를 하나 작성할 때 마다 ActivityModule 란 곳에 작성해주는 것도 꽤나 고역이라고 생각됩니다.

그래서 찾은 라이브러리가 florent37/DaggerAutoInject (https://github.com/florent37/DaggerAutoInject) 이고, 이 라이브러리가 Activity / Fragment 만 제공하던 것을 좀 더 확장해 Activity / Fragment / Service / Broadcast Receiver / ContentProvider / ViewModel (Android Architecture components) 에 대해 제공하게 한 것이 WindSekirun/DaggerAutoInject (https://github.com/WindSekirun/DaggerAutoInject) 입니다.

이번 글에서는 DaggerAutoInject 라이브러리가 어떻게 작동하는지 살펴보고 적용 방법을 설명하려 합니다.

구현 원리 및 사용법

이전 글인 Generate Kotlin Code with KotlinPoet uses Annotation Processor (https://blog.uzuki.live/generate-kotlin-code-with-kotlinpoet-uses-annotation-processor-1/) 에서도 설명한 Annotation Processor 로 특정 Annotation 가 붙은 클래스를 모두 찾아서 각 타입에 맞게 ActivityModule / ServiceModule / FragmentModule / ViewModelModule 를 생성하는 것입니다.

ActivityModule 의 구현 방법

ActivityModule / ServiceModule / FragmentModule / BroadcastReceiverModule / ContentProviderModule 에 대해서는 모두 같은 구현 방법을 취합니다.

  1. @InjectActivity 를 모두 찾아 ContributesHolder 란 객체에 담고, Map<ClassName, ContributesHolder> 로 가지고 있는다.
  2. 각 어노테이션 별 TypeSpec (클래스를 생성할 스펙) 를 만들고 map 를 반복문에 통과시켜서 @ContributesAndroidInjector abstract SimpleName contributes_SimpleName(); 라는 메서드를 생성한다.
  3. TypeSpec 를 Java 파일로 만든다.

위 과정을 모두 포함하는 것이 다음 코드입니다.

static <A extends Annotation> void processHolders(RoundEnvironment env, Class<A> cls, Map<ClassName, ContributesHolder> map) {
    for (Element element : env.getElementsAnnotatedWith(cls)) {
        final ClassName classFullName = ClassName.get((TypeElement) element);
        final String className = element.getSimpleName().toString();
        map.put(classFullName, new ContributesHolder(element, classFullName, className));
    }
}

static void constructContributesAndroidInjector(String className, Collection<ContributesHolder> holders, Filer filer) {
    final TypeSpec.Builder builder = TypeSpec.classBuilder(className)
            .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
            .addAnnotation(Constants.DAGGER_MODULE);

    for (ContributesHolder contributesHolder : holders) {
        builder.addMethod(MethodSpec.methodBuilder(Constants.METHOD_CONTRIBUTE + contributesHolder.className)
                .addAnnotation(Constants.DAGGER_ANDROID_ANNOTATION)
                .addModifiers(Modifier.ABSTRACT)
                .returns(contributesHolder.classNameComplete)
                .build()
        );
    }

    final TypeSpec newClass = builder.build();
    final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();

    try {
        javaFile.writeTo(System.out);
        javaFile.writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

첫 번째 메서드가 1번의 과정을 가지고 있으며, 두 번째 메서드가 2, 3번의 과정을 가지고 있습니다.

사용 방법

먼저, AppComponent 의 @Component 어노테이션에 ActivityModule 들을 삽입합니다.

@Singleton
@Component(modules = {
        AppModule.class,

        AndroidInjectionModule.class,
        AndroidSupportInjectionModule.class,

        ActivityModule.class,
        FragmentModule.class,
        ViewModelModule.class,
        ServiceModule.class,
        BroadcastReceiverModule.class,
        ContentProviderModule.class
})
public interface AppComponent {
    void inject(MainApplication application);

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(Application application);

        AppComponent build();
    }
}

그 다음, 적용할 Activity 나 Service 들에 @InjectActivity / @InjectFragment / @InjectService / @InjectBroadcastReceiver / @InjectContentProvider 를 부착합니다.

@InjectActivity
public class MainActivity extends BaseActivity {

    @Inject SharedPreferences sharedPreferences;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d("MainActivity", sharedPreferences.getAll());
    }

그 다음 Application 내에 @InjectApplication 어노테이션을 부착한 뒤  아래 필드를 삽입하고, HasActivityInjector, HasServiceInjector,
HasBroadcastReceiverInjector, HasContentProviderInjector 인터페이스들을 구현합니다.

@Inject DispatchingAndroidInjector<Activity> mActivityDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<Service> mServiceDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<BroadcastReceiver> mBroadcastReceiverDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<ContentProvider> mContentProviderDispatchingAndroidInjector;

그 다음, Application.onCreate 에서 생성한 AppComponent 를 DaggerAutoInject 란 클래스에 넘겨줍니다.

전체 코드는 다음과 같습니다.

@InjectApplication(component = AppComponent.class)
public class MainApplication extends Application implements HasActivityInjector, HasServiceInjector,
        HasBroadcastReceiverInjector, HasContentProviderInjector {

    @Inject DispatchingAndroidInjector<Activity> mActivityDispatchingAndroidInjector;
    @Inject DispatchingAndroidInjector<Service> mServiceDispatchingAndroidInjector;
    @Inject DispatchingAndroidInjector<BroadcastReceiver> mBroadcastReceiverDispatchingAndroidInjector;
    @Inject DispatchingAndroidInjector<ContentProvider> mContentProviderDispatchingAndroidInjector;

    @Override
    public void onCreate() {
        super.onCreate();

        final AppComponent appComponent = DaggerAppComponent.builder()
                .application(this)
                .build();

        DaggerAutoInject.init(this, appComponent);
    }

    @Override
    public AndroidInjector<Activity> activityInjector() {
        return mActivityDispatchingAndroidInjector;
    }

    @Override
    public AndroidInjector<Service> serviceInjector() {
        return mServiceDispatchingAndroidInjector;
    }

    @Override
    public AndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
        return mBroadcastReceiverDispatchingAndroidInjector;
    }

    @Override
    public AndroidInjector<ContentProvider> contentProviderInjector() {
        return mContentProviderDispatchingAndroidInjector;
    }
}

그 다음 Fragment 의 경우에는 앱의 BaseActivity 클래스에 HasSupportFragmentInjector 를 구현하고, DispatchingAndroidInjector<Fragment> dispatchingFragmentInjector;필드를 삽입해서 supportFragmentInjector() 메서드에 반환합니다.

public class BaseActivity extends AppCompatActivity implements HasSupportFragmentInjector {

    @Inject
    DispatchingAndroidInjector<Fragment> dispatchingFragmentInjector;

    @Override
    public AndroidInjector<Fragment> supportFragmentInjector() {
        return dispatchingFragmentInjector;
    }
}

이 단계에서 DaggerAutoInject 를 사용할 준비는 모두 마쳤으며, 각 구성요소들의 onCreate 에서 AndroidInjection.inject(this);를 호출하면 내부적으로 Dependency Injection 를 시행합니다. 단, Activity / Fragment 는 자동으로 inject 메서드를 호출하므로 따로 할 필요가 없습니다.

주의할 점은 이 dispatching 필드들은 Type Parameter 에 있는 클래스가 앱에 하나라도 존재 해야 작동합니다. 만일 앱에 Service 가 없는데 DispatchingAndroidInjector<Service> 를 사용하려 한다면 컴파일 단계에서 오류가 나옵니다.

ViewModelModule 의 구현 방법

ViewModel 의 경우에는 조금 다른 구현 방법 및 용도를 가집니다. ActivityModule 는 그 자체가 Contribute 의 용도를 가지지만 ViewModelModule 는 위에서도 언급했던 Map<Class<?>, Provider<ViewModel>> 를 구성하게 도와주는 용도로 사용됩니다.

processHolders 메서드까지는 같지만, 생성하는 부분은 다른 메서드를 사용합니다.

private void constructViewHolderModule() {
    final TypeSpec.Builder builder = TypeSpec.classBuilder(Constants.VIEWHOLDER_MODULE)
            .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
            .addAnnotation(Constants.DAGGER_MODULE);

    for (ContributesHolder contributesHolder : mViewModelHolders.values()) {
        TypeName typeName = contributesHolder.classNameComplete;
        String parameterName = String.valueOf(contributesHolder.className.charAt(0)).toLowerCase() +
                contributesHolder.className.substring(1);

        builder.addMethod(MethodSpec.methodBuilder(Constants.METHOD_BIND + contributesHolder.className)
                .addAnnotation(Constants.DAGGER_BINDS)
                .addParameter(typeName, parameterName)
                .addAnnotation(Constants.DAGGER_INTOMAP)
                .addAnnotation(AnnotationSpec.builder(ViewModelKey.class)
                        .addMember("value", contributesHolder.className + ".class").build())
                .addModifiers(Modifier.ABSTRACT)
                .returns(Constants.VIEWMODEL)
                .build()
        );
    }

    final TypeSpec newClass = builder.build();
    final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();

    try {
        javaFile.writeTo(System.out);
        javaFile.writeTo(mFiler);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

위 결과로 생성된 ViewModelModule 는 아래와 같은 형태를 가지게 됩니다.

@Module
public abstract class ViewModelModule {

  @Binds
  @IntoMap
  @ViewModelKey(MainViewModel.class)
  abstract ViewModel bind_MainViewModel(MainViewModel mainViewModel);
}

이 ViewModelModule 를 사용하기 위해서는 아래 요소가 필요합니다.

  1. ViewModelModule 로 통하여 제공된 Map<Class<?>, Provider<ViewModel>> 를 사용하여 실제 ViewModel 의 객체를 반환할 Factory 클래스. 이 클래스는 ViewModelProvider.Factory 를 상속하여 Android Architecture components 의 ViewModelProvider.of 로 가져올 수 있게 합니다.
  2. 1번에서 생성할 Factory 클래스를 주입할 Module 클래스.

이 요소를 구현한 것이 다음 코드입니다. 설명은 주석으로 갈음합니다.

@Singleton // 한번 의존성이 생성되고 난 후에는 기존 인스턴스를 그대로 사용
/*
 * AAC 의 ViewModelProvider.Factory 를 상속하는 클래스를 제작.
 * 생성자로는 Map<Class<*>, Provider<ViewModel>> 를 받는데, 이 생성자는 미리 @IntoMap 와 @ViewModelKey 를 부착하여
 * Module 에 제공된 ViewModel 클래스의 클래스 객체와 그 ViewModel 의 생성자를 제공하는 Provider 객체를 각각 key, value로서 받는다.
 * 따라서, Map에는 MainViewModel.class.java 라는 키에 Provider<MainViewModel> 가 제공됨

 * 이 Provider 는 해당 ViewModel 에 대한 생성자를 제공할 수 있는 기능을 가지고 있기 때문에,
 * 실제로 ViewModel 의 생성자가 수십개 이상 있어도 그 생성자가 Dagger 에 의해 제공된다면 실제로는 의존성만 가져오면 됨
 */
class DaggerViewModelFactory @Inject constructor(private val creators: Map<Class<*>,
        @JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory { 

    override fun <T : ViewModel> create(modelClass: Class<T>): T { 
        /*
         * creators 에서 주어진 key로 찾는데, 해당 값이 없으면
         * creators 에 modelClass 가 접근 가능한 요소를 찾아서 값을 얻어낸다.
         * 그래도 값이 없으면, IllegalArgumentException 예외를 발생시킨다.
         * creator 의 실제 타입이 나오지 않았는데, Kotlin에서는 타입 추론이 가능하기 때문에
         * 실제 타입을 명시하지 않아도 된다. 이 경우 추론된 타입은 Provider<T>, 즉 Provider<ViewModel> 이다.
         */
        val creator = creators[modelClass] ?:
                creators.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
                ?: throw IllegalArgumentException("unknown model class " + modelClass)

        // 찾은 Provider<ViewModel> 반환
        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }

    }
}
@Module
public abstract class BaseBindsModule {
    // 생성한 DaggerViewModelFactory 클래스를 @Binds 를 통해 @Module 에 설정
    @Binds
    abstract ViewModelProvider.Factory bindViewModelFactory(DaggerViewModelFactory factory);
}

마지막으로 ViewModel 에 @InjectViewModel 를 부착하고, Activity 에서 ViewModelProvider.of 로 가져옵니다.

@InjectViewModel
public class MainViewModel extends AndroidViewModel {

    @Inject
    public MainViewModel(@NonNull MainApplication application) {
        super(application);
    }
}
@InjectActivity 
public class MainActivity extends BaseActivity { 
    @Inject ViewModelProvider.Factory mViewModelFactory; 
    private MainViewModel mViewModel; 

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(MainViewModel.class);
    }
}

이 방법의 장점은 해당 ViewModel 의 생성자가 Dagger 에 의해 주입된다면 Activity 에서 ViewModel 의 인스턴스를 가져올 때 생성자를 신경쓰지 않아도 된다는 점입니다. 단순히 보기에는 작성할 코드가 많지만, 실제 구현체인 MainActivity, MainViewModel 를 제외하면 프로젝트 특성을 가진 코드가 아니므로 Base화를 하여 구성해도 문제가 없습니다.

마무리

어떻게 보면 최종 사용자 (End-Developer) 의 할 일을 많이 줄였지만, 아래의 개선점은 있습니다.

첫번째로, 위에서도 소개했던 Member-injections methods 의 자동화 여부 입니다. 코드가 적다고 해도 나름대로 일은 일이기 때문입니다.

두번째로, 주입될 의존성들은 앱 전역으로 inject 가 되는데, 이를 일부 범위에서 inject 되게 할 수 있는 Scope  기능을 적용하지 못합니다. 단, 이쪽은 특정 Scope 어노테이션을 부착하기만 하면 되므로 InjectActivity 의 파라미터로 제공하게 하면 문제가 없을거라 판단됩니다.

개선점을 찾게 되면 글을 업데이트 하도록 하겠습니다.

이 라이브러리를 통하여 자동화를 하게 되면, 각 Activity 에 대한 Scope 를 지정할 수 없다는 것이 문제가 되는 것은 알고 있지만, 어떻게 해야 좋은 방법일지는 아직까지 고민중입니다.


특성 이미지 출처는 https://proandroiddev.com/dagger-2-part-three-new-possibilities-3daff12f7ebf 입니다.

Using PreferenceRepository with Dagger 2 (MVVM 5)

도입

안드로이드 앱을 개발하다보면 SharedPreference를 다양한 곳에 쓰는 일이 많다.

로그인한 유저의 정보를 저장한다던지, 클라이언트 단 FCM Token 을 저장한다던지… 꽤나 많을 것이다.

이번 글에서는 Dagger 를 이용해 SharedPreference 를 사용할 수 있는 PreferenceRepository 를 만드려고 한다.

SharedPreference 를 쓰는 이유는 다양하지만, 이번 글에서는 로그인한 유저의 정보를 저장하고, 가져올 수 있게 Dagger를 이용하여 PreferenceRepository 를 제작하려고 한다.

기본적인 Dagger 사용법이나 Module, Component 설명은 Inject Retrofit with Dagger, a Dependency injection library (MVVM 2) 글을 참고하면 될 것 같다.

PreferenceRepository, PreferenceRepositoryImpl 구현

먼저, Dagger 에 Inject 할 PreferenceRepository 를 만든다.

public interface PreferenceRepository {
    MemberBean getLoginMemberBean();
    void setLoginMemberBean(MemberBean memberBean);
}

그리고 실제로 SharedPreference 코드를 구현하는 PreferenceRepositoryImpl 를 만든다.

@Singleton
public class PreferenceRepositoryImpl implements PreferenceRepository {
    private RPreference mPreference;
    private Gson mGson = new Gson();
    private static String PREF_MEMBER_BEAN = "53bb19f0-fbe7-4001-b0b3-77c48e6ac14f";

    @Inject
    public PreferenceRepositoryImpl(MainApplication application) {
        mPreference = RPreference.getInstance(application);
    }

    @Override
    public MemberBean getLoginMemberBean() {
        String jsonStr = mPreference.getString(PREF_MEMBER_BEAN);
        MemberBean memberBean = new MemberBean();
        if (isNotEmpty(jsonStr)) {
            memberBean = mGson.fromJson(jsonStr, MemberBean.class);
        }
        return memberBean;
    }

    @Override
    public void setLoginMemberBean(MemberBean memberBean) {
        String jsonStr = mGson.toJson(memberBean);
        mPreference.put(PREF_MEMBER_BEAN, jsonStr);
    }
}

생성자로는 MainApplication 객체를 받고, 그 다음에는 PreferenceRepository 에 있던 메서드를 구현한다. MemberBean 자체를 저장할 수는 없어 내부적으로 Gson 을 사용하여 String로 변환하여 저장하였다.

참고로 빠른 개발을 위해 RichUtilsKt 에 있는 RPreference 를 사용하였다.

@Provides 로 Dagger 에 의존성 주입

@Module
public class AppProvidesModule {
    @Provides
    @Singleton
    PreferenceRepository providePreferenceRepository(PreferenceRepositoryImpl impl) {
        return impl;
    }

    @Provides
    @Singleton
    PreferenceRepositoryImpl providePreferenceRepositoryImpl(MainApplication application) {
        return new PreferenceRepositoryImpl(application);
    }
}

실제 앱에서 사용할 부분은 PreferenceRepository 이나 실제 구현체인 PreferenceRepositoryImpl 가 필요하기 때문에, 양쪽 클래스 둘 다 @Provides 를 통해 Dagger에 주입한다.

실제 사용

이제 Activity거나 ViewModel이거나 상관없이 @Inject PreferenceRepository mPreferenceRepository; 한 줄이면 PreferenceRepository 에 접근할 수 있게 되었다.

if (mPreferenceRepository.getLoginMemberBean().getMemNo() != 0) {
    startActivity(MainActivity.class);
    finishAllActivities();
    return;
}

사용할 때는 이런 식으로 사용하면 된다.

 

Inject Retrofit with Dagger, a Dependency injection library (MVVM 2)

도입

프로그래밍에서 의존성(Dependencies) 이란 개념은 두 모듈 간의 연결이라고 볼 수 있다.

평범한 경우라면 val coffee = new Coffee() 이런 식으로 서로의 의존성을 생성하지만, 이 방법에는 많은 문제가 있다.

첫번째로 해당 클래스에 변동점이 생기면 해당 클래스와 의존성을 갖는 클래스 전부에 변동사항을 적용해야 한다는 문제점이 있다. 만일 Coffee의 생성자에 파라미터 하나가 바뀌었다고 해보면, Coffee를 사용하는 모든 클래스에서 변경을 해야될 것이다. 물론, Secondary Constructor 를 사용할 수는 있지만, 거의 대부분 프로그래밍이 그럴 듯이 개발자의 생각대로 굴러가는 건 아닐 것이다.

두번째로 해당 클래스를 독립적으로 테스트하기가 어렵다는 문제점이 있다. 이런 의존성을 가지는 클래스를 테스트하려면 실제 객체를 Mock 객체로 대체하여야 하는데 그럴 수 없어 테스트하기가 어려워진다.

이 문제점을 해결하기 위해 외부에서 의존성을 만들고 그 의존성을 필요로 하는 클래스에 주입(Injection)하는 개념이 나왔는데, 그것이 바로 의존성 주입(Dependency Injection) 이다. 의존성 주입 기술을 이용하면 해당 클래스와 사용하는 클래스 간의 의존성을 직접 생성하지 않으므로 독립적이게 된다.

안드로이드에서는 Dagger 라는 라이브러리를 사용하여 이 Dependency Injection 기능을 사용할 수 있는데, 이 글에서는 Dagger를 이용해 Retrofit를 주입하는 방법을 알아보려 한다.

Dagger 불러오기

api "com.google.dagger:dagger:2.14.1"
kapt 'com.google.dagger:dagger-compiler:2.14.1'
kapt "com.android.databinding:compiler:3.0.1"
compileOnly 'org.glassfish:javax.annotation:10.0-b28'
compileOnly 'javax.annotation:jsr250-api:1.0'
api 'javax.inject:javax.inject:1'
api 'com.google.dagger:dagger-android-support:2.14.1'
kapt 'com.google.dagger:dagger-android-processor:2.14.1'

현재 (2018-03-23) 기준 Dagger의 최신 버전은 2.14.1 버전이다. 그러므로 임포트를 각각 해준다.

AppComponent 구현

Component 란 Dagger 관련 기능을 관리하는 인터페이스로 클래스에 @Component 란 어노테이션을 붙임으로서 선언할 수 있다. 이 Component 에는 modules 라는 Class<?>의 배열을 선언할 수 있는데, 이 modules 에는 해당 Component가 관리할 모듈을 적어넣으면 된다.

@Singleton
@Component(
        modules = {
               ProvidesModule.class
        }
)
public interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(MainApplication application);

        AppComponent build();
    }

    void inject(MainApplication mainApp);
}

여기서 @Singleton 라는 어노테이션이 나오는데, 이름이 의미하듯이 한번 인스턴스를 생성하면 그 인스턴스에 대한 메모리 참조를 다른 데에서도 같이 사용할 수 있는 것이다.

@Component에는 바로 다음 섹션에서 구현할 ProvidesModule 를 적고, 밑에는 거의 공통 내용이니 따라 적으면 될 것 같다. 요약하면, 해당 컴포넌트에 대한 빌더 클래스를 만들어 application을 설정하게 하고, 얻은 컴포넌트의 inject 메서드에 Application 인스턴스를 넣음으로서 해당 앱이 Dagger에 의해 주입될 수 있다는 의미가 된다고 요약할 수 있다.

ProvidesModule 구현 – OKHttpClient 주입

Dagger에서 Module 란 Dagger에 의존성을 주입해주는 역할을 하는 클래스로, 주로 하나의 Component 밑에 다수개의 Module이 붙을 수 있다. 이 Module라는 것은 일반 클래스에 @Module 란 어노테이션을 부착하는 것으로 구현할 수 있으며 이 클래스에는 의존성을 주입하는 어노테이션을 붙인다.

Dagger에 의존성을 주입하는 어노테이션은 총 두 가지의 방법이 있는데, @Provides@Binds가 있는데, 기본적으로 @Provides를 쓰나 @Binds는 2.4 버전에 추가된 어노테이션으로 ‘Adds @Binds API for delegating one binding to another` 라는 설명을 가지고 있다. 두 개의 차이점은 @Provides를 이용하면 일반 메서드처럼 의존성을 주입할 수 있는 것이고, @Binds를 이용하면 abstract class로서 주입할 대상과 주입할 파라미터만 선언해주면 그 나머지 코드들에 대해서는 자동으로 처리해준다는 차이점이 있다.

Retrofit를 주입하는 데에는 다른 코드들이 필요하므로 @Provides 를 사용하기로 하고, 이 클래스의 이름은 ProvidesModule라 해보자.

Retrofit 객체를 생성하기 위해서 필요한 요소는 OKHttpClient, ConverterFactory 등이 있는데, 이를 각각 주입받아 사용하기로 한다.

먼저, OKHttpClient 객체를 주입해보자.

@Module
public class BaseProvidesModule {

    @Provides
    public OkHttpClient provideClient(Interceptor interceptor) {
        OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
        builder.readTimeout(20000, TimeUnit.MILLISECONDS);
        builder.connectTimeout(10000, TimeUnit.MILLISECONDS);
        builder.addInterceptor(interceptor);
        return builder.build();
    }
}

OKHttpClient 의 빌더 객체를 만들고, 각각 readTimeout, connectTimeout 값을 설정해준다.

그 다음, 파라미터로 받은 Interceptor 객체를 빌더에 설정하는데, 기본적으로 @Provides나 @Binds 어노테이션이 선언된 메서드의 파라미터는 그 요소가 Dagger에 의해 의존성이 주입되어야 한다. 즉, Interceptor를 쓰고 싶으면 모듈에서 Dagger에 Interceptor 에 대한 의존성을 주입해야 한다.

그러므로 바로 밑에서 Interceptor를 Dagger에 주입하는 코드를 작성한다.

@Provides
public Interceptor provideInterceptor() {
    HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
    interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
    return interceptor;
}

여기서는 전송/결과에 대한 로그를 표시하는 HttpLoggingInterceptor를 주입시키기로 한다.

이런 과정을 거치면 드디어 Dagger를 통해 코드의 어느 곳에서나 OKHttpClient, Interceptor를 주입받아 사용할 수 있다.

물론, 여기서 끝나지 않다. 최종적인 목적은 Retrofit 객체를 주입해서 Retrofit Service들을 Repository 들에서 사용하게 하는 것이다.

ProvidesModule 구현 – Retrofit 주입

@Provides
public Retrofit provideRetrofit(OkHttpClient okHttpClient) {
    return new Retrofit.Builder()
            .baseUrl("http://123.123.123.123:1234")
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build();
}

메서드를 하나 만들고, OKHttpClient를 파라미터로 받아서 Retrofit 객체를 새로 생성한다.

Retrofit 서비스 구현 및 주입

여기서는 JSONPlaceholder 의 api들을 연동시키는 서비스를 구현한다.

public interface JSONService {

    @GET("/comments")
    Call<List<Comment>> getComments();

    @GET("/photos")
    Call<List<Photo>> getPhotos();

    @GET("/posts/{id}")
    Call<Post> getPost(@Path("id") int id);

    @GET("/posts")
    Call<List<Post>> getPost();
}

그리고 만든 서비스 객체를 Dagger에 주입시킨다.

@Singleton
@Provides
JSONService provideJSONService(Retrofit retrofit) {
     return retrofit.create(JSONService.class);
}

여기까지 거치면 드디어 JSONService를 다른 곳에서 쓸 수 있게 된다.

실제로 사용하기 – Repository 패턴

public class CommentRepository {

    private JSONService api;

    @Inject
    public CommentRepository(JSONService jsonApi) {
        this.api = jsonApi;
    }
}

해당 Repository 의 생성자에 @Inject를 붙여, 생성자의 파라미터에 JSONService를 주입받는 것이다.

그리고 이 Repository를 활용해야되는 ViewModel에는 아래와 같이 작성할 수 있다.

@InjectViewModel
public class DemoFragmentViewModel extends BaseViewModel {
    private CommentRepository mRepository;

    @Inject
    public DemoFragmentViewModel(Application application, CommentRepository mRepository) {
        super(application);
        this.mRepository = mRepository;
    }

    public LiveData<Resource<List<Comment>>> getCommentList() {
        return mRepository.getCommentList();
    }
}

마찬가지로 DemoFragmentViewModel 생성자의 파라미터인 Application, CommentRepository 들에는 각각 Dagger에 의해 파라미터 값이 주어진다.

마지막으로 이 ViewModel를 사용하는 Fragment에는 아래와 같이 작성할 수 있다.

package com.github.windsekirun.baseapp.demo.fragment;

@InjectFragment
public class DemoFragment extends BaseFragment<DemoFragmentBinding> {
    @Inject ViewModelProvider.Factory mViewModelFactory;
    private DemoFragmentViewModel mViewModel;

    ...

    private void init() {
        mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(DemoFragmentViewModel.class);
    }
}

마무리

위에 생략한 과정이 많지만, Dagger를 사용하기 위해서는 Component 와 Module를 통해 주입할 의존성을 구현하고, 다른 곳에서는 @Inject 메서드를 통해 주입받는 형태이다.

물론, Fragment 나 Activity, 위에서 사용한 ViewModelFactory 들을 사용하려면 더 많은 작업을 거쳐야 되지만, 아마 그 작업들은 다음 글에서 설명할 수 있을 것 같다.