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 들을 사용하려면 더 많은 작업을 거쳐야 되지만, 아마 그 작업들은 다음 글에서 설명할 수 있을 것 같다.