Grokking Rx Java, Part 4: Reactive Android

Jong Yun Lee
Nspoons
Published in
11 min readFeb 27, 2017

--

이 글은 Dan Lew Codes 의 글을 번역한 것입니다.

원문 : http://blog.danlew.net/2014/09/30/grokking-rxjava-part-4/

Part 1, 2, 3 에서는 RxJava가 어떻게 동작하는 지를 알아보았습니다. 하지만 안드로이드 개발자들에게 이것이 어떻게 도움이 될 수 있을까요? 이 질문에 대한 실용적인 예시들을 여기 준비해 보았습니다.

RxAndroid

RxAndroid 는 안드로이드를 위한 RxJava의 extension 입니다. 당신의 삶을 훨씬 쉽게 만들 특별한 바인딩(bindings)들을 포함합니다.

먼저, 안드로이드 threading 시스템을 위해 이미 만들어져 있는 AndroidSchedulers 라는 것이 있습니다. UI thread에서 실행해야 하는 코드라구요? 문제 없습니다. AndroidSchedulers.mainThread() 를 이용해 보세요.

retrofitService.getImage(url)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(bitmap -> myImageView.setImageBitmap(bitmap));

만약에 여러분 자신만의Handler 가 있다면, HandlerThreadScheduler 에 연결된 scheduler를 만들수도 있습니다.

다음으로 Android의 생성주기와 맞물려 동작하는 AndroidObservable 을 제공합니다. bindActivity()bindFragment() 라는 메소드를 제공하며, 이들은 자동적으로 AndroidSchedulers.mainThread() 를 관찰(observing)하는데 사용하며, 아이템을 내보내는(emit) 것을 Activity와 Fragment가 끝나는 시점에 멈춥니다. (그래서 여러분들은 유효한 시간이후에 갑자기 state를 바꿀 필요가 없는 것이죠)

AndroidObservable.bindActivity(this, retrofitService.getImage(url))
.subscribeOn(Schedulers.io())
.subscribe(bitmap -> myImageView.setImageBitmap(bitmap));

저는 AndroidObservable.fromBroadcast() 도 즐겨 이용하는데요, 이것은 여러분이 ObservableBroadcastReceiver 처럼 동작하도록 만들 수 있게 해줍니다. 네트워크 상태가 바뀔때마다 알려주는 예시를 한번 살펴볼까요?

IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
AndroidObservable.fromBroadcast(context, filter)
.subscribe(intent -> handleConnectivityChange(intent));

마지막으로 Views 들과 바인딩되는 ViewObservable 이 있습니다. ViewObservable.clicks() 라는 메소드는 View 가 클릭될때마다 여러분이 이벤트를 받을수 있도록 합니다. ViewObservable.text()TextView 가 내용을 바꿀때마다 이벤트를 받습니다.

ViewObservable.clicks(mCardNameEditText, false)
.subscribe(view -> handleClick(view));

Retrofit

RxJava를 지원하는 주목할 만한 라이브러리가 있습니다. 이름하여 Retrofit! 정말 인기있는 안드로이드를 위한 REST 클라이언트입니다. 보통 비동기 메소드를 정의할때 Callback 을 추가해서 만들지요:

@GET("/user/{id}/photo")
void getUserPhoto(@Path("id") int id, Callback<Photo> cb);

RxJava가 설치 되어 있는 경우엔, 이런 방식 대신에Observable 을 반환하도록 만들 수 있습니다.

@GET("/user/{id}/photo")
Observable<photo> getUserPhoto(@Path("id") int id);

이제 Observable 을 여러분이 원하는 대로 연결할 수 있게 되었네요; data를 받을 뿐만 아니라 전환 할 수도 있습니다!

Retrofit은 Observable 이 많은 REST call들을 하나로 합치는 것을 용이하게 해줍니다. 예를들어, 사진을 받는 호출과, 메타데이터를 받는 호출이 있다면 그것들을 함께 압축할수 있습니다.

Observable.zip(
service.getUserPhoto(id),
service.getPhotoMetadata(id),
(photo, metadata) -> createPhotoWithData(photo, metadata))
.subscribe(photoWithData -> showPhoto(photoWithData));

비슷한 예시를 part2 에서 보여드렸었죠.. (flatMap()을 이용하는 방법이었어요). 여러개의 REST call들을 하나로 만드는 것이 RxJava + Retrofit 이 조합을 가지고 얼마나 쉽게 구현할 수 있는지를 보여드리고 싶었습니다.

Old, Slow Code

Retrofit이 Observables 을 반환하는것은 깔끔하죠, 하지만 여러분이 이런 기능을 제공하지 않는 다른 라이브러리를 사용하고 있다면 어떻게 해야 할까요? 또는 내부적인 어떤 코드를 Observables 로 변환되도록 할 수 있을까요? 근본적으로 말하자면, 낡은 코드를 모든것을 고치지 않고 어떻게 새롭게 짤 수 있을까요?

Observable.just()Observable.from()Observable 객체를 낡은 코드로 부터 만드는 데에 거의 대부분 가능합니다.

private Object oldMethod() { ... }public Observable<Object> newMethod() {
return Observable.just(oldMethod());
}

이 코드는 oldMethod() 가 빠를땐 잘 작동합니다. 하지만 느릴때는 어떨까요? 느린 경우에는 Observable.just() 로 넘기기 전에 oldMethod() 를 호출하기 때문에 thread를 block하게 됩니다.

