안드로이드에서 Koin과 더불어 DI (Dependency Injection)시에 가장 많이 사용되는 툴이다. Koin은 Kotlin에서만 사용할 수 있는 반면에 Dagger2는 Java와 Kotlin 둘 다에서 사용될 수 있다는 장점이 있다. 대신 Koin에 비해 learning curve가 높은 편이고 코드도 Koin이 더 간결하다고 한다.
확실하게 이해하기 위해서는 실제로 사용해보는 방법밖에 없다고 생각해서 간단한 예제를 찾아보던 중 유튜브에서 괜찮은 튜토리얼 플레이리스트를 찾아서 따라해보면서 나름대로 정리 해보려한다.
https://www.youtube.com/playlist?list=PLrnPJCHvNZuA2ioi4soDZKz8euUQnJW65
Dagger2에서 의존성을 주입하는 데에는 여러가지 방법이 있는데 대표적으로 Constructor Injection, Provision Methods, Field Injection, Provides 등을 사용하는 방법이 있다.
컴포넌트는 Injector라고도 불리는데 Dagger2에서 객체를 만들어내고 저장하고 제공하는 역할을 한다. 컴포넌트는 클래스가 아닌 Interface로 선언해야한다.
컴포넌트에서 매개변수에 아무 값이 없이 반환타입이 클래스인 메소드를 Provision Method라고 한다. 이 메소드를 컴포넌트에 선언해주면 Dagger2에서 알아서 해당 객체를 반환하는 메소드를 만들어준다.
Dagger2에서 위의 Provision Methods 처럼 객체를 만들어내는 방법을 알아야할때 컨스트럭터에 @Inject 어노테이션을 사용할수 있다.
@Component
public interface CarComponent {
Car getCar(); // Provision Method
}
public class Car {
private static final String TAG = "Car";
private Engine engine;
private Wheels wheels;
// Provision Method에서 Car 객체를 만들어내기 위해 필요한 컨스트럭터
@Inject
public Car(Engine engine, Wheels wheels) {
this.engine = engine;
this.wheels = wheels;
}
public void drive() {
Log.d(TAG, "driving...");
}
}
// Car 객체를 만들어내기 위해 Wheels와 Engine 객체도 필요하기 때문에 둘다 @Inject를 붙혀준다
public class Wheels {
@Inject
public Wheels(){}
}
public class Engine {
@Inject
public Engine(){}
}
// MainActivity에서 사용 예
public class MainActivity extends AppCompatActivity {
private Car car;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
CarComponent component = DaggerCarComponent.create();
car = component.getCar();
car.drive();
}
}
// 실제 Dagger2에서 Car 객체 생성을 위해 만들어준 코드
@Override
public Car getCar() {
return new Car(new Engine(), new Wheels());}
하지만 실제로 안드로이드 개발에서 사용하는 Activity 같은 구성요소들은 개발자가 생성하는게 아니라 Android Framework에서 생성해주기 때문에 Contructor Inejction과 Provision Methods를 사용한 방법은 적절하지 않다. 이러한 이슈를 해결할 수 있는 방법이 Field Injeciton 이다.
변수에 @Inject 어노테이션을 붙혀서 바로 의존성 주입을 해주는 방법이다. 이때 어떤 액티비티 내에서 Field Injection이 일어나야하는지 Dagger2에게 알려줘야되기 때문에 컴포넌트에 inject 메소드를 선언해줘야 한다.
public class MainActivity extends AppCompatActivity {
@Inject // Field Injection을 사용하는 객체는 접근제어자가 최소 package level이어야 한다
Car car;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
CarComponent component = DaggerCarComponent.create();
component.inject(this); // Dagger2에게 MainActivity에서 Field Injection이 필요하다는 걸 알려준다
car.drive();
}
}
@Component
public interface CarComponent {
void inject(MainActivity activity);
}
객체가 완전히 생성된 후에 객체 내의 메소드를 불러줘야 할때 사용된다
public class Car {
private static final String TAG = "Car";
private Engine engine;
private Wheels wheels;
@Inject
public Car(Engine engine, Wheels wheels) {
this.engine = engine;
this.wheels = wheels;
}
@Inject
public void connectRemote(Remote remote) {
remote.setRemote();
}
public void drive() {
Log.d(TAG, "driving...");
}
}
public class Remote {
private static final String TAG = "Remote";
@Inject
public Remote() {}
public void setRemote() {
Log.d(TAG, "Remote Connected");
}
}
public class MainActivity extends AppCompatActivity {
@Inject
public Car car;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
CarComponent component = DaggerCarComponent.create();
component.inject(this);
car.drive();
}
}
결과
D/Remote: Remote Connected
D/Car: driving...
위에서 언급한 의존성 주입 방법들은 모두 개발자 본인이 직접 작성한 클래스에서만 사용 가능하다. 외부 라이브러리에서 가져온 객체의 의존성을 주입하기 위해서 사용되는 개념이 Module과 Provider Methods이다.
Module안에서 주입하고자 하는 객체의 Provider Methods를 정의한다.
public class Wheels {
private Rims rims;
private Tires tires;
public Wheels(Rims rims, Tires tires){
this.rims = rims;
this.tires = tires;
}
}
public class Tires {
private static final String TAG = "Tires";
void fillAir() {
Log.d(TAG, "filling air to tires...");
}
}
public class Rims {
}
@Module
public class WheelsModule {
@Provides // Provder Method를 정의하는 어노테이션
static Rims provideRims() { // instance가 따로 필요하지 않을때는 static을 사용해서 최적화한다
return new Rims();
}
@Provides
static Tires provideTires() {
Tires tires = new Tires();
tires.fillAir(); // 객체를 반환하기 전에 해당 객체의 메소드를 사용할수 있다
return tires;
}
@Provides
static Wheels provideWheels(Rims rims, Tires tires) {
return new Wheels(rims, tires);
}
}
@Component(modules = WheelsModule.class) // 컴포넌트에 모듈을 포함시켜 준다
public interface CarComponent {
void inject(MainActivity activity);
}
@Binds 어노테이션은 기본적으로 @Provides와 같은 기능을 수행한다. 다만 퍼포먼스적인 면에서 @Binds를 통해 생성되는 코드의 양이 @Provides보다 훨씬 적고 효율적이여서 가능하면 @Provides 보다는 @Binds를 사용하는 게 더 좋다. 실제로 @Provides를 사용하게 되면 모든 Provider Method마다 Factory class를 생성해서 굉장히 많은 양의 코드를 생성한다.
하지만 @Binds는 매개변수로 반환타입으로 어싸인 가능한 단 한개의 변수만 허용하기 때문에 사용함에 있어 @Provides 보다는 제한적이다. 그래서 @Provides를 사용할때 조금 더 최적화를 위해서 고려해볼만한 방법으로 static 제어자를 사용하는 방법이 있다.
@Provides 를 사용했을 때 생성되는 Factory classes
public interface Engine {
void start();
}
public class DieselEngine implements Engine {
private static final String TAG = "DieselEngine";
@Inject
public DieselEngine() {}
@Override
@Inject
public void start() {
Log.d(TAG, "engine start...");
}
}
@Module
public abstract class DieselEngineModule {
@Binds
abstract Engine bindEngine(DieselEngine engine);
// 매개변수로 Engine을 implement하는 DieselEngine 변수 하나만 들어있고 abstract 메소드로 선언되어 있다.
}
결과
D/DieselEngine: engine start...
D/Tires: filling air to tires...
D/Remote: Remote Connected
D/Car: driving...