4장.

기존 애플리케이션에

리액티브 프로그래밍 적용하기

RxJava를 활용한

리액티브 프로그래밍

기존 애플리케이션에

리액티브 프로그래밍 적용하기

  • 보편적인 자바 애플리케이션의 패턴과 아키텍처에 Rx를 적용한다
  • 명령형 방식에서 함수형리액티브 방식으로

Contents

  1. 컬렉션에서 Observable로

  2. BlockingObservable : 리액티브 세상에서 벗어나기

  3. 느긋함 포용하기

  4. Observable 구성하기

  5. 명령형 방식의 동시성 (zip(), zipWith())

  6. flatMap()을 비동기 체이닝 연산자처럼

  7. 스트림으로 콜백 대체하기

  8. 주기적으로 변경사항 풀링하기

  9. RxJava의 멀티 스레딩

  10. 요약

1. 컬렉션에서 Observable로

class PersonRepository {
    List<Person> listPeople() {
        return query("SELECT * FROM PEOPLE");
    }

    private List<Person> query(String sql) {
        return new ArrayList<>();
    }
}
public Observable<Person> listPeople() {
    final List<Person> people = query("SELECT * FROM PEOPLE");
    return Observable.from(people);
}

Observable.from(Iterable<T>)

Duality (쌍대성)

event Iterable<T> (pull) Observable<T> (push)
데이터 가져오기 T next() onNext(T)
에러 처리 throws Exception onError(Throwable)
완료 !hasNext() onCompleted()
public Observable<Person> listPeople() {
    final List<Person> people = query("SELECT * FROM PEOPLE");

    return Observable.create(subscriber -> {
        Iterator<Person> iterator = people.iterator();

        while (iterator.hasNext()) {
            subscriber.onNext(iterator.next());
        }
        subscriber.onCompleted();
    });
}

Observable.create()

* 권장하지는 않음

2. BlockingObservable

리액티브 세상에서 벗어나기(Observable을 Collection으로)

String getJson() {
    List<Person> people = personRepository.listPeople();
    String json = marshal(people);
    return json;
}

BlockingObservable

  • Observable의 변종(variety)
  • Observable에 독립적
  • 리액티브가 아닌 환경에서 Observable을 다루기 쉽게 해줌
  • blocking 연산자를 제공
  • Observable이 완료될 때 까지 연산자가 블록됨

BlockingObservable vs. Observable

​BlockingObservable.forEach() : 모든 이벤트가 처리되고 완료될 때 까지 블록됨

 

Observable.forEach() : 이벤트가 오는 대로 비동기 수신

BlockingObservable 만들기

  1. Observable.toBlocking()

  2. BlockingObservable.from(Observable)

2. BlockingObservable

String getJson() {
    Observable<Person> peopleStream = personRepository.listPeople();
    Observable<List<Person>> peopleList = peopleStream.toList();
    BlockingObservable<List<Person>> peopleBlocking = peopleList.toBlocking();
    List<Person> people = peopleBlocking.single();

    String json = marshal(people);
    return json;
}

toList()

Observable<List<Person>> peopleList = peopleStream.toList();
  • Observable<Person> Observable<List<Person>> 으로 바꿈

  • onCompleted() 이벤트를 받을 때 까지 모든 Person을 메모리에 버퍼 처리한다

  • 모든 이벤트가 도착할 때 까지 기다리지 않고 lazy하게 버퍼처리함.

  • 완료 통지를 받으면 모든 이벤트를 포함하는 List<Person> 단일 이벤트 방출

toBlocking()

BlockingObservable<List<Person>> peopleBlocking = peopleList.toBlocking();
  • Observable<List<Person>> BlockingObservable<List<Person>>으로 바꿈

single()