문제를 해결하기 위해서, 제가 항상 쓰는 방법을 보여 드릴게요 — 느린 부분을 defer() 로 감싸는 방법입니다:

private Object slowBlockingMethod() { ... }
public Observable<Object> newMethod() {
return Observable.defer(() -> Observable.just(slowBlockingMethod()));
}

이제, 반환된 Observable 객체가 slowBlockingMethod() 를 subscribe 하기 전까지 호출하지 않게 되었습니다.

Lifecycle

제일 어려운 부분을 마지막으로 남겨 두었는데요. 어떻게 Activity의 생성주기를 다룰수 있을까요? 이를 위해 두개의 이슈를 살펴봐야 합니다.

  1. configuration이 변할 때 Subscription 을 계속하기 (e.g. rotation)
    여러분이 Retrofit을 이용해 REST 호출을 만들고 결과를 ListView 에 보여주고 싶다고 생각해 봅시다. 만약에 유저가 스크린을 회전시키면 어떻게 해야 할까요? 여러분은 같은 요청을 계속 진행하고 싶을 것입니다, 하지만 어떻게 해야 할까요?
  2. Context 의 복사본 객체들을 유지하는 Observables 로 인한 메모리 누수
    이 문제는 Context 를 유지하는 subscription으로 인해서 발생합니다. View 들과 상호 작용할때 그렇게 많이 하지요! 만약 Observable 이 제 시간에 끝나지 않는다면, 여러분은 쓸모없는 메모리를 유지한 채로 끝내게 될 것입니다.

유감이지만, 이 두 문제에 있어서는 묘책이 없습니다, 하지만 이를 좀 더 쉽게 다루기 위해서 지킬 수 있는 가이드라인들이 있습니다.

첫번째 문제는 RxJavad에 내장된 캐시 메커니즘으로 해결될 수 있습니다. 같은 Observable 객체를 구독/재구독 (unsubscribe/resubscribe) 함으로 같은 작업의 반복이 없도록 하는 것이지요. 특별히, cache() ( 혹은 replay() )는 unsubscribe 하였더라도 원래의 요청들을 계속 진행할 것입니다. 이 말은 Activity가 재생성되었을때 (recreation) 새로운 subscription으로 재개(resume) 할 수 있다는 의미입니다.

Observable<Photo> request = service.getUserPhoto(id).cache();
Subscription sub = request.subscribe(photo -> handleUserPhoto(photo));
// ...Activity가 다시 생성되려 할대
sub.unsubscribe();
// ...Activity가 다시 생성되었을때
request.subscribe(photo -> handleUserPhoto(photo));

여기서 두가지 경우 모두 같은 request 를 사용하였다는 것을 유념해 주세요; 이 방법으로 근간이 되는 호출은 딱 한번만 일어납니다. 어디에 request 를 두어야 하는지는 여러분에게 남겨 놓았는데요, 이 객체는 어떤 식으로든 생성주기 밖에 있어야 합니다. (예를 들어 fragment나 singleton)

두번째 문제는 생성주기에 따라서 적절히 unsubscribe을 하면 해결 될 수 있습니다. 이를 위해 CompositeSubscription 을 사용하여 모든 Subscription 들을 가지고 한꺼번에 onDestroy()onDestroyView() 에서 unsubscribe 하는 것이 자주 이용되는 패턴입니다.

private CompositeSubscription mCompositeSubscription = new CompositeSubscription();private void doSomething() {
mCompositeSubscription.add(
AndroidObservable.bindActivity(this, Observable.just("Hello, World!"))
.subscribe(s -> System.out.println(s)));
}
@Override
protected void onDestroy() {
super.onDestroy();
mCompoiteSubscription.unsubscribe();
}

조금더 알려 드리면, 여러분은 CompositeSubscription 을 가지고 추가하고 나중에 자동으로 unsubscribe 하는 루트 Activity/Fragment 를 만들 수도 있습니다.

이것은 주의하세요! 한번 CompositeSubscription.unsubscribe() 를 호출하면 그 객체는 더이상 쓸 수 없습니다, 여러분이 추가한 모든 것을 자동적으로 unsubscribe 하기 때문이죠! 만약 여러분이 재 사용 패턴을 고려하고 있다면 여러분은 대체해서 새로운 CompositeSubscription 을 생성해야만 합니다.

두 문제를 해결하기 위해서 코드를 추가해야 하는데, 언젠가는 어떤 지니어스 한 분이 이런 가이드라인 (boilerplate) 없이도 해결할 수 있는 방법을 가지고 나타나기를 기대해 봅니다.

결론?

RxJava는 여전히 새롭고 Android에도 계속해서 적응을 해가고 있는 상황입니다. 사람들은 이것을 계속해서 보완해 나가고 있습니다; RxAndroid는 활발히 개발 중이고 아직 알려진 좋은 사용 예시들은 거의 없습니다. 지금부터 일년쯤 걸려서 여기에 있는 몇몇 조언들이 조명을 받게 될것이라 생각합니다.

지금까지, 저는 RxJava가 코딩을 쉽게 만들어 줄 뿐만 아니라 정말 재밌다는 것도 알게 되었습니다. 여러분에게 여전히 납득이 되지 않는다면, 저를 찾아주시고 나중에 맥주 한잔 하면서 얘기해 봅시다.

Thanks again to Matthias Kay for proofreading this article. Join him in making RxAndroid awesome!

--

--