RxJava2를 도입하며

Sunghoon Kang
Banksalad Tech
Published in
12 min readApr 10, 2017

레이니스트에서는 뱅크샐러드 안드로이드 앱의 경우에는 RxJavaRxAndroid, iOS의 경우 RxSwift, 공식 웹사이트에서는 RxJS를 사용하는 등 플랫폼을 가리지 않고 ReactiveX를 적극적으로 사용하고 있습니다.

RxJava1을 (나름대로) 잘 사용하고 있었지만, RxJava2 문서를 살펴본 결과 기존 안드로이드 코드 베이스에서 발생하던 문제를 해결할 수 있을 것 같다는 생각에 도입하기로 했습니다. 이 글을 통해 저희가 RxJava2를 도입하면서 겪은 바에 대해 공유하고자 합니다.

tl; dr

  • RxJava에는 일반 Observable뿐만 아니라 Completable, Single 등 다양한 Observable type들이 있습니다.
  • RxJava2에서 새롭게 추가된 Maybe를 활용하면 Optional Type에 대해 null을 깔끔하게 처리할 수 있습니다.

RxJava2로 넘어가야 하는 이유

  • Maybe가 등장하고, Completable이 안정화 되면서 데이터의 흐름을 좀 더 명확하게 표현할 수 있게 되었습니다. 이 부분은 아래에서 더 자세하게 살펴보도록 하겠습니다.
  • RxJava1이 1년 뒤에 개발이 중단됩니다. 큰 문제가 없을 수 있지만, 지원 중단되는 라이브러리를 안고 간다는 건 결국에 기술부채를 안고 가는 것과 마찬가지기에 RxJava2를 사용하기로 했습니다.
얼마 남지 않았습니다 ㅠㅠ

RxJava2에서 달라진점은?

  • 더이상 null을 보낼 수 없습니다.
전에는 일부러 안봤지만 앞으로는 null 보고싶어도 볼 수 없어…

이제 Observable에서 null을 보내게 되면 바로 NullPointerException 이 발생합니다. Completable, 또는 새로 추가된 Maybe를 활용하거나 enum을 선언해서 null 대신 enum을 emit 하는 방법으로 대체할 수 있습니다.

  • Flowable이 추가되었습니다.

FlowableBackpressure를 지원하는 Observable입니다. RxJava2부터는 ObservableBackpressure handling을 추가할 수 없으므로, Backpressure 지원을 위해서는 Flowable을 무조건 사용해야 합니다.

하지만 Flowable을 사용한다 해서 MissingBackpressureException이 사라진 건 아닙니다. OperatoronNext를 더는 호출하지 못하는 경우, MissingBackpressureException이 발생합니다.

  • SubscriptionDisposable로 변경되었습니다.

Reactive Streams 명세에서 SourceConsumer의 상호작용을 나타내는데 Subscription이라는 이름을 사용했기 때문에, 이름 충돌을 막기 위해 Disposable로 변경되었습니다.

  • Maybe가 추가되었습니다.

값을 보낼 수도, 안 보낼 수도 있습니다.

간보는 Observable

값이 있는 경우엔 onSuccess, 값이 없는 경우엔 onComplete이 호출됩니다. 저도 처음에 보고 잘 이해가 되지 않아 예제 코드를 통해 간단하게 Maybe를 소개해드리고자 합니다.

먼저 1번 예제와 2번 예제를 보면, 1번 예제의 경우 값이 있으므로 Single처럼 onSuccess 가 호출됩니다. 따라서 Maybe가 콘솔에 찍히게 됩니다. 반면에 2번 예제의 경우 값이 없기 때문에 onComplete이 호출되게 됩니다.

Maybe를 사용하다 보면, flatMap을 활용해서 다른 Observable type으로 변경하고 싶을 때가 있습니다. 위 예제는 MaybeSingle로 바꾸는 예제인데요, 3번 예제를 실행시키면 어떻게 될까요? 바로 NoSuchElementException이 발생합니다. MaybeFlatMapSingle을 살펴보면, Maybe(Source)에서 onComplete이 호출되는 경우 Source Observer(flatMap에서 Maybeobserve하는 부분)의 onError를 호출하는 것을 확인할 수 있습니다.

