'아키텍처를 알아야 앱 개발이 보인다' 책을 보며 정리 및 간단 리뷰를 남긴다
어느정도 아키텍처, 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"
- ex) @Provides @Named("hello") fun provideHello() = "Hello"
- 의존성을 주입받는 곳에서도 @Named 어노테이션을 붙여준다
- ex) @Inject @Named("hello") String strHello;
@Inject @Named("world") String strWorld;
- ex) @Inject @Named("hello") String strHello;
커스텀 한정자
- @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 |
댓글