Naver Tech Concert Day-2
05. 안드로이드에서 코루틴은 어떻게 적용할 수 있을까? : 코루틴 적용 및 ReactiveX(RxJava/RxKotlin)와 비교한다면?
발표자 : 권태환 (요기요 / 안드로이드 개발)
슬라이드 : https://www.slideshare.net/NaverEngineering/25-121499000
세션설명 : 1.3에 정식으로 포함될 코루틴! 안드로이드에서 코루틴의 적용은 어떻게 할 수 있으며, ReactiveX(RxJava/RxKotlin)과 비교 한다면 좋은점과 부족한 점, 그리고 실무 프로젝트에 적용한 코루틴을 소개해본다.
목차 :
1. 코루틴 소개(서브루틴과 코루틴) 2. 코루틴을 통해 할 수 있는것? 3. 코틀린 코루틴에서 제공하는 주요 기능들 4. 실제 업무에 적용한 코루틴 소개 5. 코루틴 써보니? RxJava와 비교
코루틴 적용 및 ReactiveX(RxJava)와 비교
Kotlin coroutines
coroutines이란?
Subroutine의 사전적 의미 : 평소 사용하는 함수, 메서드 등.
subroutine의 return이 불러지기 전까진 다음 라인을 실행하지 않음.
private fun MutableList<Int>.sum(): Int = this.sumBy{ it } // sum이 끝나야 return
@Test
fun test() {
val sum = (0..10).toMutableList().sum()
println(sum) // sum이 끝나야 println
}
Coroutines : 1958년 어셈블리에서 언급되었고, 1963년 논문을 통해 설명됨.
코틀린 뿐만 아니라 파이썬, C#에서도 지원함.
- Entry point 여러 개 허용하는 subroutine (쉽게 쓰레드라 생각하면 됨.)
- 언제든 일시 정지하고 다시 실행 가능
- event loops, iterators, 무한 리스트, 파이프 같은 것을 구현하는데 적합
private suspend fun Int.countDown(currentIndex: Int) {
for(index in this downTo 1) { //countdown from 10 to 1
tv_message.test = "Now index $currentIndex Countdown $index" // update text
delay(200)
}
Log.i("TEMP", "Now index $currentIndex Done!")
}
var currentIndex = 0
fab.onClick {
CoroutineScope(Dispatchers.Main).launch {
10.countDown(++currentIndex)
}
}
suspend라는 키워드를 넣어야 코루틴으로 동작되고 delay(기존 Thread.sleep과는 다름)를 사용함.
코루틴에서는 CPU와 관계를 최소한으로 줄여 루틴마다 스위치가 일어날 때 CPU 영향을 적게 받음.
여러 쓰레드에서 하던 부분을 단일 쓰레드에서 처리할 수 있게 해줌.
Dispatchers.Main 지정으로 여러번 click해도 메인 쓰레드에서 동작함.
Kotlin coroutines vs RxJava
Android onClick - RxJava vs coroutines
kotlin coroutines
var currentIndex = 0
fab.onClick {
CoroutineScope(Dispatchers.Default).launch { // 새로운 scope를 생성하고 default로 launch (일반 Work thread)
val job = launch(Dispatchers.Main) { // launch를 Main thread로 변경
10.countDown(++currentIndex)
}
job.join() // join()으로 UI thread 종료하기 전까지 대기
}
}
private suspend fun Int.countDown(currentIndex: Int) { // 상위 scope thread에 따름(여기선 UI)
for(index in this downTo 1) { //countdown from 10 to 1
tv_message.test = "Now index $currentIndex Countdown $index" // update text
delay(200)
}
Log.i("TEMP", "Now index $currentIndex Done!")
}
RxJava
private val clickEventSubject = PublishSubject.create<View>() // onClick 처리를 위한 Subject 생성
private var currentIndex = 0
fab.setOnClickListener {
clickEventSubject.onNext(it) // 클릭이 이루어지면 onNext를 호출하며 View를 넘겨줌.
}
clickEventSubject
.throttleFirst(500, TimeUnit.MILLISECONDS) // 첫 번째만 처리하기 위한 throttleFirst (500ms 이후에 동작하게 함.)
.observeOn(Schedulers.io()) // 스케쥴러를 가지고 io 쓰레드에서 동작하게 함.
.map {
currentIndex++ // index 증가.
}
.switchMap { // switch에 2개의 zip을 묶음.
Observable.zip(Observable.range(0, 10), // 1 부터 10 출력을 위한 range
Observable.interval(200, TimeUnit.MILLISECONDS), // 200ms interval
BiFunction<Int, Long, Int> { range, _ ->
10 - range
})
}
.observeOn(AndroidSchedulers.mainThread()) // UI thread로 변경하고, 메시지 노출
.subscribe({
tv_message.text = "Now index $currentIndex Countdown $index"
}, {})
Subject, ObserveOn 등등 10 몇가지를 알아야 이해를 할 수 있음.
Rx는 observeOn 등 몇가지 지정을 하고 subject를 통해야 실행을 함.
Coroutines은 CoroutineScope를 지정하고 launch를 하면 실행이 됨.
suspend는 RxJava 처럼 subscribe가 올 때 처리하고 싶을때 선언하고 사용하면 되고, CoroutineScope 안에서만 호출이 가능함.
RxJava
- 장점
- Observable과 Streams
- 기존 Thread보다 간단한 코드로 처리 (어디선가 callback이 올거라 보고 처리해야 함.)
- stream을 통해 데이터 처리 용이
- Thread간 교체가 간단
- RxJava를 활용한 수 많은 라이브러리 활용 가능 (Retrofit 등등)
- 예제도 많고 문서도 잘 되어 있음.
- 단점
- 용어를 모르면 코드 활용 이유를 알 수 없음
- 처음 학습 비용이 높음
Kotlin coroutines
- 장점
- 함수 형태라 읽기 쉬움
- light-weight threads
- 모든 routine 동작을 개발자가 처리 가능
- 처음 학습 비용이 낮음
- 단점
- 아직은 필요한 라이브러리를 구현해서 사용해야 함. (RxView와 같은 라이브러리 개발 필요)
- 직접 만들 수 있고, 문서도 잘 되어 있음.
- 아직은 필요한 라이브러리를 구현해서 사용해야 함. (RxView와 같은 라이브러리 개발 필요)
Kotlin도 Coroutines 처럼 처음은 쉽지만 그 다음부터 어려워짐.
Kotlin coroutines의 장단점
- 장점
- 함수 형태라 읽기 쉬움
- light-weight threads
- 모든 routine 동작을 개발자가 처리 가능
- 단점
- 아직은 필요한 라이브러리를 구현해서 사용해야 함. (RxView와 같은 라이브러리 개발 필요)
Kotlin coroutines 이 제공하는 것들
Kotlin 1.3과 함께 coroutines 1.0 정식 릴리즈됨.
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
주의할 점 : 코루틴 0.25버전에서부터 deprecated 되었던 부분들이 1.0에서 다 삭제됨. 릴리즈 문서 참고.
Kotlin 다양한 platform 제공
Server-side
Desktop
Mobile Application
다양한 언어에서 제공하던 주요 라이브러리 제공
- C#, ECMAScript : async/await
- Go : channels, select
- C#, Python : generators/yield
Kotlin coroutines guide
Coroutines : https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html
Android Coroutines - codelab : https://codelabs.developers.google.com/codelabs/kotlin-coroutines/index.html
blocking과 no-blocking
@Test
fun test() {
GlobalScope.launch {
delay(300L) // non-blocking
println("World!")
}
pirntln("Hello,")
Thread.sleep(500L) // block main thread. UI 쓰레드에서 동작이라 화면이 멈춤
}
- Thread.sleep() : Thread 처리 전까지 Main Thread가 멈춤. UI 쓰레드에선 최대한 안써야 함.
- delay : 별도의 coroutine에서 동작하여 멈추지 않고 넘어감.
그래서 runBlocking을 제공함. (아래 두가지 코드는 동일하게 동작함.)
@Test
fun test() {
CoroutineScope(Dispatchers.Unconfined).launch {
delay(300L)
println("World!")
}
pirntln("Hello,")
runBlocking {
delay(500L)
}
}
@Test
fun test() = runBlocking {
CoroutineScope(Dispatchers.Unconfined).launch {
delay(300L)
println("World!")
}
pirntln("Hello,")
delay(500L)
}
runBlocking
- runBlocking 은 함수의 처리가 끝날때까지 대기
- delay()를 걸어두면 delay 시간만큼 대기하고 return
- Android에서 runBlocking을 UI에서 잘못 사용하면 멈추는 현상 발생
- delay를 이용해 non-blocking을 할 수 있음
- runBlocking, CoroutineScope, GlobalScope 안에서 동작해야 함
CoroutineScope, GlobalScope
CoroutineScope
- 가장 기본적인 Scope
- Thread 형태를 지정(Main, Default, IO 등을 지정)
- CoroutineScope(Main, Default, IO, ...)
- launch, async 등을 통해 scope를 실행
- 중요한 점
- Activity/Fragment LifeCycle에 따라야 함
- onDestroy() : cancel하도록 코드 추가
- CoroutineScope(/* thread type */).launch { } 로 실행
- launch { }의 return job의 동작을 지정 가능
- join() : scope 동작이 끝날때까지 대기하며, suspend에서 호출 가능
- cancel() : 동작을 종료하도록 호출
- start() : scope가 아직 시작하지 않을 경우 start, scope의 상태를 확인
- Activity/Fragment LifeCycle에 따라야 함
CoroutineScope의 interface 정의
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job())
public interface CoroutineScope {
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
public val isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true
/** Context of this scope. */
public val coroutineContext: CoroutineContext
}
CoroutineScope 활용 - Delay 대신 job.join()
@Test
fun test() = runBlocking {
val job = CoroutineScope(Dispatchers.Unconfined).launch { // 새로운 scope를 생성하고 default로 launch. (Work Thread로 동작함)
//launch에서 CoroutinScope에서 지정한 default Thread로 사용. (별도 지정 없으면 부모를 따라감)
delay(300L)
println("World!")
}
pirntln("Hello,")
// delay(500L) <-- 이 부분은 CoroutineScope에서 네트워크 같은 처리시 의미가 없어짐.
job.join() // join()으로 default thread 종료하기 전까지 대기
}
GlobalScope
전역에서 돌아가야할 경우
CoroutineScope 상속 받아 구현
Demon, Application 등에서 사용
Application의 lifetime에 따라 동작하는 scope에서 사용 추천
GlobalScope는 Dispatchers.Unconfirned(worker thread) 에서 동작
GlobalScope.launch(/* thread type */) { } 로 실행.
위에서 thread가 이미 지정되어 있어 launch에서 쓰레드 타입을 지정. 기본은 Default임.
GlobalScope API 코드
object GlobalScope : CoroutineScope {
/**
* @suppress **Deprecated**: Deprecated in favor of top-level extension property
*/
@Deprecated(level = DeprecationLevel.HIDDEN,
message = "Deprecated in favor of top-level extension property")
override val isActive: Boolean
get() = true
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
GlobalScope 사용 예
fun ReceiveChannel<Int>.sqrt(): ReceiveChannel<Double> =
GlobalScope.produce(Dispatchers.Unconfined) {
for(number in this@sqrt) {
send(Math.sqrt(number.toDouble()))
}
}
suspend
- suspend를 활용하여 함수 분할을 할 수 있음.
- suspend로 만들어진 함수는 사용하기 전까지는 동작하지 않음.
- suspend 키워드를 사용하는 함수는 CoroutineScope에서만 사용할 수 있음.
suspend 사용 예
suspend fun CorountineScope.loadData(body: suspend CoroutineScope.(item: String) -> Unit) {
val item = ""
delay(100L)
body(item)
}
CoroutineScope(Dispatchers.Main).launch {
loadData { item ->
// Coroutine scope 정의
}
}
Job
- CoroutineScope의 return에는 job 객체를 넘겨줌.
- job을 통해 routine의 취소, 실행, 종료를 대기할 수 있음
- job.cancel() : 종료하도록 유도함.
- job.join() : 종료를 대기하며 호출은 suspend에서 가능함.
- job.start() : coroutine의 시작을 확인할 수 잇으며, 시작 상태라면 true
Android onClick 잘 활용하기
연속해서 빠르게 입력하는 경우를 막을 경우
RxJava 버튼 처리
첫 번째 이벤트만 허용하기 위해서
throttleFirst 활용
시간으로 first의 시간을 지정하여 문제 발생 외 처리 필요.
(문제점 : 특정 시간 후에 다음 이벤트가 발생함)
Coroutine으로 처리
- 첫 번째 이벤트만 허용하기 위해서
- 버튼의 상태를 변경해서 처리?
- coroutine에서는 그럴 필요 없음.
- GlobalScope + actor 활용
Coroutine GlobalScope.actor<T> 활용하기
private fun View.onClick(action: suspend (View) -> Unit) {
// 5. 이 때 Higher-Order function 정의는 suspend가 포함되어야 함.
val event = GlobalScope.actor<View>(Dipatchers.Main) { // 1. Singletone의 GlobalScope 활용
for(event in channel) { // 2. actor 이용 event 받기
action(event) // 4. 받은 event를 Higher-Order function으로 넘겨서 정의하도록 함.
}
}
setOnClickListener {
event.offer(it) // 3. actor에 offer로 event 보내기
}
}
var currentIndex = 0
fab.onClick {
10.countDown(currentIndex++) // 6. 람다 표현으로 countDonw 구현
}
Android Coroutines
안드로이드에서 코루틴 활용
- UnitTest/Main Thread에서 활용하기 쉽게 Dispatchers 하나로 활용
- UnitTest에서 Main thread를 활용할 수 없기에 기본으로 처리 할 수 있도록 작성
- CoroutineScope를 Base에 작성하여 release를 쉽게 하도록 처리
- onClick에 coroutine 활용을 위한 GlobalScope 적용
Dispatchers 정의(UnitTest/Default Thread)
sealed class DispatchersProviderSealed {
open val main: CoroutineContext by lazy { Dispatchers.Main }
open val default: CoroutineContext by lazy { Dispatchers.default }
}
/**
* 기타 Thread를 위한 Dispatchers 정의
*/
object DispatchersProvider: DispatchersProviderSealed()
/**
* Unit Test를 위한 Dispatchers 정의
*/
// Rx 기준으로 볼때 테스트시 전부 백그라운드 쓰레드로 돌려야할 경우
object TestDispatchersProvider: DispatchersProviderSealed() {
override val main: CoroutineContext = Dispatchers.Unconfined
override val default: CoroutineContext = Dispatchers.Unconfined
}
CoroutineScope를 상속받아 구현
abstract class CoroutineScopeActivity: AppCompatActivity(), CoroutineScope {
private val job: Job = Job() // Job을 미리 생성하여 CoroutineContext에 미리 지정할 수 있음
override val coroutineContext: CoroutineContext // Activity에서 사용할 기본 Context를 정의
get() = Dispatchers.Main + job
override fun onDestroy() {
super.onDestroy()
job.cancel() // onDestroy()에서 job을 종료하도록 함
}
}
- CoroutineScope를 상속받아 구현하면 기본 CoroutineScope로 정의되어 있음.
- launch, actor<E> 를 사용하면 코드 간결 및 자동으로 종료 처리해 줌.
- Default 상위 Activity에서 정의한 CoroutineScope의 Thread를 활용함.
- 필요시 launch, actor에서 Thread를 언제든 변경 가능함.
- 다양한 CoroutineScope 구현
- ViewModel
- LifeCycleObservable
- Fragment
CoroutineScope 상속받은 Activity에서 routine 사용하기
abstract class CoroutineScopeActivity: CoroutineScopeActivity() {
launch {
// UI Thread에서 처리
}
launch(Dispatchers.Default) {
// Default Thread에서 처리
}
actor<Generic Type> {
// UI Thread에서 event 처리
for(event in channel) action(event)
}
actor<Generic Type>(Dispatchers.Default) {
// Default Thread에서 처리
for(event in channel) action(event)
}
}
onClick 처리
- Higher-Order function + kotlin Extensions을 활용.
- GlobalScope을 활용하거나, CoroutineScope을 활용 가능.
- 동작 범위에 따라서 GlobalScope, CoroutineScope을 선택함이 좋음.
onClick 만들기, background에서 처리하고, UI에 던져주기
class CoroutinesSendChannelOnClickEvent<E>(
private val view: View, // View: click을 위한 View
private val bgBody: suspend (item: View) -> E, // background : Higher-Order Function
private val dispatcherProvider: DispatchersProviderSealed = DispatchersProvider, // Provider : 지정
private val job: Job? = null) { // Job : 종료 처리를 위한 job 추가
fun consumeEach(uiBody: (item: E) -> Unit): CoroutinesSendChannelOnClickEvent<E> {
val clickActor = CoroutineScope(dispatcherProvider.main + (job ?: EmptyCoroutineContext)).actor<View> {
this.channel.map(context = dispatcherProvider.default, transform = bgBody).consumeEach(uiBody)
}
view.setOnClickListener { clickActor.offer(it) } // Offer 처리를 위한 CoroutineScope 생성
return this
}
}
fun <E> View.onClick(dispatcherProvider: DispatchersProviderSealed = DispatchersProvider,
job: Job? = null, bgBody: suspend (item: View) -> E): CoroutinesSendChannelOnClickEvent<E> =
CoroutinesSendChannelOnClickEvent(this, bgBody, dispatcherProvider, job)
infix fun <E> CoroutinesSendChannelOnClickEvent<E>.consume(uiBody: (item: E) -> Unit) { // 생성을 간단하게 하기 위한 function 2개
this.consumeEach(uiBody)
}
fab.onClick(job = job) { // click을 처리하고, background에서 loadnetwork()
loadNetwork()
} consume {
tv_message.text = it
}
private suspend fun loadNetwork(): String { // Temp load network
delay(300)
return "currentIndex ${currentIndex++}"
}
Retrofit2 kotlin coroutines adapter
JakeWharton 배포
Dependency 추가
- implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
Deferred<E>를 활용하여 return을 처리할 수 있음.
interface RetrofitService { @GET("/posts") fun getPosts(): Deferred<Response<List<Post>>> }
// 초기화시키는 코드 object RetrofitFactory { const val BASE_URL = “https://url" fun makeRetrofitService(): RetrofitService { return Retrofit.Builder() .baseUrl(BASE_URL) .addCallAdapterFactory(CoroutineCallAdapterFactory()) .build().create(RetrofitService::class.java) } }
// 사용하는 코드 launch { val request = service.getPosts() val response = request.await() if (response.isSuccessful) { // Pass in the response.body() to your adapter } else { toast("Error ${response.code()}") } }
Q & A
Q : Dispatcher.Unconfined는 부모 코루틴 컨텍스트에서 코루틴을 실행하고 제일 처음 suspend 함수 이후에는 해당 suspend에서 사용했던 쓰레드에서 동작하는 걸로 알고 있음. 예제에서 Unconfined를 쓰셨던데 안드로이드에서 활용 가능한 부분이 있는지?
A : 테스트 코드에서 활용하기 위해 넣은 거임. UI Thread(Main Thread)에서 활용하기 위해 넣어야 함.
Q : Rx를 안쓰고 Coroutine 기반 라이브러리들로 코드 베이스를 변화시키거나 변화시킬 계획인지?
A : Coroutine을 조금씩 적용할 예정이며 Retrofit을 사용하기 위해 새로 팔 예정임. onClick에 먼저 적용 중.
Q : 코루틴이 rx java의 operator 기능들의 유연함을 못따라간다고 들었는데 어떤지?
A : 유연함을 못따라가는 것은 맞음. 아직 라이브러리들이 없음. 기본적인 틀만 제공 중인 상태.
'IT > 행사' 카테고리의 다른 글
[행사] Naver Tech Concert Day-1 요약 (0) | 2019.01.14 |
---|---|
[행사] Naver Tech Concert Day-2 06 (1) | 2019.01.14 |
[행사] Naver Tech Concert Day-2 04 (0) | 2019.01.14 |
[행사] Naver Tech Concert Day-2 03 (0) | 2019.01.14 |
[행사] Naver Tech Concert Day-2 02 (0) | 2019.01.14 |