깨알 개념/Android

[Android] Dagger2 - 매개변수

interfacer_han 2024. 6. 24. 11:29

#1 이전 글

 

[Android] Dagger2 - @Provides

#1 이전 글 [Android] Dagger2 - 기초#1 이전 글 의존성 주입 (Dependency Injection)#1 의존성 주입(Dependency Injection)이란?#1-1 Dependent와 Dependencyfun main() { val car = Car() car.startCar() } class Car { private val engine = Engine

kenel.tistory.com

위 게시글의 완성된 앱을 일부 수정해서, @Provides 또는 @Module이 매개변수를 가지게 만들어본다.

 

 

Dagger 라이브러리를 쓰는 프로젝트에서 @Provides 또는 @Module에 인수를 전달하기 위해서는 먼저 dagger에 의해 생성되는 코드의 구조를 살펴볼 필요가 있다. 아래 코드를 보자.

 

#2 Component 클래스와 그 빌더 클래스

#2-1 dagger에서의 의존성 주입 준비는 컴파일 때 이뤄진다

...

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedState: Bundle?) {
        super.onCreate(savedState)
        setContentView(R.layout.activity_main)
/*
        val crankshaft = Crankshaft()
        val cylinder = Cylinder()
        val piston = Piston(crankshaft, cylinder)
        val engine = Engine(piston)
        val airbag = Airbag()
        val battery = Battery()

        val car = Car(engine, airbag, battery)
*/
        val car = DaggerCarComponent.create().getCar()
        car.startCar()
    }
}

수동적 의존성 주입(주석 처리된 코드)과 dagger의 의존성 주입의 차이는 '의존성 주입의 암시성'뿐만이 아니다. 그것은 바로, Dependency들을 모아서 최종적인 Dependent를 만드는 검증 과정이 런타임이 아닌 컴파일 시점에 이뤄진다는 것이다. 주석 처리된 코드에선 Car 인스턴스를 만들기 위해 하위 Dependency들의 인스턴스들을 단계적으로 생성해나가고 있다. 이 과정은 런타임에서 진행된다.

 

반면, dagger에서는 어떤가? 주석 처리된 코드 부분의 과정을 컴파일 시점에 검증 및 저장하고, 최종 Dependent인 Car 인스턴스를 getCar() 메소드를 통해 바로 얻어내고 있다. 아래 코드를 보자. 

 

#2-2 생성된 @Component 클래스 살펴보기 (DaggerCarComponent.java)

더보기
// Generated by Dagger (https://dagger.dev).
package com.example.parameter;

import dagger.internal.DaggerGenerated;
import dagger.internal.Preconditions;

@DaggerGenerated
@SuppressWarnings({
    "unchecked",
    "rawtypes",
    "KotlinInternal",
    "KotlinInternalInJava",
    "cast"
})
public final class DaggerCarComponent {
  private DaggerCarComponent() {
  }

  public static Builder builder() {
    return new Builder();
  }

  public static CarComponent create() {
    return new Builder().build();
  }

  public static final class Builder {
    private MyModule myModule;

    private Builder() {
    }

    public Builder myModule(MyModule myModule) {
      this.myModule = Preconditions.checkNotNull(myModule);
      return this;
    }

    public CarComponent build() {
      if (myModule == null) {
        this.myModule = new MyModule();
      }
      return new CarComponentImpl(myModule);
    }
  }

  private static final class CarComponentImpl implements CarComponent {
    private final MyModule myModule;

    private final CarComponentImpl carComponentImpl = this;

    private CarComponentImpl(MyModule myModuleParam) {
      this.myModule = myModuleParam;

    }

    private Piston piston() {
      return new Piston(new Crankshaft(), new Cylinder());
    }

    private Engine engine() {
      return new Engine(piston());
    }

    private Airbag airbag() {
      return new Airbag(MyModule_ProvidesAirbagManufacturerFactory.providesAirbagManufacturer(myModule));
    }

    private Battery battery() {
      return new Battery(MyModule_ProvidesBatteryManufacturerFactory.providesBatteryManufacturer(myModule));
    }

    @Override
    public Car getCar() {
      return new Car(engine(), airbag(), battery());
    }
  }
}

