Young Jun Park (박영준)
Java back-end developer
컬렉션에서 Observable로
BlockingObservable : 리액티브 세상에서 벗어나기
느긋함 포용하기
Observable 구성하기
명령형 방식의 동시성 (zip(), zipWith())
flatMap()을 비동기 체이닝 연산자처럼
스트림으로 콜백 대체하기
주기적으로 변경사항 풀링하기
RxJava의 멀티 스레딩
요약
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);
}
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();
});
}
* 권장하지는 않음
String getJson() {
List<Person> people = personRepository.listPeople();
String json = marshal(people);
return json;
}
BlockingObservable.forEach() : 모든 이벤트가 처리되고 완료될 때 까지 블록됨
Observable.forEach() : 이벤트가 오는 대로 비동기 수신
Observable.toBlocking()
BlockingObservable.from(Observable)
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;
}
Observable<List<Person>> peopleList = peopleStream.toList();
Observable<Person> 을 Observable<List<Person>> 으로 바꿈
onCompleted() 이벤트를 받을 때 까지 모든 Person을 메모리에 버퍼 처리한다
모든 이벤트가 도착할 때 까지 기다리지 않고 lazy하게 버퍼처리함.
완료 통지를 받으면 모든 이벤트를 포함하는 List<Person> 단일 이벤트 방출
BlockingObservable<List<Person>> peopleBlocking = peopleList.toBlocking();
Observable<List<Person>>을 BlockingObservable<List<Person>>으로 바꿈
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;
}
public Observable<Person> listPeople() {
final List<Person> people = query("SELECT * FROM PEOPLE");
return Observable.from(people);
}
public Observable<Person> listPeople() {
return Observable.defer(() -> Observable.from(query("SELECT * FROM PEOPLE")));
}
구독하기 전까지 Observable의 실제 생성을 최대한 늦춘다.
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);
}
List<Person> listPeople(int page) {
return query("SELECT * FROM PEOPLE ORDER BY id LIMIT ? OFFSET ?",
PAGE_SIZE,
page * PAGE_SIZE);
}
Observable<Person> allPeople(int initialPage) {
return Observable
.defer(() -> Observable.from(listPeople(initialPage)))
.concatWith(Observable.defer(() -> allPeople(initialPage + 1)));
}
Observable이 완료되어도 완료를 전파하지 않고, 인자로 받은 Observable을 구독한다.
두 개의 Observable을 하나로 모아 첫 번째가 끝나면 두 번째 Observable로 인계한다.
모든 가능한 페이지 번호를 만들어내고 개별로 각각 호출한다.
void allPages() {
Observable<List<Person>> allPages = Observable
.range(0, Integer.MAX_VALUE)
.map(this::listPeople)
.takeWhile(list -> !list.isEmpty());
}
데이터베이스 질의는 아직 발생하지 않음!
Observable<Person> people = allPages.concatMap(list -> Observable.from(list));
Observable<Person> people = allPages.concatMap(Observable::from);
Observable<Observable<Person>>을 Observable<Person>으로 펼쳐줌
순서가 보장됨
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>을 반환해야 한다.
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<Flight> rxLookupFlight(String flightNo) {
return Observable.defer(() -> Observable.just(lookupFlight(flightNo)));
}
Observable<Passenger> rxFindPassenger(int id) {
return Observable.defer(() -> Observable.just(findPassenger(id)));
}
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를 사용한다.
블로킹 방식과 같은 방식으로 동작한다.
느긋하지만 작동 순서는 같다.
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()가 제공)에서 실행된다.
목록이 매우 길 수 있다.
메일 하나를 보내는데 수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));
}
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();
}
전통적인 API는 대부분 블로킹 방식
보통 API를 호출하기 위해서는 이벤트 리스너라 부르는 콜백을 제공해야함
대표적으로 자바 메시지 서비스(JMS)가 있음
리스너들을 Observable로 교체가 가능하다.
@Component
class JmsConsumer {
@JmsListener(destination = "orders")
public void newOrder(Message message) {
// ...
}
}
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;
}
}
Consumer와 Provider의 역할을 동시에 수행
extends Observable<T> implements Observer<T>
hot Observable
Subscribe 여부와 상관 없이 이벤트 방출 시작
Observer는 구독 시점부터의 이벤트를 받는다.
아무도 구독하지 않으면 이벤트는 버려진다.
단일 값을 전달하는 long getOrderBookLength() 의 변경사항을 추적하려면?
=> 해당 메서드를 충분히 자주 호출해서 차이점을 잡아내야 한다.
Observable.interval(10, TimeUnit.MILLISECONDS)
.map(x -> getOrderBookLength())
.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() : 거쳐간 모든 항목의 기록을 유지. 동일한 항목이 또 나타나면 무시함
subscribeOn()이나 observeOn()으로 요청을 받을 때 마다 새로운 스레드를 생성
스레드를 재사용 할 수 없기 때문에 좋은 선택은 아님
실무에서는 사용할 일이 많지 않을것
newThread()와 비슷하지만 이미 시작된 스레드를 재사용한다.
풀 크기의 제한이 없는 ThreadPoolExecutor와 미슷하게 동작함
CPU리소스가 필요 없는 IO작업에 적합하다.
io() 스케쥴러는 엄청난 숫자의 스레드를 시작할 수 있는데, 응답성 저하의 원인이 됨.
CPU 연산 위주의 작업에 적합하다.
개별 작업이 CPU코어 하나를 완전히 점유할것으로 가정하기 때문에, 코어 숫자보다 많은 작업을 병렬로 실행해도 이득은 없다.
병렬 실행되는 스레드의 개수를 Runtime.getRuntime().availableProcessors() 수로 제한한다.
모든 스레드 앞에 크기 제한이 없는 큐를 둔다.
작업이 스케줄링 되었지만 모든 코어가 사용중이라면 해당 작업은 큐잉된다.
Executor를 Scheduler로 만들어준다.
의미 없는 스레드 이름을 사용하게 됨
높은 부하를 처리하는 프로젝트에서만 권장됨
RxJava는 Executor내부에서 생성되는 독립적인 스레드에 대한 제어는 할 수 없다
캐시의 locality를 위해 같은 작업을 같은 스레드에서 처리할 수 없음
클라이언트 스레드에서 블로킹 방식으로 작업을 진행
Creates and returns a Scheduler that executes work immediately on the current thread.
어떠한 스케줄러도 사용하지 않은 것과 동일한 효과
observeOn()이 여러번 쓰였을 경우 immediate()를 선언한 바로 윗쪽의 스레드에서 진행
호출 스레드를 블로킹하므로 용도가 제한적
이 스케줄러를 사용할 일은 없을것이다
immediate()와 비슷함
같은 스레드에서 작업을 수행한다.
앞서 스케줄링된 모든 작업이 끝났을 때 시작한다.
immediate()는 주어진 작업을 즉시 시작하는 반면, 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
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
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
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
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
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
하나의 스레드에서 완전히 순차적으로 실행됨
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));
}
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()과 동일하다
Observable<BigDecimal> getTotalPrice3() {
return Observable
.just("bread", "butter", "milk", "tomato", "cheese")
.flatMap(prod -> purchase(prod, 1).subscribeOn(Schedulers.io()))
.reduce(BigDecimal::add)
.single();
}
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
마침내 진정한 동시성을 달성!
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
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
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
By Young Jun Park (박영준)