Kotlin Coroutines을 알아보고, 안드로이드에 library 적용하기
개인 광고 영역
async/await 이야기가 가장 흔한 coroutine. 안드로이드 개발자도 이제 coroutine을 적용할 수 있다. kotlin에 Coroutine을 제공하고 있는데, Kotlin 1.1부터 제공하고 있다. 다만 아직은 별도 라이브러리를 통해 Coroutine을 제공하고 있다.
추후 Coroutine은 1.3부터 공식적으로 Kotlin에 포함되어있어 별도의 라이브러리 추가 없이 사용이 가능해지게 된다.
이번 글에서는 Kotlin을 간략하게 알아보고, 안드로이드에서 사용 가능하도록 Coroutine 라이브러리 적용하는 방법을 소개한다.
Coroutines
Coroutine에 대해서 이야기할 때 async/await에 대해서 이야기를 가장 많이 했었다. 그래서 코드 가독성과 구현 방법을 우선해서 기존 Java Thread와 AsyncTask, Rxjava 2.0을 통해 살펴보며 Coroutines에 대하여 좀 더 상세하게 살펴보고 넘어가자.
다음 샘플은 아래와 같은 가정하에 작성하였다.
- 버튼을 누른다.(여기서는 제외한다)
- 네트워크 처리를 위해서 Progress를 실행
- loadNetworkSomething()으로 네트워크를 처리
- progress 숨김
- UI 갱신
Java Thread
Java Thread는 Java에서 기본으로 제공하는 라이브러리이다. 이를 이용하여 다운로드를 구현해보면 아래와 같다.
val thread = Thread(Runnable {
var data = ""
Handler(Looper.getMainLooper()).post {
// show Progress on UI Thread
}
data = loadNetworkSomething()
Handler(Looper.getMainLooper()).post {
// UI data update from UI thread
// Hide Progress from UI thread
}
})
thread.start()
다만 kotlin에서 람다 표현식을 사용하여 코드가 짧지만 Java 7의로 작성한다면 이보다 많이 길어지며, 지저분하다.
Android AsyncTask
안드로이드에서는 이러한 Thread를 구조화 시켜 AsyncTask를 제공하고 있는데 아래와 같이 구현이 가능하다. Background, UI Thread을 구분해준 메소드를 통해 이를 활용할 수 있다. Java Thread보다는 높은 가독성을 가지고 있다.
다만 추가가 쉽지 않고, 취소에 대한 처리를 별도로 해야 한다.
val loadData = object : AsyncTask<Unit, Unit, String>() {
override fun doInBackground(vararg params: Unit?): String {
return loadSomethingData()
}
override fun onPreExecute() {
super.onPreExecute()
// Show progress from UI thread
}
override fun onPostExecute(result: String?) {
super.onPostExecute(result)
// UI data update from UI thread
// Hide Progress from UI thread
}
}
@Test
fun test() {
loadData.execute(Unit)
}
RxJava 2.0
ReactiveX 라이브러리인 RxJava 2를 이용하여 구현해보았다. Observable Pattern과 Stream으로 사용이 간단하다. observeOn 메소드를 통해 Thread 형태를 자유롭게 변경이 가능하여 AsyncTask보다 자유롭게 사용이 가능하다.
위에서부터 아래로 가독성이 높아진다. 다만 Observable Pattern이라 콜백이 많이 생길 수 있다.
loadSomethingData()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.map {
}
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
// Show progress from UI thread
}
.doOnDispose {
// Hide Progress from UI thread
}
.subscribe { t1, t2 ->
// UI data update from UI thread
}
Coroutine
이제 Coroutine으로 이들을 정리해보자. UI 스레드로 블록을 지정하고, async/await을 이용해 loadSomethingData를 불러온다. 위에서 보았던 코드들 보다 더 높은 가독성을 가지고 있다.
CoroutineScope(Dispatchers.Main).launch {
// Show progress from UI thread
var data = ""
CoroutineScope(Dispatchers.Default).async {
// background thread
data = loadNetworkSomething()
}.await()
// UI data update from UI thread
// Hide Progress from UI thread
}
루틴(Routine)
백그라운드와 UI를 구분해서 처리해보았다. 이제 Coroutine을 좀 더 자세하게 알아보자. 이러한 루틴은 서브루틴(Subroutine)과 코루틴(Coroutine)으로 나누어 설명하겠다.
서브루틴(Subroutine)
개발을 하다 보면 중복 코드를 자연스럽게 함수로 만들어 호출해서 사용한다. 굳이 동일한 코드를 여러 장소에 두어 사용할 필요가 없고, 수정 시 수정 범위를 최소화하기 위해서이기도 하다. 예를 들면 다음과 같은 Fragment replace 하는 함수를 만들어두고, replace가 필요한 여러 곳에서 활용이 가능하다.
fun AppCompatActivity.replace(@IdRes frameId: Int, fragment: android.support.v4.app.Fragment, tag: String? = null) {
supportFragmentManager.beginTransaction().replace(frameId, fragment, tag).commit()
}
이런 형태를 Subroutine이라 부른다. Wiki에 따르면 언어별로 부르는 용어가 다양한데 Java에서는 function이라 부른다. 결과적으로 서브루틴은 함수를 호출하고, 서브루틴의 모든 처리가 완료되어야 다음 줄의 코드를 실행할 수 있다.
코루틴(Coroutines)
서브루틴의 확장 개념인 Coroutines은 아래와 같은 장점을 가지고 있다.
- 여러 다른 지점에서 입력과 종료가 일어난다.
- 실행을 일시 중지하고 호출자 또는 다른 Coroutine으로 이동할 수 있다. 호출자는 언제든 다시 Coroutine을 실행시킬 수 있다.
단순하게는 Thread처럼 보일 수 있지만 Thread 보다 훨씬 저렴한 비용으로 동작하게 된다. Subroutine처럼 함수 형태로 코드를 작성하여 가독성이 높다.
Coroutine과 Thread
Java Thread는 하나의 프로세스에 여러 개의 작업을 실행할 수 있지만 OS의 Native Thread에 직접 링크되어 동작하여 많은 시스템 자원을 사용한다. Thread 간 전환 시에도 CPU의 상태 체크가 필요하므로 그만큼의 비용이 발생한다.
반면 Coroutine은 즉시 실행하는 게 아니며, Thread와 다르게 OS의 영향을 받지 않아 그만큼 비용이 들어가지 않는다. 그리고 개발자가 직접 루틴을 언제 실행할지, 언제 종료할지 모두 지정이 가능하다. 이렇게 생성한 루틴은 작업 전환 시에 시스템의 영향을 받지 않아 그에 따른 비용은 발생하지 않는다. 또한 suspension 포인트를 개발자가 지정하기 때문에 무작위로 종료할 순 없다.
자세한 내용은 : stackoverflow에 설명되어있는 Coroutine과 Thread를 참고할 수 있다.
Coroutine 언제 사용하면 좋을까?
Coroutine은 결국 대용량 처리, 복잡한 계산, 게임 등에서 유용하게 사용할 수 있다. 보통 유니티에서 많이 사용하고 있다. 코틀린에서도 이를 제공하여 Android 개발에서도 Coroutine 사용이 가능하다.
다만 ReactiveX처럼 강력한 라이브러리 형태를 제공하지는 못한다. Github을 통해 그래도 괜찮은 라이브러리를 몇 개 찾을 수 있긴 하지만 ReactiveX 만큼의 편의성은 아직이다. 필요시 만들어 쓰는 방법과 그냥 RxJava 활용을 하는 편이 좋다.
Kotlin Coroutine
1.2 버전에서는 별도의 라이브러리를 통해 이를 제공하고 있는데, 1.3 버전부터는 코틀린에 포함되어 제공할 예정이다. 현재 RC를 통해 Kotlin 1.3 RC을 미리 사용할 수 있다.
kotlin coroutine의 경우 다른 언어에서 제공하는 비동기 처리에 대해서도 추가로 제공하고 있다. C#과 ECMAScript에서 제공하는 async/await. Go에서 제공하는 channels, select. C#과 Python에서 제공하는 generators/yield도 사용할 수 있다.
더 많은 내용은 Coroutine 문서를 확인하기 바란다.
안드로이드에 코루틴(Coroutine) 적용하기
안드로이드에서 코틀린 적용하는 방법은 간단하다. 코틀린을 사용하고 있다면 다음 라인만 추가해주면 Coroutine의 사용이 가능한데, Kotlin 1.3부터는 이렇게 할 필요가 없으니 참고하길 바란다.
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.30.1'
}
kotlin {
experimental {
coroutines "enable"
}
}
ProGuard를 사용하는 경우 아래를 추가해야 한다.
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}
Android Coroutine
Android Coroutine을 추가하면 UI 스레드의 사용이 가능하다. coroutens.android에서 제공하는 샘플 코드는 아래와 같다.
아래와 같이 job을 생성하고, 이를 cancel 처리해줄 수 있다.
fun setup() {
val job = GlobalScope.launch(Dispatchers.Main) { // launch coroutine in the main thread
delay(1000)
for (i in 10 downTo 1) { // countdown from 10 to 1
tv_message.text = "Countdown $i ..." // update text
delay(500) // wait half a second
}
tv_message.text = "Done!"
}
fab.setOnClickListener {
job.cancel() // cancel coroutine on click
}
}
위 코드는 launch {} 부분이 별도의 Main thread에서 동작하고, 나머지는 cancel을 할 수 있도록 setOnClickListener을 등록한다. 생성한 job을 가지고 cancel() 할 수 있다.
마무리
Coroutine은 간단하지만 기존 Thread나 AsyncTask, RxJava 보다 가독성 높게 Lightweight Thread를 구현할 수 있다. 다만 RxJava에서 제공하는 유용한 라이브러리가 필요하다. 그래서 필자가 setOnClickListener를 간단하게 처리할 수 있도록 만들긴 했는데 throttleFirst에 대해서 만들어두긴 했다. Event 처리 글 작성 시 소개하려고 한다.
Comments