log.info

Spring Webflux (1) - Overview 본문

카테고리 없음

Spring Webflux (1) - Overview

log.info 2022. 8. 7. 03:06

본 포스팅은 https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html 의 글을 번역하며 주관적인 해석, 자문자답, 개인적인 경험, 코드 등이 추가(하늘색 글씨, 이탤릭체)된 글입니다. 

 

Spring Webflux

Spring Framework, Spring Web MVC와 같은 기존 웹 프레임워크들은 Servlet API와 Servlet 컨테이너용으로 개발되었다. Reactive-Stack의 웹 프레임워크인 Spring Webflux는 Spring 5.0 버전 이후에 추가되었고, 완전한 Non-Blocking과 Reactive Streams의 백프레셔를 지원하며, Netty, Undertow, Servlet 3.1 이상의 컨테이너에서 작동한다.

 

Spring Web MVCSpring Webflux 모두 각자의 이름으로 미러링되어 Spring Framework에서 나란히 존재하고, 각 모듈은 선택사항(Optional)이다. Application에서는 두 모듈 중 하나를 사용하거나, 특정 상황에서는 둘 다(ex. Spring MVC Controllers + WebClient) 사용하기도 한다.

개요

Spring Webflux는 왜 만들어졌을까?

 

첫 번째로는 적은 수의 스레드로 동시성을 처리하고, 더 적은 하드웨어 리소스로 스케일링할 수 있는 Non-Blocking 웹 스택의 필요성 때문이다. Servlet 3.1도 Non-Blocking API를 제공하긴 한다. 하지만 이를 사용하면 동기식(Filter, Servlet)이거나 Blocking(getParameter, getPart)인 다른 Servlet API를 사용하기 어려워진다. 이것은 새로운 공통 API가 모든 Non-Blocking 런타임에서 기초 역할을 하게 된 동기가 되었다. 이는 비동기/Non-Blocking으로 잘 설계된 Netty와 같은 서버들에게 중요하다.

 

두 번째로는 함수형 프로그래밍 때문이다. Java 5에 Annotation(ex. @Controller, @Test)이 추가된 것처럼, Java 8에는 Lambda expression이 추가되면서 함수형 API의 기회가 생겨났다. 이것은 비동기 로직의 선언적 구성(Declarative Composition)을 허용하는 Non-Blocking 애플리케이션 및 Continuation-Style API(CompletableFuture 및 ReactiveX에 의해 대중화됨)에 대해 이점이 있다. 프로그래밍 단에서는, Spring Webflux에서 'Annotated Controller'와 함께 '함수형 웹 엔드포인트'를 가능하게 했다.

1.1. "Reactive"의 정의

우리는 앞서 "non-blocking"과 "functional"을 다루었다. 그럼 "reactive"는 무엇일까?

 

"reactive"라는 단어는 변화에 반응하도록 구축된 프로그래밍 모델을 말한다. (ex. network components reacting to I/O events, UI controllers reacting to mouse events, and others) 그런 의미에서 "non-blocking"은 "blocking"하는 대신 다른 알림이나 data를 기다렸다가 반응하기 때문에 "reactive"하다. (항상 참은 아니지 않을까? non-blocking으로 실행만 하고 무언가에 '반응'하지 않고 작업을 끝내는 경우가 있을 수 있음. 역(reactive is non-blocking)은 항상 참이 맞는 듯 하다)

 

Spring 팀에서 "reactive"와 연관시키는 또 다른 중요한 메커니즘이 있는데, 그것은 non-blocking back pressure다. 동기식(synchronous)이고 blocking인 명령형(imperative) 코드는 호출차(caller)가 대기하도록 강제하기 때문에 back pressure가 자연스럽다. non-blocking 코드는 빠른 producer가 destination을 압도하지 않도록 이벤트 속도를 제어하는 것이 중요하다. (대략, 공급이 수요보다 많아서 문제가 발생하지 않도록 해야 한다는 말)

 

