• Dependency Injection에 관한 내용은 이 글을 참고하자.
  • DI를 구현할 수 있는 방법 중 Koin에 대해서 알아보려고 한다.

Koin 이란?

Koin은 Android에서 주로 사용되는 경량화된 의존성 주입용 프레임워크이다. 순수 코틀린만으로 작성된 라이브러리이며 학습 곡선이 높은 Dagger에 비해 상대적으로 낮은 학습 곡선을 가지고 있다. 어노테이션 프로세싱 및 리플렉션을 사용하지 않기 때문에 상대적으로 가볍다.

더 자세한 내용은 Koin의 공식 사이트에서 확인할 수 있다.

  • 장점
    • 어노테이션 과정이 없으므로 컴파일이 빠르다.
    • 학습 곡선이 낮고, 설치도 간단하다.
  • 단점
    • 런타임 중 에러가 발생한다.
    • Dagger에 비해 런타임시 오버헤드가 발생할 확률이 높다.

Set Up

  • 최신 버전은 Koin Github을 참고하자.
  • 2020.07.06 기준으로 최신 버전은 '2.1.6’이다.
  • root의 build.gradle 파일에 버전을 정의한다.
1
koin_version = '2.1.6'
  • jcenter를 repositories 부분에 추가한다.
1
2
3
4
// Add Jcenter to your repositories if needed
repositories {
jcenter()
}
  • 그리고 app의 build.gradle 파일에 dependency를 설정한다.
1
2
3
4
5
6
7
8
9
10
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "org.koin:koin-gradle-plugin:$koin_version"
}
}

apply plugin: 'koin'
  • 마지막으로 아래와 같이 dependency를 필요에 따라 설정하면 된다.
1
2
3
4
5
6
7
8
// Koin for Kotlin
implementation "org.koin:koin-core:$koin_version"
// Koin extended & experimental features
implementation "org.koin:koin-core-ext:$koin_version"
// Koin for Unit tests
testImplementation "org.koin:koin-test:$koin_version"
// Koin for Java developers is now part of core
// implementation "org.koin:koin-java:$koin_version"

Start Koin

  • Koin은 DSL을 사용하여 프로젝트 의존성을 관리한다. main() 함수에서는 단순하게 아래처럼 시작할 수 있다.
1
2
3
4
5
6
7
8
9
fun main(args: Array<String>){
startKoin {
// 로그를 찍어볼 수 있다.
printlogger()

// 리스트로 된 모든 모듈을 가져온다.
modules(appModules)
}
}
  • Android에서도 쉽게 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
class MyApplication : Application(){
override fun onCreate(){
super.onCreate()
// start Koin!
startKoin {
// Android Context
androidContext(this@MyApplication)
// modules
modules(appModules)
}
}
}

Modules & Definitions

  • modules() 안에서 의존성을 관리하는 모듈에 대해 알아보자.
  • 아래처럼 Repository 클래스를 사용한다고 하자.
1
2
interface Repository
class MemoRepository : Repository
  • 그리고 아래처럼 간단하게 모듈을 생성할 수 있다.
1
2
3
val appModules = module {
single<Repository> { MemoRepository() }
}
  • module 안에 singleMemoRepository()를 정의했다.
  • Repository를 singleton으로 주입하되 그것의 구현체로 MemoRepository()를 주입한다는 것을 의미한다.
  • 이를 통해서 인터페이스를 통한 느슨한 결합을 정의할 수 있다.

[About Koin DSL]

  • module { } : koin 모듈 또는 하위 모듈을 정의할 때 사용한다.
  • factory { } : inject할 때마다 항상 새로운 인스턴스를 생성한다.
  • single { } : 싱글톤 타입으로 인스턴스를 생성한다.
  • get() : 타입 추론을 통해 컴포넌트 내에서 알맞은 의존성을 주입한다. (컴포넌트 종속성을 해결해줌)
  • named() : Enum이나 String으로 한정자를 정의해준다.
  • bind : 지정된 컴포넌트의 타입을 추가적으로 바인딩해준다.
  • getProperty() : 필요한 프로퍼티를 가져온다.
  • 예시를 위해 아래의 클래스 및 인터페이스를 사용한다고 해보자.
1
2
3
4
5
class MemoRepository()

interface ApiService
class MemoApiService(val repository: MemoRepository) : ApiService
class MemoHttpClient(val url: String)
  • koin을 사용하여 아래처럼 정의할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
