Android

의존성 주입 - Dagger, 모듈과 컴포넌트

컴퍼 2020. 8. 17. 21:21

Review

  1. Module은 Component에 연결되어 의존성 객체를 생성한다.
  2. Component는 연결된 Module을 이용하여 의존성 객체를 생성하고, 요청받은 인스턴스에 생성한 객체를 주입한다.

모듈 : class, 컴포넌트에 의존성을 제공함, 컴포넌트에 연결, @Module은 의존성을 제공할 클래스에 붙이고, @Provides는 의존성을 제공하는 메서드에 붙임. 주입할 내용이 여기에 작성됨.

컴포넌트 : interface, 바인드(연결)된 모듈을 이용해 의존성 객체를 생성, @Component에서 어떤 모듈과 바인드(연결)될지 설정할 수 있음[ ex, @Component(modules = MyModule.class) ]

결과 : Activity에서

MyComponent myComponent = DaggerMyComponent.create(); 로 myComponent를 생성

myComponent.getString(); 를 통해서 값을 불러옴

MyModule.java

package kr.hcjung;

import dagger.Module;
import dagger.Provides;

@Module // 의존성을 제공하는 클래스에 붙임
public class MyModule {
    @Provides // 의존성을 제공하는 메서드에 붙임
    String provideHelloWorld(){
        return "Hello World : 의존성 주입 성공!";
    }
}

MyComponent.java

package kr.hcjung;

import dagger.Component;

//MyComponent 인터페이스 내에는 제공할 의존성들을 메서드로 정의해아 함

//@Component 에 참조된 모듈 클래스로부터 의존성을 제공받음
//여기서는 (modules = MyModule.class)
@Component(modules = MyModule.class)

//Component 메서드의 반환형을 보고 모듈과 관계를 맺으므로,
//바인드된 모듈로부터 해당 반환형을 갖는 메서드를 찾지 못하면 컴파일 타임 에러 발생
//여기서는 MyModule 의 String provideHelloWorld() 함수의 return "Hello World"; 에서 String 반환형을 찾음
public interface MyComponent {
    String getString(); //프로비전 메서드, 바인드된 모듈로부터 의존성을 제공
}

MainActivity.java

package kr.hcjung;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyComponent myComponent = DaggerMyComponent.create();
        TextView tv1 = findViewById(R.id.tv1);
        tv1.setText(myComponent.getString());

    }
}

모듈(Module)

모듈 : class, 컴포넌트에 의존성을 제공함, 컴포넌트에 연결, @Module은 의존성을 제공할 클래스에 붙이고, @Provides는 의존성을 제공하는 메서드에 붙임. 주입할 내용이 여기에 작성됨

프로바이더(Provider)

모듈 클래스 내에 선언되는 메서드에는 @Provides 애노테이션을 붙이는 것으로 컴파일 타임에 의존성을 제공하는 바인드 된 프로바이더를 생성한다.

메서드의 반환형을 보고 컴포넌트 내에서 의존성이 관리되므로 중복되는 타입이 하나의 컴포넌트 내에 존재하면 안된다. (무엇을 반환해야 할 지 모호해 지므로 에러가 발생한다)

컴포넌트 내 바인드된 메서드의 반환형(여기선 String : "Comper", int : 100)은 @Provides 메서드의 매개변수로 사용할 수 있다.

@Module
public class MyModule {
        @Provides
        String provideName() {
                return "Comper"
        }

        @Provides
        int provideAge() {
                return 100;
        }

        @Provides
        Person providePerson(String name, int age) {
                return new Person(name, age);
        }
}

의문 : 그럼 매개변수는 자료형별로 하나밖에 제공하지 못하나?, Person의 구조는 다음과 같이 작성하기만 해도 되는것인가?

Null의 비허용

@Provides 메서드는 null을 반환하는 것을 기본적으로 제한한다. 그러므로 @Provides 메서드에서 null을 반환하는 경우 컴파일 타임에 NullPointerException을 발생시킨다.

@Provides 메서드의 반환값이 null인 것을 명시적으로 허용하려면 메서드에 @Nullable을 추가해야 한다. 의존성을 주입받는 부분에도 마찬가지로 @Nullable을 추가해야 한다.

@Module
public class MyModule {
        ...
        @Provides
        @Nullable
        Integer provideInteger(){
                return null;
        }
}
@Component(modules = MyModule.class)
public class MyComponent {
        @Nullable
        Integer getInteger();
}

모듈의 상속

@Module 애노테이선이 가질 수 있는 속성 중 includes 라는 것이 있다. includes에 다른 모듈 클래스들의 배열을 정의하는 것만으로 @Provides 메서드의 상속이 가능하다. 예를 들어 ModuleA와 ModuleB가 존재하고, ModuleB가 ModuleA를 상속하는 코드는 다음과 같다.

@Module
public class ModuleA {
        @provides
        A provideA() {
                return new A();
        }
}
@Module(includes = ModuleA.class)
public class ModuleB(){
        @Provides
        B provideB(){
                return new B();
        }
}

컴포넌트를 선언할 때 ModuleB를 참조하는 경우 ModuleA를 상속해 A 타입의 객체도 바인딩된다. 주의해야할 점은 모듈 간 상속을 할 떼 중복되는 타입이 존재하면 안된다는 것이다.