List<Person> single = peopleBlocking.single();
  • Observable을 걷어내고 BlockingObservable에서 항목 하나를 뽑아낸다.

  • Observable에서 방출하는 항목이 하나인지 확인한다. (없거나 그 이상인 경우 error)

  • onCompleted() 콜백이 호출될 때 까지 블록된다.

  • Observable<T>.single()   =>   Observable<T>

  • BlockingObservable<T>.single()   =>   T

String getJson() {
    List<Person> people = personRepository
        .listPeople()
        .toList()
        .toBlocking()
        .single();

    String json = marshal(people);
    return json;
}

3. 느긋함 포용하기

public Observable<Person> listPeople() {
    final List<Person> people = query("SELECT * FROM PEOPLE");
    return Observable.from(people);
}

Observable.defer()


public Observable<Person> listPeople() {
    return Observable.defer(() -> Observable.from(query("SELECT * FROM PEOPLE")));
}

구독하기 전까지 Observable의 실제 생성을 최대한 늦춘다.

느긋함(lazy)이 중요한 이유 ?

public void bestBookFor(Person person) {
    Book book;

    try {
        book = recommend(person);
    } catch (Exception e) {
        book = bestSeller();
    }

    display(book.getTitle());
}

적합한 책 추천을 하다가 실패하면 베스트 셀러를 보여주는 코드

public void bestBookFor(Person person) {
    recommend(person)
        .onErrorResumeNext(bestSeller())
        .map(Book::getTitle)
        .subscribe(this::display);
}

RxJava의 선언적인 오류 처리

  • onErrorResumeNext() : upstream에서 발생하는 오류를 잡은다음 제공된 Observable을 구독하는 연산자

private Observable<Book> recommend(Person person) {
    return Observable.defer(Observable::empty);
}

private Observable<Book> bestSeller() {
    return Observable.defer(Observable::empty);
}

4. Observable 구성하기

  • concatWith()

  • concatMap()

  • concatMapIterable()


List<Person> listPeople(int page) {
    return query("SELECT * FROM PEOPLE ORDER BY id LIMIT ? OFFSET ?", 
        PAGE_SIZE, 
        page * PAGE_SIZE);
}

페이지 단위로 데이터를 가져오는 전통적 API


Observable<Person> allPeople(int initialPage) {
    return Observable
            .defer(() -> Observable.from(listPeople(initialPage)))
            .concatWith(Observable.defer(() -> allPeople(initialPage + 1)));
}
  • Observable이 완료되어도 완료를 전파하지 않고, 인자로 받은 Observable을 구독한다.

  • 두 개의 Observable을 하나로 모아 첫 번째가 끝나면 두 번째 Observable로 인계한다.

  • allPeople(0).take(3) 처럼 사용

concatWith()

느긋한 페이지 분할과 이어 붙히기

모든 가능한 페이지 번호를 만들어내고 개별로 각각 호출한다.


void allPages() {
    Observable<List<Person>> allPages = Observable
        .range(0, Integer.MAX_VALUE)
        .map(this::listPeople)
        .takeWhile(list -> !list.isEmpty());
}

데이터베이스 질의는 아직 발생하지 않음!

concatMap()

Observable<Person> people = allPages.concatMap(list -> Observable.from(list));

Observable<Person> people = allPages.concatMap(Observable::from);
  • Observable<Observable<Person>>Observable<Person>으로 펼쳐줌

  • 순서가 보장됨

concatMapIterable()

Observable<Person> people = allPages.concatMap(list -> Observable.from(list));

Observable<Person> people = allPages.concatMapIterable(list -> list);


// 사용 예 : 15페이지 까지의 데이터 가져오기
people.take(15).subscribe(...);
  • concatMap()을 적용하려면 각 페이지별 List<Person>을 일일이 Observable<Person>으로 변환해주어야 함

  • concatMap()과 같은 일을 하면서 개별 업스트림 값이 Iterable<Person>을 반환해야 한다.

concatMap() vs. flatMap()

5. 명령형 방식의 동시성

