Dagger2를 알아보자 – 기본편
Dagger2를 알아보자 – Scope 
Dagger2를 알아보자 – Injection의 종류 
Dagger2를 알아보자 – Qualifier 
Dagger2를 알아보자 – Binding 
Dagger2를 알아보자 – Multibinding 
Dagger2를 알아보자 – SubComponent (You’re here)
Dagger2를 알아보자 – Android
Dagger2를 알아보자 – Testing(준비중)
Dagger2를 알아보자 – Dynamic Feature에 적용하기


Subcomponent

하위 컴포넌트(Subcomponent)는 상위 컴포넌트(Component)의 그래프를 상속하고 확장하는 컴포넌트 입니다. 

특징

  • 그래프를 분할하여 캡슐화 합니다.
  • 상위 컴포넌트를 상속하므로 하위 컴포넌트는 상위 컴포넌트의 객체에 의존할 수 있습니다.
  • 하위 컴포넌트는 상위 컴포넌트와 생명주기를 다르게 가져갈 수 있습니다.
  • 하위 컴포넌트는 또 다른 하위 컴포넌트를 가질 수 있습니다.
    예) ApplicationComponent -> ActivitySubcomponent-> FragmentSubcomponent

Subcomponent를 활용하여 Starbucks 만들기

많은 Dagger2 예제들이 커피머신을 구현하는 예제를 사용합니다. 저는 커피머신 대신 스타벅스라는 앱을 만들어 보겠습니다. (창의성 매우 부족)

먼저 우리가 이미 알고 있는 스타벅스를 상기해보겠습니다. 스타벅스라는 카페가 있고, 그곳에서 일하는 점원이 있습니다. 스타벅스에서는 점원을 파트너(Partner)라고 칭하더군요. 스타벅스를 가서 파트너에게 음료를 주문하면 파트너가 음료를 만들어 줍니다. 파트너는 여러명이 될 수 있고, 같은 파트너가 만든 동일한 메뉴의 음료여도 주문할때마다 서로 다른 음료(다른 인스턴스)가 되어 나와야 합니다. 스타벅스를 방문한 손님 A와 손님 B가 아메리카노를 똑같이 주문했다고 한잔의 아메리카노를 나눠마실순 없으니까요.

자 이제 Starbucks 클래스를 만들어 보도록 하겠습니다

public class Starbucks(){
   ...
}

스타벅스 건물을 하나 지은 셈이지요. 스타벅스를 운영하려면 재료가 필요하겠죠.

@Module
public class IngredientModule {
    @Provides
    CoffeeBean provideCoffeeBean(){
        return new CoffeeBean();
    }

    @Provides
    Water provideWater(){
        return new Water();
    }

    @Provides
    Milk provideMilk(){
        return new Milk();
    }
}

재료를 제공해주는 모듈을 하나 만들었습니다. 커피콩과 물, 우유를 공급해주는 모듈입니다. 같은 커피콩을 재탕해서 에스프레소는 만드는 일이 없어야하므로 스코프는 지정하지 않습니다. 

Note : Scope가 없으면 요청시 매번 다른 객체를 제공하게 됩니다. 자세한 내용은 Scope편을 참조해주세요.

이 재료를 가지고 음료를 만들게 되겠죠? 

Drink interface를 구현한 음료 Class들을 미리 만들어 두겠습니다.

public interface Drink {
}

public class Espresso implements Drink {
    private Water water;
    private CoffeeBean coffeeBean;

    @Inject
    public Espresso(Water water, CoffeeBean coffeeBean) {
        this.water = water;
        this.coffeeBean = coffeeBean;
    }
}

public class Americano implements Drink {

    private Espresso espresso;
    private Water water;

    @Inject
    public Americano(Espresso espresso, Water water) {
        this.espresso = espresso;
        this.water = water;
    }
}

public class Latte implements Drink {
    private Espresso espresso;
    private Milk milk;

    @Inject
    public Latte(Espresso espresso, Milk milk) {
        this.espresso = espresso;
        this.milk = milk;
    }
}

이제 IngredientModule을 연결할 StarbucksComponent를 만들어보겠습니다.

@Singleton
@Component(modules = {IngredientModule.class})
public interface StarbucksComponent {
    void inject(Starbucks starbucks);
}

@Singleton 스코프인 StarbucksComponent를 만들었고, 멤버인젝션을 위한 inject(…)메소드도 만들었습니다. 

이제 아래의 코드를 통해 의존성 주입을 실행해보겠습니다.

public class Starbucks {

    public Starbucks() {
        StarbucksComponent starbucksComponent = DaggerStarbucksComponent.create();
        starbucksComponent.inject(this);
    }
}

재료가 준비되었지만 음료를 만들어줄 파트너가 없으므로 파트너를 고용(?)해보겠습니다.
StarbucksComponent가 상위 컴포넌트가 되고, Partner의 컴포넌트 하위 컴포넌트가 될것입니다.

Partner 컴포넌트를 먼저 만들어보겠습니다

@Scope
public @interface PartnerScope {
}

@PartnerScope
@Subcomponent(modules = PartnerModule.class)
public interface PartnerComponent {
    @Subcomponent.Builder
    interface Builder{
        Builder partnerModule(PartnerModule module);
        PartnerComponent build();
    }
    void inject(Partner partner);
}

기본적인 구조는 Component와 동일합니다. Scope가 있고, Module을 가집니다. 다른점은 @Subcomponent애노테이션을 사용해야하며, 반드시 명시적으로 @Subcomponent.Builder 또는 @Subcomponent.Factory 구현해야 한다는 점입니다.

PartnerModule.class는 다음과 같이 구현했습니다.

@Module
public class PartnerModule {

