본문 바로가기
Android개발/Dagger2

Dagger2 기본 #3 - Component

by 궝테스트 2020. 5. 29.
'아키텍처를 알아야 앱 개발이 보인다' 책을 보며 정리 및 간단 리뷰를 남긴다
어느정도 아키텍처, dagger, rxJava, Jetpack 등의 선행학습 후 복습 차원에서 보면 좋을 것 같다

 

  • 바인딩된 Module 로부터 Object graph (또는 Container) 를 생성하는 역할을 한다
  • @Component 어노테이션을 사용하여 interface 와 abstract 클래스에만 붙일 수 있다.
  • 컴파일 타임에 어노테이션 프로세서에 의해 생성된 클래스에는 prefix 로 'Dagger' 가 붙는다
    ex) 컴포넌트명 = HelloComponent -> DaggerHelloComponent
  • @Component 속성으로는 modules, dependencies 가 있다.
    • modules : Component 에 바인드되는 @Module 이 지정된 클래스 배열 선언
    • dependencies : Component 에 다른 Component 의 의존성을 사용하는 경우 클래스 배열 선언

1. Object graph (또는 Container)
: Component, Module, Object 등의 관계를 Container 또는 Object graph 라고 한다.

2. Method

  • 최소한 하나의 추상적인 메소드를 가져야 한다
  • 메소드 파라미터와 리턴 타입은 규칙을 지켜야 한다
  • 규칙에 따라 Provision methods 와 Member-injection methods 로 구분된다

Provision methods
: 파라미터가 없고 리턴 타입은 Module 로부터 제공되거나 주입되는 메소드이다.

@Component(modules = [SomeModule::class])
interface SomeComponent {

	fun getSomething() : String
    
}

getSomething() 을 호출하면 SomeModule 로부터 제공되거나 주입된 String 객체를 리턴한다.

Member-injection methods
: 파라미터가 있는 메소드

// ex1. 리턴값이 없는 Member-injection method

// 의존성을 주입 받는 클래스
class Hello {

    @set:Inject // 의존성 주입 받을 메서드로 @Inject 어노테이션을 붙인다
    var str: String? = null

}

@Module
class ParentModule {

    @Provides
    fun provideHelloWorld() : String? = "Hello World"

}

@Component(modules = [ParentModule::class])
interface HelloComponent {

    fun inject(hello: Hello) // member-injection method

}

// 테스트를 해보자
@Test
fun testHelloWorld() {
	val hello = Hello()
	var str = hello.str
	assertNull("result must be null", str)
	println("result1 = $str")

	val helloComponent = DaggerHelloComponent.create()
	helloComponent.inject(hello)
	str = hello.str
	assertEquals("Hello World", str)
	println("result2 = $str")
}


/**
 * 테스트 결과 member-injection method 에 의해 필드 주입이 되었다
 * result1 = null
 * result2 = Hello World
 */
// ex2. 파라미터 없이 MembersInjector<T> 반환

@Component(modules = [ParentModule::class])
interface HelloComponent {

    fun getInjector() : MembersInjector<Hello>

}

@Test
fun testHelloWorld() {
	val hello = Hello()
	println("result1 = ${hello.str}")

	val helloComponent = DaggerHelloComponent.create()
    
	// MemberInjector 객체의 injectMembers(T) 를 호출하면 ex1 예제와 동일 결과 나옴
	helloComponent.getInjector().run { injectMembers(hello) }
	println("result2 = ${hello.str}")
}

/**
 * result1 = null
 * result2 = Hello World
 */

 

3. 의존성 주입

  • 3가지 의존성 주입 방법이 있으며, @Inject 어노테이션이 붙은 필드, 생성자, 메소드에 인스턴스를 주입한다.
  • 상속된 클래스에 의존성 주입할 경우,
    • 아래와 같이 각각의 클래스에 의존성이 주입될 필드를 가지고 있고,
      - class Parent { @Inject A a; }
      - class Self extends Parent { @Inject B b; }
      - class Child extends Self { @Inject C c; }
    • Component 에는 member-injection method 인 inject(Self) 가 있다
    • Child 의 인스턴스를 member-injection method 의 파라미터로 참조하여 호출하면,
      Child 의 인스턴스에는 a, b 만 주입되고 c 는 주입되지 않는다

4. @Component.Builder / @Component.Factory

  • Component 객체화 시 Builder/Factory 를 통해 직접 만들 수 있다
  • Builder/Factory 가 없으면 @Component 에 선언된 Module 및 의존성을 참조하여 Builder 를 자동 생성한다
// DaggerHelloComponent.java 를 살펴보면 Builder 가 자동생성된걸 알 수 있다
// Generated by Dagger (https://dagger.dev).

@SuppressWarnings({
    "unchecked",
    "rawtypes"
})
public final class DaggerHelloComponent implements HelloComponent {
	
    private Provider<String> provideHelloWorldProvider;

	private Provider<MembersInjector<Hello>> helloMembersInjectorProvider;

	private DaggerHelloComponent(ParentModule parentModuleParam) {
		initialize(parentModuleParam);
	}

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

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

	@SuppressWarnings("unchecked")
	private void initialize(final ParentModule parentModuleParam) {
		this.provideHelloWorldProvider = ParentModule_ProvideHelloWorldFactory.create(parentModuleParam);
		this.helloMembersInjectorProvider = InstanceFactory.create(Hello_MembersInjector.create(provideHelloWorldProvider));
	}

	@Override
	public MembersInjector<Hello> getInjector() {
		return helloMembersInjectorProvider.get();
	}

	public static final class Builder {
    
		private ParentModule parentModule;

