1. Introduction


최근 Spring Framework를 공부하던중 Dependency Injection을 처음 접하게 되었습니다. “코드는 제대로 분리해야 제맛이지!”라는 생각을 가지고 있는 한 사람으로서, Dependency Injection은 굉장히 신기한 디자인 패턴이었습니다.

그리고 Square사의 안드로이드 라이브러리들을 둘러보던 중에 Dagger라는 Dependency Injection 라이브러리를 보게 되었는데, 꼭 적용해보자!라는 마음으로 정리를 해보려 합니다.

2. Dependency Injection


WIKIPEDIA에 보니, “소프트웨어 엔지니어링 분야에서, Dependency Injection은 Dependencies 결정을 조정하는 주입기능을 구현하는 디자인 패턴이다.” 라고 되어 있네요.

(In software engineering, dependency injection is a software design pattern that implements inversion of control for resolving dependencies)

Dependencies 결정을 조정하는 주입 기능을 구현한다는 것이 무슨 말일까요?

읽다보니 세가지를 살펴 봐야 할 것 같습니다.

  • Service(Dependencies) : 주입될 녀석
  • Client(Dependent Object) : 주입받는 녀석
  • Injector(Injecting Code) : 주입을 하는 녀석

이때 주목할 점은, Client는 Service가 어떤 녀석인지 알필요가 없다는 것입니다. Client 는 단지 Service를 이용하기위한 약속(Interfaces)만 알면 되는 것입니다.

이렇게 세가지 개념으로 분리 되었을 때 객체의 사용과, 생성을 분리할 수 있다고 합니다.

(This separates the responsibilities of use and construction.)

간단하게 말하면, 새끼 새(Client) 가 어미새(Injector)로부터 먹이(Service)를 받아 먹어야 하는데 “받으면 잘근잘근 씹어서 목구멍을 넘겨야 한다” 라는 어미새의 가르침(Interface)만 따르면 어떤 먹이라도 소화해 낼 수 있다는게 Dependency Injection의 개념으로 이해됩니다.

이렇게 했을때 장점은, 먹이가 바뀔때마다 새끼를 갈아치우지(?) 않아도 된다는 것입니다. 예를 들어서, 정해둔 Interface를 지켜가며 Service와 Test Client를 짜면, Client를 테스트 하는데 테스트용 Client를 초기화 하는 작업을 다시 하지 않아도 되는 것입니다.

3. Dagger


Dagger - A fast dependency injector for Android and Java

Dependency Injection에 대해 알아봤으니, 적용을 해 봐야겠죠?

Dagger는 Android와 Java를 위한 빠른 Dependency Injector라고 합니다. (A fast dependency injector for Android and Java)

Introduction이 인상깊네요. 며칠전부터 읽기 시작한 에서 "생성자 대신 정적 팩터리 메서드를 사용할 수 없는지 생각해 보라"는 규칙을 본 기억이 납니다. Constructor가 여러개 존재할때 각각이 어떤 기능을 하는지 명시적이지 않아서 정적 팩터리 메서드를 사용하는 것이 좋다는 내용이었는데요. Dagger의 Introduction을 읽다보니 그 방법도 사실 명확성을 위해서 좀더 많은 코드를 작성하게 되는 것이니 최선의 방법이라고 보기는 어려울것 같다는 생각이 드네요.

Dagger를 이용하면 그런 부분을 말끔히 해결할 수 있다 하니 살펴 보겠습니다.

Dependency Injector라고 하니, Service, Client, Injector 세가지로 빠르게 보면 될것 같습니다.

  • Client
class Thermosiphon implements Pump {
  private final Heater heater;

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

  ...
}

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

  ...
}

Client가 될 Class를 정의하고, 주입하고 싶은 변수들과 constructor에 @Inject 어노테이션을 붙입니다. constructor에 @Inject 가 붙으면 Parameter에 Inject됩니다.

  • Service
@Module(
  injects = CoffeeApp.class
)
class DripCoffeeModule {
  @Provides Heater provideHeater() {
    return new ElectricHeater();
  }

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

@Provide 어노테이션을 붙인 injector 메소드를 선언합니다. Provider의 return 타입 맞추어 Inject됩니다. Pump의 Provider를 Thermosiphon pump 라는 파라미터를 가지도록 선언한것은 Thermosiphon의 Constructor에 붙은 Inject 어노테이션이 작동하도록 하기 위함인것 같네요. 모듈(Service)는 Inject될 Client를 명시해주어야 합니다. (injects = CoffeeApp.class => DripCoffeeModule 객체가 CoffeeApp에 inject됨)

  • Injector
ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
class CoffeeApp implements Runnable {
  @Inject CoffeeMaker coffeeMaker;

  @Override public void run() {
    coffeeMaker.brew();
  }

  public static void main(String[] args) {
    ObjectGraph objectGraph = ObjectGraph.create(new DripCoffeeModule());
    CoffeeApp coffeeApp = objectGraph.get(CoffeeApp.class);
    ...
  }
}

마지막으로 ObjectGraph로 Inject된 CoffeeApp Client를 생성합니다.

  • Singletons
@Provides @Singleton Heater provideHeater() {
  return new ElectricHeater();
}
@Singleton
class CoffeeMaker {
  ...
}

Singleton 객체를 주입하고 싶을때 @Singleton 어노테이션을 Class나 provide 메소드 앞에 붙여줍니다

  • Lazy Injection
class GridingCoffeeMaker {
  @Inject Lazy<Grinder> lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      // Grinder created once on first call to .get() and cached.
      lazyGrinder.get().grind();
    }
  }
}

Lazy<T> 로 감싼 객체는 T.get() 함수가 호출 되었을때 처음 생성됩니다

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

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

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

return 타입으로 inject되는 객체를 구분하는 것이 아니라 name으로 구분하도록 해서 같은 타입의 객체라도 다른 데이터가 inject되도록 합니다.

4. 결론


Dependency Injection은 Client를 독립적으로 만들수 있다는 점에서 매우 흥미로운 디자인 패턴입니다. DI 툴에 의존하게 된다는 단점이 있기는 하지만, Spring Framework로 웹 서버를 만들거나 안드로이드 어플을 만들때 간결하고 깔끔한 코드 작성에 상당한 도움이 될 것이라 생각됩니다.

5. 더 알아볼것

Dagger를 Android에 적용해서 어떻게 쓸지 공부해야겠습니다.