RxJAVA, Part 1 : 기초

CCanary Park
13 min readDec 8, 2014

--

안드로이드 개발자들에게서 RxJava가 새로 부상하고 있다. RxJava의 유일한 문제는 진입장벽이 높다는 것이다. 만일 당신이 명령형 프로그래밍의 세상에서 왔다면 함수적 반응 프로그래밍(Functional Reactive Programming)은 어려울 것이다. 하지만 단 한 번만 이해한다면 이것이 최고임을 알 게 될 것이다.

여기서는 RxJava의 맛만을 보여주려고 한다. 이 연재의 목표는 문에 발을 딛게 하는 것이다. 모든 것을 설명하려고 하지 않을 것이다(그렇게 할 수도 없다.). 나는 그저 당신이 RxJava와 그 동작에 대해 관심을 가지게 하고 싶을 뿐이다.

The Basics

반응적인(Reactive) 코드의 기본적인 빌딩 블록(building block)은 Observable들과 Subscriber들이다. Observable은 아이템들을 발행(emit)하고, Subscriber는 이 아이템들을 소비한다(consume).
(*Subscribe — 가장 작은 빌딩 블록은 사실 Observer이다. 하지만 당신을 Observable에 연결하기 위한 방법이 Subscriber이므로 실제로는 훨씬 더 자주 사용된다.)

아이템들이 발행되는 방법에는 패턴이 있다. Observable은 0을 포함한 어떠한 개수의 아이템이라도 발행 할 수 있으며 성공적인 완료 또는 에러에 의해서 종결된다. Observable이 가지고 있는 각각의 Subscriber마다 ObservableSubscriber.onNext()을 몇 번이나 호출하고 Subscriber.onComplete() 또는 Subscriber.onError()가 뒤따른다.

이것은 표준적인 옵저버 패턴과 훨씬 닮아 보인다. 하지만 중요 부분이 하나 다르다 - Observable은 보통 누군가 명시적으로 아이템을 구독(subscribe)하기 전까지는 아이템을 발행하지 않는다*. 다른 말로 하면는 : 듣는 이가 없으면, 나무는 숲에 넘어지지 않는다.

(* "hot" 대 "cold" Observable라는 용어를 사용한다. Hot observable은 듣는이가 없어도 항상 아이템들을 발행한다. Cold observable은 구독자가 있을 때만 아이템들을 발행한다 (그리고 내 모든 예제에서 이러하다). 이 차이는 처음 RxJava를 배울 때에는 중요하지는 않다.)

Hello, World!

이제 구현된 예제로 이 프레임워크가 동작하는 것을 보자. 먼저 기본적인 Observable을 생성하자 :

Observable<String> myObservable = Observable.create(
new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> sub) {
sub.onNext(“Hello, world!”);
sub.onCompleted();
}
}
);

Observable은 "Hello, world!”를 발행한 뒤 종료된다. 이제 데이타를 소비하는 Subscriber을 생성해보자 :

Subscriber<String> mySubscriber = new Subscriber<String>() {
@Override
public void onNext(String s) { System.out.println(s); }
@Override
public void onCompleted() { }
@Override
public void onError(Throwable e) { }
};

이것은 Observable이 발행한 각각의 String을 프린트하는 것이 하는 일의 전부이다.

이제 우리는 myObservablemySubscriber을 가졌으며 subscribe()를 사용하여 서로를 연결시킬 수 있다 :

myObservable.subscribe(mySubscriber);
// Outputs “Hello, world!”

구독(subscription)을 만들면 myObservable은 subscriberonNext()onComplete() 메소드를 호출한다. 그 결과 mySubscriber는 "Hello, world!”를 출력한 뒤 종료된다.

Simpler Code

단지 “Hello, world!”를 말하게 하였을 뿐인데 너무 많은 상용구(boilerplate) 코드를 만들었다. 사실 이는 내가 무슨 일이 일어 났는지를 정확하게 보여주기 위해서 장황한 방법을 사용했기 때문이다. RxJava는 코드 작성을 쉽게 만들어 주는 지름길을 많이 제공한다.

