RxJava와 Kotlin Coroutines 비교해보기



개인 광고 영역

RxJava는 안드로이드 개발에 없어서는 안 될 존재가 되었다. 많은 개발자들이 사용하고 있으며, 구글도 RxJava 관련한 API를 제공한다. 하지만 RxJava를 이해하기까지 상당한 시간이 걸린다.

다음 코드를 이해하려면 과연 몇 개의 정보를 이해하고 있어야 할까?

Flowable.range(0, 10)
        .observeOn(Schedulers.io())
        .subscribeOn(Schedulers.io())
        .map {
            it + 1
        }
        .filter {
            it > 4
        }
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(
                {
                    println(it)
                    // UI 에서 처리
                },
                {
                    it.printStackTrace()
                    // 오류 처리
                }
        )


RxJava에서 사전에 알아야 할 구문

위 코드를 이해하기 위한 최소한 아래의 정보를 이해할 수 있어야 코드를 이해할 수 있다.

  • Flowable : Reactive-Stream을 구현하기 위한 패턴의 시작
  • subscribeOn : Reactive-Stream에서 사용할 기본 스케줄러이며, observeOn으로 스케줄러 교체하기 전까지 기본으로 사용한다.
  • observeOn : 스케줄러의 변경이 필요하면 observeOn을 이용 스케줄러 교체가 가능하다. 다음 observeOn이 오기전까지 유효하다.
  • map : Stream에서 넘어온 데이터를 처리할 수 있다.
  • filter : Stream에서 넘어온 데이터에 filter를 걸 수 있다. 단, 조건의 결과값이 true가 아닌 경우 다음 구문을 실행하지 않는다.
  • subscribe : 모든 Reactive-Stream은 subscribe 정의가 없으면 동작하지 않는다. subscribe 정의에서는 onNext, onError, onCompleted을 제공한다.
  • Schedulers.io() : Rx에서 사용하는 Schedulers이며 AndroidSchedulers.mainThread()를 별도로 제공한다.

RxJava 하나를 이해하기 위해서 위와 같은 구문을 알아야 한다. 여기에는 당연하게도 Observer pattern을 이해하고 있어야 한다.


RxJava 대신 Coroutine으로 작성하면?

이번엔 RxJava로 작성한 코드를 Coroutine으로 변경해보자. 이 코드에는 kotlin에서 제공하는 stream을 함께 사용하여 더 짧은 코드로 작성하였다.

CoroutineScope(Dispatchers.Default).launch {
    (0..9) // 0부터 9까지의 배열을 가진다.
            .map { it + 1 } // 0~9까지의 값에 + 1을 한다.
            .filter { it > 4 }.forEach { // 4보다 큰 값에 대해서 true를 리턴한다.
                launch(Dispatchers.Main) {
                    // UI 처리
                    println(it) // UI에서 출력한다.
                }
            }
}


Coroutine에서 알아야 할 것은?

이번엔 위 Coroutine 코드를 이해하기 위한 구문을 나눠보자.

기본 형태는 함수와 동일하여 이해하기 쉬운데 크게 나누면 CoroutineScope, launch와 RxJava에서와 같은 스케줄러인 Dispatches를 이해하면 간단하게 읽을 수 있다.

  • CoroutineScope : Coroutine을 동작하기 위해서 CoroutineScope을 정의할 수 있다. CoroutineScope은 기본 스케줄를 함께 지정해야 하며, RxJava의 subscribeOn과 같다.
  • launch : 코루틴으로 동작해야 할 코드 스쿱을 지정한다. 이때 이 스쿱에서 사용할 스케줄러를 함께 지정할 수 있으며, 지정하지 않을 경우 CoroutineScope에서 지정한 스케줄러를 사용한다.
  • Dispatchers : 코루틴에서 사용할 스케줄러를 정의하며, Default(Work Thread), Main(UI Thread) 등을 지정할 수 있다.

RxJava보다 알아야 할 구문이 적어 이해하기 쉽다. 처음에는 RxJava보다 이해하기 쉽지만 시간이 지날수록 RxJava 만큼의 학습 비용은 들어간다.


RxJava vs Coroutine

그러면 RxJava와 Coroutine의 서로 비슷한 점을 먼저 정리하는데, 이는 RxJava를 활용한 사용자가 Coroutine을 쉽게 활용할 수 있도록 정리해보았다.

코드의 시작 부분 - 명시

RxJava

RxJava는 용도별로 Reactive-Stream 형태를 제공한다. 다음과 같이 5 가지를 제공하는데 Reactive-Stream을 시작할 수 있다.

Coroutine

Coroutine은 interface로 정의되어있으며, 모든 Scope은 CoroutineScope을 최상위로 두고 있다. 이를 활용한 GlobalScope을 함께 제공한다. RxJava처럼 subscribe 호출 시 동작하도록 하려면 suspend로 함수 정의를 해둘 수 있는데, 단순 CoroutineScope은 즉시 실행 및 event 대기를 한다.

launch의 return 결과인 Job을 통해 일시 정지, 종료, 취소, 시작 등을 제어할 수 있다.

  • CoroutineScope : 새로운 CoroutineScope을 생성할 수 있으며, 스케줄러를 함께 초기화해야 한다. CoroutineScope은 Activity 라이프사이클에 따라 동작하도록 베이스 코드를 함께 사용하는 것을 추천하고 있다.
  • GlobalScope : CoroutineScope을 상속받아 구현되어있으며, GlobalScope은 object인 싱글톤으로 만들어져있다. GlobalScope은 Application의 라이프 사이클을 따라 동작해야 하며, 서비스와 같은 형태에서 사용하는 것을 추천한다.