엔터프라이즈 애플리케이션 : 스레드 하나당 요청 하나 처리

  • TCP/IP 연결 요청 수락

  • HTTP 요청 해석

  • 컨트롤러나 서블릿 호출

  • 데이터베이스 요청을 하는동안 블로킹

  • 결과 처리

  • 결과값을 (JSON 등으로) 인코딩

  • 클라이언트에 바이트 패킷 전달

외부의 여러 API 호출이나 서로 독립적인 SQL 호출을 병렬화 하면 성능을  향상 시킬 수 있다!

전형적인 블로킹 코드

Flight flight = lookupFlight("LOT 783");
Passenger passenger = findPassenger(42);
Ticket ticket = bookTicket(flight, passenger);
sendEmail(ticket);
  • lookupFlight() 와 findPassenger()는 서로 관련이 없다

  • 스레드풀, Future, 콜백 등을 사용할 수 있지만 다루기 힘들다

블로킹 코드를 Observable로 포장하기

Observable<Flight> rxLookupFlight(String flightNo) {
    return Observable.defer(() -> Observable.just(lookupFlight(flightNo)));
}

Observable<Passenger> rxFindPassenger(int id) {
    return Observable.defer(() -> Observable.just(findPassenger(id)));
}

zip(), zipWith()

Observable<Flight> flight = rxLookupFlight("LOT 783");
Observable<Passenger> passenger = rxFindPassenger(42);
Observable<Ticket> ticket = flight.zipWith(passenger, (f, p) -> bookTicket(f, p));
ticket.subscribe(this::sendEmail);

여러 Observable을 동시에 구독하기 위해 zip이나 zipWith를 사용한다.

  • 블로킹 방식과 같은 방식으로 동작한다.

  • 느긋하지만 작동 순서는 같다.

subscribeOn(Scheduler)

Observable<Flight> flight = rxLookupFlight("LOT 783").subscribeOn(Schedulers.io());
Observable<Passenger> passenger = rxFindPassenger(42).subscribeOn(Schedulers.io());
Observable<Ticket> ticket = flight.zipWith(passenger, (f, p) -> bookTicket(f, p));
ticket.subscribe(this::sendEmail);

subscribeOn() : 별도의 Scheduler에서 구독을 수행하도록 한다.

  • flight의 구독과 passenger의 구독이 별도의 스레드에서 실행됨

  • lookupFlight()와 findPassenger()가 동시에 시작된다.

  • bookTicket()은 여전히 블로킹 방식

Observable<Ticket> rxBookTicket(Flight flight, Passenger passenger) {
    return Observable.defer(() -> Observable.just(bookTicket(flight, passenger)));
}


Observable<Ticket> ticket = flight
    .zipWith(passenger, (f, p) -> rxBookTicket(f, p))
    .flatMap(obs -> obs);
  • zipWith()는 Observable<Observable<Ticket>>을 반환한다.

  • flatMap()을 사용해 하나의 Observable<Ticket>으로 합쳐주자.

  • bookTicket()도 main 스레드가 아닌 별도의 스레드(Schedulers.io()가 제공)에서 실행된다.

6. flatMap()을 비동기 체이닝 연산자처럼

Ticket 목록을 발송하려고 할 때 고려해야 할 것

  • 목록이 매우 길 수 있다.

  • 메일 하나를 보내는데 수ms가 필요하며 몇초가 걸릴 수도 있다.

  • 실패하는 경우에도 애플리케이션은 계속해서 깔끔하게 실행되어야 한다.

  • 실패한 티켓은 재발송 대상으로 보고해야 한다.

List<Ticket> failures = new ArrayList<>();

for (Ticket ticket : tickets) {
    try {
        sendMail(ticket);
    } catch (Exception e) {
        log.warn("Failed to send", ticket, e);
        failures.add(ticket);
    }
}

단일 스레드를 사용하여 순차적으로 메일을 보내는 코드

