Koin 이란?
Koin
은 Android에서 주로 사용되는 경량화된 의존성 주입용 프레임워크이다. 순수 코틀린만으로 작성된 라이브러리이며 학습 곡선이 높은 Dagger
에 비해 상대적으로 낮은 학습 곡선을 가지고 있다. 어노테이션 프로세싱 및 리플렉션을 사용하지 않기 때문에 상대적으로 가볍다.
더 자세한 내용은 Koin 의 공식 사이트에서 확인할 수 있다.
장점
어노테이션 과정이 없으므로 컴파일이 빠르다.
학습 곡선이 낮고, 설치도 간단하다.
단점
런타임 중 에러가 발생한다.
Dagger에 비해 런타임시 오버헤드가 발생할 확률이 높다.
Set Up
최신 버전은 Koin Github을 참고하자.
2020.07.06 기준으로 최신 버전은 '2.1.6’이다.
root의 build.gradle 파일에 버전을 정의한다.
jcenter를 repositories 부분에 추가한다.
1 2 3 4 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 implementation "org.koin:koin-core:$koin_version" implementation "org.koin:koin-core-ext:$koin_version" testImplementation "org.koin:koin-test:$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) } }
1 2 3 4 5 6 7 8 9 10 11 12 class MyApplication : Application (){ override fun onCreate () { super .onCreate() startKoin { androidContext(this @MyApplication ) modules(appModules) } } }
Modules & Definitions
modules() 안에서 의존성을 관리하는 모듈에 대해 알아보자.
아래처럼 Repository 클래스를 사용한다고 하자.
1 2 interface Repository class MemoRepository : Repository
그리고 아래처럼 간단하게 모듈을 생성할 수 있다.
1 2 3 val appModules = module { single<Repository> { MemoRepository() } }
module 안에 single
로 MemoRepository()
를 정의했다.
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) : ApiServiceclass MemoHttpClient (val url: String)
koin을 사용하여 아래처럼 정의할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 val appModules = module { single { MemoRepository() } factory<ApiService> { MemoApiService(get ()) } 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 ()) } } startKoin { modules(module1, module2) }
정의한 모듈을 다른 모듈에서 재정의하여 사용할 수도 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 val module1 = module { single<ApiService> { MemoApiService(get ()) } } val module2 = module { single<ApiService>(override = true ) { DiaryApiService() } } val myModule1 = module { single<ApiService> { MemoApiService(get ()) } } 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 () { private val repository : MemoRepository by inject() override fun onCreate () { super .onCreate() 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 { 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 () { val viewModelWithoutParam : MemoViewModel by viewModel() val viewModelWithParam : MemoViewModel by viewModel { parameterOf(1 ) } override fun onCreate () { super .onCreate() val viewModelWithoutParam : MemoViewModel = getViewModel() 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 (){ private val fragmentViewModel : DetailViewModel by viewModel() private val activityViewModel : DetailViewModel by sharedViewModel() }
상당히 쉽고 간편하게 Dependency Injection
을 사용할 수 있는 것이 Koin의 큰 장점이다. 하지만 성능적으로 Dagger에 미치지 못한다는 평이 많았는데, 2.0 버전에서 inject 성능이 비슷해질 정도로 많이 향상되었다고 한다.
Reference