Tasting Dagger 2 on Android (번역)

Fernando Cejas 가 작성한 ‘Tasting Dagger 2 on Android’ 를 원작자의 동의를 받아 번역한 글입니다.


블로그를 다시 쓰기로 하였고, 지난 주동안 내가한 일에 대해서 공유하고자 합니다. 이번에는 Dagger2 에 대한 내 경험에 대해 이야기하려고 합니다. 우선, 의존성 주입(Dependency Injection)이 왜 중요하고 안드로이드 앱에서 왜 써야만 하는지에 대해 간단히 설명하려고 합니다.

독자들이 의존성 주입Dagger, Guice 와 같은 툴에 대한 기본적인 지식이 있다고 생각하고 글을 쓰겠습니다. 그렇지 않다면 링크의 튜토리얼을 참고해주세요.

왜 의존성 주입인가?

의존성 주입에 대해 가장 중요한건 Inversion of Control 원리를 사용하는 것입니다. Inversion of Control 은 앱의 흐름을 프로그램이 실행하는 동안 생성된 객체 그래프에 의존하게 합니다. 추상화를 통해 정의된 객체들의 상호작용에 의해 동적인 흐름이 가능해집니다. 의존성 주입 혹은 Service locator 같은 방식으로 런타임 바인딩이 됩니다.

의존성 주입에는 중요한 장점들이 있습니다.

  • 의존성은 외부에서 주입되고 환경설정되고 재사용할 수 있습니다.
  • 추상화된 것을 주입할때, 많은 코드를 바꿀 필요없고 해당 객체의 구현만 변경하면 됩니다. 주입되는 객체는 고립되있고(isolated) 분리되어있습니다(decoupled)
  • 의존성은 컴포넌트에 주입될 수 있습니다. 테스트를 쉽게하기위해 의존성에 맞게 mock을 구현해서 주입할 수 있습니다.

우리는 생성된 인스턴스의 스코프를 다룰 수 있습니다. 그리고 개발자 관점에서 볼 때 앱의 객체들은 인스턴스의 생성과 생명주기에 대해 전혀 알필요가 없습니다. 그것들은 DI 프레임워크에서 알아서 해주기 때문입니다.

출처: http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/

JSR-330 이란?

‘Dependency Injection for Java’는 재사용 확대, 자바 코드의 테스트 가능성과 유지 가능성을 위해 표준 어노테이션들을 정의했습니다. Dagger 1 과 2 모두 이 표준을 사용해서 일관성을 유지하고, 의존성 주입의 표준 방법을 제공합니다.

Dagger1

Dagger 1 은 글의 주제 밖이므로 간단하게 알아보겠습니다. Dagger 1은 많은 기능을 제공하고 안드로이드에서 가장 인기있는 의존성 주입 프레임워크입니다. Dagger 1은 Guice 에 영감을 받아 Square 에서 만들었습니다.

핵심기능:

  • 다양한 인젝션 지점
  • 다양한 바인딩
  • 다양한 모듈
  • 다양한 객체 그래프

Dagger 1 컴파일 시간에 바인딩을 할뿐만아니라 리플렉션도 사용합니다. 객체를 인스턴스화 할필요가 없더라도, 그래프 합성을 위해서 리플렉션을 사용합니다. Dagger 는 모두가 적합한 방법을 찾아내는 것을 런타임시에 진행합니다. 그래서 가끔 비효율성이 발생하고 디버깅할 때 어려움이 있기도 합니다.

Dagger 2