CoroutineScope, GlobalScope으로 객체를 생성하였다면, 코드 동작을 위한 액션을 지정해주어야 한다.

  • launch : 일반적인 코루틴을 사용할 때 사용하며, launch에서 스케줄러를 지정하지 않으면, CoroutineScope의 스케줄러를 따르며, 지정할 경우 블록 안에서만 스케줄러에 따라 동작한다.
  • async : async/await이 필요할 경우 사용할 수 있다.
  • actor : actor를 사용할 경우 이벤트 전달이 가능하며, suspend에서 데이터 전달과 send와 기본 Thread에서 이벤트 전달이 가능한 offer 두 가지를 제공한다. 이를 Android에서는 OnClick에 함께 사용할 수 있다. 이전 글 참고

이것 외에도 다양하게 있으니 코루틴 가이드 문서를 참고하길 바란다.


스케줄러 교체

이번엔 스케줄러 교체 방법을 알아보자.

RxJava

RxJava는 기본적으로 전체 스트림에서 사용할 기본 subscribeOn과 매번 교체할 수 있는 observeOn을 제공한다.

  • subscribeOn : Reactive-Stream에서 기본으로 사용할 스케줄러를 지정한다. observeOn으로 새로운 스케줄러 지정하기 전까지 유지된다.
  • observeOn : Reactive-Stream의 진행 중 UI/Background Thread를 언제나 교체할 수 있다. observeOn으로 스케줄러를 지정하면 스케줄러가 변경되며, 이후 stream에서 이를 따른다.

Coroutine

코루틴은 CoroutineScope에서는 생성 시 Dispatches을 지정하며, GlobalScope은 기본으로 EmptyCoroutineContext으로 지정되어있다. 이 Dispatches 정의는 RxJava의 subscribeOn과 동일하게 동작한다.

이후 RxJava의 observeOn과 동일한 행동을 원할 경우 launch()에 새로운 Dispatches을 지정해주면 아래와 같이 지정할 수 있다.

  • launch(Dispatches.Default) : Work 스레드를 사용하여 동작한다.
  • launch(Dispatches.Main) : Main 스레드를 사용하여 동작한다.

위 두 정의의 Dispatches 선택에 따라 UI/Background Thread의 형태를 지정할 수 있다. launch에서 Dispatches 지정하지 않으면, 상위(CoroutineScope, GlobalScope)의 기본 Dispatches에 따라 동작한다.

이 동작은 observeOn과 동일한 결과를 가지지만, RxJava처럼 이후 스트림까지 영향을 받지는 않는다.


Release 하기

RxJava

RxJava에서는 subscribe가 호출되면 disposable을 리턴한다. Disposable을 묶어서 Android Lifecycle의 onDestroy에서 disposable.clear을 호출하여 기존 Reactive-Stream을 종료할 수 있다.

Coroutine

Coroutine에서는 Job이 return 되거나, coroutineContext에 Job을 함께 지정할 수 있는데 아래와 같이 할 수 있다.

val job: Job = Job()

val coroutineContext: CoroutineContext
    get() = DispatchersProvider.main + job

이러한 job은 onDestroy에서 cancel 처리할 수 있다.

override fun onDestroy() {
    super.onDestroy()

    job.cancel()
}


RxJava to Coroutine

표로 한 번 더 정리하면 아래와 같다.

내용 RxJava Coroutine
시작 Flowable CoroutineScope(dispatchers)
0..10 range(0, 10)(RxJava에서 제공) (0..9)(코틀린 문법)
시작 스케줄러 subscribeOn(Schedulers.io()) Dispatchers.Default
중간 스케줄러 변경 observeOn(Schedulers.io()) launch(Dispatchers.Main)
index + 1 map {} map {} (코틀린 stream 문법)
index > 4 filter {} filter {} (코틀린 stream 문법)
출력 subscribe의 onNext에서 출력 (구독을 통한 실행) launch를 통한 MainThread로 변경 후 출력


마무리

RxJava 개발자가 Coroutine을 이해하는 방법을 나름대로 정리해보았다. 정리하면 코루틴은 RxJava에 비해서 읽기 편한 코드를 제공하며, 초기 러닝 커브가 낮아 이해하는 게 쉽다. RxJava는 처음에 알아야 할 게 많은 대신 활용성이 높다.

다양한 라이브러리를 제공하고 있으며, 네트워크부터 대부분의 코드를 RxJava를 활용하여 작성이 가능하다. Coroutine은 아직까지 다양한 라이브러리의 부족으로 직접 개발하거나, Kotlin extension + Higher-Order function 등을 활용하여 직접 개발을 하면서 사용해야 편하게 쓸 수 있다.

결국 아직은 RxJava가 더 편하다는 결론이다.

다행히 Coroutine의 샘플 중에는 RxJava와 함께 사용할 수 있는 샘플을 제공하는데, 아직 라이브러리로 배포를 하지는 않았다. 다음에 작성하는 글에서 이를 확인해보려고 한다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments