IT/Spring

[번역] Concurrency in Spring WebFlux

물통꿀꿀이 2020. 11. 28. 02:27

WebFlux에 대해 알아볼겸 아래 페이지를 번역해 보려고 한다. 개인적으로 이해하면서 쓰기 때문에 생략 및 의역이 많이 포함되어 있으므로 자세한 내용은 원문 참조

www.baeldung.com/spring-webflux-concurrency

 

The Motivation for Reactive Programming

일반적인 웹 어플리케이션은 복잡한 부분들로 구성되어 있다. 특히 데이터베이스 호출과 같이 대부분의 Interaction은 Block되어 있다.

그러나 몇몇은 병렬적으로 작업을 처리하려 한다. 예를 들어, 2 명의 사용자가 웹 서버에 요청할 때 각기 다른 스레드로 핸들링된다.

멀티 코어 플랫폼에서는 이런 처리 방법이 전체 응답 시간 측면에서 효과가 있다. 또한 이렇게 처리 하는 방식은 thread-per-request model 즉, 요청당 스레드 모델이라고 알려져 있다.

위 그림을 보면, 각 스레드는 단일 요청만 핸들링한다. 그러나 여전히 할당 받은 스레드는 Block 된다. 더군다나 동시성을 처리하기 위해 할당된 스레드들은 Context switch로 인해 상당한 비용을 낭비하게 된다. 때문에 웹 어플리케이션에서 요청이 점점 많아 질 수록 기대하던 성능이 미치지 못하게 된다.

결과적으로 스레드를 덜 사용하면서 더 많은 요청을 처리 할 수 있는 모델인 reactive programming이 등장하게 된 계기가 되었다.

Concurrency in Reactive Programming

Reactive programming은 데이터 flow 관점에서 프로그램을 구조화하기 편하게 한다. 때문에 완전한 non-blocking 환경에서, 리소스를 이전 모델보다 더 낫게 사용하여 보다 성능이 높은 동시성을 구현할 수 있게 한다. 그러나 thread based 와 근본적으로 다른점은 reactive programming은 비동기라는 것이다. 즉, 프로그램의 flow를 동기 작업에서 비동기 이벤트 스트림으로 변환된다.

예를 들어, 데이터베이스를 호출할 때 비동기로 작업한다. 그렇기 때문에 호출에 대한 응답(리턴 값)은 구독한 publisher가 된다. 이후 subscriber는 이후 이벤트를 처리한다.(해당 부분은 reactive programming의 특징) 

위 그림에서 첫 번째 모델과 다른 점은 reactive programming은 스레드를 중점으로 보지 않고 비동기 이벤트 스트림을 중점으로 본다.

publisher와 subscriber는 같은 스레드일 필요 없으며 이 모델은 스레드 자원을 보다 효율적으로 사용하는데 도움이 된다. (+ 전체 성능도 함께)

Event Loop

이번 세션에서는 reactive programming에서 어떻게 적은 스레드로 동시성의 성능을 높이는지 알아보도록 하겠다.

먼저 event loop 모델이다.

위 그림은 reactive programming에서 비동기 아이디어를 나타낸 event loop의 개념 그림이다.

- event loop는 단일 스레드로 계속 동작한다. (물론 코어 갯수에 만큼 event loop 가 있다.)

- event loop는 순차적으로 event queue의 event를 처리한다. 그리고 platform에 callback을 등록한 후 바로 리턴한다.

- event loop는 작업 완료 callback을 trigger 를 발생시키고 결과를 반환한다.

해당 event loop 모델은 Node.js, Netty, Nginx를 비롯하여 여러 플랫폼에서 구현된다. 때문에 전통의 플랫폼인 Apache HTTP Server, Tomcat, JBoss보다 더 나은 확장성을 가진다.

Reactive Programming with Spring WebFlux

이제 Spring WebFlux에 대해 살펴보자. WebFlux는 Spring의 reactive-stack 웹 프레임워크이다. 

그럼 WebFlux가 기존 웹 스택을 어떻게 보완했는지 알아보도록 하자.

위 그림에서 볼 수 있듯이, Spring WebFlux는 기존 웹 프레임워크가 나란히 위치하고 있다. 즉, 대체하진 않는다.

여기서 주목할 점은

- Spring WebFlux는 기존 annotation-based programming을 확장한 개념이다.