이 클래스는 프로그래머가 작성한 @Component 인터페이스에 기반해 컴파일 시점에 dagger가 자동으로 생성한 @Component 클래스다. dagger는 컴파일 시점에 말단 Dependency까지의 모든 의존성 주입을 검증하고, 그 주입 결과를 프로그래머가 원클릭으로 얻을 수 있는 메소드(getCar())를 생성한다. 위 코드에서 해당 메소드가 속한 클래스인 CarComponentImpl와 그 빌더 클래스(이하 Component Builder)를 확인할 수 있다.

 

#2-3 매개변수 할당 타이밍

말단 Dependency에서 시작해 차곡차곡 단계를 밟아 올라가는 수동적 의존성 주입은 런타임에 이런저런 매개변수를 프로그래머 입맛대로 손 쉽게 할당할 수 있다. 반면, dagger의 경우는 구조상 그럴 수 없다. ComponentImpl 클래스는 컴파일 시점에 만들어지기 때문이다. 따라서, 우리는 ComponentImpl 인스턴스를 만드는 빌더 클래스를 통해 매개변수를 삽입하며, 그 결과로 매개변수가 삽입된 버전의 ComponentImpl이 얻어지는 걸 목표로 두어야 한다.

 

#3 코드 수정 - @Module 클래스에 생성자 매개변수가 존재하는 경우

#3-1 MyModule.kt

...

@Module
class MyModule(private val countryOfManufacture: String) {

    @Provides
    @Named("Airbag")
    fun providesAirbagManufacturer(): String {
        return "KENEL" + " (MADE IN ${countryOfManufacture})"
    }

    @Provides
    @Named("Battery")
    fun providesBatteryManufacturer(): String {
        return "TISTORY" + " (MADE IN ${countryOfManufacture})"
    }
}

MyModule 클래스의 생성자에 매개변수를 추가하고, init { ... }에 매개변수 확인용 Log.i( ... )도 넣어준다. 그리고 [Build] - [Rebuild Project]한다. 에러가 난다면, MainActivity에서 주석 아래쪽 코드를 지우고 다시 시도해본다.

 

#3-2 @Component 인터페이스

// package com.example.parameter

import dagger.BindsInstance
import dagger.Component
import javax.inject.Named

@Component(modules = [MyModule::class])
interface CarComponent {
    fun getCar(): Car

    @Component.Builder
    interface Builder {
        fun setMyModule(myModule: MyModule): Builder

        fun build(): CarComponent
    }
}

빌더 클래스를 변경해야 한다. 위와 같이 코드를 짜면, dagger가 알아서 오버라이드하여 아래와 같은 코드를 생성한다.

 

#3-3 생성된 @Component 클래스 살펴보기 (DaggerCarComponent.java)

더보기
// Generated by Dagger (https://dagger.dev).
package com.example.parameter;

import dagger.internal.DaggerGenerated;
import dagger.internal.Preconditions;

@DaggerGenerated
@SuppressWarnings({
    "unchecked",
    "rawtypes",
    "KotlinInternal",
    "KotlinInternalInJava",
    "cast"
})
public final class DaggerCarComponent {
  private DaggerCarComponent() {
  }

  public static CarComponent.Builder builder() {
    return new Builder();
  }

  private static final class Builder implements CarComponent.Builder {
    private MyModule myModule;

    @Override
    public Builder setMyModule(MyModule myModule) {
      this.myModule = Preconditions.checkNotNull(myModule);
      return this;
    }

    @Override
    public CarComponent build() {
      Preconditions.checkBuilderRequirement(myModule, MyModule.class);
      return new CarComponentImpl(myModule);
    }
  }

  private static final class CarComponentImpl implements CarComponent {
    private final MyModule myModule;

    private final CarComponentImpl carComponentImpl = this;

    private CarComponentImpl(MyModule myModuleParam) {
      this.myModule = myModuleParam;

    }

    private Piston piston() {
      return new Piston(new Crankshaft(), new Cylinder());
    }

    private Engine engine() {
      return new Engine(piston());
    }

    private Airbag airbag() {
      return new Airbag(MyModule_ProvidesAirbagManufacturerFactory.providesAirbagManufacturer(myModule));
    }

    private Battery battery() {
      return new Battery(MyModule_ProvidesBatteryManufacturerFactory.providesBatteryManufacturer(myModule));
    }

    @Override
    public Car getCar() {
      return new Car(engine(), airbag(), battery());
    }
  }
}

