DI(의존성 주입)라고 들어보았나? 의존성 주입을 하게 되면 아래와 같은 이점을 얻게 된다.
- 의존 관계 설정이 실행시에 이루어지므로 컴포넌트 간의 결합도를 낮춘다.
- 코드 재사용성이 증가한다.
- 단위 테스트의 편의성을 높여 준다.
- 스코프를 사용하여 객체를 관리할 수 있다.
그래도 모르겠다면 아래를 읽어보자.
분명히 과거에 Koin을 공부했었다. 그런데 지금 왜 다시? 그건 내가 Dagger를 공부했기 때문이다. 공부해본 사람이라면 공감할 것이다. Dagger. 보통 어려운 것이 아니다. 하지만 DI를 쉽게 사용하고 싶었다. 그래서 과거에 사용했던 Koin을 다시 가져와 보았다. 그런데 작년 5월에 공부했던거라 기억이 가물가물하다. 복습이라고 생각하고 다시 꺼내보자. (Koin만을 다루기 위해 샘플코드에 등장하는 RxJava와 관련된 내용은 생략한다.)
먼저 Daager와 Koin이 어떻게 다른지 확인해보자. 아래 내용은 "해리의유목코딩"에서 가지고 왔다.
- Dagger
- 장점
- 순수자바
- 안정적이고 유연함
- 런타임 에러가 발생하지 않음
- 런타임시에 매우 빠름
- 단점
- 컴파일시 오버헤드가 발생
- 학습곡선이 상당함
- 장점
- Koin
- 경량화된 의존성 주입용 프레임워크
- 장점
- Annotation 과정이 없어 컴파일이빠름
- 학습하기 쉽고 설치도 쉬움
- 단점
- 런타임중 에러가 발생
- Daager에 비해 런타임시 오버헤드가 있음
- 기타
- MVP패턴을 사용한다면 일반적으로 Presenter에 Scope를 주어 메모리를 효율적으로 관리
- 그러나 Dagger만큼 유연하지 않음
0. 사전 작업
먼저 각종 라이브러리 반영 및 권한 세팅 등을 해둔다.
그리고 다음과 같은 코드가 이미 작성되어 있음을 가정한다. 참고로 아래 번호 순서대로 작성하는 것이 유의미하다. 순서에 따라 import 이슈가 있을 수 있기 때문이다. layout 파일은 생략해 두었다. 액티비티나 아이템 레이아웃 정도 뿐이니 충분히 유추하여 추가할 수 있을 것이다.
- SingleLiveEvent
- SnackbarMessage
- BaseKotlinViewModel
- BaseKotlinActivity
- ImageSearchResponse
- KakaoSearchSortEnum
- DataModel
- KakaoSearchService
- DataModelImpl (BuildConfig.KAKAO_API_KEY 관련해서는 local.properties 포스팅을 참고하자)
위 코드를 모두 완성한 후, 남은 부분은 아래 진도와 맞게 필자의 github를 참고해서 완성해나가면 된다.
1. Koin 모듈 생성
의존성을 주입할 대상을 선언하는 곳이 바로 Module이다. 이 모듈을 만들어보자.
// MyModule.kt
package what.the.mvvm.di
var retrofitPart = module {
// something...
}
Koin에서 다루는 DSL은 무엇이 있을까? (특히 중요한 것은 bold로 처리했음)
- module { } : 코인 모듈 또는 하위 모듈 생성 (모듈 내부)
- factory { } : 항상 새로운 인스턴스를 생성하도록 해줌.
- single { } : 싱글톤 타입으로 지정해줌.
- get() : 컴포넌트 종속성을 해결해줌 (필요한 컴포넌트를 주입받음)
- viewModel : viewModel 인스턴스를 생성
- named() : Enum이나 String으로 한정자를 정의해줌.
- bind : 지정된 컴포넌트의 타입을 추가적으로 바인딩해줌.
- getProperty() : 필요한 프로퍼티를 가져옴.
- build : 생성할 객체를 다른 타입으로 바꾸고 싶을 때 사용
이를 바탕으로 위 예시 코드의 something 부분을 구체화해보자.
2. Retrofit 객체 생성
갑자기 헷갈려서 다뤄보는 OkHttp와 Retrofit의 차이.
- OkHttp : HTTP 프로토콜 통신을 위한 클라이언트 라이브러리
- Retrofit : TypeSafe한 HttpClient 라이브러리. 기본적으로 OkHttp에 의존하고 있다.
이제 single 키워드를 사용하여 싱글톤으로 작동하는 Retrofit의 API 클래스 객체(API 구현체가 존재하는 인터페이스인 서비스 객체)를 주입할 수 있는 모듈을 생성해보자. Retrofit의 객체는 App 전체 주기동안 계속 살아서 이용할 수 있는 객체로서 생성되어야 하기 때문에 single 키워드를 사용한다.
// MyModule.kt
package what.the.mvvm.di
var retrofitPart = module {
single<KakaoSearchService> {
Retrofit.Builder()
.baseUrl("https://dapi.kakao.com")
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(KakaoSearchService::class.java)
}
}
위와 같이 생성된 객체는 어디서 주입이 될까? 2가지 방법이 있다. 변수 선언시, by inject()를 붙여서 lazy하게 호출시 생성하는 방법이 있고 아래와 같이 get()으로 주입하는 방법이 있다.
// MyModule.kt
package what.the.mvvm.di
var modelPart = module {
factory<DataModel> {
DataModelImpl(get())
}
}
DataModelImpl은 객체 생성시 KakaoSearchService를 받아야 하는데, 동일한 모듈 내에서 get()으로 서비스 객체를 받을 수 있다. 생성자에 의해 의존성을 주입받는 형태이다.
지금까지 retrofitPart, modelPart. 2개의 코인 모듈을 생성했다. 이 모듈들을 하나로 묶어두면 startKoin에서 비교적 쉽고 편하게 모듈을 호출할 수 있다. startKoin은 바로 뒤에서 설명할테니 잠시 기다리자.
// MyModule.kt
var myDiModule = listOf(modelPart, retrofitPart)
이렇게 모듈은 준비가 되었다. 하지만 이 모듈은 작동하지 않는다. 왜냐? Application 클래스에서 이 모듈을 호출해야 전역으로 쓰일 수 있기 때문이다.
3. startKoin
모듈을 사용하기 위해 startKoin라는 DSL을 사용한다.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 모듈 등록
startKoin {
// Koin이 로그를 남기는 레벨을 지정 - Level.INFO by default
androidLogger()
// Koin은 코틀린 환경이라면 어디든 동작한다
// 코틀린 환경의 안드로이드 앱 개발시 androidContext로 Context를 주입시킨다
// inject Android context
androidContext(this@MyApplication)
// 선언한 모듈 지정
modules(myDiModule)
}
}
}
또한 이렇게 새로운 Application 클래스를 생성해서 사용하게 될 경우 아래와 같이 AndroidManifest에 등록할 필요가 있다. (아래 코드에서 android:name 부분)
// AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="what.the.mvvm">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
// 이하 생략
이제부터는 중간중간에 MainActivity나 기타 다른 코드를 수정하면서 아래 내용을 진행하는 것이 좋다.
4. ViewModel 의존성 주입
기존에는 ViewModelProviders를 이용하여 ViewModel을 사용해왔다. Koin으로 ViewModel 의존성 주입을 하면 이 과정이 생략된다. 우선 MainViewModel을 생성하자.
class MainViewModel(private val model: DataModel) : BaseKotlinViewModel() {
// 일부 생략
fun getImageSearch(query: String, page: Int, size: Int) {
addDisposable(
model.getData(query, KakaoSearchSortEnum.Accuracy, page, size)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
it.run {
if (documents.size > 0) {
Log.d(TAG, "documents : $documents")
_imageSearchResponseLiveData.postValue(this)
}
Log.d(TAG, "meta : $meta")
}
}, {
Log.d(TAG, "response error, message : ${it.message}")
})
)
}
}
앞에서 RxJava는 설명하지 않겠다고 했지만 불가피한 과정에서는 조금 언급해볼까 한다. addDisposable을 통해 데이터의 구독을 시작하면서 CompositeDisposable 객체를 추가한다. 그리고 이 CompositeDisposable는 Observing을 그만두게 될 때, 즉 ViewModel(View도 마찬가지)이 사라질 때, CompositeDisposable을 비워줌으로서 메모리 누수를 방지한다.
이 MainViewModel은 DataModel의 의존성 주입이 필요한데 앞에서 언급한 생성자에 의한 의존성 주입으로 해결이 가능하다. 이미 modelPart로 DataModel의 의존성 주입이 이루어지고 있기 때문이다. MainViewModel의 모듈 추가가 완료되면 myDiModule에도 갱신을 해줘야한다.
// MyModule.kt
var modelPart = module {
factory<DataModel> {
DataModelImpl(get())
}
}
var myDiModule = listOf(viewModelPart, modelPart, retrofitPart, adapterPart)
이제 ViewModel을 사용할 준비가 다 되었다. 실제 사용하는 포인트를 살펴보자. by viewModel()를 사용하면 lazy한 방식으로, getViewModel()를 사용하면 non-lazy한 방식으로 의존성을 주입할 수 있다. 아래는 lazy한 방식의 의존성 주입이다.
// MainActivity
private val mainSearchRecyclerViewAdapter: MainSearchRecyclerViewAdapter by inject()
5. RecyclerViewAdapter 의존성 주입
마지막으로 RecyclerViewAdapter 의존성 주입을 해보자. 먼저 MainSearchRecyclerViewAdapter를 만든다. 이 adapter는 MainActivity에서 사용하게 된다. 그러므로 의존성 주입 역시 MainActivity에서 이뤄져야 한다. 기존에 get()을 이용한 의존성 주입이 "non-lazy"한 방식이라면 by inject()를 사용하는 경우는 lazy한 방식이라고 이해하면 된다.
// MainActivity
private val mainSearchRecyclerViewAdapter: MainSearchRecyclerViewAdapter by inject()
🤪 부록 : The function 'invoke()' is not found
아래와 같은 에러 메시지가 나타나는 경우가 있다.
Expression 'viewModel' of type 'MainViewModel' cannot be invoked as a function. The function 'invoke()' is not found
이는 Koin의 viewModel이 import가 되어 있지 않은 경우이다. by viewModel()를 사용하는 곳에 아래와 같이 import를 해준다면 이 이슈는 해결이 된다. (참고자료)
// MainActivity - by viewModel()을 사용하는 위치
import org.koin.androidx.viewmodel.ext.android.viewModel
class MainActivity : BaseKotlinActivity<ActivityMainBinding, MainViewModel>() {
// 일부 생략
override val viewModel: MainViewModel by viewModel()
🧐 부록 : Koin과 Dagger의 성능
대표적인 2개의 DI인 Koin과 Dagger의 성능을 비교한 사례가 있다. (원문, 한글 번역, 각종 디바이스 결과) Koin 1.0 당시, Dagger와 수십배 이상 차이나던 퍼포먼스가 2.0이 되고 난 뒤 상당히 개선되었음을 확인할 수 있다. 아래는 Koin 2.0과 다른 의존성 주입 라이브러리의 퍼포먼스 비교표이다. 각 라이브러리의 설치와 의존성 주입에 대한 결과값이다. 설치는 1회성이기 때문에 의존성 주입(Inject) 위주로 보면된다. Dagger가 더 우수한 것은 동일하지만 과거 1.0과 비교해서는 엄청난 개선이 되었음을 볼 수 있다.
😎 부록 : Koin Test
의존성 주입 과정을 테스팅하기 위한 툴(Tool)로 koin-test가 제공된다. koin-test는 koin core와 동일한 버전을 가진다. koin-test에서 JUnit도 활용 가능하다. (Koin 테스팅 사례 : 말리빈 Devlog)
// koin
implementation "org.koin:koin-androidx-scope:$koinVersion"
implementation "org.koin:koin-androidx-viewmodel:$koinVersion"
testImplementation "org.koin:koin-test:$koinVersion"
testImplementation "org.koin:koin-test-junit4:$koinVersion"
2021.03.24 추가
실습한 내용의 전체 코드가 궁금한 분께서는 제 레포지토리를 참고해주세요.
(피드백 주신 신***님 감사합니다.)
참고자료
- Koin vs Dagger 그리고 추가된기능
- Java - Retrofit이란?
- [Android] Koin을 이용한 의존성 주입
- Koin 잘 사용하기
- Android Kotlin MVVM패턴으로 간단한 검색 앱 만들기
- KOIN을 이용한 Dependency Injection (DI) 구현하기
- 안드로이드 앱에 Koin으로 DI 적용하기 (추천👍)
'Android, iOS' 카테고리의 다른 글
[Android] java.time 패키지 때문에 String to date casting이 안될 때 (0) | 2021.04.14 |
---|---|
[안드로이드] BindingAdapter!! 두둥등장!! (0) | 2021.03.17 |
[안드로이드] local.properties에 API Key값 숨기기 (0) | 2021.02.26 |
Kotest로 해보는 안드로이드 테스트 (하) (0) | 2021.02.07 |
Kotest로 해보는 안드로이드 테스트 (상) (0) | 2021.01.22 |
Comment