[Dagger Official Reference] Dagger

15 Jun 2016

원본

애플리케이션에서 가장 좋은 클래스들은 자신의 일을 하는 클래스들이다 : BarcodeDecoder, KoopaPhysicsEngine, 그리고 AudioStreamer. 이 클래스들은 의존(dependency)들을 가지고 있다; 아마 BarcodeCameraFinder, DefaultPhysicsEngine, 그리고 HttpStreamer같은 것들일 것이다.

이와는 대조적으로 애플리케이션에서 최악의 클래스들은 전혀 기여하는 것 없이 공간을 차지하는 것들이다: BarcodeDecodeFactory, CameraServiceLoader, MutableContextWrapper. 이 클래스들은 관심의 대상들을 묶어주는 서투른 덕트 테이프이다.

Dagger는 이러한 FactoryFactory 클래스들의 대체품이다. Dagger는 상용구boilerplate code 작성에 대한 부담없이 의존성 주입 디자인 패턴을 구현한다. Dagger는 관심의 대상인 클래스들에 초점을 맞출 수 있게 해준다. 의존 관계(dependency)들을 선언하고, 이를 충족시키는 방법을 지정한 뒤,애플리케이션을 출시하라.

javax.inject 어노테이션(JSR 330)을 기반으로 하였기에 각 클래스를 쉽게 테스트할 수 있다. RpcCreditCardService을 FakeCreditCardService로 교환하는 것만을 위한 상용구가 많이 필요하지 않는다.

의존성 주입은 테스트만을 위한 것이 아니다. Dagger는 재사용 가능하고, 교체 가능한 모듈을 쉽게 만들수 있게 해준다. 동일한 AuthenticationModule을 애플리케이션 전체에공유할 수 있다. 그리고 각 상황에 적절한 행동을 얻기 위해 개발에서는 DevLoggingModule을 실행하고 운영에서는 ProdLoggingModule을 실행 할 수도 있다.

Dagger 2가 다른 이유

의존성 주입 프레임워크는 오래전부터 주입(injecting)과 설정(configuring)을 위한 온갖 종류의 다양한 API들로 존재해왔다. 그런데, 왜 바퀴를 재발명하는가? 전체 스택을 자동으로 생성된 코드로 구현하도록 한 것은 Dagger 2가 처음이다. 지침 원칙은 사용자가 의존성 주입이 가능한 간단하고, 추적 가능하며traceable, 고성능이도록 직접 작성한 코드를 모방한 코드를 생성하는 것이다. 디자인에 대한 더 많은 배경은 +Gregory Kick강연(슬라이드)을 보라.

Dagger 사용하기

우리는 커피 메이커를 만들면서 의존성 주입과 Dagger의 작동 과정을 시연 할 것이다. 컴파일하고 실행할 수 있는 완전한 샘플 코드는 Dagger의 커피 예제를 참조하라.

의존(dependency)을 선언하기.

Dagger는 애플리케이션의 클래스들의 인스턴스들을 생성하고 그 인스턴스의 의존(dependency)을 만족시킨다. Dagger는 관심을 가져야 할 필드들과 생성자들을 식별하기 위해 javax.inject.Inject 어노테이션을 사용한다.

Dagger가 클래스의 인스턴스를 생성할 때 사용할 생성자에 @Inject 어노테이션를 추가하라. 새 인스턴스가 요청되면 Dagger는 필수 파라미터들의 값들을 가져와서 생성자를 호출한다.

class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }

  ...
}

Dagger는 필드에 직접 주입할 수 있다. 이 예제에서 Dagger는 heater 필드를 위해 Heater 인스턴스를, pump 필드를 위해 Pump 인스턴스를 획득한다.

class CoffeeMaker {
  @Inject Heater heater;
  @Inject Pump pump;

  ...
}

만약 당신의 클래스가 @Inject 어노테이션된 필드를 가지고 있지만 @Inject 어노테이션을 가진 생성자가 없다면, Dagger는 요청을 받았을 때 해당 필드들을 주입하지만 새로운 인스턴스를 만들지는 않는다. Dagger에게 인스턴스 역시 생성하여야 한다는 것을 알려주기 위해서는 @Inject 어노테이션을 가진 인수가 없는 생성자를 추가하라.