Reactive Streams는 asynchronous component들과 back-pressure 사이의 상호작용을 정의하는 작은 규약이다. 예를 들어, 어떤 data repository(Publisher 역할)는 HTTP 서버(Subscriber 역할)를 위해 데이터를 생성할 수 있다. Reactive Streams의 주된 목적은 Publisher가 데이터를 얼마나 빠르거나 느리게 생성하는지를 Subscriber가 제어하도록 하는 것이다.

  • Reactive Streams은 non-blocking backpressure을 사용하여 비동기 스트림 처리를 다루는 '표준(스펙)'
    • Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure. - 출처: Reactive Streams 공식 문서
  • Reactor는 Reactive Streams를 구현한 구현체 '라이브러리'
    • Reactor is a fourth-generation reactive library, based on the Reactive Streams specification, for building non-blocking applications on the JVM - 출처: Project Reactor 공식 홈페이지
  • Netty는 비동기 이벤트 드리븐 '웹 프레임워크'
    • Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients - 출처: Netty Project 공식 홈페이지

 

Q. Publisher가 속도를 늦출 수 없다면?

Reactive Streams의 목적은 메커니즘과 경계를 설정하는 것이다. Publisher가 속도를 늦출 수 없는 경우 버퍼링, 삭제 또는 실패 여부를 결정해야 한다. 

Spring Webflux에서는 어떻게 이것을 설정할까? 기본값은 뭘까?

 

1.2. Reactive API

Reactive Streams는 상호운용성을 위한 중요한 역할을 한다. 그것은 라이브러리나 인프라스트럭쳐에 관심이 많지만, 너무 저수준이기 때문에 Application API에 유용하지 않다(?). Application Level에서는 비동기 로직을 만들기 위한 풍부한 고수준의 함수형 API가 필요하다.(Java 8 Stream API와 비슷하지만 Collection만을 위한 것이 아님) 이것이 reactive libraries의 역할이다.

 

Reactor는 Spring Webflux에서 선택한 Reactive library다. 이는 ReactiveX vocabulary of operators에 정리된 풍부한 명령들을 통해 Mono(0..1)와 Flux(0..N) API타입을 제공한다. Reactor는 Reactive Streams 라이브러리 중 하나이므로 그것의 모든 명령들은 non-blocking back pressure를 제공한다. Reactor는 server-side Java에 강한 포커스를 갖고 있으며, Spring과 긴밀히 협력하도록 개발되었다.

왜 Spring은 Reactor를 선택했을까? 다른 대안은 없었나? RxJava는?

RxJava에서 2.x버전부터는 Reactive Streams을 따르지만 1.x버전은 아닌 듯 하다. 또, 2.x에서 Reactive Streams 표준을 따르지 않는 것들도 있는 듯 하다.

"RxJava supports Reactive Streams since version 2. x with its Flowable base class, but it's a more significant set than Reactive Streams with several base classes like Flowable, Observable, Single, Completable"

그리고 조금 다른 얘기지만, Spring 5.3.11에서는 RxJava의 자체 EOL 조언과 RxJava 3로의 업그레이드 권장에 의해 RxJava 1, 2에 대한 지원이 끊겼다.

"As of Spring Framework 5.3.11, support for RxJava 1 and 2 is deprecated, following RxJava’s own EOL advice and the upgrade recommendation towards RxJava 3"

WebFlux는 core 의존성으로 Reactor를 요구하지만 다른 Reactive Streams 기반의 Reactive library와 상호 운용 가능하다. 일반적으로 WebFlux API는 기본 Publisher를 입력으로서 받아들이고, 그것을 내부적으로 Reactor type으로 변환(adapt)하고, Mono나 Flux를 출력으로서 반환한다. 그래서 당신은 어떠한 Publisher를 입력으로 전달할 수 있고, 출력에 작업을 적용할 수 있지만, 다른 Reactive library와 사용하려면 출력을 변환(adapt)해야 한다. Webflux는 가능하다면 RxJava나 다른 Reactive library 사용을 위해 투명하게 변환한다. 

