[Android] DI, Dagger, Koin

kimji1
8 min readJul 27, 2020

--

안드로이드에서 흔히 사용되는 DI 라이브러리인 Dagger, Koin을 알아보기 전에 의존성이 무엇인지와 의존성 주입, 이와 유사한 ServiceLocator에 대해서 살펴볼 것이다.
먼저 DI와 ServiceLocator에 대해 이해한 후 Dagger와 Koin의 공식 홈페이지에 나온 간단한 예제를 보며 사용법을 익히고, 이 두가지의 장단점을 비교해보려 한다.

DI

의존성

어떤 클라이언트 클래스에서 서비스 클래스를 사용할 때, 구체(Concrete) 클래스의 직접적인 언급을 통해 사용하면 클래스 사이에 관계(결합)가 만들어진다고 한다.

특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현하고 있는 인터페이스를 사용하여 전달받으면, 그 클래스의 오브젝트를 받아 사용할 수 있다. 오브젝트 사이의 관계는 클래스 사이의 관계와 다르게 느슨한 연결 고리를 갖도록 할 수 있다.

서비스 클래스의 변화가 클라이언트 클래스도 변경되어야 하는 경우 클라이언트 클래스가 서비스 클래스에 의존성을 갖는다고 한다. 인터페이스를 통해 전달받는 느슨한 연결 고리를 갖는 경우, 서비스 클래스의 구현이 변경될 때 받는 영향이 줄어 “결합도가 낮다”고도 한다.

제어의 역전

일반적인 제어 흐름이라고 하면, 프로그램이 시작되는 지점에서 다음에 사용할 오브젝트를 결정하고, 결정한 오브젝트를 생성하고, 만들어진 오브젝트에 있는 메소드를 호출하고, 그 오브젝트 메소드 안에서 다음에 사용할 것을 결정하고 호출하는 식의 작업이 반복된다.

제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 당연히 생성하지도 않는다. 또 자신도 어떻게 만들어지고 어디서 사용되는지를 알 수 없다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다. 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다.

의존성 주입

의존성 주입이란 다음과 같은 세 가지 조건을 만족하는 작업을 말한다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

얻게 되는 이점

  • client를 더 독립적으로 만들어 유닛 테스트가 쉬워진다.
  • client 객체 변경의 유연하다.
  • provider component가 의존성을 관리하기 때문에 boilerplate code를 줄일 수 있다.
  • 결합도를 줄일 수 있다.

ServiceLocator

  • D.I와 매우 유사한 기능을 제공하는 패턴 (IoC 를 가능하게 한다는 점에서)
  • 특정 작업을 수행하는데 필요한 정보를 반환하는 ServiceLocator라는 중앙 레지스트리를 사용하는 패턴이다.
  • 안티 패턴이라는 의견이 있는 패턴이다.
    (service locator 패턴에 대해 자세히 설명된 링크를 첨부한다.)

이제부터 Android 에서 사용되는 DI Framework인
Dagger와 Koin 에 대해서 알아보고자 한다.

Dagger

@Inject

Dagger가 클래스의 인스턴스를 만들어야 하는 생성자 / 필드에 사용되며,
새 인스턴스가 요청되면 Dagger는 요구되는 파라미터를 얻어 생성자를 호출하거나 필드를 얻어준다.
만약, @Inject val someClass : SomeClass 와 같이 필드에 inject annotation을 붙여줬으나, SomeClass의 생성자에 inject 어노테이션이 붙어있지 않으면 필드는 만들어지지만 새 인스턴스를 할당받지는 못한다.

class Thermosiphon implements Pump {
private final Heater heater;

@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}

...
}
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;

...
}

Inject annotation은 다음과 같은 경우에 사용할 수 없다.

  • 인터페이스
  • third-party class

이러한 경우 사용할 수 있는게 @Provide 이다.

// 1
@Provides static Heater provideHeater() {
return new ElectricHeater();
}
// 2
@Provides static Heater provideHeater(ElectricHeater heater) {
return heater;
}
// 3 : 2와 같은 경우 heater에 alias를 설정해줄 뿐이다.
// 이러한 경우에 한정하여 아래와 같이 표현할 수 있다.
@Binds abstract Heater bindHeater(ElectricHeater impl);

모든 @Provide 메서드는 @Module에 속해야 한다.

@Module
interface HeaterModule {
@Binds abstract Heater bindHeater(ElectricHeater impl);
}

위의 예는 naming convention에 맞춰 짜여진 건데,
@Module이 붙은 경우 suffix로 Module을 붙여주며,
@Provide가 붙는 경우 prefix로 provide를,
@Bind가 붙는 경우 prefix로 bind를 붙여준다.

@Inject와 @Provide 어노테이션 클래스는 그래프에서 클래스 사이의 의존 관계로 연결된 객체를 형성한다. @Component 어노테이션은 modules parameter에 module type을 넘기며, interface에 적용된다.

@Component(moudles = DripCoffeeModule.class) 
interface CoffeeShop{
CoffeeMaker maker();
}

Dagger가 구현한 CoffeeMaker이 inject 되는 CoffeeShop을 사용할 수 있는 CoffeeApp이 완성됐다.

public class CoffeeApp {
public static void main(String[] args) {
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
coffeeShop.maker().brew();
}
}

Koin

Koin은 Dagger처럼 Annotation 방식이 아닌 DSL 방식을 사용하여 DI를 구현할 수 있다.

먼저 Repository를 정의해준다.

interface HelloRepository {
fun giveHello(): String
}

class HelloRepositoryImpl() : HelloRepository {
override fun giveHello() = "Hello Koin"
}

Repository를 사용하는 Presenter를 만들어준다.

class MySimplePresenter(val repo: HelloRepository) {

fun sayHello() = "${repo.giveHello()} from $this"
}

module function을 사용하여 module을 정의해준다.

val appModule = module {

// single instance of HelloRepository
single<HelloRepository> { HelloRepositoryImpl() }

// Simple Presenter Factory
factory { MySimplePresenter(get()) }
}

application 클래스에서 startKoin() function을 호출해서 Koin을 사용할 수 있다.

class MyApplication : Application(){
override fun onCreate() {
super.onCreate()
// Start Koin
startKoin{
androidLogger()
androidContext(this@MyApplication)
modules(appModule)
}
}
}

by inject()로 inject 해줄 수 있다.

class MySimpleActivity : AppCompatActivity() {

// Lazy injected MySimplePresenter
val firstPresenter: MySimplePresenter by inject()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

//...
}
}

instance를 얻는 방식에는 by inject(), get() 두 가지 function이 있다.
by inject()는 안드로이드의 component의 실행 시점에 맞춰서 얻을 수 있고,
get()은 바로 instance를 얻을 수 있다.

Dagger vs Koin

Dagger

  • DI
  • compile-time 시 에러 발생, build time 이 오래 걸림
    runtime 에 빠르게 동작하고, 에러가 발생하지 않음
  • 자동적으로 동적 기능 모듈에 바인드

Koin

  • service locator pattern
  • build time은 짧지만
    runtime 시에 오버헤드나 에러 발생하므로 앱 실행 중 문제 발생할 수 있음
  • 수작업으로 모듈을 바인드

--

--

kimji1
kimji1

No responses yet