보통 생성자 주입이나 필드 주입이 더 선호되지만 Dagger는 method 주입도 지원한다. (역: Dagger가 생성하는 코드를 보면 코드의 흐름이 다음과 같다 1. @Inject 어노테이션을 가진 생성자의 파라미터들의 값을 획득, 2. 생성자에 값을 전달하여 인스턴스를 생성 3. 생성된 인스턴스에 @Inject 어노테이션이 붙은 멤버들을 주입 4. @Inject 어노테이션이 붙은 메소드의 파라미터의 값들을 획득한 뒤 그 값을 넘겨주면서 메소드를 호출)

@Inject 어노테이션이 누락된 클래스는 Dagger에 의해 생성될 수 없다.

의존(dependency) 만족시키기

기본적으로 Dagger는 위에서 설명된대로 요청된 타입의 인스턴스를 생성하여 각각의 의존dependency을 충족시킨다. 당신이 CoffeMaker를 요청하면, new CoffeeMaker()를 호출하고 그것의 주입 가능한 필드들을 설정하여 얻는다.

하지만 @Inject가 어디에서나 동작하지는 않는다 :

@Inject가 불충분하거나 곤란한 경우, @Provides-어노테이션 메소드를 사용하여 의존을 충족시킨다. 이 메소드의 반환 타입은 그것이 충족시키는 의존을 정의한다.

예를 들어, provideHeater()은 Heater가 요청될 때 마다 호출된다:

@Provides static Heater provideHeater() {
  return new ElectricHeater();
}

@Provides 메소드가 자신의 의존(dependency)을 가지는 것이 가능한다. 이것은 Pump가 요청될 때마다 Thermosiphon을 반환한다:

@Provides static Pump providePump(Thermosiphon pump) {
  return pump;
}

모든 @Provides 메소드들은 module에 속해있어야 한다. 이것들은 단지 @Module 어노테이션을 가진 클래스들이다.

@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater() {
    return new ElectricHeater();
  }

  @Provides static Pump providePump(Thermosiphon pump) {
    return pump;
  }
}

관습에 따라 @Provides 메소드들은 provide 접두사를 가지고 module 클래스들은 Module 접미사를 가져야 한다.

그래프 만들기

@Inject와 @Provides 어노테이션된 클래스들은 그들의 의존들(dependencies)로 연결된 객체들의 그래프를 형성한다. 애플리케이션의 main 메소드나 Android Application같은 코드 호출은 잘 정의된 뿌리 집합roots of set을 통해 해당 그래프에 접근한다. Dagger2에서 이 집합은 인수를 가지지 않고 요구하는 타입을 반환하는 메소드를 가진 인터페이스에 의해 정의된다. 그런 인터페이스에 @Component 어노테이션을 적용하고 modules 파라미터에 module 타입들을 전달하면, Dagger2는 그 계약의 구현을 완전히 생성한다.

@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
  CoffeeMaker maker();
}

이 구현은 인터페이스의 이름에 ‘Dagger’ 접두사를 붙인 이름을 가진다. 해당 구현에서 builder() 메소드를 호출해 인스턴스를 얻고 의존들을 설정하기 위해 반환된 builder를 사용한 뒤 build()로 새로운 인스턴스를 만든다.

CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();

Note: 만약 당신의 @Component가 최상위 타입이 아니라면, 생성된 component의 이름은 에워싸고 있는 타입의 이름을 밑줄(underscore)로 결합하여 포함 할 것이다. 예를 들어, 이 코드는:

class Foo {
  static class Bar {
    @Component
    interface BazComponent {}
  }
}

DaggerFoo_Bar_Baz라는 이름을 가진 component를 생성할 것이다.

기본 생성자로 접근 가능한 모듈은 설정되지 않은 경우 builder가 자동으로 인스턴스를 생성할 것이므로 생략할 수 있다. 그리고 모든 @provides 메소드가 static인 모듈의 경우, 구현에 인스턴스가 전혀 필요하지 않는다. 만약 사용자가 의존 인스턴스를 만들지 않아도 모든 의존들이 생성될 수 있는 경우, 생성된 구현은 builder 없이도 새로운 인스턴스를 얻을 수 있는 create() 메소드를 가질 것이다.

CoffeeShop coffeeShop = DaggerCoffeeShop.create();

이제 우리의 CoffeeApp은 완전히 주입된 CoffeMaker를 얻기 위해 CofeeShop의 Dagger가 생성한 구현을 간단히 사용할 수 있다.

public class CoffeeApp {
  public static void main(String[] args) {
    CoffeeShop coffeeShop = DaggerCoffeeShop.create();
    coffeeShop.maker().brew();
  }
}