public void sendMailAsync(List<Ticket> tickets) {
    List<Pair<Ticket, Future<SmtpResponse>>> tasks = tickets.stream()
        .map(ticket -> Pair.of(ticket, sendEmailAsync(ticket)))
        .collect(Collectors.toList());

    List<Ticket> failures = tasks.stream()
        .flatMap(pair -> {
            try {
                Future<SmtpResponse> future = pair.getRight();
                future.get(1, TimeUnit.SECONDS);
                return Stream.empty();
            } catch (Exception e) {
                Ticket ticket = pair.getLeft();
                log.warn("Failed to send", ticket, e);
                return Stream.of(ticket);
            }
        })
        .collect(Collectors.toList());
}

private Future<SmtpResponse> sendEmailAsync(Ticket ticket) {
    return pool.submit(() -> sendMail(ticket));
}

스레드풀을 사용하여 비동기 처리하는 코드

public void rxSendEMail(List<Ticket> tickets) {
    Observable.from(tickets)
        .flatMap(ticket -> rxSendEmail(ticket)
            .flatMap(response -> Observable.empty())
            .doOnError(e -> log.warn("Failed to send", ticket, e))
            .onErrorReturn(err -> ticket))
        .toList()
        .toBlocking()
        .single();
}

private Observable<SmtpResponse> rxSendEmail(Ticket ticket) {
    return Observable.fromCallable(() -> sendEmail(ticket));
}

Observable로 구현한 코드

public void rxSendMail2(List<Ticket> tickets) {
    Observable.from(tickets)
        .flatMap(ticket -> rxSendEmail(ticket)
            .ignoreElements()
            .doOnError(e -> log.warn("Failed to send", ticket, e))
            .onErrorReturn(err -> ticket)
            .subscribeOn(Schedulers.io()))
        .toList()
        .toBlocking()
        .single();
}

ignoreElements() 연산자 사용

7. 스트림으로 콜백 대체하기

  • 전통적인 API는 대부분 블로킹 방식

  • 보통 API를 호출하기 위해서는 이벤트 리스너라 부르는 콜백을 제공해야함

  • 대표적으로 자바 메시지 서비스(JMS)가 있음

  • 리스너들을 Observable로 교체가 가능하다.

@Component
class JmsConsumer {
    @JmsListener(destination = "orders")
    public void newOrder(Message message) {
        // ...
    }   
}

JmsConsumer example

private final PublishSubject<Message> subject = PublishSubject.create();

@Component
class JmsConsumer {
    @JmsListener(destination = "orders")
    public void newOrder(Message message) {
        subject.onNext(msg);
    }

    Observable<Message> observe() {
        return subject;
    }
}

PublishSubject

Subject

  • Consumer와 Provider의 역할을 동시에 수행

  • extends Observable<T> implements Observer<T>

  • hot Observable

PublishSubject

  • Subscribe 여부와 상관 없이 이벤트 방출 시작

  • Observer는 구독 시점부터의 이벤트를 받는다.

  • 아무도 구독하지 않으면 이벤트는 버려진다.

8. 주기적으로 변경 사항을 폴링하기

단일 값을 전달하는 long getOrderBookLength() 의 변경사항을 추적하려면?

=> 해당 메서드를 충분히 자주 호출해서 차이점을 잡아내야 한다.

Observable.interval(10, TimeUnit.MILLISECONDS)
        .map(x -> getOrderBookLength())
        .distinctUntilChanged();

interval()과 distinctUntilChanged() 사용하기

  • 10ms 마다 long값을 만들어 간단한 카운터로 사용

  • 10ms마다 getOrderBookLength()를 호출함

  • distinctUntilChanged() : 이전 호출과 값이 같은경우 건너뛴다

조금 더 발전시켜서...

Observable<Item> observeNewItems() {
    return Observable
        .interval(1, TimeUnit.MILLISECONDS)
        .flatMapIterable(x -> query())
        .distinct();
}

private List<Item> query() {
    // 파일 시스템의 디렉토리나 DB테이블의 스냅샷
    return new ArrayList<>();
}

