hojunho / main

1 stars 0 forks source link

[210224] 실습 4 #9

Open hojun-lee opened 3 years ago

hojun-lee commented 3 years ago

개요

내용

1. Reactive Streams 복습

2. Reactor(Flux, Mono) 복습

3. Spring WebFlux + Mustache 기반 김치프리미엄 사이트 만들기

4. Docker Image 만들기

5. AWS ec2

hojun-lee commented 3 years ago

김준호 TodoList

이호준 TodoList

joonokimu commented 3 years ago

Reactive Streams

  1. Non-Blocking (논블로킹) & BackPressure (백프레셔)를 이용하여 비동기 서비스를 할 때 기본이 되는 스펙이다.
    • RxJava, Spring5 Webflux의 Core에 있는 Project Reactor 모두 해당 스펙을 따르고 있다.

Goals, Design and Scope

  1. 계속적으로 들어오는 스트림 데이터를 효율적으로 처리하기 위해서는 비동기 시스템이 효과적이다.
  2. 다만, 데이터 처리가 목적지의 리소스 소비를 예측가능한 범위에서 신중하게 제어할 수 있어야 한다.
    • 즉, data가 들어올 지 예측이 가능하도록 해야 한다.
    • 이 부분이 바로 BackPressure가 이를 달성할 수 있도록 해주는 중요한 부분이다.

요약

  1. 잠재적으로 무한한 숫자의 데이터 처리
  2. 순서대로 처리
  3. 컴포넌트간에 데이터를 비동기적으로 전달
  4. backpressure를 이용한 데이터 흐름 제어

BackPressure (배압)

  1. 한 컴포넌트가 부하를 이겨내기 힘들 때, 과부하 상태의 컴포넌트에서 치명적인 장애가 발생하거나 제어 없이 메시지를 유실해서는 안 된다.
  2. 컴포는터가 대처할 수 없고 장애가 발생해서는 안되게 때문에 컴포넌트는 상류 컴포넌트들에 자신이 과부하 상태라는 것을 알려 부하를 줄여야 한다.
  3. 이러한 방법이 바로 BackPressure 이다.

API Components

  1. Reactive Stream API의 구성요소는 아래와 같다.

    • Publisher
    • Subscriber
    • Subscription
    • Processor
  2. Publisher는 무한한 데이터를 제공한다. 제공된 data는 Subscriber가 구독하는 형식으로 처리된다.

    • Publisher.subscribe(Subscriber)의 형식으로 data 제공자와 구독자가 연결을 맺게 된다.
    • 호출되는 순서는 아래와 같다.

      onSubscribe -> onNext -> (onError | onComplete)?

명세서

public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}
public interface Subscriber<T> {
    public void onSubscribe(Subscription s);
    public void onNext(T t);
    public void onError(Throwable t);
    public void onComplete();
}
public interface Subscription {
    public void request(long n);
    public void cancel();
}

flow

image

  1. Publisher에 본인이 소유할 Subscription을 구현하고 publishing 할 data를 만든다.
  2. Publisher는 subscribe() 메서드를 통해 subscriber를 등록한다.
  3. Subscriber는 onSubscribe() 메서드를 통해 Subscription을 등록하고 Publisher를 구독하기 시작한다. 이는 Publisher에 구현된 Subscription을 통해 이루어진다. 이렇게 하면 Publisher와 Subscriber는 Subscription을 통해 연결된 상태가 된다. onSubscribe() 내부에 Subscription의 request()를 요청하면 그때부터 data 구독이 시작된다.
  4. Subscriber는 Subscription 메서드의 request() 혹은 cancel() 호출을 통해 data의 흐름을 제어할 수 있다.
  5. Subscription의 request()에는 조건에 따라 Subscriber의 onNext(), onComplete() 또는 onError()를 호출한다.
  6. 그리고 Subscriber의 해당 메서드의 로직에 따라 request() 혹은 cancel()로 제어하게 된다.