4번을 실행시키면 어떻게 될까요? 정답은 onComplete이 호출됩니다. 3번과 마찬가지로 MaybeFlatten을 살펴보면 4번 같은 경우에는 Source onComplete이 바로 호출되는 것을 확인할 수 있습니다.

따라서 Maybe를 다른 Observable Type으flatMap하는 것 보다 .toMaybe() 를 활용해 Maybe로 유지하면 NullableSource를 깔끔하게 처리할 수 있습니다. Element가 없어서 onError가 호출되는 3번 예제는 다음과 같이 수정하면 onComplete에서 좀 더 명확하게 처리할 수 있겠죠.

더 자세한 내용은 RxJava Wiki에서 확인하실 수 있습니다.

기존 코드 바꾸기

  • Observable 대신 Single, Completable, Maybe로 바꾸기

기존에는 Observable을 위주로 사용하고, 앞서 언급한Completable 정도만 드물게 사용했었는데 RxJava2로 변경하면서 필요한 경우 외에는 Observable 사용을 최대한 지양하고 Single, Completable, Maybe사용을 지향하기로 했습니다. EventBus를 제외하고는 Source에서 값을 두 개 이상 받는 경우가 사실상 없었기 때문에, 값을 한 개만 받을 때는 Single, 값을 안받는 경우는 Completable을 사용하는 게 데이터의 흐름을 표현하는 데 있어서 더 낫다고 판단되었기 때문입니다.

Single은 아예 사용하지 않고, Completable은 조금씩 사용하고 있었는데요, SingleCompletable을 사용함으로써 메소드의 의미를 좀 더 명확하게 할 수 있었습니다.

Single의 예를 들어보자면,

API Call의 경우에는 당연히 하나의 값만 Return합니다. Observable 을 사용한다면, ObserveronNext 가 있기에 값이 여러 개 올 수 있다는 생각을 가지게 할 수 있지만, Single의 경우에는 ObserveronSuccess(와 onError) 밖에 없으므로 값이 하나만 떨어진다는 것을 좀 더 명확하게 할 수 있습니다.

다음으로 Completable의 예를 들어보자면,

기존에 어쩔 수 없이 nullable한 값을 return할 수 없어 의미 없는 value를 return 해야만 하는 상황이 있었습니다. 이런 경우에는 subscribe를 할 때 onNext에서 처리를 해야 할 지, 아니면 onComplete에서 처리해야 할 지 굉장히 모호해지게 됩니다. (저희는 값을 사용한다면 onNext, 값을 사용하지 않는다면 onComplete을 호출하는 식으로 처리했었습니다.) 하지만, Completable을 활용하고 나니, onNext는 없어지고, onComplete이 강제되니 코드를 좀 더 명확하게 처리할 수 있었습니다.

Maybe의 등장은 어쩔 수 없이 발생했던 문제를 해결해주었습니다. 기존에 로그를 조회하는 경우, 로그가 없으면 nullemit할 수 없으니 Exceptionthrow해서 onError에서 처리를 했으나, Maybe로 변경하고 나니 다음과 같이 깔끔하게 처리할 수 있게 되었습니다. (RxJava2부터 nullemit할 수 없게 강제되었으나, 저희는 UseCase단에서 nullableemit 할 수 없게 강제했었습니다.) 기존에는 DefaultSubscriber를 만들어서 활용하고 있었기 때문에, 기존의 방식대로 Exceptionthrow하면 Fabric에서 Error로 잡혔으나, 이제는 onComplete 으로 떨어지니 더는 FabricError로 잡히지 않게 되었습니다.

onError대신 onComplete에서 처리하세요

이제 Observablebackpressure옵션을 추가할 수 없어 Flowable로 대체하고 나니, 사실상 코드에서 Observable이 사라지게 되고, 코드가 좀 더 명확해지게 되었습니다.

  • UseCase / DefaultSubscriber

저희는 Business LogicUseCase형태로 정의해놓고 사용하고 있는데, 이 UseCase를 추상화시켜 필요한 UseCase를 직접 구현하도록 강제했습니다. 마찬가지로 Subscriber(RxJava2에서는 Observer. Flowable의 경우에는 Subscriber를 그대로 사용합니다.)DefaultSubscriber를 만들어 onError를 명시적으로 처리하지 않아도 DefaultSubscriber를 상속받는다면 onError를기본적으로 처리되도록 (Fabric Reporting 등) 구현해 놓았기에 사용을 강제했습니다.