distinct() : 거쳐간 모든 항목의 기록을 유지. 동일한 항목이 또 나타나면 무시함

9. RxJava의 멀티 쓰레딩

  • RxJava는 명령형 동시성이 아닌 선언적 동시성

  • 수동으로 쓰레드를 생성하고 관리하지 않음

  • ExecutorService 같은 스레드풀에서 한걸음 더 나아간다.

  • CompletableFuture처럼 논블로킹이기도 하지만 lazy하다.

  • 비동기 Observable은 별도의 스레드에서 Subscriber의 콜백 메서드를 호출한다.

  • Observable은 기본적으로 블로킹 되지만, Scheduler를 사용해 비동기적으로 구현할 수 있다.

Scheduler

  • java.util.concurrent의 ScheduledExecutorService와 비슷함

  • 미래의 가능한 시점에 임의의 코드블록을 실행

  • Observable을 만들 때 subscribeOn()과 observeOn()연산자에 사용된다.

  • 내장 Scheduler를 제공한다.

Schedulers.newThread()

  • subscribeOn()이나 observeOn()으로 요청을 받을 때 마다 새로운 스레드를 생성

  • 스레드를 재사용 할 수 없기 때문에 좋은 선택은 아님

  • 실무에서는 사용할 일이 많지 않을것

Schedulers.io()

  • newThread()와 비슷하지만 이미 시작된 스레드를 재사용한다.

  • 풀 크기의 제한이 없는 ThreadPoolExecutor와 미슷하게 동작함

  • CPU리소스가 필요 없는 IO작업에 적합하다.

  • io() 스케쥴러는 엄청난 숫자의 스레드를 시작할 수 있는데, 응답성 저하의 원인이 됨.

Schedulers.computation()

  • CPU 연산 위주의 작업에 적합하다.

  • 개별 작업이 CPU코어 하나를 완전히 점유할것으로 가정하기 때문에, 코어 숫자보다 많은 작업을 병렬로 실행해도 이득은 없다.

  • 병렬 실행되는 스레드의 개수를 Runtime.getRuntime().availableProcessors() 수로 제한한다.

  • 모든 스레드 앞에 크기 제한이 없는 큐를 둔다.

  • 작업이 스케줄링 되었지만 모든 코어가 사용중이라면 해당 작업은 큐잉된다. 

Schedulers.from(Executor executor)

  • Executor를 Scheduler로 만들어준다.

  • 의미 없는 스레드 이름을 사용하게 됨

  • 높은 부하를 처리하는 프로젝트에서만 권장됨

  • RxJava는 Executor내부에서 생성되는 독립적인 스레드에 대한 제어는 할 수 없다

  • 캐시의 locality를 위해 같은 작업을 같은 스레드에서 처리할 수 없음

Schedulers.immediate()

  • 클라이언트 스레드에서 블로킹 방식으로 작업을 진행

  • Creates and returns a Scheduler that executes work immediately on the current thread.

  • 어떠한 스케줄러도 사용하지 않은 것과 동일한 효과

  • observeOn()이 여러번 쓰였을 경우 immediate()를 선언한 바로 윗쪽의 스레드에서 진행

  • 호출 스레드를 블로킹하므로 용도가 제한적

  • 이 스케줄러를 사용할 일은 없을것이다

Schedulers.trampoline()

  • immediate()와 비슷함

  • 같은 스레드에서 작업을 수행한다.

  • 앞서 스케줄링된 모든 작업이 끝났을 때 시작한다.

  • immediate()는 주어진 작업을 즉시 시작하는 반면, trampoline()은 현재 작업이 끝날때 까지 기다린다.

immediate() vs. trampoline()

Scheduler scheduler = Schedulers.immediate();
Scheduler.Worker worker = scheduler.createWorker();