그래프가 생성되고 진입점은 주입되었으니, coffer maker 앱을 실행한다. Fun.

$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[_]P coffee! [_]P

그래프의 바인딩들

위의 예제는 좀 더 일반적인 바인딩을 사용하여 component를 작성하는 방법을 보여 주지만 그래프에 바인딩을 제공하는 방법들은 다양하다. 다음은 의존으로써 사용할 수 있으며 올바른 형식의 component를 생성하기 위해 사용될 수 있다:

싱글톤들과 Scoped 바인딩들

@Provides 메소드 또는 주입 가능한(injectable) 클래스에 @Singleton 어노테이션을 붙이면 그래프는 모든 클라이언트에게 동일한 인스턴스를 제공할 것이다.

@Provides @Singleton static Heater provideHeater() {
  return new ElectricHeater();
}

주입 가능한injectable 클래스의 @Singleton 어노테이션은 문서적인 역활도 한다. 이것은 잠재적 관리자maintainer에게 이 클래스는 여러 쓰레드에 의해 공유될 수 있음을 상기시킨다.

@Singleton
class CoffeeMaker {
  ...
}

Dagger2는 그래프의 scope가 지정된 인스턴스들과 component 구현의 인스턴스들을 연관시키므로 component 자체는 자신이 대표하는 scope를 선언해야 한다. 예를 들어 동일한 component가 @Singleton 바인딩과 @RequestScoped 바인딩을 가지는 것은 말이 되지 않는다. 이 scope들은 다른 생명주기들을 가지고 있기 때문이며 다른 생명주기를 가지는 component들에서 존재해야한다. component가 주어진 scope와 연관되어 있음을 선언하려면 scope 어노테이션을 component 인터페이스에 적용하면 된다.

@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
  CoffeeMaker maker();
}

Component가 다수의 scope 어노테이션을 가질 수 있다. 이는 그들 모두가 동일한 scope의 별칭임을 선언하므로 해당 component는 자신이 선언한 scope들이 지정된 바인딩들을 포함할 것이다.

Reusable scope

때로는 @Inject 생성된 클래스가 인스턴스화되거나 @Provides 메소드가 호출되는 횟수를 제한하고 싶지만 특정 component나 subcomponent의 존속 기간동안 정확히 동일한 인스턴스가 사용되도록 보장할 필요는 없는 경우도 있다. 이는 안드로이드처럼 할당allocation에 비용이 많이 들 수 있는 환경에서 유용할 수 있다.

이런 바인딩들에서는 @Reusable scope를 적용할 수 있다. 다른 scope들과는 달리 @Reusable scope된 바인딩들은 어느 단일 component에만 관련되지 않는다; 대신, 실제로 이 바인딩을 사용하는 각 component는 반환되거나 인스턴스화 된 객체를 캐쉬한다.

즉, 만약 당신이 component에 @Reusable 바인딩으로 module을 설치하지만, subcomponent만 이 바인딩을 실제로 사용한다면 오직 그 subcomponent만이 바인딩의 객체를 캐쉬할 것임을 의미한다. 만약 조상(ancestor)을 공유하지 않는 두 subcomponent들이 각각 바인딩을 사용하면, 그들은 각각 자체적으로 객체를 캐쉬할 것이다. 만약 component의 조상이 이미 캐쉬된 객체를 가지고 있다면, subcomponent는 그것을 재사용할 것이다.

component가 바인딩을 한 번만 호출할 지에 대한 보장이 없으므로, 변할 수 있는(mutable) 객체들이나 동일 인스턴스를 참조하는 것이 중요한 곳의 객체들에 @Reusable를 적용하는 것은 위험하다. 할당이 몇 번이나 되는지를 신경쓰지 않아서 scope되지 않은 상태로 남겨젔던 불변(immutable) 객체들을 위해 Reusable를 사용하는 것이 안전하다.

@Reusable // 스쿠퍼를 많이 사용해도 상관은 없지만 낭비하지는 않겠다.
class CoffeeScooper {
  @Inject CoffeeScooper() {}
}

@Module
class CashRegisterModule {
  @Provides
  @Reusable // DON'T DO THIS! 당신은 당신의 현금을 넣을 금전 등록기에 관심을 가진다.
		    // 대신 특정한 scope를 사용하라.
  static CashRegister badIdeaCashRegister() {
    return new CashRegister();
  }
}


