RxJava, Part 2: Operator Operator

Jong Yun Lee
Nspoons
Published in
10 min readFeb 14, 2017

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

원문 : http://blog.danlew.net/2014/09/22/grokking-rxjava-part-2/

Part1 에서는 RxJava의 기본 구조에 대해서 살펴보았고, map() map() operator에 대해서 소개 해 드렸습니다. 하지만 여러분들이 여전히 RxJava를 사용하는데 있어서 어색한것은 전혀 이상한 것이 아닙니다. 아직 충분히 사용해 보지 않았으니까요. :)

RxJava가 강력한 힘을 자랑하는 이유중 큰 부분을 차지하고 있는 것이 RxJava framework에 있는 operators 때문인데요. 더 다양한 operators들을 여러분들에게 소개해 드리기 위해 예시들을 따라가보며 살펴보도록 합시다.

시작하기

이런 함수를 선언했다고 해봅시다.

// Returns a List of website URLs based on a text search
// text search를 가지고 웹싸이트의 URL 리스트를 반환하는 함수
Observable<List<String>> query(String text);

텍스트로 검색하고 그 결과를 띄워주는 견고한 시스템을 만들고 싶은데요. 지난 시간 배운 내용을 가지고 다음과 같이 프로그램을 만들 수 있을것 같네요:

query("Hello, world!")
.subscribe(urls -> {
for (String url : urls) {
System.out.println(url);
}
});

물론 이 프로그램으로는 데이터 스트림(data stream)을 전환(transform)할 수 있지 않아 충분하지 않을것 같네요. 예를들어 만약 각각의 URL을 수정하려고 할때, 그 작업을 모두 Subscriber에서 해야 합니다. 그럼 우리가 알고 있는 map() 트릭으로 가볍게 해결해 봅시다!

저는 urls -> urls 이런식으로 전달하는 map() 함수를 만들 수 도 있었습니다. 하지만 그렇게 하면 모든 map() 함수들이 for-each loop를 그 안에 담아야 한다는 것을 알게 되었죠.

한가닥 희망

Observable.from() 이라는 함수가 있습니다. 이 함수는 아이템들을 가지고 하나하나씩 내보내도록(emit) 합니다:

