https://dagger.dev/hilt/custom-components


5.6 Core APIs – Custom Components

사용자화 컴포넌트가 필요한가?

Hilt는 개발자를 위해 관리되는 안드로이드용으로 미리 정의된 컴포넌트들을 가지고 있다. 하지만 표준 Hilt 컴포넌트가 객체의 생애와 맞지 않거나 특정 기능을 필요로 하는 상황들이 생기기 마련이다. 이런 경우에는 사용자화 컴포넌트가 필요하다. 그러나 사용자화 컴포넌트를 생성하기 전에 논리적으로 사용자화 컴포넌트가 반드시 필요한 것인지 고려해야한다.

예를 들면 백그라운드 작업을 고려해볼 때, 작업이 스코프에 대해 타당하고 명확한 생애를 가지고 있다고 가정하자. 또한 해당 작업을 위해 요청된 객체들이 있는 경우 Dagger에 있는 바인딩을 작업 매개변수 일부로 전달할 수 있다. 하지만 대부분의 백그라운드 작업은 컴포넌트가 실제로 필요하지 않거나 단순히 몇개의 객체를 전달하기 위해 복잡도만 늘어나게 된다. 사용자화 컴포넌트를 추가하기 전에 다음과 같은 문제점들을 집고 넘어가자.

  • 각 컴포넌트/스코프에 복잡성이 더해진다. (cognitive overhead)
  • 사용자화 컴포넌트와 스코프는 조합론적으로 봤을 때 그래프를 복잡하게 만든다. (예: 컴포넌트가 개념적으로 ViewComponent의 하위 컴포넌트라면, ViewComponent 및 ViewWithFragmentComponent에 대해 두 컴포넌트를 추가해야한다)
  • 컴포넌트들은 단지 하나의 상위 컴포넌트만 가질 수 있다. 컴포넌트 계층은 다이아몬드형으로 구성될 수 없다. 더 많은 컴포넌트를 생성하는 것은 다이아몬드 의존 관계를 필요로 하는 상황의 가능성을 증대시킨다. 불행히도 이런 다이아몬드 의존 관계에 대한 문제의 솔루션은 없고, 이를 예측하거나 회피하는 것도 어렵다.
  • 사용자화 컴포넌트는 표준화된 규격에 반하기 때문에 더 많은 사용자화 컴포넌트가 사용될 수록 공용 라이브러리 사용은 더 어려워지게 된다.

이를 염두에 두고 사용자화 컴포넌트가 필요한지 결정하는데 사용해야 하는 몇가지 기준이 있다.

  • 컴포넌트는 명확한 생애를 가져야 한다.
  • 컴포넌트의 개념은 이해하기 쉽고 넓게 적용 가능해야 한다. Hilt 컴포넌트는 앱에 전반적으로 적용되므로 개념적으로 모든 곳에 적용 가능해야 한다. 전반적으로 이해하기 쉽다는 것은 복잡성 문제를 해결한다.(Issues with cognitive overhead)
  • Hilt가 아닌(일반적인 Dagger) 컴포넌트로 충분한지 고려해야 한다. 제한된 목적을 갖는 컴포넌트의 경우 때로는 Hilt 이외의 컴포넌트를 사용하는 것이 좋다. 예를 들어, 단일 백그라운드 작업을 표현하는 프로덕션 컴포넌트가 있다고 생각해보자. Hilt 컴포넌트들은 코드가 분리형 / 모듈형 코드로 작업해야 하는 상황에 탁월하다. 컴포넌트가 실제로 확장성을 갖지 못하는 경우 사용자화 컴포넌트를 사용하는 것이 맞지 않을 수 있다.

사용자화 컴포넌트의 제약

사용자화 컴포넌트는 현재 다음과 같은 제약조건을 가지고 있다.

  • 사용자화 컴포넌트는 반드시 ApplicationComponent의 직접적 또는 간접적으로 하위 컴포넌트여야 한다.
  • 사용자화 컴포넌트는 표준 컴포넌트 사이에 추가되면 안된다. 예를 들어 사용자화 컴포넌트는 ActivityComponent와 FragmentComponent사이에 추가 될 수 없다.

사용자화 Hilt 컴포넌트 추가하기

사용자화 Hilt 컴포넌트를 생성하기 위해서는 @DefineComponent와 함께 클래스를 생성해야 한다. 이는 @InstallIn 어노테이션에서 사용되는 클래스가 된다.

상위 컴포넌트는 @DefineComponent 어노테이션의 멤버 값으로 정의 되어야 한다. @DefineComponent 클래스는 스코프 어노테이션을 추가하여 객체가 해당 컴포넌트의 생애와 함께 하도록 할 수 있다.

예를 들면 다음과 같다.

@DefineComponent(parent = ApplicationComponent::class)
interface MyCustomComponent

빌더 인터페이스가 반드시 정의되어야 한다. 빌더 인터페이스는 상위 컴포넌트에서 주입 가능해야하고, 빌더 인터페이스를 통해 컴포넌트의 인스턴스를 생성할 수 있어야 한다. 이런 사용자 컴포넌트들은 일단 인스턴스가 생성되고 나면 적절한 시점에 컴포넌트 인스턴스를 유지하거나 해제하는 작업이 된다.

빌더 인터페이스는 @DefineComponent.Builder 어노테이션을 사용하여 정의된다. 빌더는 반드시 @DefineComponent 어노테이션을 갖는 타입을 반환하는 메서드 하나를 가져야 한다. 빌더는 @BindsInstance 메서드와 같이 일반적인 Dagger 컴포넌트 빌더의 메서드를 추가적으로 가질 수 있다.

@DefineComponent.Builder
interface MyCustomComponentBuilder {
    fun fooSeedData(@BindsInstance Foo foo): MyCustomComponentBuilder
    fun build(): MyCustomComponent
}

@DefineComponent.Builder 클래스가 @DefineComponent내에 중첩 될 수 있지만, 일반적으로는 별도의 클래스로 사용하는 것이 좋다. @HiltAndroidApp 클래스 또는 @HiltAndroidTest 클래스가 전이적 의존성인 경우 다른 클래스로 분리 될 수 있다. 많은 곳에서 @InstallIn을 통해 @DefineComponent 클래스가 참조되기 때문에 빌더의 의존성이 컴포넌트에 설치된 모든 모듈에 전이 의존성이 되지 않도록 빌더를 분리하는 것이 좋다.

과도한 의존성을 피하기 위해 @DefineComponent 인터페이스에서는 메서드를 사용할 수 없고, 대신에 Entry point를 통해 Dagger 객체에 접근해야 한다.

@EntryPoint
@InstallIn(MyCustomComponent::class)
interface MyCustomEntryPoint {
    fun getBar(): Bar
}

class CustomComponentManager 
@Inject constructor(componentBuilder: MyCustomComponentBuilder) {

    fun doSomething(foo: Foo) {
        val component = componentBuilder.fooSeedData(foo).build();
        val bar = EntryPoints.get(component, MyCustomEntryPoint::class.java).getBar()
        // 필요하다면 컴포넌트 인스턴스를 이곳에서 멤버 변수로 관리 할 수 있다.
    }
}
카테고리: Dagger2

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.