@Reusable // DON'T DO THIS! 당신은 매번 새로운 필터를 원하므로 scope되지 않아야 한다.
class CoffeeFilter {
  @Inject CoffeeFilter() {}
}

Releasable references

바인딩이 Scope 어노테이션을 사용하는 것은 component 객체는 자신이 Garbage collect되기 전까지 바인딩된 객체에 대한 참조를 유지함을 의미한다. Android처럼 메모리에 민감한 환경에서는 애플리케이션이 메모리 부족 상태에 있을 때 현재는 사용되지 않는 scope된 객체들을 Garbage collection동안 삭제되기를 원할 수도 있다.

이 경우, 당신은 scope를 지정하고 거기에 @CanReleaseReferences 어노테이션을 추가할 수 있다.

@Documented
@Retention(RUNTIME)
@CanReleaseReferences
@Scope
public @interface MyScope {}

해당 Scope에 저장된 객체들이 현재 다른 객체들에서 사용되지 않고 있다면 Garbage collection동안 삭제되기를 원하도록 결정한 경우, 당신은 ReleaseableReferenceManager 객체를 당신의 scope를 위해 주입하고 그것의 releaseStrongReferences()를 호출 할 수 있다. 이는 component가 Strong reference 대신 WeakReference를 유지하도록 만들 것이다:

@Inject @ForReleasableReferences(MyScope.class)
ReleasableReferences myScopeReferences;

void lowMemory() {
  myScopeReferences.releaseStrongReferences();
}

If you determine that the memory pressure has receded, then you can restore the strong references for any cached objects that have not yet been deleted during garbage collection by calling restoreStrongReferences():

void highMemory() {
  myScopeReferences.restoreStrongReferences();
}

메모리 압박 상태가 감소되었다고 판단되면 restoreStrongRefrences()를 호출하여 Garbage collection동안 아직 상제되지 않은 캐시된 객체들의 Strong reference을 복원할 수 있다.

void highMemory() {
  myScopeReferences.restoreStrongReferences();
}

Lazy injections

때로는 객체를 lazy하게 인스턴스화해야 할 때가 있다. 어떤 binding T에 대해, get() 메소드가 처음 호출할 때까지 인스턴스화를 미루는 Lazy를 생성할 수 있다. T가 싱글톤이면, Lazy는 객체그래프 내의 모든 주입에 대해 동일한 인스턴스일 것이다. 그렇지 않으면, 각 주입 지점은 자신들만의 Lazy 인스턴스를 가지게 된다. 여하튼, 주어진 Lazy의 인스턴스에 대한 후속 호출은 T의 동일한 내제된 인스턴스를 반환한다.

class GridingCoffeeMaker {
  @Inject Lazy<Grinder> lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      // Grinder는 .get()이 호출될 때 한 번 생성되고 캐쉬된다.
      lazyGrinder.get().grind();
    }
  }
}

Provider injections

때로는 단지 단일 값을 주입하는 것 대신 다수의 인스턴스들의 반환되는 것을 원할 때가 있다. 다양한 선택지가 있을 수 있지만(Factories, Builders, etc.) 그 중 하나는 단순한 T 대신 Provider를 주입하는 것이다. Provider는 .get()이 호출될 때 마다 매번 T를 위한 바인딩 로직을 호출한다. 만약 해당 바인딩 로직이 @Inject 생성자라면 새로운 인스턴스가 생성될 것이지만 @Provides 메소드에는 이런 보장이 없다.

class BigCoffeeMaker {
  @Inject Provider<Filter> filterProvider;

  public void brew(int numberOfPots) {
  ...
    for (int p = 0; p < numberOfPots; p++) {
      maker.addFilter(filterProvider.get()); //new filter every time.
      maker.addCoffee(...);
      maker.percolate();
      ...
    }
  }
}

Note: Provider를 주입하는 것은 혼란스러운 코드를 생성할 가능성을 가지며, scope가 잘못되었거나 구조가 잘못된 객체들이 당신의 그래프에 있다는 디자인적 악취일 수 있다. 종종 T를 주입할 수 있도록 Factory나 Lazy를 사용하거나 코드의 수명이나 구조를 재구성하려 할 수 있다. 이때 Provider는 경우에 따라 생명의 은인이 될 수 있다. 일반적인 용도는 당신이 당신 객체의 본래의 수명과 일치하지 않는 레거시 아키텍처를 사용해야 하는 경우이다(예: 서블릿은 싱글톤으로 디자인되었지만, 요청 관련 데이터의 컨텍스트에서만 유효하다).

