Dagger 와 같은 Dependency Injection(이하 DI)을 사용하는 것을 종종 볼 수 있다. 각 컴포넌트간의 의존성을 외부 컨테이너에서 관리하는 방식을 통해 코드 재사용성을 높이고 Unit Test도 편하게 할 수 있게 되는 장점을 가지고 있다. 최근에는 코틀린이 안드로이드 앱 개발 공식 언어가 되면서 코틀린의 다양한 언어적 장점을 이용한 DI 라이브러리가 생겨났고 그 중 하나가 Koin 라이브러리다.
사실 엄밀히 따져말하면 Koin은 DI 라이브러리라기 보단 Service Locator Pattern의 구현체라고 봐야한다. Service Locator Pattern 과 Dependency Injection Pattern은 약간의 차이가 있으며 개발자들간에 이를 바라보는 시각도 다양하다.
초기화
Koin의 초기화는 정말 간단하다. Application을 상속받은 클래스에 startKoin 함수를 호출하면서 미리 정의한 module들을 인자로 넘긴다.
class KoinApp: Application(){
override fun onCreate() {
super.onCreate()
startKoin(this, appModules)
}
}
모듈 정의
Koin에서는 오직 필요한 모듈들을 정의하고 필요한 곳에 by inject() 키워드를 통해 의존성을 주입하면 된다. 추가적인 component 나 subcomponent들은 필요없다. Koin은 모듈을 정의할때 kotlin dsl를 사용하여 좀 더 직관적으로 정의가 가능하다.
Koin에서 사용하는 dsl 키워드는 총 5가지가 있다.
1) module - Koin모듈을 정의할때 사용
2) factory - Dagger에서의 ActivityScope, FragmentScope와 유사한 기능으로 inject하는 시점에 해당 객체를 생성
3) single - Dagger에서의 Singleton 과 동일하며 앱이 살아있는 동안 전역적으로 사용가능한 객체를 생성
4) bind - 생성할 객체를 다른 타입으로 바인딩하고 싶을때 사용
5) get - 주입할 각 컴포넌트끼리의 의존성을 해결하기 위해 사용합니다.
val apiModule: Module = module {
single {
Retrofit.Builder()
.client(OkHttpClient.Builder().build())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(getProperty<String>("BASE_URL"))
.build()
.create(Api::class.java)
}
}
val photoListModule: Module = module {
factory {
PhotoPresenterImpl(get()) as PhotoPresenter
}
}
val appModules = listOf(apiModule, photoListModule)
코틀린은 이런 경우 따로 class 를 만들필요가 없기 때문에 위와 같이 단순히 모듈들을 변수로 정의했다. 여기에서 single, factory 키워드를 사용해 객체를 생성했다. 이렇게 생성할 경우 Retrofit api 객체는 전역적으로 한개의 객체만 생성이 가능하며 PhotoPresenter 객체는 생성한 액티비티 혹은 프래그먼트로 scope가 제한된다. Koin모듈 정의를 보면 get() 키워드를 사용하고 있다. 여기서 get()을 호출하면 apiModule 에 정의된 retrofit Api 클래스 객체가 넘어간다. Koin은 생성하고자 하는 객체의 인자 타입을 보고 의존성을 판단해 쉽게 컴포넌트간의 의존성을 해결한다. 실제로 PhotoPresenterImpl의 클래스 정의는 아래와 같다.
class PhotoPresenterImpl(val api: Api): PhotoPresenter() {
override fun requestPhoto(id: Long) {
//구현체
}
}
좀 더 쉽게 설명하자면 아래와 같은 의존성이 있는 클래스들이 있다면
class ComponentA()
class ComponentB(val componentA : ComponentA)
모듈 정의를 아래와 같이 하게 된다.
val moduleA = module {
// Singleton ComponentA
single { ComponentA() }
}
val moduleB = module {
// Singleton ComponentB with linked instance ComponentA
single { ComponentB(get()) }
}
의존성 주입
이제 사용한 모듈들을 정의했으니 사용하고 싶은 곳에 주입해보겠습니다. Koin에서 주입은 by inject() 키워드를 사용(Kotlin Delegated Properties 방식을 사용)합니다. 이는 Dagger에서의 @Inject 키워드와 유사하며 항상 지연초기화(lazy init)을 사용하게 됩니다. 즉 객체를 사용하는 시점에 생성을 하므로 성능상 이점이 있습니다.
class PhotoActivity : AppCompatActivity(), PhotoScene {
private val presenter: PhotoPresenter by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_photo)
presenter.scene = this
presenter.requestPhoto(1)
}
...
}
이 외에 ViewModel 주입을 더 편하게 해주는 koin-android-viewmodel 라이브러리도 있고 Scoping을 편하게 해주는 koin-android-scope 라이브러리도 제공을 하고 있다. Dagger와 비교했을때 실제 학습비용은 매우 낮지만 장단점이 있다. 판단은 각자의 몫이지만 좀더 가벼운 프로젝트를 시작할때 한번쯤 사용해보는것도 좋을 것 같다.