Dagger 2 는 구글에서 Dagger 1을 이어받아 만들었고 현재 2.0 버전입니다. Dagger 2 는 AutoValue 프로젝트에 영감을 받았습니다.(https://github.com/google/auto, equals 와 hashcode 메서드를 작성하기 귀찮을 때 효율적입니다) Dagger 2 는 의존성을 만들고 제공하는 코드를 직접 쓰는걸 코드 생성으로 대체하자는 아이디어를 가지고 있습니다.

이전 버전과 비교했을 때 많은 부분이 비슷하지만 중요한 차이점들이 있습니다.

  • 리플렉션을 전혀 사용하지 않습니다. 그래프 유효성, 환경설정, 전제조건들을 컴파일 타임에 검사합니다.
  • 쉽게 디버깅 할 수 있고 추적 가능합니다: 의존성 제공과 생성에 대한 전체 call stack 을 볼 수 있습니다.
  • 더 효율적입니다: 구글에 따르면 13% 의 성능 향상이 있다고합니다.
  • 코드 난독화: 손으로 쓴 코드 처럼 ‘method dispatch’ 를 사용합니다.

물론 이런 좋은 기능들은 비용을 수반하고 덜 유연하게 만듭니다. 예를들면 리플렉션을 사용하지 않아서 ‘dynamism’ 이 없습니다.

더 깊게 알아보기

Dagger 2 를 이해하기 위해서 의존성 주입의 기본 원리와 아래의 어노테이션 각각의 컨셉을 아는 것은 매우 중요합니다.

  • @Inject: 이 어노테이션은 의존성을 요청합니다. 의존성 주입을 통해서 해당 어노테이션이 달린 클래스나 필드에게 값을 주입해 달라고 Dagger 에게 요청합니다. Dagger 는 어노테이션이 달린 클래스의 인스턴스를 생성하고 그것들의 의존성을 만족시킵니다.
  • @Module: 모듈들은 의존성을 제공하는 메서드들을 가진 클래스입니다. 의존성을 제공하는 클래스를 정의하고 @Module 어노테이션을 답니다. 그러면 Dagger 는 클래스 인스턴스를 만들 때 의존성을 만족시키기 위한 정보를 찾을 수 있습니다.
  • @Provide: 모듈 안에서 해당 어노테이션이 달린 메서드를 정의합니다. 해당 어노테이션이 달린 메서드가 Dagger 가 어떻게 의존성에 맞게 객체를 만들고 제공하는지 알려줍니다.
  • @Component: 컴포넌트는 @Inject 와 @Module 사이 다리이며 의존성을 주입하는 역할을 합니다. 컴포넌트는 미리 정의한 모든 타입의 인스턴스를 줍니다. @Component 어노테이션은 인터페이스에다만 달아야합니다 그리고 컴포넌트를 구성하는 모든 @Module 이 달린 클래스 목록을 적어야합니다. 컴포넌트에서 사용하는 모듈들중 하나라도 없다면 컴파일 타임에 에러를 만듭니다. 모든 컴포넌트들은 컴포넌트에 포함된 모듈들을 통해 의존성의 범위를 알 수 있습니다.
  • @Scope: 스코프는 매우 유용하고 Dagger 2 에서 사용자 정의 어노테이션을 통해 범위를 나누는 명확한 방법입니다. 나중에 예제에서 보겠지만 이것은 매우 강력한 기능입니다. 앞에서 언급한바와 같이 하기 때문에, 모든 객체는 자기 자신의 인스턴스를 관리하는 방법에 대해 알필요가 없습니다. 예를들어 사용자가 지정한 @PerActivity 어노테이션이 달려있는 클래스는 액티비티가 살아있는 동안 존재합니다. 다시말하자면 객체 범위의 단위를 정의할 수 있습니다.
  • @Qualifier: 클래스의 유형이 종속성을 식별하기 불충분할 때 사용하는 어노테이션입니다. 예를 들어 안드로이드의 경우, 많은 경우 컨텍스트의 다양한 타입이 필요합니다, 그래서 “@ForApplication”, “@ForActivity” 같은 식별자 어노테이션을 정의합니다. 컨텍스트를 주입할 때 이 식별자 어노테이션을 이용해서 Dagger 가 어떤 타입을 제공할지 정해줍니다.

코드 살펴보기

지금까지 너무많은 이론을 설명했는데 이제 Dagger 2 코드를 보겠습니다. 먼저 build.gradle 파일에 dependencies 를 추가합시다.

필수적으로 javax 어노테이션, 컴파일러, 런타임 라이브러리, apt plugin 를 추가해야합니다. 그렇지 않으면 Dagger 어노테이션이 올바르게 작동하지 않습니다. 특히 안드로이드 스튜디오에서 문제를 겪었습니다.

예제

몇 달전 안드로이드에서 Bob 의 ‘Clean Architecture’ 를 어떻게 구현해야되는지에 대해 글을 작성했습니다. 우리가 앞으로 하는 것을 더 잘 이해하고 싶다면 읽어보기를 추천합니다. 아래와 같이 그 당시에 내 해결책에 연관된 대부분의 객체들의 종속성을 생성하고 제공할 때 문제가 있었습니다. (주석을 확인하세요)

알다시피 의존성 주입은 이러한 문제를 해결할 수 있습니다. (가독성이 떨어지고 이해할수 있는) 보일러플레이트 코드를 제거할 수 있습니다: 이 클래스는 객체 생성과 의존성 공급에 대해 전혀 알 필요가 없습니다.

그래서 어떻게 할 수 있나요? 물론 Dagger 2 의 기능을 사용할 수 있습니다. 아래의 의존성 주입 그래프를 보세요.

이 그래프를 파헤쳐보고 코드를 더해 설명해보겠습니다.

Application Component: 어플리케이션의 수명이 컴포넌트의 수명입니다. 이것은 AndroidApplication 와 BaseActivity 모두에 주입됩니다.

보다시피 이 컴포넌트에 어플리케이션당 하나의 인스턴스로 제약을 거는 @Singleton 어노테이션을 사용했습니다. Context 클래스와 나머지 클래스들을 공개한 이유가 궁금할 수 있습니다. 이 부분은 Dagger 의 컴포넌트가 어떻게 동작하는지에 대한 중요한 property(클래스 멤버) 입니다: 명시적으로 클래스를 이용하지 않을거라면 모듈로 부터 해당 타입을 공개하지 마시기 바랍니다. 이 경우에는 특별히 하위 그래프에 위의 클래스들을 공개했습니다. 그중 하나를 지우면 컴파일 에러가 발생합니다.

Application Module: 이 모듈은 어플리케이션 생명주기동안 살아있는 객체들을 제공합니다. 즉, @Provide 가 달린 모든 메서드에 @Singleton 스코프를 사용하는 이유입니다.

Activity Component: 액티비티의 생명주기동안 살아있는 컴포넌트입니다.

@PerActivity 는 객체의 수명이 액티비티의 수명을 따를 경우 사용하는 사용자 정의 스코프 어노테이션입니다. 이걸 사용하는 것은 매우 좋은 방법입니다. 다음과 같은 장점들을 가지고 있습니다.

  • 액티비티가 생성되어있길 요구하는 부분에 객체 인젝트.
  • 액티비티 기반의 싱글톤 객체 사용.
  • 액티비티에서만 사용하는 것을 글로벌 객체 그래프와 분리.

아래 코드를 살펴보겠습니다.

Activity Module: 이 모듈은 그래프의 자손들에게 액티비티를 제공합니다. 예를 들어 프래그먼트에서 액티비티 컨텍스트를 사용합니다.

User Component: ActivityComponent 를 상속하고 @PerActivity 스코프에서 작동하는 컴포넌트입니다. 유저와 관련된 프래그먼트들에게 객체들을 주입하기 위해 사용합니다. (이전에 말했듯이) ActivityModule 이 액티비티를 그래프에 제공하기 때문에, 의존성을 만족하기 위해 액티비티 컨텍스트 필요할 때마다 Dagger 가 ActivityModule 로 부터 컨텍스트를 가져와 주입해줍니다: 즉 하위 모듈에 액티비티를 재정의할 필요가 없습니다.

User Module: 이 모듈은 유저와 관련된 협력자들을 제공합니다. 예를 보면, 유저의 사용사례들을 제공하고 있습니다.

코드 함께보기

지금까지 의존성 주입 그래프를 구현하는 법에 대해 알아봤습니다. 이제 어떻게 의존성들을 주입할까요? Dagger 는 의존성을 주입하는 다양한 옵션들을 제공합니다.

  1. 생성자 주입: 클래스의 생성자에 @Inject 어노테이션을 답니다.
  2. 필드 주입: 클래스의 private 식별자가 아닌 필드에 @Inject 어노테이션을 답니다.
  3. 메서드 주입: 메서드에 @Inject 어노테이션을 답니다.

의존성을 바인딩할 때 Dagger 는 위의 순서대로 동작합니다. 객체 생성 시점에 의존성이 초기화되어있지 않는 이상한 동작이나 NullPointException 이 발생할 수 있기 때문에 Dagger 의 의존성 바인딩 순서는 중요합니다. 안드로이드에서는 생성자에 접근할 수 없기 때문에 Activity 나 Fragment 에서 보통 필드 주입을 사용합니다.

다시 예제로 돌아와서 어떻게 BaseActivity 에 필드를 주입하는지 살펴보겠습니다. 예제에서는 우리 앱의 네비게이션 흐름을 관리하는 Navigator 라는 클래스를 가지고 해보겠습니다.

Navigator 는 필드 주입을 통해 결합되기 때문에, ApplicationModule 에서 @Provide 어노테이션을 사용하여 명시적으로 제공되야합니다. 마침내 컴포넌트들을 초기화하였고 클래스 멤버들에 주입하기 위해 inject() 메서드를 사용하였습니다. 액티비티의 onCreate() 메서드 안에서 getApplicationComponent() 를 호출해서 주입을 하였습니다. 이 함수는 재사용을 하기위해 해당 클래스에 구현되었고 어플리케이션 객체에서 초기화된 ApplicationComponent 를 재사용하는게 주목적입니다.

프래그먼트에서 presenter 를 가지고 같은 것을 해보겠습니다. 이번에는 per-activity 로 스코프된 컴포넌트를 사용하기 때문에 접근 방식이 약간 다릅니다. UserDetailsFragment 에 주입할 UserComponent 는 UserDetailActivity 에 있습니다.

액티비티의 onCreate() 메서드 안에서 같은 방식으로 초기화 하겠습니다.

Dagger 가 어노테이션을 처리할 때 보면 컴포넌트 인터페이스를 구현하고 앞에 “Dagger” 라는 접두사를 붙이는걸 볼 수 있습니다. 그리고 컴포넌트를 만들 때 (컴포넌트와 모듈과 같은) 모든 의존성을 넘겨줘야합니다. 컴포넌트가 준비됬기 때문에 다음으로 프래그먼트 의존성을 만족시키기 위해 컴포넌트를 접근가능하게 만들어야합니다.

이미 생성된 컴포넌트를 얻어오고 프래그먼트를 inject() 메서드의 파라미터로 넘겨서 호출해서 UserDetailFragment 의존성을 결합합니다.

완전한 예제는 깃헙 저장소에서 확인하세요. 여기에는 약간의 리팩토링할 일들이 있습니다. 컴포넌트를 가지는 모든 클래스가 구현할 인터페이스를 가지게 하는 것 이고 이것은 (공식 예제에서 말하는) 주요 아이디어중 하나입니다.

그러므로 클라이언트(ex. 프래그먼트)는 (액티비티의) 컴포넌트를 사용할 수 있습니다.

여기서 제네릭의 사용은 타입 캐스팅을 필수적으로 만듭니다. 하지만 클라이언트가 사용할 컴포넌트를 얻을 수 없다면 바로 실패합니다. 이 문제를 더 잘 해결할 방법이 있다면 알려주세요.

Dagger 2 코드 생성

Dagger 의 주요기능들을 맛보았으니, 이제 어떻게 그런 작업을 하는지 살펴보려합니다. 이전에 설명한 Navigator 클래스가 어떻게 생성되서 주입되는지 살펴보겠습니다. 우선 ApplicationComponent 의 구현체인 DaggerApplicationComponent 를 살펴보겠습니다.

두 가지 중요한점이 있습니다. 첫번째로, 액티비티에 주입을 하기 때문에 멤버 주입이 있습니다. (이 예제에서 Dagger 는 BaseActivity_MembersInjector 라고 명명합니다)

이것은 액티비티의 주입 가능한 모든 멤버들에게 객체를 제공하는 코드를 가지고 있습니다. inejct() 를 호출할 때 접근 가능한 필드들을 가지고 의존성 결합을 해줍니다.

두번째는 DaggerApplicationComponent 에 대해 살펴보겠습니다. 여기엔 단지 클래스 인스턴스를 제공하는 인터페이스인 Provider<Navigator> 있습니다. Provider<Navigator> 는 생성된 클래스의 스코프를 기억하는 ScopedProvider 에 의해 (initialize() 메서드에서) 생성됩니다.

Dagger 는 클래스의 스코프된 인스턴스를 얻기위해 사용하는 ScopedProvider 에 파라미터로 넘길ApplicationModule_ProvideNavigatorFactory 도 생성합니다.

이 클래스는 매우 간단합니다.(@Provide 어노테이션이 달린 메서드를 포함하는) ApplicationModule 에 Navigator 클래스의 생성을 위임합니다.

결론적으로, 이러한 코드 생성은 손으로 쓴 코드같아보이고 이해하기 쉽고 디버깅하기도 쉽습니다. 아직도 여기에는 많은 작업이 남아있습니다. 디버깅을 시작해보는건 좋은 방법이고 Dagger 가 의존성 결합을 어떻게 하는지 잘 살펴볼 수 있습니다.

테스트

여기서 너무 많은 것을 말하지는 않겠습니다. 유닛테스트에 대해서는 모든 인젝터를 만들필요가 있다고 생각하지는 않습니다. 그래서 저는 Dagger 를 사용하지 않았습니다. 하지만 통합 테스트에 관해서는 수동적으로 가짜 객체(mock)를 넣어서 잘 해결했습니다. Dagger 는 기존의 모듈들을 가짜 객체를 제공하는 다른 모듈로 대체하는데 좋습니다. 여기에 첨언할 테스트 경험을 알려주신다면 고맙겠습니다.

마무리

지금까지 Dagger 로 할 수 있는 것들에 대해서 알아봤습니다. 그러나 아직 갈길이 많습니다. 문서들을 읽어보고 비디오를 보고 예제들을 보는것을 추천합니다. (각각 링크 연결하기) 이것은 간단한 예제입니다 이것이 유용했으면 좋겠습니다. (링크 연결하기) 언제든 어떤 피드백이던 환영합니다.

소스코드

예제: https://github.com/android10/Android-CleanArchitecture

더 읽어볼 것

  1. Architecting Android..the evolution
  2. Architecting Android..the clean way?
  3. The Mayans Lost Guide to RxJava on Android
  4. It is about philosophy: Culture of a good programmer

참고자료

Architecting Android…The clean way?.
Dagger 2, A New Type of Dependency Injection.
Dependency Injection with Dagger 2.
Dagger 2 has Components.
Dagger 2 Official Documentation.