Qualifiers

때로는 타입만으로 의존을 식별하기에 부족하다. 예를 들어 복잡한 커피 메이커 앱은 핫 플레이트와 물을 위한 히터를 분리하고 싶을 수 있다.

이런 경우에는 qualifier 어노테이션을 추가한다. 이것은 @Qualifier 어노테이션을 가진 어노테이션이다. 여기 javax.inject에 포함된 한정자 어노테이션 @Named의 선언이 있다:

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
  String value() default "";
}

당신만의 qualifier를 어노테이션을 만들거나 단순히 @Named를 사용할 수 있다. 대상이 되는 필드나 파라미터에 어노테이션을 추가하여 qualifier를 적용한다. 타입과 한정자 어노테이션은 둘 다 의존을 식별할 때 사용될 것이다.

class ExpensiveCoffeeMaker {
  @Inject @Named("water") Heater waterHeater;
  @Inject @Named("hot plate") Heater hotPlateHeater;
  ...
}

해당하는 @Provides 메소드에 어노테이션을 추가하여 한정값(qualified values)을 제공하라.

@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
  return new ElectricHeater(70);
}

@Provides @Named("water") static Heater provideWaterHeater() {
  return new ElectricHeater(93);
}

의존들은 다수의 qualifier 어노테이션을 가질 수 없다.

Optional bindings

의존들의 일부가 component에 바인딩되지 않더라도 바인딩이 동작하도록 하려면, @BindsOptionalOf 메소드를 모듈에 추가할 수 있다.

@BindsOptionalOf abstract CoffeeCozy optionalCozy();

@Inject 생성자들과 멤버들 그리고 @Provdies 메소드들은 Optional 객체에 의존할 수 있음을 의미한다. 만약 CoffeeCozy에 대한 바인딩이 있으면, Optional은 present이고, CoffeeCozy에 대한 바인딩이 없으면, Optional은 absent가 될 것이다.

구체적으로, 다음 중 하나를 주입할 수 있다.

Optional<CoffeeCozy>
Optional<Provider<CoffeeCozy>>
Optional<Lazy<CoffeeCozy>>
Optional<Provider<Lazy<CoffeeCozy>>>

(당신은 또한 Provider나 Lazy, Lazy의 Provider도 역시 주입할 수 있지만 이는 그다지 유용하지는 않을 것이다.)

만약 Subcompent가 내제된 타입에 대한 바인딩을 가지고 있다면, 한 component에는 존재하지 않는 optional 바인딩이 subcomponent에는 존재할 수 있다.

당신은 Guava의 Optional또는 Java 8의 Optional을 사용할 수 있다.

Compile-time Validation

The Dagger annotation processor is strict and will cause a compiler error if any bindings are invalid or incomplete. For example, this module is installed in a component, which is missing a binding for Executor:

@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater(Executor executor) {
    return new CpuHeater(executor);
  }
}

When compiling it, javac rejects the missing binding:

[ERROR] COMPILATION ERROR :
[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.
Fix the problem by adding an @Provides-annotated method for Executor to any of the modules in the component. While @Inject, @Module and @Provides annotations are validated individually, all validation of the relationship between bindings happens at the @Component level. Dagger 1 relied strictly on @Module-level validation (which may or may not have reflected runtime behavior), but Dagger 2 elides such validation (and the accompanying configuration parameters on @Module) in favor of full graph validation.

Compile-time Code Generation

Dagger’s annotation processor may also generate source files with names like CoffeeMaker_Factory.java or CoffeeMaker_MembersInjector.java. These files are Dagger implementation details. You shouldn’t need to use them directly, though they can be handy when step-debugging through an injection. The only generated types you should refer to in your code are the ones Prefixed with Dagger for your component.

Using Dagger In Your Build

Gradle Users

You will need to include the dagger-2.2.jar in your application’s runtime. In order to activate code generation you will need to include dagger-compiler-2.2.jar in your build at compile time.

In a Maven project, one would include the runtime in the dependencies section of your pom.xml, and the dagger-compiler artifact as a dependency of the compiler plugin:

<dependencies>
  <dependency>
    <groupId>com.google.dagger</groupId>
    <artifactId>dagger</artifactId>
    <version>2.2</version>
  </dependency>
  <dependency>
    <groupId>com.google.dagger</groupId>
    <artifactId>dagger-compiler</artifactId>
    <version>2.2</version>
    <optional>true</optional>
  </dependency>
</dependencies>