Reactor Core

  1. 리액터는 JVM 위에서 동작하는, 완전한 논블로킹 리액티브 프로그래밍을 위한 기반 라이브러리로, backpressure를 관리한다.

  2. 리액터는 자바 8의 함수형 API ( CompletableFuuture, Stream, Duration)을 직접 통합한다.

    • 이를 통해 비동기 시퀀스 API (Flux, Mono)를 구현한다.
  3. 리액터를 사용하면 reactor-netty 프로젝트 프로세스와 논블로킹 방식으로 통신할 수 있다.

    • 리액터 네티는 MSA에 적합한 backpressure를 지원하는 HTTP (웹소켓 포함), TCP, UDP 네트워크 엔진을 제공한다.
  4. Reactor 3는 BOM 모델을 지원한다.

Reactive Programming

1 .마이크로소프트가 닷넷(.NET) 생태계에 만든 Reactive Extension(Rx) 라이브러리가 반응형 프로그래밍의 출발점이었다. 이후 RxJava는 JVM 위에서 실행하는 리액티브 프로그래밍을 구현했다. 시간이 지남에 따라, 리액티브 스트림 표준화의 일환으로 JVM 위에서 동작하는 리액티브 라이브러리의 인터페이스 셋과 상호작용 규칙을 정의한 자바 표준이 등장했다. 이 인터페이스들은 자바 9의 Flow 클래스로 통합됐다.

  1. 리액티브 스트림을 구현한 모든 라이브러리는 Iterable-Iterator 쌍과 성격이 유사하다. 주요 차이점 중 하나는 이터레이터는 pull 기반, 리액티브 스트림은 push 기반이라는 것이다.

    • 이터레이터를 사용하는 것은, 값에 접근하는 것은 전적으로 Iterable 책임이다. (명령형 프로그래밍 패턴). 데이터 시퀀스에서 next() 아이템에 접근하는 것은 개발자에 달려 있다.
    • 리액티브 스트림에선 Publisher-Subscriber 쌍이 이를 대신한다. 단, 새로운 데이터가 있음을 Publisher가 Subscriber에게 통지하며, 이런 push 방식이 리액티브의 핵심이다. 또한, push 받은 데이터에 적용할 연산은 명령형이 아닌 선언형으로 표현한다: 프로그래머는 정확한 제어 흐름을 작성하는 대신 계산 논리를 표현한다.
  2. 리액티브 스트림은 데이터를 push하는 것 외에 에러 처리와 완료 처리도 잘 정의하고 있다. Publisher는 Subscriber에 새 값을 푸쉬할 수 있을 뿐 아니라(onNext 호출함으로써), 에러(onError 호출)나 완료(onComplete 호출) 신호를 보낼 수도 있다. 에러, 완료 신호 모두 시퀀스를 종료한다. 이는 다음과 같이 요약된다:

onNext x 0..N [onError | onComplete]

  • 이 접근법은 굉장히 유연하다. 값이 없거나, 하나거나, n개일 때를(연속적인 시간 값 같은 무한 시퀀스도 포함) 모두 커버한다.

Blocking Can Be Wasteful

  1. 프로그램의 성능을 끌어 올리는 방법은 크게 두 가지가 있다.

    • 더 많은 스레드와 하드웨어 리소스를 사용해 병렬 처리한다.
    • 현재 사용 중인 리소스를 더 효율적으로 사용할 방법을 찾는다.
  2. 자바 개발자는 보통 블로킹 코드로 프로그램을 작성한다. 성능에 병목이 생기지만 않는다면 이 방법도 괜찮다. 이때까진 유사한 블로킹 코드를 실행할 스레드를 늘리면 된다. 하지만 이 방식은 리소스를 더 사용하는 쪽으로 확장하기 때문에 경합이나 동시성 이슈가 발생하기 쉽다.

    • 더 큰 문제는 블로킹은 리소스를 낭비한다는 점이다. 자세히 관찰해보면, 대기 시간이 필요한 요청을 처리하기 시작하면(주로 데이터베이스 요청이나 네트워크 호출 같은 I/O), 스레드는(아마 꽤 많은 양이) 데이터를 기다리는 동안 아무 일도 하지 않는다.
    • 즉 병렬 처리는 만능 해결책이 아니다. 가능한 한 하드웨어 리소스를 전부 사용하는 게 좋지만, 리소스를 낭비하는 경우는 생각보다 많으며 원인을 알아내는 것도 복잡한 일이다.