RxJava2로 넘어가면서, UseCase와 Subscriber모두에 약간의 변경이 필요했습니다. 더는 subscribesubscription(RxJava2에서는 Disposable)return하지 않기 때문에 subscribeWith를 사용해야 했고, 마찬가지로 Subscriber의 경우에도 Disposable의 구현체인 DisposableObserver를 사용하도록 변경해야 했습니다.

  • Schedulers.immediate → Schedulers.trampoline

몇몇 상황에서 immediate를 사용했었지만, RxJava2에서 immediate가 없어졌습니다. immediate가 잘못 사용되고, Scheduler를 맞게 구현하지 않았기 때문에 trampoline을 사용하라고 공식 문서에 나와 있습니다.

The immediate scheduler is not present in 2.x. It was frequently misused and didn't implement the Scheduler specification correctly anyway; it contained blocking sleep for delayed action and didn't support recursive scheduling at all. Use Schedulers.trampoline()instead.

trampolinetaskQueue에 쌓아놓았다, 현재 실행 중인 task가 끝나면 Queue에 쌓인 task를 순차적으로(FIFO) 실행시켜주는 Scheduler입니다. 만약 idle 상태인 경우에는 현재 thread에서 queuing 없이 바로 task가 실행되기 때문에 공식 문서대로 trampoline으로 교체했습니다.

Retrofit의 경우에는 CallAdapterFactory만 교체해주면 RxJava2를 쉽게 적용할 수 있습니다.

EventBusRxRelayPublishRelay SerializedRelay를 활용해서 사용하고 있었는데 Retrofit과 마찬가지로 약간의 수정이 필요했습니다.

  • Proguard

사실 RxJava의 문제가 아닌 retrofit-rxjava의 문제이긴 하지만, RxJava2 도입 이전에 retrofit에서 Completable을 사용하는 경우 Release 빌드에서 문제가 발생했습니다. 그 이유는 retrofit-rxjava 내부에서 Completable인지 아닌지를 Canonical Name으로 판별하고 있었기 때문입니다. 이 부분에 대해서 PR을 보냈으나…

ㅠㅠ

Completable이 안정적인 API가 아니라는 이유로 Completable을 난독화하지 말라는 리뷰를 받았습니다. retrofit-rxjava2에는 Completable 비교가 정상적으로 되어있기 때문에 proguard-rule에서 Completable을 난독화 규칙을 제거했습니다.

(하나뿐인) 문제점

  • Type 추론이 정상적으로 작동하지 않습니다.

이 부분 때문에 고민을 많이 했습니다. 기존에는 아무런 문제가 없던 부분이었는데, zip을 할 때 zipper function의 타입을 명시해야만 작동하는 문제가 있습니다. 이 부분에 대해서는 저희도 원인을 몰라 임시방편으로 타입을 명시해서 사용하고 있습니다. (혹시 이유를 알고 계신다면 댓글로 알려주시면 감사하겠습니다 ㅠㅠ)

몇몇 경우에는 parameter의 type까지 명시를 해줘야 되는 경우도 있습니다…

글을 맺으며

사실 당장 적용이 필요한 부분은 아니지만, 언젠가 적용을 해야 할 부분에 대해서 먼저 처리를 해 놓으니 마음의 짐을 조금 덜어낸 기분이 듭니다. RxJava2를 적용하면서 팀원들과 더 나은 코드에 관해서 토론을 하고, 그 결과를 코드에 반영하는 과정을 해서 굉장히 만족스러웠지만, 뭔가 풀리지 않는 수수께끼(타입 명시)가 남아있다는 부분에서 조금 아쉬운 부분이 있습니다.

읽어주셔서 감사합니다 :D

마지막으로 코드리뷰를 진행해주신 팀원분들께 다시한번 감사의 말씀을 전합니다 ㅠㅠ

끝으로, 레이니스트에서는 다양한 기술적 문제들을 함께 고민하면서 해결할 소프트웨어 엔지니어를 찾고 있습니다!

--

--

Sunghoon Kang
Banksalad Tech

Software engineer who is interested in developer productivity and happiness