Introduction

Koin은 코틀린 개발자를 위한 실용적인 API제공을 하는 경량화된 의존성 주입 프레임워크입니다.
(자바 개발자 또는 의존성 주입에 대해서 알고 싶다면 Dagger 포스트를 참조해주세요) 

순수 코틀린으로만 작성되어있으며 프록시, 애노테이션 프로세싱을 통한 코드 생성, 리플렉션을 사용하지 않기 때문에 가볍습니다.

Android Gradle설정(https://github.com/InsertKoinIO/koin)

def koin_version="0.9.3" // 최신버전은 위의 github링크 참조
implementation "org.koin:koin-android:$koin_version"

왜 DSL을 사용했을까?

코틀린에서는 DSL을 제공하기때문에 애노테이션을 통한 코드 생성 대신 DSL을 활용하여, 의존성을 주입을 하기위한 똑똑하고 실용적인 API를 만들어낼 수 있습니다.

Koin DSL

Koin에서 사용하는 DSL 키워드는 다음과 같습니다

  • applicationContext – Koin 모듈 생성
  • factory -인스턴스 제공 (매번 생성)
  • bean – 싱글톤 인스턴스 제공
  • bind – 주어진 인스턴스와 추가적인 타입을 바인딩 할 때 쓰임
  • get – 컴포넌트내에서 알맞은 의존성을 주입받음
  • getProperty – 속성을 가져옴
  • context – 논리적 context를 선언한다.

모듈생성하기

Koin모듈은 applicationContext함수를 이용하여 생성할 수 있으며 모든 컴포넌트를 한번에 정의 가능합니다.

val myModule = applicationContext {
    //이곳에서 의존성주입을 위한 컴포넌트 생성을 합니다
}

bean또는 factory 함수를 이용하여 컴포넌트를 정의할수 있습니다.

  • bean – 싱글톤 생성( 인스터스를 한개만 생성)
  • factory – 매번 인스턴스 생성

싱글톤을 만들어봅시다

bean키워드를 사용하여 아래와 같이 정의 할 수 있습니다.

class Repository()

val myModule = applicationContext {
    //Repository라는 단일 인스턴스를 만듭니다.
    bean { Repository() }
}

객체생성과 함께 Koin이 이 객체를 싱글톤으로 관리하게 됩니다.

factory키워드로 컴포넌트 만들기

class Controller()

val myModule = module {
    // 의존성 주입을 할 때마다 Controller객체를 생성합니다
    factory { Controller() }
}

factory키워드를 사용할 시 Koin은 의존성 주입이 일어날때마다 객체를 생성하며, 해당 객체를 보관 하지 않습니다.

만든 컴포넌트를 이용해서 의존성 주입해보기

모듈에서 만든 컴포넌트를 주입하기위해서는 get()함수만 기억하면 됩니다.  예제를 확인해보겠습니다.

class Repository()
class MyPresenter(val repo : Repository)

val myModule = applicationContext {
    // 싱글톤으로 만들어진 Repository객체
    bean { Repository() }
    // get()으로부터 Repository객체를 주입받아 MyPresenter를 생성한다.
    bean { MyPresenter(get()) }
}

Repository객체를 싱글톤으로 만들어두었기 때문에 get()함수를 호출하면 타입추론을 통해 이미 생성된 객체를 참조하게 됩니다.

인터페이스 바인딩하기

bean 또는 factory는 람다식내에서 사용한 타입에 의해 타입이 결정됩니다.  일치하는 타입이 있을 경우에 주어진 객체를 주입하는데, 인터페이스를 타입으로 하는 객체를 주입 하려는 경우 이를 상속한 구현체를 주입할수도 있습니다. 다음 예제를 확인해보도록 하겠습니다.

//인터페이스 정의
interface Repository{
    fun loadData()
}

// Repository 인터페이스 구현체 
class RepositoryImp() : Repository{
    fun loadData() { ... }
}

Koin 모듈내에서 as 키워드를 통해 다음과 같이 인터페이스 구현체를 바인딩을 할 수 있습니다

val myModule = module {
    // RepositoryImp객체 싱글톤으로 생성하므로 Repostiory에는 매칭이 되지 않음
    bean { RepositoryImp() }

    // Repository 인터페이스로 바인딩
    bean { RepositoryImp() as Repository }

}

몇몇 경우에 한해서 여러가지 타입을 하나의 정의로 매칭 시키길 원할 때가 있습니다. 그럴 때 어떻게 하는지 살펴보도록 하겠습니다.

// Repository 인터페이스
interface Repository{
    fun doSomething()
}

// Repository 구현체
class RepositoryImp() : Repository{
    fun doSomething() { ... }
}

이런 경우 bind연산자를 통해 추가적인 타입을 바인딩 할 수도있습니다.

val myModule = module {
    bean { RepositoryImp() } bind Repository::class
}

컴포넌트 타입이 중첩되는경우 이름으로 구분하기

같은 타입인데 서로 다르게 의존성이 주입되어야 하는 경우가 있습니다. Dagger에서는 이런경우 Named 애노테이션으로 구분하죠. Koin에서는 bean이나  factory의 인자로 이름을 넣어 주면 됩니다. 예제를 확인하겠습니다.

val myModule = module {
    bean("default") { "Hello World" }
    single("test") { "My name is Charlezz" }
}

val str : String by inject(name = "default")

위의 경우 Koin내에서 String 객체를 싱글톤으로 두가지 관리하나 실제로 주입되는 객체는 name을 key 주입되기때문에 Hello World가 str에 초기화 되게 됩니다.

get()을 이용한 의존성 주입하기

Koin은 get()을 호출하는 것만으로 적당한 객체를 찾아 주입하게 됩니다. 

class ComponentA()
class ComponentB(val componentA : ComponentA)

val myModule = applicationContext {
   bean { ComponentA() }
   bean { ComponentB(get()) }
}

위의 예제를 보면 ComponentB의 생성자에 대한 매개변수로 ComponentA를 받고 있습니다. 이때 미리 정의해둔 ComponentA객체가 있다면 생성자에 get()을 호출하여 간단히 객체를 주입할 수 있습니다.

다수의 모듈 사용하기

Koin에서는 모든 모듈에대한 정의는 늦은 초기화(lazy init)가 기본값입니다. 그렇기 때문에 사용자는 필요로하는 여러 모듈들을 조합해서 사용하는것이 가능합니다. 아래의 예제를 확인해 보겠습니다.

class ComponentA()
class ComponentB(val componentA:ComponentA)
val myModule1 = applicationContext {
   bean { ComponentA() }
}
val myModule2 = applicationContext {
   bean { ComponentB(get()) }
}

다음과 같이 myModule1모듈과 myModule2모듈을 분리하여 정의하여도 상호간에 조합 or 참조가 가능합니다.

startKoin(this, listOf(myModule1, myModule2)) //다수의 모듈 사용

Conclusion

Dagger2와 비교했을때 확실히 가볍다는 느낌을 받았으며, 사용법 또한 쉬웠습니다. 또한 아키텍처 컴포넌트의 ViewModel을 이용하기 위해 별도의 라이브러리도 제공하고 있어 손쉽게 MVVM패턴을 만들 수 있었습니다. 하지만 아직 UIController별 Scope관리 라던지 Compile시간에 오류를 확인하는 등의 기능은 Dagger사용자로써는 아쉬움이 남는 부분입니다. (유닛테스트를 통해 Runtime에 에러가 발생하는 것을 방지할 수는 있습니다)

1.0버전이 아직 나오지 않은 프레임워크라 더욱 기대가 되긴 하지만 현재로서는 Dagger2의 자리를 대체하기는 힘든 것으로 보입니다.

 

카테고리: Kotlin

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.