- 더군다나 HTTP runtime을 사용할 수 있다.

- 때문에 WebFlux는 Tomcat, Reactor, Netty, Undertow와 같이 다양한 reactive runtimes을 지원한다.

- 마지막으로 reactive 및 non-blocking HTTP 클라이언트인 WebClinet를 제공한다.

Reactor Netty

Reactor Netty는 Spring Boot WebFlux starter에 담긴 기본 서버이다. Netty가 기본으로 생성하는 스레드를 살펴보자.

시작할 때, 다른 의존성이나 WebClient를 사용하지 않을 것이다. 그래서 Spring WebFlux 어플리케이션이 시작되면, 기본 스레드를 볼 수 있다.

서버의 기본 스레드를 제외하고 Netty는 요청 처리를 위해 worker thread를 생성한다. (위 그림은 쿼드 코어를 가진 머신의 결과로 일반적으로 CPU 코어 갯수보다 많지 않다.)

Netty는 event loop 모델을 사용한다. 그럼 Netty가 Java NIO를 활용하여 어떻게 event loop를 구현했는지 알아보자.

EventLoopGroup이 하나 이상의 EventLoop를 관리한다. (단, EventLoop를 가용할 수 있는 코어 갯수보다 더 만드는 것은 추천하지 않는다.) 그리고 EventLoopGroup은 EventLoop를 각 Channel에 할당한다. 이 말 즉슨, 각 Channel은 EventLoop와 같은 스레드에서 실행된다.

Threading Model in WebClient

WebClient는 Spring WebFlux의 한 부분인 reactive HTTP client 이다. WebClient는 REST-based communication을 할 때나 end-to-end reactive 어플리케이션을 만들때 사용한다. 특히 reactive 어플리케이션은 적은 스레드로 작업을 처리하기 때문에 WebClient는 WebFlux에 더더욱 중요하다. 구현 방식은 위와 같이 간단하다.

@GetMapping("/index")
public Mono<String> getIndex() {
    return Mono.just("Hello World!");
}

 

WebClient.create("http://localhost:8080/index").get()
  .retrieve()
  .bodyToMono(String.class)
  .doOnNext(s -> printThreads());

그렇다면 위에서 언급했었던, 스레드들과 함께 살펴보도록 하자.

Understanding the Threading Model

스레드 모델은 WebClient 어떻게 동작할까.

WebClient는 event loop 모델을 사용하여 동시성을 구현한다. 만일 Reactor Netty 위에서 동작하면, WebClient는 Netty가 사용하는 event loop를 공유해서 사용한다. 따라서 WebClient를 쓴다해서 큰 차이는 없다.

 

그러나 경우에 따라서 클라이언트와 서버의 스레드풀을 별도로 놓을 수 있다. 물론 해당 방식은 Netty의 기본 방식은 아니지만 필요하다면 WebClient의 전용 스레드 풀을 제공할 수 있다. 이것이 어떻게 가능한지 다음 세션에서 확인해보자.

Threading Model in Data Access Libraries

기존 라이브러리는 데이터베이스나 메시지 브로커와 통신할 때 여전히 Block을 한다. 하지만 점점 바뀌어 몇몇 데이터베이스는 reactive 라이브러리를 제공한다. 그리고 대부분의 라이브러리는 Spring Data에 있다. 

 

한 예로, Spring Data MongoDB를 살펴보자.

Spring Data MongoDB는 reactive streams driver를 제공한다. 해당 driver는 Reactive Streams API를 구현한 것이다. 그리고 해당 라이브러리를 사용하면 event loop의 스레드를 사용하여 동작한다.

Scheduling Options in WebFlux

앞서 언급한 것처럼 reactive programming은 적은 스레드 환경에서 non-blocking 을 사용한다. 이 말은 즉, 코드 flow에 blocking이 포함되어 있으면 성능이 좋지 않을 것이라는 것을 의미한다. (event loop가 전체적으로 blocking으로 인해 freeze 되므로)

그렇다면 오래 진행되는 프로세스나 blocking 작업은 어떻게 reactive programming에서 다뤄야 할까?

사실 최선의 방법은 피하는 것이 상책이다. 그렇지만 꼭 해야 한다면, 스케줄링을 사용하면된다.

Spring WebFlux는 data flow chain 에서 처리 중인 작업을 다른 스레드풀로 전환하는 메커니즘을 제공한다.