Reactive APIs 외에도 Webflux는 Kotlin의 Coroutine API를 사용할 수도 있다. Coroutine API는 좀 더 명령형(imperative) 스타일로 프로그래밍할 수 있게 해준다.

 

1.3. Programming Models

spring-web 모듈에는 HTTP 추상화, 지원되는 서버를 위한 Reactive Streams 어댑터, 코덱, 그리고 Servlet API와 비슷하지만 non-blocking contract가 있는 핵심 WebHandler API를 포함하여 Spring WebFlux의 기반이 되는 reactive 개념이 포함되어 있습니다.

 

이를 기반으로, Spring WebFlux는 두 프로그래밍 모델을 제공한다.

  • Annotated Controllers: Spring MVC와 같이 spring-web 모듈의 어노테이션을 사용. Spring MVC와 WebFlux의 Controller 모두 Reactive(Reactor, RxJava) 리턴 타입을 지원하므로 둘을 구분하기는 쉽지 않다. 한 가지 주목할만한 차이점은 WebFlux는 reactive @RequestBody 인자를 지원한다는 것이다. 자세한 것은 뒤에 나올 것이지만, 아래와 같이 사용이 가능하다.
    • @RequestBody flowablePeople: Flow<Person>
  • Functional Controllers: Lambda 기반의 가벼운 함수형 프로그래밍 모델. 요청을 라우팅해주고 다루는 작은 라이브러리나 유틸 모음이라고 여기면 된다. 어노테이션 기반 컨트롤러와의 큰 차이는 어노테이션으로 의도를 선언해서 콜백 받기보단, 요청을 어플리케이션이 처음부터 끝까지 제어한다는 것이다.

1.4. Applicability

Spring MVC or WebFlux?

 

자연스러운 질문이지만 불건전한 이분법적 질문이다. 실제로 둘 다 함께 작동하여 선택의 폭을 넓혀준다. 둘 모두 지속성과 일관성을 위해 설계됐으며, 나란히 사용될 수 있고, 서로의 피드백이 둘 모두에게 도움이 된다.