log.info("Main start");
worker.schedule(() -> {
    log.info(" Outer start");
    sleepOneSecond();
    worker.schedule(() -> {
        log.info("  Inner start");
        sleepOneSecond();
        log.info("  Inner end");
    });
    log.info(" Outer end");
});
log.info("Main end");
worker.unsubscribe();

22:48:04.949 [main] Main start
22:48:05.064 [main]  Outer start
22:48:06.071 [main]    Inner start
22:48:07.074 [main]    Inner end
22:48:07.076 [main]  Outer end
22:48:07.076 [main] Main end

immediate() vs. trampoline()

Scheduler scheduler = Schedulers.trampoline();
Scheduler.Worker worker = scheduler.createWorker();

log.info("Main start");
worker.schedule(() -> {
    log.info(" Outer start");
    sleepOneSecond();
    worker.schedule(() -> {
        log.info("  Inner start");
        sleepOneSecond();
        log.info("  Inner end");
    });
    log.info(" Outer end");
});
log.info("Main end");
worker.unsubscribe();

23:10:28.312 [main] Main start
23:10:28.441 [main]   Outer start
23:10:29.445 [main]   Outer end
23:10:29.445 [main]     Inner start
23:10:30.447 [main]     Inner end
23:10:30.447 [main] Main end

subscribeOn()을 활용한 선언적 구독

  • Observable과 subscribe() 사이에 어디든 subscribeOn()을 삽입하여 실행될 scheduler를 선언적으로 선택

  • 독립적인 Scheduler에서 작업이 수행되므로 블로킹되지 않음

subscribeOn()

log.info("Starting");
final Observable<String> obs = simple();
log.info("Created");

obs.subscribeOn(Schedulers.io())
        .subscribe(
                x -> log.info("Got " + x),
                Throwable::printStackTrace,
                () -> log.info("Completed"));

log.info("Exiting");

23:29:20.999 [main]  - Starting
23:29:21.179 [main]  - Created
23:29:21.235 [RxIoScheduler-2]  - subscribed
23:29:21.236 [RxIoScheduler-2]  - Got A
23:29:21.236 [RxIoScheduler-2]  - Got B
23:29:21.236 [RxIoScheduler-2]  - Completed

23:29:21.236 [main]  - Exiting

subscribeOn()

log.info("Starting");
final Observable<String> obs = simple();
log.info("Created");
obs.subscribeOn(Schedulers.computation())
        .map(String::toLowerCase)
        .subscribeOn(Schedulers.io())
        .subscribe(
                x -> log.info("Got " + x),
                Throwable::printStackTrace,
                () -> log.info("Completed"));
log.info("Exiting");

23:37:29.023 [main] - Starting
23:37:29.229 [main] - Created
23:37:29.299 [main] - Exiting
23:37:29.300 [RxComputationScheduler-1] - subscribed
23:37:29.301 [RxComputationScheduler-1] - Got a
23:37:29.301 [RxComputationScheduler-1] - Got b
23:37:29.301 [RxComputationScheduler-1] - Completed

수많은 스레드로 구성된 Scheduler를 사용하면 자동으로 이벤트를 분기하여 동시에 처리하고 마지막에 모든 결과를 함께 결합한다고 생각하는데,  그렇지 않다!!

subscribeOn()

23:37:29.023 [main] - Starting
23:37:29.229 [main] - Created
23:37:29.299 [main] - Exiting
23:37:29.300 [RxComputationScheduler-1] - subscribed
23:37:29.301 [RxComputationScheduler-1] - Got a
23:37:29.301 [RxComputationScheduler-1] - Got b
23:37:29.301 [RxComputationScheduler-1] - Completed

 => flatMap()과 merge()를 사용해야 진정한 병렬처리를 달성할 수 있다!

Observable<BigDecimal> getTotalPrice() {
    return Observable
            .just("bread", "butter", "milk", "tomato", "cheese")
            .subscribeOn(Schedulers.io())
            .map(prod -> doPurchase(prod, 1))
            .reduce(BigDecimal::add)
            .single();
}