일단 Observable을 간소화하자. RxJava는 일반적인 작업을 위한 Observable 생성 메소드들을 여러 개 내장하고 있다. 이 경우에는 Observable.just()가 아이템을 하나 발행한 뒤 종료한다. 코드는 아래와 같다 :

Observable<String> myObservable =
Observable.just(“Hello, world!”);

(3 엄밀하게 말하면, Observable.just()는 우리의 초기 코드와 정확히 동일하지 는 않다. 하지만 왜 그런지는 part 3이전에는 다루지 않을 것이다.)

다음에는 불필요하게 장황한 Subscriber를 다뤄보자. 우리는 onCompleted()onError() 둘 다 상관하지 않는다. 그 대신에 우리는 onNext()에서 무엇을 수행할지를 정의하는 더 간단한 클래스를 사용할 수 있다.

Action1<String> onNextAction = new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
};

액션들은 Subscriber의 각 파트를 정의할 수 있다. Observable.subscribe()onNext(), onError() 그리고 onComplete()를 대신할 하나, 둘 또는 세 개의 액션 파라미터를 다룰 수 있다. 복제된 우리의 이전 Subscriber는 다음과 같이 생겼다 :

myObservable.subscribe(onNextAction, onErrorAction, onCompleteAction);

하지만, 우리는 onError()onComplete()를 무시할 것이므로 첫 번째 파라미터만 필요로 한다.

myObservable.subscribe(onNextAction);
// Outputs “Hello, world!”

이제 메소드 호출을 서로 연쇄(chaining)시켜 변수들을 제거한다 :

Observable.just(“Hello, world!”)
.subscribe(new Action1<String>() {
@Override
public void call(String s) {
System.out.println(s);
}
});

마지막으로 흉칙한 모습의 Action1 코드를 제거하기 위해 Java8의 람다(lambda)를 이용하자.

Observable.just(“Hello, world!”)
.subscribe(s -> System.out.println(s));

만약 당신이 Android(그러하여 Java8을 사용할 수 없다면)를 개발한다면, retrolambda를 추천한다. 이것은 당신 코드에 있는 장황함을 놀라울 정도로 제거해준다.

Transformation

이제 양념을 조금 쳐보도록 하자.

"Hello, world” 출력에 나의 서명을 덧붙이고 싶다고 하자. 한가지 가능성은 Observer를 바꿔보는 것이다.

Observable.just(“Hello, world! -Dan”)
.subscribe(s -> System.out.println(s));

이것은 당신이 당신의 Observable를 제어할 수 있는 경우에는 잘 될 것이다. 하지만 이것이 보장이 되지 않는 경우도 있다 - 외부 라이브러리를 사용하는 경우에는? 또 다른 잠재적인 문제는 : Observable을 여러 장소에서 사용하지만 가끔만 서명을 추가하고 싶다면 어쩌나?

그럼 대신 우리의 Suscriber를 변경해보는 것은 어떻까?

Observable.just(“Hello, world!”)
.subscribe(s -> System.out.println(s + “ -Dan”));

이 것도 다른 이유들로 인해 여전히 만족스럽지 못하다 : Subscriber은 메인 스레드에서 동작해야 할 수도 있으므로 최대한 가벼운 상태로 두고 싶다. 더 개념적인 레벨에서, Subscriber들은 반응(reacts)하기로 되어 있는 것이지 변화(mutates)시키는 것이 아니다.

다른 중간 과정에서 “Hello, world!”를 변형시킬 수 있다면 쿨해지지 않을까?

Introducing Operators

이제 아이템 변형(transformation) 문제를 조작자(operator)로 해결하는 방법을 보자. Operator는 발행된 item들을 원천인 Observable과 최종의 Subscriber 사이에서 조작하기 위해 사용될 수 있다. RxJava에는 수많은 operator들이 있다. 하지만 유용한 것에 먼저 집중하는 것이 가장 좋다.

이 상황에서는, map() operator를 하나의 발행된 아이템을 다른 것으로 변형하는데 사용할 수 있다.

