February 29, 2020
Spring Framwork5에서 새롭게 추가된 모듈이다. web-flux는 client, server에서 reactive 스타일의 어플리케이션 개발을 도와주는 모듈이라고 한다. 스프링 공식문서에 있는, Spring WebFlux에 대한 소개와 Reactive에 대한 개념을 정리한다.
원래 스프링 프레임워크는 Servlet API와 Servlet 컨테이너로 이뤄졌는데 5버전에서 WebFlux가 추가 됐다. WebFlux는 reactive-stack web framework이며 non-blocking에 Reactive stream을 지원한다. webmvc나 webflux 둘다 사용 가능하다.
WebFlux가 생긴 이유는,
(1) 적은 양의 스레드와 최소한의 하드웨어 자원으로 동시성을 핸들링하기 위해 만들어졌다. 서블릿 3.1이 논블로킹을 지원하지만, 일부분이다. 이는 새로운 공통 API가 생긴 이유가 됐으며, netty와 같은 잘 만들어진 async, non-blocking 서버를 사용한다.
(2) 함수형 프로그래밍 때문이다. Java5에서 Rest controllers나 unit test가 만들어지고, Java8에서는 함수형 API를 위한 Lambda 표현식이 추가됐다. 이는 논블로킹 어플리케이션 API의 토대가 됐다.
non-blocking이나 functional이란 단어는 잘 와닿지만, reactive라는 단어는 잘 와닿지 않는다. reactive라는 건 무엇을 의미하는 것일까?
“reactive”라는 건 변화에 반응하게 만들어진 프로그래밍 모델이다. I/O 이벤트에 따라 네트워크 컴포넌트가 반응하고 마우스 이벤트나 다른 것들에 UI 컨트롤러가 반응하는 것과 같은. 즉, non-blocking은 reactive한 것이다. 블락킹을 하는 대신에 오퍼레이션이 완료되거나 데이터가 유효하다는 것에 따른 noti에 반응을 하는 것이다.
동기성에서는, 블로킹 콜은 콜러를 기다리게 하는 자연스러운 형태이다. 논블로킹 코드에서는 빠른 생산자가 목적지를 앞지르지 않게 이벤트 속도를 제어하는 것이 중요하다. reactive stream은 (java9에 포함된) 비동기성 컴포넌트간의 상호작용에 정의된 작은 스펙이다. 예를 들어, publisher가 subscriber가 응답에 쓸 데이터를 생산한다고 하자. reactive stream의 주 목적은 subscriber가 publisher가 생산하는 데이터의 속도를 제어하는 것이다.
결국 spring이 가지지 못했던 비동기 프로그래밍을 보완하기 위한 Spring 버전이라는 의미인 것 같다. 그 버전이 Spring5이고 webflux 모듈을 사용한 것이다.
🔆 WebFlux는 아래와 같은 용도로 사용하는 것을 추천 한다고 합니다. (by 토비)
출처: https://kimyhcj.tistory.com/343 [기억과 기록]
🔆 ‘Spring WebFlux는 어떻게 적은 리소스로 많은 트래픽을 감당할까?‘란 궁금증을 시작으로 여기까지 왔다. 이에 대한 답은 I/O를 Non Blockkng을 이용하여 잘 사용하는 것과 Request를 Event-Driven을 통해서 효율적으로 처리하기 때문에 가능하다.
출처 : https://alwayspr.tistory.com/44
webflux를 사용한다는 건, 위와 같은 경우에 사용한다는 것이다. 사실 동기프로그래밍이 코드를 작성하거나 디버깅하는 데에 더 쉽기 때문에, 동일한 말로 생산성이 높다고 표현할 수 있다. (비동기 프로그래밍은 트레이싱하기가 일단 어렵다.) 그리고 비동기 프로그래밍 중에, 동기화되는 부분의 코드가 있다면 그 나머지 비동기 코드로 작성된 건 무의미해질 수 있다. 동기화되는 부분에서 결국 블락킹이 되기 때문인 것 같다. Java에서 DB connection 쪽이 논블로킹 라이브러리가 잘 사용되지 않는다고 한다. R2DBC처럼 개발이 진행 중인 라이브러리, 최근에 release된 jasync sql, MongoDB, Redis 등의 NoSQL은 지원중이라고 한다.
webflux는 분명 마이크로서비스 아키텍처에 적합한 모듈인 건 분명하지만, webflux를 사용하려면 비동기 프로그래밍에 대한 이해가 우선시 돼야 할 것 같다.
*요새 마이크로서비스 아키텍처를 사용한 개발을 하고 있는데, 서비스 간의 호출이 빈번한 게 뭔지 여실히 느꼈다. 그리고 RestTemplate과 같이 service콜을 하는 모듈이 Blocking I/O일 때의 한계점도 여실히! asyncRestTemplate을 사용해서 Blocking I/O의 한계를 조금은 보완하고 하긴 했으나, 확실히 서비스를 잘게 쪼갠 상태의 아키텍처에는 Non-Blocking I/O는 필요하다.*
Blocking I/O
@Test
public void blocking() {
final RestTemplate restTemplate = new RestTemplate();
final StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 3; i++) {
final ResponseEntity<String> response =
restTemplate.exchange(THREE_SECOND_URL, HttpMethod.GET, HttpEntity.EMPTY, String.class);
assertThat(response.getBody()).contains("success");
}
stopWatch.stop();
System.out.println(stopWatch.getTotalTimeSeconds());
}
Spring의 HTTP 요청 라이브러리인 RestTemplate을 사용하여 3초가 걸리는 API를 3번 호출하였다. 결과는 여러분도 알다시피 9.xx초가 나온다. 이유는 I/O가 요청 중일 때에는 아무 작업도 할 수 없기 때문이다.
Non Blocking I/O
@Test
public void nonBlocking3() throws InterruptedException {
final StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < LOOP_COUNT; i++) {
this.webClient
.get()
.uri(THREE_SECOND_URL)
.retrieve()
.bodyToMono(String.class)
.subscribe(it -> {
count.countDown();
System.out.println(it);
});
}
count.await(10, TimeUnit.SECONDS);
stopWatch.stop();
System.out.println(stopWatch.getTotalTimeSeconds());
}
WebFlux에서 제공하는 WebClient를 사용해서 위와 동일하게 3초가 걸리는 API를 호출하였다. for문 안의 변수인 LOOPCOUNT는 100으로 코드상에서 설정되어있다. 3초 걸리는 API를 100번 호출한다 하더라도 3.xx초 밖에 걸리지 않는다. 더 나아가서 LOOPCOUNT를 1000으로 변경하더라도 필자의 컴퓨터에서는 4.xx초 밖에 걸리지 않는다. Blocking I/O와 비교해봤을 때 정말 효율적이라고 볼 수 있다.
만약, Blocking을 위처럼 많은 요청을 동시에 처리하려면 그 만큼의 Thread이 생성되어야 한다. 그러나 이렇게 처리한다 해도 Context Swiching에 의한 오버헤드가 존재할 것이다.
This part of the documentation covers support for reactive-stack web applications built on a Reactive Streams API to run on non-blocking servers, such as Netty, Undertow, and Servlet 3.1+ containers. Individual chapters cover the Spring WebFlux framework, the reactive WebClient, support for testing, and reactive libraries. For Servlet-stack web applications, see Web on Servlet Stack.
Both web frameworks mirror the names of their source modules (spring-webmvc and spring-webflux) and co-exist side by side in the Spring Framework. Each module is optional. Applications can use one or the other module or, in some cases, both — for example, Spring MVC controllers with the reactive WebClient.
1.1. Overview Why was Spring WebFlux created?
Part of the answer is the need for a non-blocking web stack to handle concurrency with a small number of threads and scale with fewer hardware resources. Servlet 3.1 did provide an API for non-blocking I/O. However, using it leads away from the rest of the Servlet API, where contracts are synchronous (Filter, Servlet) or blocking (getParameter, getPart). This was the motivation for a new common API to serve as a foundation across any non-blocking runtime. That is important because of servers (such as Netty) that are well-established in the async, non-blocking space.
The other part of the answer is functional programming. Much as the addition of annotations in Java 5 created opportunities (such as annotated REST controllers or unit tests), the addition of lambda expressions in Java 8 created opportunities for functional APIs in Java. This is a boon for non-blocking applications and continuation-style APIs (as popularized by CompletableFuture and ReactiveX) that allow declarative composition of asynchronous logic. At the programming-model level, Java 8 enabled Spring WebFlux to offer functional web endpoints alongside annotated controllers.
1.1.1. Define “Reactive” We touched on “non-blocking” and “functional” but what does reactive mean?
The term, “reactive,” refers to programming models that are built around reacting to change — network components reacting to I/O events, UI controllers reacting to mouse events, and others. In that sense, non-blocking is reactive, because, instead of being blocked, we are now in the mode of reacting to notifications as operations complete or data becomes available.
There is also another important mechanism that we on the Spring team associate with “reactive” and that is non-blocking back pressure. In synchronous, imperative code, blocking calls serve as a natural form of back pressure that forces the caller to wait. In non-blocking code, it becomes important to control the rate of events so that a fast producer does not overwhelm its destination.
Reactive Streams is a small spec (also adopted in Java 9) that defines the interaction between asynchronous components with back pressure. For example a data repository (acting as Publisher) can produce data that an HTTP server (acting as Subscriber) can then write to the response. The main purpose of Reactive Streams is to let the subscriber to control how quickly or how slowly the publisher produces data.