잘못 구현된 코드

private BigDecimal doPurchase(String productName, int quantity) {
    log.info("Purchasing " + quantity + " " + productName);
    SleepUtil.sleep(1000);
    log.info("Done " + quantity + " " + productName);
    return new BigDecimal("1");
}

잘못 구현된 코드 : 실행 결과

00:00:59.212 [RxIoScheduler-2] - Purchasing 1 bread
00:01:00.218 [RxIoScheduler-2] - Done 1 bread
00:01:00.220 [RxIoScheduler-2] - Purchasing 1 butter
00:01:01.222 [RxIoScheduler-2] - Done 1 butter
00:01:01.223 [RxIoScheduler-2] - Purchasing 1 milk
00:01:02.226 [RxIoScheduler-2] - Done 1 milk
00:01:02.226 [RxIoScheduler-2] - Purchasing 1 tomato
00:01:03.228 [RxIoScheduler-2] - Done 1 tomato
00:01:03.228 [RxIoScheduler-2] - Purchasing 1 cheese
00:01:04.230 [RxIoScheduler-2] - Done 1 cheese

하나의 스레드에서 완전히 순차적으로 실행됨

단순히 flatMap()으로만 바꾼 코드

Observable<BigDecimal> getTotalPrice2() {
    return Observable
            .just("bread", "butter", "milk", "tomato", "cheese")
            .subscribeOn(Schedulers.io())
            .flatMap(prod -> purchase(prod, 1))
            .reduce(BigDecimal::add)
            .single();
}
Observable<BigDecimal> purchase(String productName, int quantity) {
    return Observable.fromCallable(() -> doPurchase(productName, quantity));
}

단순히 flatMap()으로만 바꾼 코드 : 실행 결과

00:00:59.212 [RxIoScheduler-2] - Purchasing 1 bread
00:01:00.218 [RxIoScheduler-2] - Done 1 bread
00:01:00.220 [RxIoScheduler-2] - Purchasing 1 butter
00:01:01.222 [RxIoScheduler-2] - Done 1 butter
00:01:01.223 [RxIoScheduler-2] - Purchasing 1 milk
00:01:02.226 [RxIoScheduler-2] - Done 1 milk
00:01:02.226 [RxIoScheduler-2] - Purchasing 1 tomato
00:01:03.228 [RxIoScheduler-2] - Done 1 tomato
00:01:03.228 [RxIoScheduler-2] - Purchasing 1 cheese
00:01:04.230 [RxIoScheduler-2] - Done 1 cheese

map()과 동일하다

subscribeOn()의 위치를 변경!

Observable<BigDecimal> getTotalPrice3() {
    return Observable
            .just("bread", "butter", "milk", "tomato", "cheese")
            .flatMap(prod -> purchase(prod, 1).subscribeOn(Schedulers.io()))
            .reduce(BigDecimal::add)
            .single();
}

subscribeOn()의 위치를 변경! : 결과는?

00:12:19.508 [RxIoScheduler-2] - Purchasing 1 bread
00:12:19.508 [RxIoScheduler-4] - Purchasing 1 milk
00:12:19.508 [RxIoScheduler-5] - Purchasing 1 tomato
00:12:19.508 [RxIoScheduler-3] - Purchasing 1 butter
00:12:19.509 [RxIoScheduler-6] - Purchasing 1 cheese
00:12:20.517 [RxIoScheduler-2] - Done 1 bread
00:12:20.518 [RxIoScheduler-4] - Done 1 milk
00:12:20.519 [RxIoScheduler-5] - Done 1 tomato
00:12:20.519 [RxIoScheduler-3] - Done 1 butter
00:12:20.519 [RxIoScheduler-6] - Done 1 cheese

마침내 진정한 동시성을 달성!