컴포넌트

컴포넌트 : interface, 바인드(연결)된 모듈을 이용해 의존성 객체를 생성, @Component에서 어떤 모듈과 바인드(연결)될지 설정할 수 있음

컴포넌트는 바인딩된 모듈로부터 오브젝트 그래프를 생성하는 핵심적인 역할을 한다. @Component 사용을 통해 컴포넌트를 생성할 수 있으며, @Component 애노테이션은 interface 또는 abstract 클래스에만 붙일 수 있다.

@Component(modules = MyModule.class)
public interface MyComponent {
    String getString(); //프로비전 메서드, 바인드된 모듈로부터 의존성을 제공
}

컴파일 타임의 애노테이션 프로세서에 의해 생성된 클래스는 접두어 'Dagger'와 @Component가 붙은 클래스 이름이 합쳐진 형식의 이름을 갖는다.

MyComponent myComponent = DaggerMyComponent.create();
TextView tv1 = findViewById(R.id.tv1);
tv1.setText(myComponent.getString());

@Component 속성으로 modules와 dependencies가 있다. 앞에서 살펴보았듯이 modules에는 컴포넌트에 바인드되는 @Module이 지정된 클래스 배열을 선언한다.

모듈이 다른 모듈을 포함하는 경우 컴포넌트에 선언된 모듈뿐만 아니라 포함된 모듈도 컴포넌트에 구현될 수 있도록 해야한다. dependencies에는 컴포넌트에 다른 컴포넌트의 의존성을 사용하는 경우 클래스 배열을 선언한다.

오브젝트 그래프

Dagger에서는 컴포넌트, 모듈, 객체 등의 관계를 컨테이너 또는 오브젝트 그래프라고 한다. 짧게 표현해서 그래프라고 하는 경우도 있다. Hello World 예제에 대한 오브젝트를 다음과 같이 도식화할 수 있다.

컴포넌트 메서드

@Component가 붙은 모든 타입은 최소한 하나의 추상적인 메서드를 가져야 한다. 메서드의 이름은 상관없지만, 메서드 매개 변수와 반환형은 규칙을 엄격하게 따라야 한다. 이렇게 정해진 규칙에 따라 프로비전 메서드와 멤버-인젝션 메서드로 구분된다.

프로비전 메서드(Provision methods)

Dagger의 컴포넌트에서 매개변수를 갖지 않으면서 반환형은 모듈로부터 제공되거나 주입되는 메서드를 프로비전 메서드라고 칭한다.

@Component(modules = SomeModule.class)
public interface SomeComponent {
        SomeType getSomeType();
}

getSomeType()메서드를 호출하면 SomeModule로 부터 제공받거나 주입받은 SomeType 객체를 반환한다.

멤버-인젝션 메서드(Member-Injection mothods)

Dagger의 컴포넌트에서는 하나의 매개변수를 갖는 메서드를 멤버-인젝션 메서드라고 칭한다.

멤버-인젝션 메서드는 void를 반환하거나 빌더 패턴처럼 메서드 체이닝이 가능한 메서드를 만들기 위해 매개변수 타입을 반환형으로 갖는 메서드로 선언할 수 있다.

다음과 같은 멤버-인젝션 메서드를 컴포넌트 내에 선언할 수 있다.

@Component(modules = SomeModule.class)
public interface SomeComponent {
        void injectSomeType(SomeType someType);
        SomeType injectAndReturnSomeType(SomeType someType);
}

앞의 Hello World 예제를 조금 수정하여 멤버-인젝션 메서드를 구현해 본다. 먼저 의존성을 주입받도록 MyClass를 만든다. 의존성 주입을 받을 필드에 @Inject 애노테이션을 붙인다.

MyClass.java

package kr.hcjung;

import javax.inject.Inject;

public class MyClass {
    @Inject
    String str;

    public String getStr(){
        return str;
    }
}

MyComponent.java

package kr.hcjung;

import dagger.Component;

//MyComponent 인터페이스 내에는 제공할 의존성들을 메서드로 정의해아 함

//@Component 에 참조된 모듈 클래스로부터 의존성을 제공받음
//여기서는 (modules = MyModule.class)
@Component(modules = MyModule.class)

public interface MyComponent {
    void inject(MyClass myClass);
}

MainActivity.java

package kr.hcjung;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyClass myClass = new MyClass();
        String str = myClass.getStr();
        TextView tv1 = findViewById(R.id.tv1);
        tv1.setText(str);

        MyComponent myComponent = DaggerMyComponent.create();
        myComponent.inject(myClass);
        str = myClass.getStr();
        TextView tv2 = findViewById(R.id.tv2);
        tv2.setText(str);

    }
}

MyComponent.inject(MyClass) 메서드를 호출하기 전에는 myClass 의 필드가 null이었다가 메서드 호출 이후에는 "hello world"가 주입된 것을 확인할 수 있다. 멤버-인젝션 메서드에 의해 필드 주입이 일어난 결과다.

 

출처 | 아키텍처를 알아야 앱 개발이 보인다, 옥수환 저