Dagger vs. Guice


원문보기

당신이 dependency injection에 익숙하고 Java-land에 있다면, 아마 Google Guice에 대해 들어 보았을 것이다. Square의 fine folks에서 Dagger라고 불리는 새로운 D-I 프레임워크를 들어 보았을 수도 있다. 우리는 엄청나게 Guice를 사용한 상당한 크기의 코드베이스를 가지고 있지만 나는 반짝 반짝 윤이나는 새로운 것이 좋아서 견디지 못하는 사람이다. so I started playing with Dagger to see what it would take to migrate away from Guice. Read on to learn more.

Why Dagger?

  • 안드로이드에서 동작한다!
  • 아마 Guice보다 빠를 것이다. Guide가 런타임에 리플렉션을 사용하는 반면 Dagger는 코드 생성을 선행해서 동작하기 때문이다.
  • 메이븐 컴파일러 플러그인(필수는 아니다)은 컴파일 타임에 여러 종류의 에러를 알려줄 수 있고 이거은 매우 유용할 수 있다. 예를 들면:
  • 순환(cyclic) 의존 관계를 검출한다.
  • 미사용 또는 중복 바인딩을 검출한다.
  • 완성되지 않은 모듈을 검출한다.(이는 객체 그래프를 만들 때 필요한 모든 바인딩이 제공되지 않았을 때이다.)
  • Guice와 비교하면 간단한 (하지만 제한적인) API
  • 가능한 표준 JSR 어노테이션들을 사용한다.

모든 것이 괜찮아 보인다. 그러면 내가 만난 문제는 무엇이 있을까?

Pain Points

Dagger는 어디서 무엇을 주입할 수 있는 가에 대한 여러가지의 제약이 있다. 대부분의 경우 이런 제약들은 코드가 리스트럭처링/리팩토링할 필요가 있다는 것을 알려주기 때문에 유용하다. 하지만 특정 경우에는 Dagger는 과도한 제약을 주고 있고 그저 동작하기 위해서도 과도하게 고생해야 한다는 것을 알게 되었다.

No cyclic dependencies

순환 의존 관계들은 일반적으로 불쾌하고 피해야 하는 것이다. 그렇기는 하지만 큰 코드베이스의 현실상 수용되어야 할 필요가 있다. 만약 의존관계를 깨트릴 수 없다면 그것을 피하기 위해 Lazy<T>를 사용해야 한다.

Injectable objects MUST have @Inject constructor

Dagger는 @Inject로 어노테이트되지 않은 객체는 주입할 수 없다. Dagger와 달리 Guice는 인수없는 생성자로 자동으로 객체를 주입할 수 있고, 이것은 3rd party 라이브러리를 처리할 때 엄청나게 편리하다. 이 제한은 개발자에게 주입을 고려하게 하고 의도를 명확하게 하기 때문에 의도치않은/우연한 주입에 의한 버그를 피할 수 있다는 점에서 당신의 코드를 위해서는 괜찮다. 하지만 3rd party 라이브러리들과 Dagger를 함께 사용할 때는 결국 다음과 같은 Providers 보일러플레이트들로 귀결될 것이다.

@Provides
ThirdPartyFoo provideThirdPartyFoo() {
return new ThirdPartyFoo();
}

No shortcut to bind interfaces to implementations

Guice는 인터페이스와 구현을 쉽게 바인드할 수 있게 해주는 여러가지 간단한 바인드 메소드들을 가지고 있다. 이것은 서로 다른 바인딩들을 production과 testing으로 사용할 때 특히 편리하다. 불행히도 Dagger는 이런 종류의 편의성은 없고, 더 많은 Providers 보일러플레이트들이 생기게 된다.

@Provides
@Singleton
Foo provideFoo(FooImpl fooImpl)
return fooImpl;
}

Restrictions, restrictions

Dagger는 Guice와 달리 모듈과 providers 메소드, 주입 가능한 객체들에게 너무 많은 제약을 가한다. 만약 Guice를 쓰고 있었다면 다음 상황들과 부딧칠 것이다.

  • 모듈은 abstract나 private이 될 수 없다.
  • @Provides 메소드는 static이 될 수 없다.
  • private나 final 멤버에 주입할 수 없다.
  • 예외를 던지는 생성자는 주입할 수 없다.

Injecting unit tests

우리는 테스트를 위해 junit4를 사용하며 대부분의 테스트들은 어떤 필드들을 주입해야 할 필요가 있다. Guice라면 우리는 멤버들을 주입하는 TestRule을 간편하게 가질 수 있다.

// module is a test module that overrides production settings
Guice.createInjector(module).injectMembers(target);

불행히도 Dagger는 injects 절에 도입 지점(entry point)을 명시할 것을 요구한며, 이런 불편을 경감할 수 있는 방법이 없는 듯 하다. 이것은 사실상 위와 같은 Rule기반 접근을 불가능하게 한다. 대신 테스트는 자신의 모듈을 명시해야 한다.

class MyTest {
@Module(injects = MyTest.class, includes = TestModule.class)
static class MyTestModule {
}
}

이것은 결과적으로 모든 테스트에 대해 수많은 보일러플레이트 코드가 필요해진다. 나는 ‘injects’절이 Dagger에게 에러를 선행 검출해 줄 수 있게 해줄거라고 이해하고 있다. 하지만 유닛 테스트의 멤버를 주입할 편리한 방법이 있어야 한다. 지금은 사실상 이것이 나에게 장벽이되고 있다. Tracked by #269

Other pitfalls

  • While Dagger is being used in production, it is still a young piece of software. Be prepared for weird bugs in the short-term (e.g. #258)
  • Guice와 완전히 작별해야 한다! : Dagger는 JSR javax.inject 어노테이션에 의존한다. Guice에서 마이그레이션하면 모든 com.google.inject 어노테이션을 제거해야 한다. 아니면 이상한 일들이 발생한다.
  • 시종일관 @Singleton을 사용한다: Foo 인터페이스와 FooImpl 구현을 가지고 있고, FooImpl를 Foo로 주입할 메소드가 필요하다고 하자. 만약 이 메소드를 @Singleton으로 어노테이트하면 구현에도 @Singleton 어노테이트해야 한다. 그렇지 않고 호출 지점에서 구현을 직접 주입하면 나쁜 일이 생긴다.

Conclusion

나에게 Dagger의 가장 큰 장점은 컴파일러 플러그인 —의존 관계 문제를 컴파일타임에 검출한다는 부가가치는 막대하다. 제한적인 API 외형이 매력적이기도 하다 —there are too many ways of doing one thing in Guice. 나는 어플리케이션을 Dagger로 동작하게 해봤는데 성능에 대한 장점은 무시해도 좋을 정도다.

불행히도, Dagger는 여전히 몇몇 코드들을 마이그레이션하기 어렵게 만드는 많은 차이점(gaps)이 있었다. 지금 현재 진정으로 유일한 방해요소는 유닛 테스트들을 주입하는 지원이 부족한 것이다. 확실히 내 프로젝트들에 Dagger를 쓸 수 있기를 고대한다.