val appModules = module {
// 싱글톤
single { MemoRepository() }

// factory로 정의하여 항상 새로운 인스턴스를 ApiService 타입으로 생성한다.
// get()을 통해 타입 추론을 사용하고 필요한 의존성을 해결(주입) [MemoRepository()]
factory<ApiService> { MemoApiService(get()) }

// 싱글톤
// "server_url"이라는 프로퍼티를 가져와서 사용한다.
single { MemoHttpClient(getProperty("server_url")) }
}
  • 또한, 모듈을 만든 후, 다른 모듈과 합쳐서 사용할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
val module1 = module {
single { MemoRepository() }
}

val module2 = module {
factory<ApiService> { MemoApiService(get()) }
}

// combine
startKoin {
modules(module1, module2)
}
  • 정의한 모듈을 다른 모듈에서 재정의하여 사용할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// definition level 재정의
val module1 = module {
single<ApiService> { MemoApiService(get()) }
}

val module2 = module {
// 현재 definition만 재정의할 수 있음
single<ApiService>(override = true) { DiaryApiService() }
}

// module level 재정의
val myModule1 = module {
single<ApiService> { MemoApiService(get()) }
}

// 모든 definitions를 재정의할 수 있음
val myModule2 = module(override = true) {
single<ApiService> { DiaryApiService() }
}

For Android

  • 이제 Android에서 사용하는 법을 알아보자.
  • 위의 방법을 사용하여 모듈을 생성하면 되는데, Android는 Context의 사용을 피할 수 없다. 파라미터로 받아서 사용하는 경우에는 Context를 주입해 줄 수 있다.
1
2
3
module {
single { MemoRepository(androidContext()) }
}
  • 이렇게 모듈에 선언한 컴포넌트들은 Activity 혹은 Fragment에서 간단하게 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
class MainActivity : AppCompatActivity() {
// 1. MemoRepository 주입
private val repository : MemoRepository by inject()

override fun onCreate(){
super.onCreate()

// 2. 다이렉트로 가져와서 사용할 수도 있다.
val memoRepository : MemoRepository = get()
}
}
  • ViewModel 또한 쉽게 주입받을 수 있다.
1
2
3
4
5
6
class MemoViewModel(val taskId: Int = 1, val repository: Repository){
...
}

interface Repository
class MemoRepository : Repository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val viewModelModule = module {
// get()을 통해 Repository를 가져온다.
viewModel { MemoViewModel(get()) }

// 싱글톤
single<Repository> { MemoRepository() }
}


val viewModelModule = module {
viewModel { (taskId: Int) ->
MemoViewModel(
taskId = taskId,
repository = get()
)
}

single<Repository> { MemoRepository() }
}
  • 정의한 모듈을 사용하여 의존성을 주입한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MainActivity : AppCompatActivity() {
// 전달한 파라미터가 없는 경우
// Lazy하게 MemoViewModel 주입
val viewModelWithoutParam : MemoViewModel by viewModel()

val viewModelWithParam : MemoViewModel by viewModel {
parameterOf(1)
}

override fun onCreate(){
super.onCreate()

// 다이렉트로 인스턴스 가져오기 without Parameter
val viewModelWithoutParam : MemoViewModel = getViewModel()

// 다이렉트로 인스턴스 가져오기 with Parameter
val viewModelWithParam : MemoViewModel = getViewModel {
parameterOf(1)
}
}
}
  • Fragment들에서는 Container 역할을 하는 Activity의 ViewModel을 공유하는 경우가 있다.
  • 뿐만 아니라, A Activity에서 B Activity의 ViewModel 객체를 사용해 데이터를 공유하는 경우도 있다.
  • 이런 경우, ViewModel 객체를 새롭게 생성한다면 서로 다른 객체이므로 데이터가 일관되지 않거나 원하는 케이스를 처리하지 못할 수 있다.
  • 또한, Memory Leak을 경험할 수도 있다. 둘의 생명주기가 다르기 때문이다.
  • Koin에서는 이런 것들도 고려한 DSL을 제공한다.(sharedViewModel)
1
2
3
4
5
6
7
8
9
10
11
class DetailActivity : AppCompatActivity(){
private val detailViewModel : DetailViewModel by viewModel()
}

class MyFragment: Fragment(){
// 내가 새롭게 만든 ViewModel 객체
private val fragmentViewModel : DetailViewModel by viewModel()

// DetailActivity에서 만들었던 ViewModel 객체를 사용
private val activityViewModel : DetailViewModel by sharedViewModel()
}

상당히 쉽고 간편하게 Dependency Injection을 사용할 수 있는 것이 Koin의 큰 장점이다. 하지만 성능적으로 Dagger에 미치지 못한다는 평이 많았는데, 2.0 버전에서 inject 성능이 비슷해질 정도로 많이 향상되었다고 한다.

Reference