다음 포인트들을 고려해보길 제안한다.

  • 당신의 Spring MVC 어플리케이션이 잘 작동한다면 Spring WebFlux로 바꿀 필요 없다. 명령형(Imperative) 프로그래밍은 작성하고 이해하고 디버깅하기 가장 쉬운 방법이다. 지금까지 대부분 blocking 방식을 사용했기 때문에 당신은 많은 라이브러리 선택지를 갖는다. (Kotlin Coroutines를 사용하면 Reactive 방식에서 명령형 방식을 사용할 수 있다)
  • Spring WebFlux는 같은 실행 모델이라는 이점을 제공하며, 여러 서버 선택지(Netty, Tomcat, Jetty, Undertow, Servlet Container 3.1+)를 제공하고, 여러 프로그래밍 모델 선택지(Annotated controllers, Functional Web Endpoints)를 제공하며, 여러 Reactive libraries를 제공한다(Reactor, RxJava, or other..)
  • Java 8 Lambda 또는 Kotlin을 이용한 가벼운 함수형 웹 프레임워크에 관심 있다면, Spring WebFlux 함수형 웹 엔드포인트를 사용할 수 있다. (함수형 웹 엔드포인트와 '가벼운'은 무슨 상관일까?) 이는 또한 더 나은 투명성과 제어의 이점을 얻을 수 있는 덜 복잡한 요구 사항을 가진 소규모 애플리케이션 또는 마이크로서비스에 좋은 선택이 될 수 있다. ('더 나은 투명성과 제어의 이점'과는 무슨 상관일까?)
  • 애플리케이션을 평가하는 가장 간단한 방법은 디펜던시들을 체크하는 것이다. 만약 blocking persistence API들(JPA, JDBC)이나 networking API들(OpenFeign, ..)을 사용한다면 보통 Spring MVC가 좋은 선택일 것이다. Reactor와 RxJava 모두 별도의 스레드에서 blocking call을 수행하는 것이 기술적으로는 가능하지만, non-blocking web stack을 최대한 활용하지 못할 것이다. (현재 기준 OpenFeign은 reactive를 공식 지원하지 않는다. OpenFeign - Reactive Support 절을 살펴보면 최대한 빨리 core에 추가할 예정이라고는 나와있다. 그 전까지는 WebClient를 이용한 feign-reactive를 대안으로 제시하고 있다.
  • 만약 Spring MVC에서 원격 서비스를 호출하고 있다면, reactive WebClient를 사용해보라. 그러면 Spring MVC controller method에서 reactive 타입 (Reactor, RxJava, or other)을 바로 반환할 수 있다. 요청 당 지연시간이나 요청간 의존성이 클수록 이점이 더 커진다. Spring MVC controller들도 다른 reactive 컴포넌트들을 호출할 수 있다.
  • 만약 당신이 커다란 팀에 있다면, non-blocking, 함수형/선언형 프로그래밍 전환의 가파른 러닝 커브를 고려해야 한다. 전체 전환 없이 시작하는 실용적인 방법은 reactive WebClient를 사용하는 것이다. 아니면 작게 시작해서 이점을 측정해라. 많은 어플리케이션에서 이러한 전환은 불필요할 것이라 예상한다. 만약 이점들이 명확하지 않다면, non-blcoking I/O가 어떻게 동작하는지와 그 효과들에대해 먼저 공부해보라.

1.5. Servers

Spring Webflux는 Tomcat, Jetty, Servlet 3.1 이상의 컨테이너들과, non-Servlet runtime인 Netty, Undertow에서 지원된다. 모든 서버들은 저수준의 common API에 적용되어 서버간에 고수준의 프로그래밍 모델이 지원할 수 있다.

 

Spring WebFlux는 서버를 시작하거나 종료하는 빌트인 지원이 없다. 하지만 Spring 설정과 WebFlux infrastructure으로 어플리케이션을 assemble하고 단 몇 줄의 코드로 실행하는 것은 쉽다.

 

Spring Boot는 이러한 단계를 자동화하는 WebFlux starter를 제공한다. 기본적으로 starter는 Netty를 사용하지만, Maven이나 Gradle의 dependencies를 변경하여 Tomcat, Jetty, Undertow로 전환할 수 있다. Spring Boot는 기본적으로 asynchronous/non-blocking에서 더 많이 사용되며 client와 server가 resources를 공유할 수 있게 하는 Netty를 사용한다.

'client와 server가 resources를 공유': 같은 EventLoop를 사용한다.

 

Tomcat과 Jetty는 Spring MVC와 WebFlux 모두에서 사용 가능하다. 하지만 사용법이 매우 다르다는 것을 고려하라. Spring MVC는 Servlet blocking I/O에 의존하며, 필요하면 어플리케이션들이 Servlet API를 그대로 사용하도록 한다. Spring WebFlux는 Servlet 3.1 non-blocking I/O에 의존하며, 뒷단의 저수준 adapter에서 Servlet API를 사용하고 직접 사용하도록 노출되지 않는다.

 

Undertow에서는 Spring WebFlux가 Undertow API들을 Servlet API 없이 그대로 사용한다.

1.6. 성능

성능은 많은 특징과 의미를 갖고 있다. Reactive와 Non-blocking은 보통 어플리케이션이 더 빠르게 동작하도록 만들어주진 않는다. 특정 상황에서(ex. WebClient로 원격 호출들을 병렬로 실행하는 등)는 가능하다. 전체적으로 Non-blocking 방식으로 작업을 수행하려면 더 많은 작업이 필요하며 처리 시간이 약간 늘어날 수 있다.

Reactive와 Non-blocking의 주요 예상 이점은 작고 고정된 개수의 스레드들과 더 적은 메모리로 스케일링할 수 있다는 것이다. 이는 애플리케이션이 더욱 예측 가능한 방식으로 확장되기 때문에 부하 시 애플리케이션의 탄력성을 높인다. 하지만 이러한 이점을 관찰하려면 약간의 지연(느리고 예측 불가능한 네트워크 I/O 포함)이 필요하다. 이는 Reactive stack이 강점을 보이는 지점이며, 차이가 극대화될 수 있다. (대략, 느린 I/O가 있을 때 Reactive/Non-Blocking의 장점이 부각된다는 뜻)

1.7. 동시성 모델

Spring MVC와 WebFlux 모두 annotated controllers를 제공하지만, 동시성 모델 에서의 핵심 차이점은 동시성 모델과 blocking/thread에 대한 기본 가정에 있다.

 

Spring MVC에서는 어플리케이션이 현재 스레드(ex. remote call)를 block할 수 있다고 가정한다. 이러한 이유로 Servlet container들은 요청을 처리하는 동안의 잠재적 blocking을 수용하기 위해 큰 Thread pool을사용한다.

 

Spring WebFlux에서는 어플리케이션이 block하지 않는다고 가정한다. 그러므로 non-blocking 서버들은 요청을 처리하기 위해 작고 고정된 개수의 thread pool(event-loop workers)을 사용한다.

 

Blocking API 호출

만약 blocking library가 필요하다면 어떻게 할까? Reactor와 RxJava 모두 다른 thread에서 처리하기 위해 publishOn 명령을 제공한다. 하지만 blocking API가 동시성 모델과는 맞지 않다는 것을 고려하자.

 

변경가능한 상태 (Mutable State)

Reactor와 RxJava에서 operator들을 통해 logic을 선언한다. 런타임 시 데이터가 개별 단계에서 순차적으로 처리되는 reactive pipeline이 형성된다. 이것의 주요 이점은 해당 파이프라인 내의 애플리케이션 코드가 동시에 호출되지 않기 때문에 애플리케이션이 변경 가능한 상태를 보호하지 않아도 된다는 것입니다.

Threading Model

Spring WebFlux에서는 어떤 스레드가 동작할까?

  • Spring WebFlux 서버에서는 서버를 위한 한 thread와 요청을 처리하기 위한 여러  thread(보통 CPU core 수만큼)를 기대할 수 있다. 하지만 Servlet container에서는 servlet blocking I/O와 servlet 3.1 non-blocking I/O를 모두 지원하기 위해 더 많은 thread(ex. Tomcat의 경우 10개)로 시작할 수 있다.
  • reactive WebClient는 event loop 방식으로 동작한다. 따라서 작고, 고정된 수의 처리 스레드들(ex. Reactor Netty connector의 `reactor-http-nio-`)을 확인할 수 있다. 하지만 Reactor Netty가 client와 server 모두로 사용된다면, 그 둘은 event loop resource를 공유한다.
  • Reactor와 RxJava는 다른 Thread Pool에서 처리하도록 전환할 때 사용하는 publishOn 연산을 사용하기 위해, `scheduler`라고 불리는 Thread Pool 추상화를 제공한다. scheduler는 특정 동시성 전략을 제안하는 이름이 있다. (ex. `parallel`(for CPU-bound work with a limited number of threads) `elastic`(for I/O-bound work with a large number of threads)) 이러한 스레드가 표시되면 일부 코드가 특정 Thread Pool scheduler 전략을 사용하고 있음을 의미한다.
  • Data 접근 라이브러리와 다른 3rd-party 의존성들은 그들만의 독자적인 thread를 만들고 사용할 수 있다.
설정

Spring Framework는 서버를 시작하고 종료하는 것을 제공하지 않는다. 서버의 threading model을 설정하기 위해서는 서버 특정 설정 API들이 필요하거나, Spring Boot를 사용한다면 각 서버의 Spring Boot 설정을 살펴보라. WebClient를 직접적으로 설정할 수도 있다.