Observable.from("url1", "url2", "url3")
.subscribe(url-> System.out.println(url);

이것이 도움이 될것 같네요, 어떻게 해결할 수 있는지 한번 봐볼까요:

query("Hello, world!")
.subscribe(urls -> {
Observable.from(urls)
.subscribe(url -> System.out.println(url));
});

for-each loop를 제거 했지만, 여전히 코드가 더럽네요. 여전히 너무 많고 복잡한 subscriptions들이 있네요.. 게다가 수정하기에도 너무 어렵고 못생긴 코드이고, 아직 RxJava의 드러나지 않은 중요한 구조들에 안좋은 영향을 끼치는 구조이기도 하구요.. 후..

더 좋은 방법

우리를 이 상황에서 구조해줄 flapMap() 을 볼때까지 조금만 참아볼까요 ㅎ

Observable.flatMap() 은 한 Observable의 emissions을 가지고 다른 Observable의 emissions를 반환합니다. 이것은 예상치 못한 변화이죠 (ol’ switcheroo): 어떤 stream을 가진것 같이 생각하지만 전혀 다른 stream을 얻게되는..

query("Hello, world!")
.flatmap(new Func1<List<String>, Observable<String>>() {
@Override
public Observable<String> call(List<String> urls) {
return Observable.from(urls);
}
})
.subscribe(url -> System.out.println(url));

위에 코드는 실제로 어떤 일이 일어나고 있는지 보여주기 위해 전체적인 모든 것을 보여 드린 것이구요, lambdas를 이용해서 간략하게 나타낸 모습은 이렇습니다:

query("Hello, world!")
.flatMap(urls -> Observable.from(urls))
.subscribe(url -> System.out.println(url));

flatMap() 이 조금 이상하게 느껴질수도 있을것 같아요. 또다른 Observable을 반환한다는 것(return another Observable)이 도대체 무슨 의미인가?하는 의문이 들 수도 있을 것 같네요. 핵심 개념은 새롭게 반환된 Observable이 바로 Subscriber 가 지켜보게 되는 대상이라는 것입니다. SubscriberList<String> 을 받지 않고, Observable.from() 이 만들어내는 각각의 String 들을 받게 됩니다.

기억을 더듬어보니, 이 파트가 저에게 이해하기 가장 힘들었던 부분인것 같네요, 하지만 아하!하는 순간을 맛본 다음부터는 RxJava에 대해서 더 잘 이해할 수 있었습니다.

더 더 좋은 방법~

지금부터 설명 하는 것은 정말 중요한 것이니 기억해주세요: flapMap() 은 어떤 Observable 든지 반환할 수 있습니다.

두번째 메소드(method)를 만들어 봅시다.

// Returns the title of a website, or null if 404
Observable<String> getTitle(String URL);

URL들을 프린트 하는 대신에, 각각의 받은 웹사이트의 제목들을 프린트 하고 싶은데요. 하지만 몇가지 이슈가 생길것 같아요: 이 메소드는 하나의 URL만 받아서 동작하기 때문입니다. 게다가 String객체를 반환하지도 않고String을 반환하는Observable 객체를 반환합니다.

flapMap() 으로 이 문제를 아주 간단한게 해결할 수 있습니다. Url 리스트를 각각쪼갠다음(split), getTitle()flapMap() 의 각각 url들에 대해서 처리해주면 됩니다.

query("Hello, world!")
.flatMap(urls -> Observable.from(urls))
.flatMap(new Func1<String, Observable<String>>() {
@Override
public Observable<String> call(String url) {
return getTitle(url);
}
})
.subscribe(title -> System.out.println(title));

그리고 한번더 lambda 표현식으로 간결하게 만듭니다.

query("Hello, world!")
.flatMap(urls -> Observable.from(urls))
.flatMap(url -> getTitle(url))
.subscribe(title -> System.out.println(title));

놀랍지 않나요? Observable 객체들을 반환하는 여러개의 메소드들을 조립했습니다! 정말 굉장하네요!

이것 뿐만이 아니라 어떻게 두개의 API 요청을 하나의 체인(chain)으로 조합했는지 눈여겨 봐 주세요. API 요청이 몇개든 간에 이런 작업을 할 수 있습니다. 아마도 모든 API 요청들을 동기화 하고, 콜백 함수들을 연결하고 데이터들을 보여주고 하는 작업이 얼마나 힘든지 알고 있으실것 같아요. 이 과정들로 인해서 콜백 지옥 (callback hell)을 벗어날수 있습니다. 기존과 똑같은 로직이지만 이 reactive call로 인해서 훨씬 간결하게 하나로 표현된 것을 볼수 있죠.

Operators 많아요~

지금까지 두개의 operators를 살펴 보았는데요, 사실 굉장히 많은 것들이 더 있습니다! 우리가 이 코드를 어떻게 더 개선할 수 있을까요?

getTitle() 은 해당 URL이 404오류를 띄울때 null을 반환합니다. “null”은 띄울 필요가 없겠죠; 이것을 필터링 할 수 있습니다.

query("Hello, world!")
.flatMap(urls -> Observable.from(urls))
.flatMap(url -> getTitle(url))
.filter(title -> title != null)
.subscribe(title -> System.out.println(title));

filter() 은 받은 것을 그대로 내보내는데요(emit), 참 거짓 확인(boolean check)을 통과한 것들만 내보냅니다.

최대 5개 결과만 보여주고 싶으면 어떻게 해야 할까요:

query("Hello, world!")
.flatMap(urls -> Observable.from(urls))
.flatMap(url -> getTitle(url))
.filter(title -> title != null)
.take(5)

take() 함수는 최대 명시된 만큼만 내보내도록 합니다. (만약 5개보다 적으면 먼저 멈추게 됩니다.)

이제 각각 제목들을 disk에 저장해보도록 합시다:

query("Hello, world!")
.flatMap(urls -> Observable.from(urls))
.flatMap(url -> getTitle(url))
.filter(title -> title != null)
.take(5)
.doOnNext(title -> saveTitle(title))
.subscribe(title -> System.out.println(title));

doOnNext() 는 아이템이 내보내질 때 마다 별도의 동작들을 추가하도록 합니다, 이 경우에는 타이틀을 저장하는 것이 되겠지요.

정말 쉽게 데이터 스트림을 다루는 것을 볼수 있습니다. 여러분들이 원하는 만큼 새로운 요소들을 이 방법으로 복잡해지지 않으면서 계속 추가할 수 있습니다.

RxJava는 많은 operator들을 제공합니다. 너무 많아서 겁을 줄 수도 있을것 같아요. 하지만 한번 쭉 훑어 봐서 어디까지 사용이 가능한지 보는 것이 좋습니다. 자신의 것으로 만드는데 시간이 조금 걸릴 수도 있겠지만 한번 해보면 큰 힘이 될것입니다.

제공되어 있는 것들을 기반으로, 자신만의 커스텀 operator들을 만들 수도 있습니다! 커스텀 operator를 만드는 것은 이 글의 범위에서 벗어나므로 다루지 않겠습니다만, 만약 해볼만 하다 여겨진다면, 한번 해보시길 바랍니다.

결론

뭐, 여러분이 굉장히 보수적일 수도 있고, 의심이 많은 사람일 수도 있습니다. 왜 이 모든 operator들을 신경을 써야 할까요?

Key idea #3: operator들이 당신이 데이터스트림을 가지고 무엇이든 할 수 있도록 해줍니다

(Key idea #3 : Operators let you do anything to the stream of data)

제한이 되는 것은 자기 자신일 뿐이죠.

여러분은 단순히 operators들의 연결(chains)들만 가지고도 복잡한 로직을 구현할 수 있습니다. 이 방법은 여러분의 코드를 구성가능한(composable) 조각들로 나눌 수 있도록 합니다. 이게 바로 함수 반응형 프로그래밍이라 할 수 있죠 (functional reactive programming). 많이 이용해 볼수록 이 방법은 여러분이 프로그램에 대해 생각하는 방식을 많이 바꾸어 놓을 것입니다.

하나더, 데이터가 전환되고 사용되는것이 얼마나 간단해 졌는지를 생각해 보세요. 이 예제를 모두 마쳤을때, 두개 API 요청을 처리할 수 있게 되었고, 데이터를 변환할 수 있었고, 그 다음 디스크에 저장할 수 있었습니다. Subscriber 는 그것을 전혀 알지 못했는데도 말이죠; Subscriber 는 단순히 Observable<String> 객체를 어떻게 처리할지만 생각하면 되었죠. 은닉화(encapsulation)가 코딩을 훨씬 쉽게 만듭니다!

Part3 에서는 RxJava의 데이터를 변환하는 (예를들어 동시성(concurrency)을 처리한다던지 하는…) 다른 좋은 특징들을 살펴보도록 하겠습니다.

Part3에서 계속됩니다

--

--