Asynchronicity to the Rescue?

  1. 위에서 언급한 두 번째 방식대로, 리소스 효율적으로 사용할 방법을 찾는다면 리소스를 낭비하지 않을 수 있다.
  2. 비동기, 논블로킹 코드를 작성하면 동일 리소스를 사용하는 또 하나의 활성 태스크로 실행을 전환하고, 비동기 처리를 완료하면 현재 프로세스로 돌아올 수 있다.
  3. 그렇다면 JVM 위에서 동작하는 비동기 코드를 만드는 방법은??
    • Callbacks : 리턴 값은 없지만 결과를 받으면 호출할 callback 파라미터를 (람다나 익명 클래스) 추가로 받는 비동기 메소드
    • Futures : 곧바로 Future<T>를 반환하는 비동기 메소드, 비동기 프로세스는 T값을 계산하고, 이를 래핑한 Future 객체로 접근한다. 이 값은 즉시 사용할 수는 없고, 사용이 가능해질 때까지 객체를 폴링할 수 있다. 예를 들어, ExecutorServiceCallable<T> 태스크를 실행할 때 Future 객체를 사용한다.
userService.getFavorites(userId)
           .timeout(Duration.ofMillis(800)) // (1)
           .onErrorResume(cacheService.cachedFavoritesFor(userId)) // (2)
           .flatMap(favoriteService::getDetails) // (3)
           .switchIfEmpty(suggestionService.getSuggestions())
           .take(5)
           .publishOn(UiUtils.uiThreadScheduler())
           .subscribe(uiList::show, UiUtils::errorPopup);

CompletableFuture<List> result = ids.thenComposeAsync(l -> { // (2) Stream<CompletableFuture> zip = l.stream().map(i -> { // (3) CompletableFuture nameTask = ifhName(i); // (4) CompletableFuture statTask = ifhStat(i); // (5)

            return nameTask.thenCombineAsync(statTask, (name, stat) -> "Name " + name + " has stats " + stat); // (6)
        });
List<CompletableFuture<String>> combinationList = zip.collect(Collectors.toList()); // (7)
CompletableFuture<String>[] combinationArray = combinationList.toArray(new CompletableFuture[combinationList.size()]);

CompletableFuture<Void> allDone = CompletableFuture.allOf(combinationArray); // (8)
return allDone.thenApply(v -> combinationList.stream()
        .map(CompletableFuture::join) // (9)
        .collect(Collectors.toList()));

});

List results = result.join(); // (10) assertThat(results).contains( "Name NameJoe has stats 103", "Name NameBart has stats 104", "Name NameHenry has stats 105", "Name NameNicole has stats 106", "Name NameABSLAJNFOAJNFOANFANSF has stats 121");