Observable.just(“Hello, world!”)
.map(new Func1<String, String>() {
@Override
public String call(String s) {
return s + “ -Dan”;
}
})
.subscribe(s -> System.out.println(s));

다시, 람다를 사용하여 단순화시킬 수 있다.

Observable.just(“Hello, world!”)
.map(s -> s + “ -Dan”)
.subscribe(s -> System.out.println(s));

멋지지 않은가? 우리의 map() operator는 기본적으로 아이템을 변형시키는 Observable이다. 우리는 map()호출을 원하는 만큼 연쇄시켜 마지막 Subscriber에서 소모가능한 형태로 데이터를 완벽하게 연마시킬 수 있다.

More on map()

map()에는 흥미로운 면이 있다 : 이것은 원천의 Observable과 동일한 타입의 아이템을 발행할 수 없다!

나의 Subscriber는 텍스트 원형을 출력하는 것에 흥미가 없고 대신 텍스트의 hash를 출력하고 싶어 한다고 가정하자 :

Observable.just(“Hello, world!”)
.map(new Func1<String, Integer>() {
@Override
public Integer call(String s) {
return s.hashCode();
}
})
.subscribe(i -> System.out.println(Integer.toString(i)));

흥미롭게도- 우리는 String으로 시작했지만 Subscriber는 Integer를 받았다. 다시 우리는 람다로 코드를 짧게 할 수 있다.

Observable.just(“Hello, world!”)
.map(s -> s.hashCode())
.subscribe(i -> System.out.println(Integer.toString(i)));

이전에 내가 말했던 것처럼 우리는 Subscriber를 가능한 작게 유지하기를 원한다. hash를 String으로 변경하게 하는 것은 다른 map()이 하도록 하자.

Observable.just(“Hello, world!”)
.map(s -> s.hashCode())
.map(i -> Integer.toString(i))
.subscribe(s -> System.out.println(s));

이것을 보라- 우리의 Observable과 Subscriber는 원래의 코드로 돌아갔다! 우리는 그 사이에 그저 약간의 변형 단계를 추가하였을 뿐이다. 심지어 이전처럼 나의 서명을 덧붙이는 변형도 추가 할 수 있다 :

Observable.just(“Hello, world!”)
.map(s -> s + “ -Dan”)
.map(s -> s.hashCode())
.map(i -> Integer.toString(i))
.subscribe(s -> System.out.println(s));

So What?

At this point you might be thinking “that’s a lot of fancy footwork for some simple code.” True enough; it’s a simple example. But there’s two ideas you should take away:

Key idea #1: Observable와 Subscriber는 무엇이든 할 수 있다.

맹렬하게 상상하라! Anything is possible.

당신의 Observable은 데이터베이스 쿼리이고 Subscriber은 그 결과를 가지고 화면에 보여주는 것일 수 있다. Observable은 화면의 클릭이고 Subscriber은 그에 대한 반응이 될 수 있다. Observable은 인터넷에서 읽은 바이트 스트림이고, Subscriber은 이것을 디스크에 저장하는 것일 수 있다.

이것은 단순히 어떤 문제를 처리할 수 있는 일반적인 프레임워크이다.

Key idea #2: Observable과 Subscriber은 그들 사이의 변형 단계들과는 독립적인다.

나는 원천의 Observable과 최종 Subscriber사이에 원하는 만큼 많은 map()호출을 끼어들게 할 수 있다. 이 시스템은 매우 구성 가능하다 : 이것은 쉽게 데이터를 다룰 수 있다. Operator가 올바른 입/출력 데이터로 일하는 한 나는 영원히 연쇄가 되도록 만들 수 도 있다. (Okay, not really, since I’ll hit the bounds of the machine at some point, but you get the idea.)

두개의 중요한 핵심 아이디어들을 조합하면 이 시스템에서 많은 가능성을 볼 수 있다. 하지만 이 시점에서 우리는 우리의 능력을 심하게 제한하는 오직 하나의 operator, map()만을 보았을 뿐이다. 파트2에서는 당신이 RxJava를 이용할 때 사용 가능한 operator들의 거대한 웅덩이에 빠져들 것이다.

Continue onwards to part 2

--

--