observeOn()으로 선언적 동시성 달성하기

  • RxJava의 동시성은 subscribeOn()과 observeOn() 연산자로 설명할 수 있다.

  • subscribeOn()은 OnSubscribe를 호출할 때 어떤 Scheduler를 사용할 것인지를 제어함

  • observeOn()은 observeOn() 이후에 발생하는 다운스트림 Scheduler를 호출할 때 어떤 Scheduler를 사용할지를 제어함

log.info("Starting");
final Observable<String> obs = simple();
log.info("Created");
obs
        .doOnNext(x -> log.info("Found 1 : " + x))
        .observeOn(Schedulers.io())
        .doOnNext(x -> log.info("Found 2 : " + x))
        .subscribe(
                x -> log.info("Got 1: " + x),
                Throwable::printStackTrace,
                () -> log.info("Completed")
        );
log.info("Exiting");

[main]- Starting
[main]- Created
[main]- subscribed
[main]- Found 1 : A
[RxIoScheduler-2]- Found 2 : A
[RxIoScheduler-2]- Got 1: A
[main]- Found 1 : B
[main]- Exiting
[RxIoScheduler-2]- Found 2 : B
[RxIoScheduler-2]- Got 1: B
[RxIoScheduler-2]- Completed

 

 

observeOn()

subscribeOn()과 여러개의 observeOn()을 함께 사용하는 코드

log.info("Starting");
final Observable<String> obs = simple();
log.info("Created");
obs
        .doOnNext(x -> log.info("Found 1 : " + x))
        .observeOn(schedulerA)
        .doOnNext(x -> log.info("Found 2 : " + x))
        .observeOn(schedulerB)
        .doOnNext(x -> log.info("Found 3 : " + x))
        .subscribeOn(schedulerC)
        .subscribe(
                x -> log.info("Got 1: " + x),
                Throwable::printStackTrace,
                () -> log.info("Completed")
        );
log.info("Exiting");

[main] - Starting
[main] - Created
[main] - Exiting
[Scheduler-C-10] - subscribed
[Scheduler-C-10] - Found 1 : A
[Scheduler-C-10] - Found 1 : B

[Scheduler-A-13] - Found 2 : A
[Scheduler-A-13] - Found 2 : B

[Scheduler-B-14] - Found 3 : A
[Scheduler-B-14] - Got 1: A
[Scheduler-B-14] - Found 3 : B
[Scheduler-B-14] - Got 1: B
[Scheduler-B-14] - Completed

Scheduler의 다른 사용법

  • Observable의 몇몇 연산자는 내부적으로 Scheduler를 사용하고있음

  • 일반적으로 별도의 Scheduler가 제공되지 않으면 Schedulers.computation()을 사용함

  • delay(), interval(), range(), timer(),  repeat(), skip(), take(), timeout() 등이 있음

  • RxJava로 확장성있고 안전한 코드를 작성하려면 스케줄러를 정복해야한다!

Observable.just("A", "B")
        .delay(500, TimeUnit.MILLISECONDS)
        .subscribe(log::info);
Observable.just("A", "B")
        .delay(500, TimeUnit.MILLISECONDS, schedulerA)
        .subscribe(log::info);

[RxComputationScheduler-1] - A
[RxComputationScheduler-1] - B

[Scheduler-A-11] - A
[Scheduler-A-11] - B

10. 요약

  • 거의 모든 API를 Observable로 매끄럽게 대체할 수 있음

  • Rx는 하위 호환성을 유지하면서 구현을 개선할 수 있도록 해줌

  • 느긋함, 선언적 동시성, 연쇄적 비동기 처리같은 이점을 누릴수있다.

  • 리액티브 Observable로 진행하는 작업은 어렵고 학습곡선이 가파름

  • 애플리케이션을 처음부터 끝까지 리액티브 익스텐션을 사용하여 만드는 방법은 5장에서 배워보자!

기존 애플리케이션에 리액티브 프로그래밍 적용하기

By Young Jun Park (박영준)

기존 애플리케이션에 리액티브 프로그래밍 적용하기

  • 196