Builder().build()를 반환하던 #2-2의 create() 메소드가 사라졌다. 또, #2-2에는 myModule의 인스턴스가 null이면 자동으로 myModule의 인스턴스를 넣는 동작이 정의되어있었는데 이것이 삭제되었다. 게다가 #3-2에서 만들었던 추상 메소드인 setMyModule()의 구현 메소드가 myModule이 null인지를 checkNotNull()이라는 메소드로 검사하고 있다. 이 메소드는 인자로 받은 객체가 null이면 java.lang.NullPointerException 에러를 일으킨다. 한마디로, myModule을 프로그래머가 반드시 명시하여 Builder 클래스에게 넘겨주도록 유도하고 있다. 이제 MainActivity에서 유도받은(?)대로 코드를 짜보겠다.

 

#3-4 MainActivity.kt

...

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedState: Bundle?) {
        super.onCreate(savedState)
        setContentView(R.layout.activity_main)
/*
        ...
*/

        val car = DaggerCarComponent
            .builder()
            .setMyModule(MyModule("KOREA"))
            .build()
            .getCar()
        car.startCar()
    }
}

 

#3-5 작동 확인 (로그 메시지)

Crankshaft is ready
Cylinder is ready
Piston is ready
Engine is ready
Airbag is ready
Airbag made by KENEL (MADE IN KOREA)
Battery is ready
Battery made by TISTORY (MADE IN KOREA)
Car is ready

 

#4 코드 수정 - @Provides 메소드에 매개변수가 존재하는 경우

#4-1 MyModule.kt

...

@Module
class MyModule {

    @Provides
    @Named("Airbag")
    fun providesAirbagManufacturer(@Named("material") material: String): String {
        return "KENEL" + " (재질: ${material})"
    }

    @Provides
    @Named("Battery")
    fun providesBatteryManufacturer(@Named("type") type: String): String {
        return "TISTORY" + " (종류: ${type})"
    }
}

이번엔 @Provides 메소드에 매개변수가 존재하는 경우다. 수정 후 [Build] - [Rebuild Project]한다. 에러가 난다면, MainActivity에서 주석 아래쪽 코드를 지우고 다시 시도해본다.

 

#4-2 @Component 인터페이스

// package com.example.parameter

import dagger.BindsInstance
import dagger.Component
import javax.inject.Named

@Component(modules = [MyModule::class])
interface CarComponent {
    fun getCar(): Car

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun setMaterialOfProvides(@Named("material") country: String): Builder

        @BindsInstance
        fun setTypeOfProvides(@Named("type") type: String): Builder

        fun build(): CarComponent
    }
}

@Named가 쓰인 이유는, MyModule의 두 함수의 생성자 인수의 종류와 갯수가 같기 때문이다. 만약 둘 다 String을 인수로 받는게 아니라, 어느 한 쪽은 String이고 반댓쪽은 Int였다면 @Named가 없어도 정상작동한다. dagger 입장에서 어느 @BindInstance 메소드가 어느 @Provide 메소드에 연결되는지 명확히 판단할 수 있기 때문이다.

 

또, #3-2에서와는 달리 추상 메소드에 @BindInstance 어노테이션이 붙었다. @BindsInstance는 Component Builder에 의해 만들어지는 ComponentImpl를 완성하기 위해 필요한 특정 인스턴스, 바로 그 특정 인스턴스를 제공하는 메소드에 붙는다. 그렇다면, 어째서 #3-2에서는 @BindInstance가 없었는가? MyModule 또한 ComponentImpl를 완성할 때 필요하지 않은가?

 

@Module의 역할은 첫째로는 @Provides 메소드를 담는 용기다. 둘째로는 @Component 어노테이션의 modules 속성에 명시적으로 등록됨으로써, Dagger에게 어떤 객체를 생성하고 제공하는지 (@Provides 메소드가 반환하는 데이터 타입들을 통해) 알리는 것이다. 따라서 MyModule 클래스가 ComponentImpl을 완성하는 데 필요한 것은 분명 맞다. 하지만 #3-2의 setMyModule()은 ComponentImpl에게(= 의존성 그래프 상에서) 직접적으로 인스턴스를 제공하는 것이 아닌, MyModule의 멤버 메소드에게 인스턴스를 제공하기 때문에 @BindInstance가 사용되지 않은 것이다.

 

