컴포넌트는 바인딩된 모듈로부터 오브젝트 그래프를 생성하는 핵심적인 역할을 한다.
@Component를 사용하여 컴포넌트를 생성할수 있으며 interface 또는 abstract 클래스에만 붙일 수 있다.
컴파일 타입의 애노테이션 프로세서에 의해 생성된 클래스는 접두어 Dagger 와 @Component가 붙은 클래스 이름이 합쳐진 형식의 이름을 갖는다
Dagger 에서는 컴포넌트, 모듈 ,객체등의 관계를 컨테이너 또는 오브젝트 그래프라고 한다.
@Component가 붙은 모든 타입은 최소한 하나의 추상적인 메서드를 가져야 한다.
메서드의 이름은 상관 없지만 메서드 매개변수와 반환형은 규칙을 엄격하게 따라야 한다
프로비전 메서드와 멤버-인젝션 메서드로 구분된다.
Dagger의 컴포넌트에서 매개변수를 갖지 않으면서 반환형은 모듈로부터 제공되거나 주입되는 메서드를 말한다
@Component(modules = SomeModule.class)
public interface SomeComponent{
SomeType getSomeType();
}
getSomeType() 메서드 호출시 SomeModule로부터 제공받거나 주입받은 SomeType 객체를 반환한다.
Dagger의 컴포넌트에서는 하나의 매개변수를 갖는 메서드를 멤버-인젝션 메서드라고 한다.
void를 반환하거나 빌더 패턴처럼 메서드 체이닝이 가능한 메서드를 만들기 위해 매개변수 타입을 반환형으로 갖는 메서드로 선언할 수 있다.
@Component(modules = SomeModule.class)
public interface SomeComponent{
void injectSomeType(SomeType someType);
someType injectAndReturnSomeType(SomeType someType);
}
멤버-인젝션 메서드를 구현해보자
의존성을 주입 받도록 MyClass 를 생성한다 의존성 주입을 받을 필드 메서드에 @Inject 애노테이션을 붙인다
public class MyClass{
@Inject
String str;
public String getStr(){
return str;
}
}
MyComponent에도 멤버-인젝션 메서드를 추가한다
@Component(modules = MyModule.class)
public interface MyComponent{
void inject(MyClass myClass);
}
멤버 인젝션이 잘 되는지 확인하려면 다음과 같이 테스트 해본다
@Test
public void testMemberInjection(){
MyClass myclass = new MyClass();
String str = myclass.getStr();
assertNotNull("조회결과 null " str); // null 이 아님을 확인
Mycomponent myComponent = DaggerMyComponent.create();
myComponent.inject(myclass);
str = myclass.getStr();
assertEquals("hello world",str)//str = hello world
}
위의 예제를 통해 myClass 필드가 null 이었다가 메서드 호출 이후에는 hello world가 주입된것을 확인 할 수가 있다.
매개변수가 없고 MembersInjector<T>
를 반환할 수도 있다 이 경우에는 반환된 MemberInjector 객체의 injectmembers(T)메서드를 호출하면 멤버-인젝션 메서드와 동일한 작업을 수행한다.
@Component(modules = MyModule.class)
public interface MyComponent{
...
MembersInjector<MyClass> getInjector();
}
@Test
public void testMemberInjector(){
Myclass myclass = new Myclass();
String str = myclass.getStr();
System.out.println("result= "+str);
MyComponent myComponent = DaggerMyComponent.create();
MembersInjector<MyClass> injector = myComponent.getInjector();
injector.injectMembers(myClass);
str = myClass.getStr();
System.out.println("result = "+str);
}
Dagger 에는 3가지 의존성 주입 방법을 제공한다
@Inject가 붙은 필드, 메서드 또는 생성자에 인스턴스를 주입하는데 실무에서는 필드주입과 생성자 주입이 주로 사용된다.
@Component(modules = PersonModule.class)
public interface PersonComponent{
PersonA getPersonA();//프로비전 메서드
void inject(PersonB personB); //멤버-인젝션 메서드
}
@Module
public class PersonModule{
@Provides
String provideName(){
return "Charles";
}
@Provides
int provideAge(){
return 100;
}
}
public class PersonA{
private String name;
private int age;
@Inject//생성자 주입
public PersonA(String name,int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
}
public class PersonB{
@Inject//필드주입
String name;
private int age;
@Inject//메서드 주입
public void setAge(int age){
this.age = age;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
이름과 나이를 제공하는 PersonModule을 PersonComponent에 추가하고 PersonA를 제공하는 프로비전 메서드와 PersonB를 제공하는 멤버-인젝션 메서드를 추가했다
@Test
public void testInjection(){
PersonComponent personComponent = DaggerPersonComponent.create();
PersonA personA = personComponent.getPersonA();
System.out.println(personA.getName()+" :" +personA.getAge());
PersonB personB = new PersonB();
DaggerPersonComponent.create().inject(personB);
assertEquals("Charles",personB.getName());
assertEquals(100,personB.getAge());
}
멤버-인젝션 메서드를 호출할때 매개 변수 타입에 서브 클래스의 객체를 넣으면 해당 슈퍼 클래스의 @Inject 멤버만 의존성 주입이 이루어진다.
ex) 컴포넌트에 멤버-인젝션 메서드인 inject(self)가 존재하고 , child의 인스턴스를 멤버-인젝션 메서드의 매개 변수로 참조하여 메서드를 호출하면 child의 인스턴스에는 a,b만 주입되고 c에는 주입되지 않음을 알수 있다
public class Parent{
@Inject
A a;
}
public class Self extends Parent{
@Inject
B b;
}
public class Child extends Self{
@Inject
C c;
}
컴포넌트를 객체화 할때는 주로 생성된 빌더나 팩토리를 통해 만들 수 있다. 컴포넌트 내에 @Component.Builder 또는 @Component.Factory타입 선언을 통해 빌더 또는 팩토리가 생성되는데 둘다 없는경우 @Component 애노테이션에 선언된 모듈 및 의존성을 참조하여 빌더를 자동으로 생성한다.
다음과 같이 Builder를 직접 작성해야 한다면 다음과 같이 작성 가능하다
@Component(modules = MyModule.class)
public interface MyComponent{
...
@Component.Builder
interface Builder{
Builder setModule(MyModule myModule);
MyComponent build();
}
}
}
컴포넌트 빌더를 만드는 조건은 엄격하며 조건이 반드시 성립해야 한다 .
- @Component.Builder 애노테이션은 컴포넌트 타입 내에 선언되어야 한다.
- 반드시 매개변수를 갖지 않고, 컴포넌트 타입또는 컴포넌트의 슈퍼 타입을 반환하는 추상 메서드를 하나 포함해야 한다. 이를 빌드(Build method)메서드 라고 한다.
- 빌드 메서드를 제외한 나머지는 세터 메서드라고 한다
- @Component 애노테이션에 Modules, dependencies로 선언된 요소들은 세터 메서드로 선언해야 한다.
- 세터 메서드는 반드시 하나의 매개변수만 가져야 하며 , 반환형으로는 void,Builder,빌더의 슈퍼타입이 될수 있다.
- 세터 메서드에 @BindsInstance를 붙이면, 해당 컴포넌트에 인스턴스를 넘겨 바인드시킨다.
@Component(modules = {BackendModule.class,FrontendModule.class})
interface MyComponent{
MyWidget myWidget();
@Component.Builder
interface Builder{
Builder backendModule(BackendModule bm);
Builder frontendModule(FrondendModule fm);
@BindInstance
Builder foo(Foo foo);
MyComponent build();
}
}
- @Component.Factory 애노테이션은 컴포넌트 타입내에 선언되어야 한다.
- 컴포넌트 팩토리는 컴포넌트 타입 또는 컴포넌트의 슈퍼타입을 반환하는 하나의 추상 메서드만 존재해야 한다 (ex newInstance(..))
- 팩토리 메서드에는 @Component 애노테이션에 modules,dependencies로 지정된 속성들을 반드시 매개변수로 가져야 한다.
- 메서드에 @BindsInstance 애노테이션이 붙은 매개 변수는 해당 컴포넌트에 인스턴스를 넘겨 바인드시킨다.
생성되는 컴포넌트 타입에는 factory()라는 정적 메소드를 갖는데 팩토리 인스턴스를 반환한다. 이 팩토리 인스턴스를 초기화 할수 있다.
@Component(modules = {BackendModule.class,FrontendModule.class})
interface MyComponent{
MyWidget myWidget();
@Component.Factory
interface Factory{
MyComponent newMyComponent(
BackendModule bm,
FrontendModule fm,
@BindsInstance Foo foo);
}
}
Lazy타입 또는 Provider 타입을 사용한다면 상황에 따라 의존성 주입의 시점을 늦추거나 새로운 객체를 요청할 수도 있다.
객체가 초기화 되는데 시간이 필요하다면 Lazy주입을 고려해 볼 수 있다.
방법은 간단하다 바인드 된 타입 <T>
을 제네릭을 갖는 Lazy<T>
타입을 만들면 된다.
Lazy<T>
의 get()메서드를 호출하기까지는 객체가 초기화 되는것을 늦출 수 있다.
@Component(modules = CounterModule.class)
public interface CounterComponent{
void inject(Counter counter);
///////
@Module
public class CounterModule{
int next=100;
@Provides
Integer provideInteger(){
System.out.println("computing...");
return next++;
}
///////
public class Counter{
@Inject
Lazy<Integer> lazy;
public void printLazy(){
System.out.println("printing...");
System.out.println(lazy.get());
System.out.println(lazy.get());
System.out.println(lazy.get());
}
}
////////
@Test
public void testLazy(){
CounterComponent component = CounterComponent.create();
Counter counter = new Counter();
component.inject(counter);
counter.printLazy();
}
실행 결과
printing...
computing...
100
100
100
매번 새로운 인스턴스를 주입받고 싶다면 Provider<T>
를 고려해 볼수 있다.
public class Counter{
@Inject
Provider<Integer> provider;
public void printProvider(){
System.out.println("printing...");
System.out.println(lazy.get());
System.out.println(lazy.get());
System.out.println(lazy.get());
}
}
@Test
public void testProvider(){
CounterComponent component = CounterComponent.create();
Counter counter = new Counter();
component.inject(counter);
counter.printProvider();
}
실행결과
printing...
computing...
100
computing...
101
computing...
102
Provider.get()호출시 새로운 객체를 생성하기에 computing... 문구와 카운트가 1씩 증가하는 결과를 나타낸다.
만약 Component가 Singletone 과 같은 특정 범위로 지정되었다면 Provider<T>
를 사용한다고 하더라도 바인드된 의존성은 싱글턴으로 관리되어 같은 인스턴스를 제공받는다.
때로는 반환형으로 바인드된 객체를 식별하기에 충분하지 않을 수 있다. 예를들어 하나의 컴포넌트에 바인드되어서 String을 반환하는 @Provides메서드가 두개이상인 경우 dagger에서 어느쪽으로 바인드할지 몰라 에러를 발생한다. 이를 방지하기 위해 나온것이 @Named 애노테이션의 멤버값에 식별가능한 문자열을 넣기만 하면 된다.
@Component(modules=MyModule.class)
public interface MyComponent{
void inject(MyClass myclass)
}
@Module
public class MyModule{
@Provides
@Named("hello")
String provideHello(){
return "Hello";
}
@Provides
@Named("World")
String provideWorld(){
return "World";
}
}
public class MyClass{
@Inject
@Named("hello")
String strHello;
@Inject
@Named("World")
String strWorld;
public String getStrHello(){
return strHello;
}
public String getStrWorld(){
return strWorld;
}
}
@Test
public void myComponent(){
MyComponent mycomponent = DaggerMyComponent.create();
MyClass myclass = new MyClass();
mycomponent.inject(myclass);
System.out.println("strHello :"+myclass.getStrHello()+
"strWorld:"+myclass.getStrWorld());
}
실행 결좌
Hello
World
@Named 가 아닌 고유 한정자를 만들때 @Qualifier를 사용해 직접 한정자를 만들 수 있다.
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface Hello{
}
////
@Component(modules = MyModule.class)
public interface MyComponent{
void setMyClass(MyClass myClass)
}
/////
@Module
public class MyModule{
@Provide
@Hello
Stirng provideHello(){
return "hello";
}
@Provide
String provideWorld(){
return "world";
}
}
/////
public class MyClass{
@Inject
@Hello
Stirng strHello;
@Inject
String strWorld;
String getStrHello(){
return strHello;
}
String getStrWorld(){
return strWorld;
}
}
/////
@Test
public void myComponent(){
MyComponent mycomponent = DaggerMyComponent.create();
MyClass myclass = new MyClass();
mycomponent.setMyClass(myclass);
System.out.println(myclass.getStrHello() +" ~"+myclass.getStrWorld());
}
실행 결과
Hello ~ World