* 나의 Example
```java
CompletableFuture<List<String>> ids =
                CompletableFuture.supplyAsync(() -> {
                    ArrayList<String> fruits = new ArrayList<String>();
                    fruits.add("apple");
                    fruits.add("banana");
                    fruits.add("grape");
                    fruits.add("lemon");
                    fruits.add("watermelon");

                    return fruits;
                });

        CompletableFuture<List<String>> result = ids.thenComposeAsync(l -> {

            Stream<CompletableFuture<String>> zip =
                    l.stream().map(element -> {
                        CompletableFuture<String> c1 = CompletableFuture.supplyAsync(() -> element + " 의 가격은");
                        CompletableFuture<Integer> c2 = CompletableFuture.supplyAsync(() -> new Integer((int) (Math.random() * 1000 + 1)));

                        return c1.thenCombineAsync(c2, (s1, s2) -> s1 + " " + s2);
                    });

            List<CompletableFuture<String>> combinationList = zip.collect(Collectors.toList());
            CompletableFuture<String>[] combinationArray = combinationList.toArray(new CompletableFuture[combinationList.size()]);

            CompletableFuture<Void> allDone = CompletableFuture.allOf(combinationArray);
            return allDone.thenApply(v -> combinationList.stream()
                    .map(CompletableFuture::join)
                    .collect(Collectors.toList()));
        });

        List<String> result2 = result.join();
        for(int i = 0; i < result2.size(); i++) {
            log(result2.get(i));
        }
23:03:23.576885 (main) apple 의 가격은 97
23:03:23.623924 (main) banana 의 가격은 333
23:03:23.624096 (main) grape 의 가격은 287
23:03:23.624216 (main) lemon 의 가격은 71
23:03:23.624339 (main) watermelon 의 가격은 6

Flux combinations = ids.flatMap(id -> { // (2) Mono nameTask = ifhrName(id); // (3) Mono statTask = ifhrStat(id); // (4)

        return nameTask.zipWith(statTask, // (5)
                (name, stat) -> "Name " + name + " has stats " + stat);
    });

Mono<List> result = combinations.collectList(); // (6)

List results = result.block(); // (7) assertThat(results).containsExactly( // (8) "Name NameJoe has stats 103", "Name NameBart has stats 104", "Name NameHenry has stats 105", "Name NameNicole has stats 106", "Name NameABSLAJNFOAJNFOANFANSF has stats 121" );


### 알아두어야 할 Java 8 함수
1. **CompletableFuture** ?
* **supplyAsync(), runAsync()**
    * 이 두 개의 메서드를 제공하여 직접 쓰레드를 생성하지 않고 작업을 async 하도록 처리할 수 있다.
    * supplyAsync() 는 ``Suppiler``, runAsync()는 ``Runnable`` 을 넘길 수 있다.

```java
CompletableFuture<String> completableFuture
                = CompletableFuture.supplyAsync(() -> {
            try {
                log("언제?");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log("리턴 끝났니?");
            return "Hi";
        });

        Thread.sleep(5000);
        log(completableFuture.get());
22:28:32.128191 (ForkJoinPool.commonPool-worker-3) 언제?
22:28:33.181045 (ForkJoinPool.commonPool-worker-3) 리턴 끝났니?
22:28:37.114568 (main) Hi
CompletableFuture<Void> future
        = CompletableFuture.runAsync(() -> log("future example"));

log("get(): " + future.get());
CompletableFuture<String> completableFuture
                = CompletableFuture.supplyAsync(() -> {
            try {
                log("DEBUG01");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log("DEBUG02");
            return "Hi";
        }).thenApply(e -> {
            try {
                log("DEBUG03");
                Thread.sleep(1000);
            } catch(Exception e2) {}
            log("DEBUG04");
            return "너는 무엇을 리턴할래? " + e;
        });

        Thread.sleep(5000);
        log(completableFuture.get());
22:30:38.878453 (ForkJoinPool.commonPool-worker-3) DEBUG01
22:30:39.931372 (ForkJoinPool.commonPool-worker-3) DEBUG02
22:30:39.931924 (ForkJoinPool.commonPool-worker-3) DEBUG03
22:30:40.933847 (ForkJoinPool.commonPool-worker-3) DEBUG04
22:30:43.847623 (main) 너는 무엇을 리턴할래? Hi

log("future.get(): " + future.get());


```console
203 (main) future2.get(): Future1 + Future2
22:32:32.210233 (ForkJoinPool.commonPool-worker-3) DEBUG01
22:32:33.245620 (ForkJoinPool.commonPool-worker-3) DEBUG02
22:32:33.245980 (ForkJoinPool.commonPool-worker-3) DEBUG03
22:32:34.246671 (ForkJoinPool.commonPool-worker-3) DEBUG04
22:32:34.256238 (ForkJoinPool.commonPool-worker-3) 끝!
22:32:37.199061 (main) result : null
22:35:28.799721 (ForkJoinPool.commonPool-worker-3) DEBUG01
22:35:29.836719 (ForkJoinPool.commonPool-worker-3) DEBUG02
22:35:33.793638 (main) result : Hi thenCompose?
22:40:25.844595 (main) DEBUG01
22:40:28.915416 (main) DEBUG02
22:40:29.933201 (main) haha Hello1, haha Hello2
22:43:35.671769 (ForkJoinPool.commonPool-worker-5) DEBUG02
22:43:37.658641 (ForkJoinPool.commonPool-worker-3) DEBUG01
22:43:37.673139 (ForkJoinPool.commonPool-worker-3) haha Hello1, haha Hello2
22:47:30.790517 (main) future1.isDone() : true
22:47:30.843937 (main) future2.isDone() : true
22:47:30.844302 (main) future3.isDone() : true
22:47:30.852526 (main) Combined: future1 + future2 + future3
  1. Map과 FlatMap의 차이는?
    • 많이 사용 되는 Optional과 Stream에서 map의 함수 정의는 아래와 같다.
      <R> Stream<R> map(Function<? super T, ? extends R> mapper);
      <U> Optional<U> map(Function<? super T, ? extends U> mapper)
String[][] data = new String[][]{ {"1", "2"}, {"3", "4"} };

// Arrays.stream(data) 가 Stream<Stream<String>> 형태 일 것임

Stream<Stream<String>> map = Arrays.stream(data).map(x -> Arrays.stream(x));
map.forEach(s -> s.forEach(System.out::println));

Stream<String> flatmap = Arrays.stream(data).flatMap(x -> Arrays.stream(x));
flatmap.forEach(System.out::println);

//without .flatMap() Stream stream = sample.stream() .filter(alpha -> "a".equals(alpha[0].toString() || "a".equals(alpha[1].toString()))) stream.forEach(alpha -> System.out.println("{"+x[0]+", "+x[1]+"}"));


```console
//output
{a, b}
{e, a}
{a, h}
String[][] sample = new String[][]{
  {"a", "b"}, {"c", "d"}, {"e", "a"}, {"a", "h"}, {"i", "j"}
};

//without .flatMap()
Stream<String> stream = sample.stream()
  .flatMap(array -> Arrays.stream(array))
  .filter(x-> "a".equals(x));

stream.forEach(System.out::println);

From Imperative to Reactive Programming

  1. 쉽게 구성 할 수 있고, 가독성이 있다.
    • 이전 태스크의 결과는 다음 태스크의 입력으로 사용되는 특징
  2. 데이터는 풍부한 연산자로 조작할 수 있는 플로우로 표현한다.
  3. 구독하기 전까진 아무일도 일어나지 않는다.
    • 리액터에서 Publisher 체인을 작성한다고 해서 데이터를 바로 공급하진 않는다. 구독을 해야 PublisherSubscriber가 연결되고, 전체 체인에 데이터 흐름이 트리거 된다.
  4. Backpressure 즉, 컨슈머가 프로듀서에 데이터 생산 속도가 너무 빠르다는 신호를 보낼 수 있다.
Flux.range(1, Integer.MAX_VALUE)
    .log()
    .concatMap(x -> Mono.delay(Duration.ofMillis(100)), 1) // simulate that processing takes time
    .blockLast();
onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription)
request(1)
onNext(1)
request(1)
onNext(2)
request(1)
onNext(3)
request(1)
onNext(4)
  1. 고수준이면서도 동시성에 구애받지 않을 정도의 높은 수준으로 추상화한다.

Reactor Core Features

  1. Publisher를 구현하고 있는 리액티브 타입을 (Flux, Mono) 다양하게 구성할 수 있으며 풍부한 연산자를 함께 제공한다.
  2. Flux 는 0~N개의 요소가 있는 리액티브 시퀀스를, Mono 객체는 결과가 단일 값 혹은 빈 값 (0~1)일 때를 나타낸다.

Flux 비동기 시퀀스 (0-N 결과)

Mono 비동기 시퀀스 (0-1 결과)

  1. Mono<T>는 최대 1개 아이템 생산에 특화된 Publisher로 onComplete 혹은 onError 신홀 종료된다.

Flux 와 Mono를 만들고 구독하는 간단한 방법

  1. FluxMono를 생성하는 가장 쉬운 방법은 팩토리 메소드 중 하나로 각 클래스를 생성하는 것이다.
  2. 예를 들어, String의 시퀀스는 아래처럼 단순히 나열하여 생성 가능
    
    Flux<String> seq1 = Flux.just("foo", "bar", "foobar");

List iterable = Arrays.asList("foo", "bar", "foobar"); Flux seq2 = Flux.fromIterable(iterable);

Mono noData = Mono.empty(); Mono data = Mono.just("foo"); Flux numbersFromFiveToSeven = Flux.range(5, 3);

* 팩토레 메소드는 값이 없어도 제네릭 타입이 필요하다는 점에 주의

3. Flux와 Mono를 구독할 때는 자바 8 람다를 사용하고, ``.subscribe()`` 메소드는 여러 가지 콜백을 조합할 수 있다.
```java
subscribe(); // (1)

subscribe(Consumer<? super T> consumer); // (2)

subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer); // (3)

subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer,
          Runnable completeConsumer); // (4)

subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer,
          Runnable completeConsumer,
          Consumer<? super Subscription> subscriptionConsumer); // (5)
  1. subscribe 메소드 예제
    Flux<Integer> ints = Flux.range(1, 3);
    ints.subscribe();
    Flux<Integer> ints2 = Flux.range(1, 3);
    ints2.subscribe(System.out::println);
    Flux<Integer> ints3 = Flux.range(1, 4)
        .map(i -> {
            if(i <= 3) return i;
            throw new RuntimeException("Go to 4");
        });
    ints3.subscribe(i -> System.out.println(i), // (5)
        error -> System.err.println("Error: " + error));
1
2
3
1
2
3
Error: java.lang.RuntimeException: Go to 4

Flux, Mono 유용 API

  1. subscribeOn()
    • subscribeOn()을 사용하면 Subscriber가 시퀀스에 대한 request 신호를 별도 스케줄러로 처리한다.
      Flux.range(1, 6)
      .log()
      .map(i -> {
          log("map: " + i + " + 10");
          try {
              Thread.sleep(5000);
          }catch(Exception e) {}
          return i + 10;
      })
      .subscribe(e -> log("e"));
      log("hi");
      14:30:33.448990 (main) map: 1 + 10
      14:30:38.455154 (main) e
      2021-03-10 14:30:38.455  INFO 20134 --- [           main] reactor.Flux.Range.1                     : | onNext(2)
      14:30:38.456016 (main) map: 2 + 10
      14:30:43.460976 (main) e
      2021-03-10 14:30:43.461  INFO 20134 --- [           main] reactor.Flux.Range.1                     : | onNext(3)
      14:30:43.461581 (main) map: 3 + 10
      14:30:48.464945 (main) e
      2021-03-10 14:30:48.465  INFO 20134 --- [           main] reactor.Flux.Range.1                     : | onNext(4)
      14:30:48.465640 (main) map: 4 + 10
      14:30:53.467300 (main) e
      2021-03-10 14:30:53.467  INFO 20134 --- [           main] reactor.Flux.Range.1                     : | onNext(5)
      14:30:53.468461 (main) map: 5 + 10
      14:30:58.473317 (main) e
      2021-03-10 14:30:58.473  INFO 20134 --- [           main] reactor.Flux.Range.1                     : | onNext(6)
      14:30:58.473830 (main) map: 6 + 10
      14:31:03.476757 (main) e
      2021-03-10 14:31:03.477  INFO 20134 --- [           main] reactor.Flux.Range.1                     : | onComplete()
      14:31:03.477850 (main) hi
Flux.range(1, 6)
                .log()
                .map(i -> {
                    log("map: " + i + " + 10");
                    try {
                        Thread.sleep(5000);
                    }catch(Exception e) {}
                    return i + 10;
                })
                .subscribeOn(Schedulers.newElastic("SUB"))
                .subscribe(e -> log("e"));
         log("hi");
14:32:43.902117 (SUB-2) map: 1 + 10
14:32:43.898682 (main) hi
14:32:48.907498 (SUB-2) e
2021-03-10 14:32:48.907  INFO 20463 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(2)
14:32:48.908243 (SUB-2) map: 2 + 10
14:32:53.908494 (SUB-2) e
2021-03-10 14:32:53.908  INFO 20463 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(3)
14:32:53.909254 (SUB-2) map: 3 + 10
14:32:58.909905 (SUB-2) e
2021-03-10 14:32:58.910  INFO 20463 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(4)
14:32:58.910561 (SUB-2) map: 4 + 10
  1. blockLast()
Flux.range(1, 6)
        .log()
        .map(i -> {
            log("map: " + i + " + 10");
            try {
                Thread.sleep(5000);
            }catch(Exception e) {}
            return i + 10;
        })
        .subscribeOn(Schedulers.newElastic("SUB"))
        .blockLast();
 log("hi");
2021-03-10 14:33:42.185  INFO 20522 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(1)
14:33:42.189677 (SUB-2) map: 1 + 10
2021-03-10 14:33:47.196  INFO 20522 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(2)
14:33:47.196895 (SUB-2) map: 2 + 10
2021-03-10 14:33:52.197  INFO 20522 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(3)
14:33:52.197841 (SUB-2) map: 3 + 10
2021-03-10 14:33:57.198  INFO 20522 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(4)
14:33:57.198830 (SUB-2) map: 4 + 10
2021-03-10 14:34:02.198  INFO 20522 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(5)
14:34:02.199265 (SUB-2) map: 5 + 10
2021-03-10 14:34:07.203  INFO 20522 --- [          SUB-2] reactor.Flux.Range.1                     : | onNext(6)
14:34:07.204528 (SUB-2) map: 6 + 10
2021-03-10 14:34:12.210  INFO 20522 --- [          SUB-2] reactor.Flux.Range.1                     : | onComplete()
14:34:12.211557 (main) hi

Disposable을 사용하여 subscribe()를 취소

  1. subscribe() 메소드는 모두 Disposable 타입을 리턴하고, 여기서 이 인터페이스를 이용해 dispose() 메소드 호출로 구독을 취소할 수 있다.
  2. Flux, Mono 관점에서 취소는 소스가 데이터 생산을 중단한다는 신호다. 하지만 즉각적인 중단을 보장하지는 않는다. 데이터 소스가 취소 명령을 받기 전에 데이터를 생산하고 완료 처리할 수도 있다.

Spring Web on Reactive Stack

  1. 스프링 웹플럭스 탄생 배경

    • 적은 쓰레드로 동시 처리 제어 논블로킹 웹 스택
    • 함수형 프로그래밍
    • continuation-style API로 비동기 로직을 선언적으로 작성
  2. "Reactive"

    • 리액티브 라는 용어는 변화에 반응한다는 뜻 (예를 들어, I/O 이벤트에 반응하는 네트워크 컴포넌트, 마우스 이벤트에 반응하는 UI 컨트롤러)
    • 논블로킹 역시 작업을 기다린다기보단 완료되거나 데이터를 사용할 수 있게 되면 반응하므로 "리액티브"의 일종
    • 중요한 메커니즘 중 하나가 backpressure이다.

2-1. Reactive API

2-2. Programming Models

2-3. Functional Endpoints

RouterFunction route = route() .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) .GET("/person", accept(APPLICATION_JSON), handler::listPeople) .POST("/person", handler::createPerson) .build();

public class PersonHandler {

// ...

public Mono<ServerResponse> listPeople(ServerRequest request) {
    // ...
}

public Mono<ServerResponse> createPerson(ServerRequest request) {
    // ...
}

public Mono<ServerResponse> getPerson(ServerRequest request) {
    // ...
}

}



2-4. Spring MVC vs Webflux?
![image](https://user-images.githubusercontent.com/78422766/110593093-7c0e1880-81be-11eb-8101-817450cf989e.png)
* Spring MVC 어플리케이션에서 외부 서비스를 호출한다면 한번 리액티브 ``WebClient`` 만 사용해 볼 수 있다. 
* 논블로킹, 함수형, 선언적 프로그래밍은 러닝커브가 높은 걸 고려하면, 한번에 전환하지 않고 리액티브 ``WebClient``부터 적용해보는 것도 좋은 방법이다.

2-5. Servers
* 스프링 웹플럭스는 톰캣, Jetty, 서블릿 3.1+ 컨테이너에서도, 서블릿 기반이 아닌 Netty나 Undertow에서도 잘 동작한다.
* 스프링 웹플럭스엔 서버 기동이나 중단을 위한 내장 기능이 없고, 스프링 설정과 웹플럭스 구조를 조립해 적은 코드로 손쉽게 어플리케이션을 실행할 수 있다.
* 스프링 부트에선 웹플럭스 스타터가 이 단계를 자동화해준다. 스타터는 기본으로 Netty를 사용하지만, 메이븐 혹은 그라들에서 Jetty, tomcat, undertow로 교체할 수 있다.
* 부트가 Netty를 디폴트로 사용하는 이유는 보통 비동기 논블로킹에 많이 사용하기도 하고, 클라이언트와 서버가 리소스를 공유 할 수 있어서다.
hojun-lee commented 3 years ago

Reactive Streams 복습

1. Reactive Streams란 non-blocking(논블로킹) BackPressure(역압)을 이용하여 비동기 서비스를 할 때 기본이 되는 스펙이다. java의 RxJava, Spring5 Webflux의 Core에 있는 ProjectReactor 프로젝트 모두 해당 스펙을 따르고 있다.

2. Reactive Stream은 다음과 같은 스트림 지향 라이브러리에 대한 표준 및 사양이다.

3. BackPressure

한 컴포넌트가 부하를 이겨내기 힘들 때, 시스템 전체가 합리적인 방법으로 대응해야 한다. 과부하 상태의 컴포넌트에서 치명적인 장애가 발생하거나 제어 없이 메시지를 유실해서는 안 된다. 컴포넌트가 대처할 수 없고 장애가 발생해선 안 되기 때문에 컴포넌트는 상류 컴포넌트들에 자신이 과부하 상태라는 것을 알려 부하를 줄이도록 해야 한다. 이러한 배압은 시스템이 부하로 인해 무너지지 않고 정상적으로 응답할 수 있게 하는 중요한 피드백 방법이다. 배압은 사용자에게까지 전달되어 응답성이 떨어질 수 있지만, 이 메커니즘은 부하에 대한 시스템의 복원력을 보장하고 시스템 자체가 부하를 분산할 다른 자원을 제공할 수 있는지 정보를 제공할 것이다.

4. API Components

Reactive Streams API 구성요소 및 API 명세서이다.

  1. Publisher
  2. Subscriber
  3. Subscription
  4. Processor

Publisher API 명세서 스크린샷 2021-03-13 오후 12 00 37

Subscriber API 명세서 스크린샷 2021-03-13 오후 12 01 19

Subscription API 명세서 스크린샷 2021-03-13 오후 12 01 35

5. 흐름도

스크린샷 2021-03-13 오후 12 08 49

  1. Publisher에 본인이 소유할 Subscription을 구현하고 publishing할 data를 만든다.
  2. Publisher는 subscribe() 메소드를 통해 subscriber를 등록한다.
  3. Subscriber는 onSubscribe() 메소드를 통해 Subscription을 등록하고 Publisher를 구독하기 시작한다.이는 Publisher에 구현된 Subscription을 통해 이루어진다. 이렇게 하면 Publisher와 Subscriber는 Subscription을 통해 연결된 상태가 된다. onSubscribe() 내부에 Subscription의 request()를 요청하면 그때부터 data 구독이 시작된다.
  4. Subscriber는 Subscription 메소드의 request() 또는 cancel() 호출을 통해 data의 흐름을 제어할 수 있다.
  5. Subscription의 request() 에는 조건에 따라 Subscriber의 onNext(), onComplete() 또는 onError()을 호출할 수 있다. 그러면 Subscriber의 해당 메소드의 로직에 따라 request() 또는 cancel로 제어하게 된다.

스크린샷 2021-02-25 오전 1 50 07

스크린샷 2021-02-25 오전 1 50 20

스크린샷 2021-02-25 오전 1 50 32

Reactor 복습

Reactor

Reactor 시작