#4-3 생성된 @Component 클래스 살펴보기 (DaggerCarComponent.java)

더보기
// Generated by Dagger (https://dagger.dev).
package com.example.parameter;

import dagger.internal.DaggerGenerated;
import dagger.internal.Preconditions;

@DaggerGenerated
@SuppressWarnings({
    "unchecked",
    "rawtypes",
    "KotlinInternal",
    "KotlinInternalInJava",
    "cast"
})
public final class DaggerCarComponent {
  private DaggerCarComponent() {
  }

  public static CarComponent.Builder builder() {
    return new Builder();
  }

  private static final class Builder implements CarComponent.Builder {
    private String setMaterialOfProvides;

    private String setTypeOfProvides;

    @Override
    public Builder setMaterialOfProvides(String country) {
      this.setMaterialOfProvides = Preconditions.checkNotNull(country);
      return this;
    }

    @Override
    public Builder setTypeOfProvides(String type) {
      this.setTypeOfProvides = Preconditions.checkNotNull(type);
      return this;
    }

    @Override
    public CarComponent build() {
      Preconditions.checkBuilderRequirement(setMaterialOfProvides, String.class);
      Preconditions.checkBuilderRequirement(setTypeOfProvides, String.class);
      return new CarComponentImpl(new MyModule(), setMaterialOfProvides, setTypeOfProvides);
    }
  }

  private static final class CarComponentImpl implements CarComponent {
    private final MyModule myModule;

    private final String setMaterialOfProvides;

    private final String setTypeOfProvides;

    private final CarComponentImpl carComponentImpl = this;

    private CarComponentImpl(MyModule myModuleParam, String setMaterialOfProvidesParam,
        String setTypeOfProvidesParam) {
      this.myModule = myModuleParam;
      this.setMaterialOfProvides = setMaterialOfProvidesParam;
      this.setTypeOfProvides = setTypeOfProvidesParam;

    }

    private Piston piston() {
      return new Piston(new Crankshaft(), new Cylinder());
    }

    private Engine engine() {
      return new Engine(piston());
    }

    private String namedString() {
      return MyModule_ProvidesAirbagManufacturerFactory.providesAirbagManufacturer(myModule, setMaterialOfProvides);
    }

    private Airbag airbag() {
      return new Airbag(namedString());
    }

    private String namedString2() {
      return MyModule_ProvidesBatteryManufacturerFactory.providesBatteryManufacturer(myModule, setTypeOfProvides);
    }

    private Battery battery() {
      return new Battery(namedString2());
    }

    @Override
    public Car getCar() {
      return new Car(engine(), airbag(), battery());
    }
  }
}

#3-3과 같은 맥락으로 #4-2에서 만든 setMaterialOfProvides() 및 setTypeOfProvides()가 구현된 모습이다.

 

#4-4 MainActivity.kt 수정

...

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedState: Bundle?) {
        super.onCreate(savedState)
        setContentView(R.layout.activity_main)
/*
        ...
*/

        val car = DaggerCarComponent
            .builder()
            .setMaterialOfProvides("나일론")
            .setTypeOfProvides("리튬이온")
            .build()
            .getCar()
        car.startCar()
    }
}

 

#4-5 작동 확인 (로그 메시지)

Crankshaft is ready
Cylinder is ready
Piston is ready
Engine is ready
Airbag is ready
Airbag made by KENEL (재질: 나일론)
Battery is ready
Battery made by TISTORY (종류: 리튬이온)
Car is ready

 

#5 코드 수정 - @Module과 @Provides 둘 다 매개변수가 존재하는 경우

#3과 #4의 합집합으로 코드를 수정하면 된다. 본 게시글에선 생략했지만, #3과 #4가 합쳐진 코드는 #7에 올려두었다.

 

#5-1 작동 확인 (로그 메시지)

Crankshaft is ready
Cylinder is ready
Piston is ready
Engine is ready
Airbag is ready
Airbag made by KENEL (재질: 나일론) (MADE IN KOREA)
Battery is ready Battery made by TISTORY (종류: 리튬이온) (MADE IN KOREA)
Car is ready

 

#6 요약

dagger를 쓸 땐, 매개변수를 Component Builder에게 전달한다.

 

#7 완성된 앱

 

android-practice/dagger2/Parameter at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com