		private Builder() { } 

		public Builder parentModule(ParentModule parentModule) {
			this.parentModule = Preconditions.checkNotNull(parentModule);
			return this;
		}

		public HelloComponent build() {
			if (parentModule == null) {
				this.parentModule = new ParentModule();
			}
			return new DaggerHelloComponent(parentModule);
		}
	}
}


@Component.Builder

  • @Component.Builder 어노테이션은 Component 내에 선언한다
  • Component 타입 또는 Component 의 super 타입을 리턴하는 추상 메소드를 포함해야 한다
    -> 빌드 메소드 라고 함
  • 빌드 메소드를 제외한 나머지를 Setter 메소드라고 한다
    • @Component 속성인 modules, dependencies 로 선언된 요소들은 Setter 메소드로 선언해야 한다
    • Setter 메소드는 하나의 파라미터만 가져야하며, 리턴 타입은 Void, Builder, Builder 의 super 타입이다
    • Setter 메소드에 @BindsInstance 를 붙이면 해당 Component 에 인스턴스를 넘겨 바인드시킨다
@Component(modules = [ParentModule::class])
interface HelloComponent {

    @Component.Builder
    interface Builder {

        fun parentModule(parentModule: ParentModule): Builder

        fun build() : HelloComponent

    }

}


@Component.Factory

  • @Component.Factory 어노테이션은 Component 내에 선언한다
  • Component 타입 또는 Component 의 super 타입을 리턴하는 추상 메소드를 포함해야 한다
  • @Component 속성인 modules, dependencies 로 선언된 요소들은 Setter 메소드로 선언해야 한다
  • 메소드에 @BindsInstance 를 붙이면 해당 Component 에 인스턴스를 넘겨 바인드시킨다
  • 생성된 Component 타입에는 factory() 라는 factory 인스턴스를 리턴하는 static 메소드를 갖는다

5. Lazy 주입 / Provider 주입

위 두 가지 방법으로 의존성 주입의 시점을 늦추거나 새로운 객체를 요청할 수 있다.

Lazy 주입

  • 바인드된 타입을 제네릭으로 갖는 Lazy<T> 로 생성, Lazy<T> 의 get() 호출 전까지 초기화를 지연시킬 수 있다
  • ex) @Inject Lazy<Integer> lazy;

Provider 주입

  • 바인드된 타입을 제네릭으로 갖는 Provider<T> 로 생성, 매번 새로운 인스턴스를 주입받을 수 있다
  • Component 가 @Singleton 으로 범위가 지정되었다면,
    Provier<T> 를 사용하여도 바인드된 의존성은 Singleton 으로 관리되어 같은 인스턴스를 제공받는다
  • ex) @Inject Provider<Integer> provider;

6. 한정자 지정

Component 에서 리턴 타입으로 바인드된 객체 식별 시 같은 타입을 리턴하는 @Provides 메소드가 여러개일 경우,
Dagger 는 어떤걸 바인딩해야 할지 애매하여 에러를 발생시킨다

@Named

  • @Named 어노테이션으로 같은 리턴 타입의 의존성을 식별할 수 있다
    • ex) @Provides @Named("hello") fun provideHello() = "Hello"
            @Provides @Named("world") fun provideHello() = "World"
  • 의존성을 주입받는 곳에서도 @Named 어노테이션을 붙여준다
    • ex) @Inject @Named("hello") String strHello;
            @Inject @Named("world") String strWorld;

커스텀 한정자

  • @Qualifier 로 아래와 같이 커스텀 한정자 어노테이션을 만들어서 @Hello 를 @Named 대신 사용
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Hello

 

7. 범위 지정

  • Component 는 @Scope 어노테이션으로 범위를 지정할 수 있다
  • Component 인스턴스는 의존성 제공 방법에 대한 동일성을 보장받을 수 있다
    • 인스턴스만 만들어서 참조하는 Singleton 패턴과 비슷한 개념이지만 생명주기를 따로 관리할 수 있다
    • 안드로이드에서는 Application, Activity, Fragment 인스턴스의 범위 지정을 다르게 관리하여
      Object graph 생성과 소멸을 각자 관리할 수 있다

@Singleton
: 범위를 지정하여 객체를 재사용할 수 있다.

@Module
class ParentModule {

	@Provides
    @Singleton
    fun provideObject() = Any()

}

@Singleton
@Component(modules = [ParentModule::class])
interface HelloComponent {

    fun getObject(): Any

}

class ExampleUnitTest {

    @Test
    fun testHelloWorld() {
        DaggerHelloComponent.create().run {
            val result1 = getObject()
            val result2 = getObject()
            assertSame(result1, result2)
        }
    }

}

/**
 * java.lang.Object@41a4555e
 * java.lang.Object@41a4555e
 */


@Reusable

  • 특정 Component 범위에 종속되지 않아서 Component 에 @Reusable 선언을 안해도 된다
  • 이전 객체 재사용 가능하면 재사용하고 아니면 새로 생성한다
  • 다른 Scope 어노테이션처럼 인스턴스 동일성을 보장하지 않아 메모리 관리에 효율적이다

@Scope 확장
: 커스텀 Scope 어노테이션을 직접 만들어 지정할 수 있다

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class HelloScope

'Android개발 > Dagger2' 카테고리의 다른 글

Dagger2 기본 #5 - Binding  (0) 2020.05.29
Dagger2 기본 #4 - Component 의존 관계  (0) 2020.05.29
Dagger2 기본 #2 - Module  (0) 2020.05.28
Dagger2 기본 #1 - 시작  (0) 2020.05.27
DI : Dependency Injection  (0) 2020.05.25

댓글