    @Provides
    @PartnerScope
    @Named("partnerId")
    String providePartnerId() {
        return UUID.randomUUID().toString(); //파트너 아이디로 랜덤한 고윳값 부여
    }

    @Provides
    @IntoMap
    @ClassKey(Americano.class)
    Drink provideAmericano(Espresso espresso, Water water) {
        return new Americano(espresso, water);
    }

    @Provides
    @IntoMap
    @ClassKey(Espresso.class)
    Drink provideEspresso(Espresso espresso){
        return espresso;
    }

    @Provides
    @IntoMap
    @ClassKey(Latte.class)
    Drink providelatte(Espresso espresso, Milk milk) {
        return new Latte(espresso, milk);
    }

}

파트너의 ID는 파트너를 고용한 후 해고시킬때까지(?) 동일한 값이여야되므로, @PartnerScope를 붙여 동일한 인스턴스에 의존합니다.

아메리카노, 에스프레소, 라테는 멀티바인딩을 활용했으며, 매번 다른 음료객체를 만들어야하므로 스코프를 지정하지 않았습니다. 

Note: PartnerModule내의 프로비젼 메소드 인자를 살펴보면 Espresso, Water, Milk등을 주입받고 있지만 PartnerComponent 해당 객체를 제공하지 않고 있습니다. 위 객채를 제공받을 수 있는 이유는 상위 컴포넌트인 StarbucksComponent의 오브젝트 그래프내에 해당 객체들이 있기 때문입니다. 

음료객체를 만들기 위한 파트너의 코드는 다음과 같습니다.

public class Partner {
    @Inject
    @Named("partnerId")
    String partnerId;

    @Inject
    Map<Class<?>, Provider<Drink>> menuMap;

    @Inject
    public Partner(PartnerComponent.Builder builder){
        builder.partnerModule(new PartnerModule()).build().inject(this);
    }

    public <T extends Drink> T makeDrink(Class<T> t){
        return (T) menuMap.get(t).get();
    }

    public String getId(){
        return partnerId;
    }
}

생성자에서는 PartnerComponent.Builder를 주입받아 파트너에게 멤버인젝션을 하고, 주입받은 menuMap에서는 음료 클래스를 클래스키로 받아 매번 새로운 음료를 PartnerModule 모듈로부터 제공받습니다.

이제 상위 컴포넌트(StarbucksComponent)와 하위 컴포넌트(PartnerCompent) 연관성을 정의할 차례입니다.하위컴포넌트의 정의는 상위컴포넌트의 모듈에 선언합니다.

먼저 상위 컴포넌트의 모듈로 StarbucksModule을 추가하고 이 모듈에 하위 컴포넌트를 연관짓겠습니다.

@Singleton
@Component(modules = {IngredientModule.class, StarbucksModule.class})
public interface StarbucksComponent {
    void inject(Starbucks starbucks);
}
@Module(subcomponents = PartnerComponent.class)
public class StarbucksModule {

    @Provides
    Partner partner(PartnerComponent.Builder builder){
        return new Partner(builder);
    }

    ...
}

모듈 애노테이션의 subcomponents에 PartnerComponent.class를 적용합니다.
이로써 상위-하위 컴포넌트 관계가 형성이 된것입니다.

이를통해 Starbucks모듈내에 Partner객체를 제공하는 프로비젼 메소드의 파라미터로 PartnerComponent.Builder를 받을 수 있게 되었습니다.

이제 다시 Starbucks 클래스로 돌아가보겠습니다.

public class Starbucks {

    public static final String TAG = Starbucks.class.getSimpleName();

    @Inject
    Provider<Partner> partnerProvider;

    public Starbucks(Logger logger) {
        System.out.println("스타벅스 Grand Opening");
        StarbucksComponent starbucksComponent = DaggerStarbucksComponent.create();
        starbucksComponent.inject(this);

        Partner partner1 = partnerProvider.get();
        Partner partner2 = partnerProvider.get();

        logger.e(TAG, "partner1:"+partner1.getId());
        logger.e(TAG, "partner2:"+partner2.getId());

        for(int i=0;i<10;i++){
            Americano americano = partner1.makeDrink(Americano.class);
            logger.e(TAG, "아메리카노:"+americano);
        }
    }
}

파트너를 생성 할 Provider<Partner>를 주입받습니다. Provider는 매번 객체를 요청할때 새로운 객체를 제공해줍니다. Provider<Partner>를 통해 두명의 파트너를 만들었습니다.(partner1, partner2)

두 파트너의 Id를 비교하여 동일한 인스턴스인지 확인합니다.

이후 한명의 파트너를 통해 아메리카노를 10잔 요청한 결과가 어떤지 확인해보겠습니다.

스타벅스 Grand Opening
E Starbucks: partner1:1c795cd8-7f25-4495-879f-cb634901626f
E Starbucks: partner2:e652e3f2-bb2a-4ff4-aad5-5d94f9e57b2b
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@58d25a40
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@1b701da1
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@726f3b58
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@442d9b6e
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@ee7d9f1
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@15615099
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@1edf1c96
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@368102c8
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@6996db8
E Starbucks: 아메리카노:com.charlezz.starbucks.menu.Americano@1963006a

10잔의 아메리카노가 생성되는것을 확인할 수 있습니다.

위 예제는 github에서 확인가능합니다.

카테고리: Dagger2Java

3개의 댓글

· 2020년 5월 14일 4:55 오후

good!

감사합니다 · 2021년 2월 20일 11:46 오전

제일 상단에 drawit diagram이라는 이미지가 안나오는것 같네요

    Charlezz · 2021년 2월 21일 10:04 오후

    제보감사합니다. 알 수 없는 이유로 원본이미지가 삭제되어 다른 이미지로 대체합니다.

답글 남기기

Avatar placeholder

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