https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux 를 번역합니다.
❗️ Spring WebFlux
Spring Framework에 포함된 오리지날 웹 프레임워크인 Spring Web MVC는 Servlet API 및 Servlet 컨테이너를 위해 특별히 제작되었다. 반응형-스택 웹 프레임워크인 Spring WebFlux는 5.0버전부터 추가되었다. Spring WebFlux는 non-blocking이며 Reactive Streams back pressure를 지원하고 Netty, Undertow, Servlet 3.1+ 컨테이너와 같은 서버에서 실행된다.
Spring WebFlux가 만들어진 이유
적은 수의 스레드로 동시 처리를 가능하게 하고 더 적은 하드웨어 리소스를 가지고 확장 가능한 non-blocking web stack이 필요하기 때문이다. Servlet 3.1에서 non-blocking I/O를 위한 API를 제공했으나 contracts가 동기식(Filter
, Servlet
)이거나 blocking(getParameter
, getPart
)이기 때문에 이를 사용하는 것은 Servlet API의 나머지 부분들과의 괴리를 만들었다. 이로인해 새로운 공통 API가 non-blocking runtime의 기반이 되었으며 이는 async, non-blocking 공간에 잘 구축된 서버(e.g. Netty) 때문에 중요하다.
두 번째는 함수형 프로그래밍이다. Java 5에 어노테이션을 추가함으로써 어노테이션으로 이뤄진 REST controller나 unit test를 구성한 기회가 생긴 것처럼 Java 8에 람다식을 추가하면 Java의 함수형 API를 구성할 기회가 생겼다. 이는 비동기 로직의 선언형 구성을 허용하는 non-blocking 어플리케이션과 continuation-style API(CompletableFuture
와 ReactiveX에서 널리 사용됨)에게 유익하다. 프로그래밍 모델 수준에서, Java 8은 Spring WebFlux가 어노테이션이 달린 컨트롤러와 함께 함수형 웹 엔드포인트를 제공하도록 만들었다.
'non-blocking'과 'functional'이란 단어를 언급했는데, 'reactive'는 무슨 의미일까?
'reactive'라는 용어는 I/O 이벤트에 반응하는 네트워크 컴포넌트, 마우스 이벤트에 반응하는 UI 컨트롤러 등과 같은 변화에 대한 반응을 중심으로 구축된 프로그래밍 모델을 나타낸다. 따라서 non-blocking은 blocking과 다르게 작업이 완료되거나 데이터를 사용가능한 상태가 되면 알림에 반응하는 방식이 있으므로 reactive하다고 말 할 수 있다.
'non-blocking back pressure' 또한 'reactive'와 연관되는 중요한 메커니즘이다. 동기식 명령형 코드에서 blocking call은 호출자가 대기하기 위한 back pressure 역할을 한다. non-blocking 코드에서는 빠른 생산자가 목적지를 압도하지 않도록 이벤트 속도를 제어하는 것이 중요하다.
Reactive Streams는 back pressure가 있는 비동기 컴포넌트간의 상호 작용을 정의하는 소규모 사양이다. (이는 Java 9에서도 채택되었다.) 예를 들어 (Publisher) 데이터 레파지토리는 (Subscriber) HTTP 서버가 응답에 사용할 데이터를 생성할 수 있다. Reactive Streams의 주된 목적은 Subscriber가 Publisher의 데이터 생성 속도(빠르게/느리게)를 제어 할 수 있도록 하는 것이다.
Publisher가 속도를 늦출 수 없는 경우에는..?
Reactive Streams의 목적은 메커니즘과 경계를 설정하는 것이다. 따라서 Publisher가 속도를 늦출 수 없다면 버퍼 사용, 제거, 실패 여부를 결정해야한다.
Reactive Streams는 상호 운용성을 위해 중요한 역할을 한다. 라이브러리와 인프라 컴포넌트와 관련은 있으나 너무 낮은 수준이기 때문에 어플리케이션 API로써는 유용성이 떨어진다. 어플리케이션은 비동기 로직을 구성하기 위해 더 높은 수준의 풍부한 함수형 API가 필요하다. (이는 Java 8의 Stream
API와 유사하나 컬렉션에만 해당되는 것이 아니다.) 이것이 reactive 라이브러리의 역할이다.
Reactor는 Spring WebFlux에서 선택한 reactive library이다. 이는 ReactiveX 어휘 연산자와 일치하는 풍부한 연산자를 통해 0..1(Mono
)과 0..N(Flux
)의 데이터 시퀀스에서 작업하기 위한 Mono
와 Flux
API 유형을 제공한다. Reactor는 Reactive Streams 라이브러리이므로 모든 연산자는 non-blocking back pressure 지원한다. Reactor는 servier-side java에 큰 비중을 두며 Spring과 협력적으로 개발되었다.
WebFlux는 Reactor에 핵심 종속성을 가지고 있지만 Reactive Stream를 통해 다른 reactive 라이브러리와 상호 운용 할 수 있다. 일반적으로 WebFlux API 일반적인 Publisher
를 입력으로 받아들이고 이를 내부적으로 Reactor 타입에 맞도록 변형(조정)하여 사용하며 Flux
나 Mono
를 출력으로 반환한다. 따라서 어떤 Publisher
든 입력으로 전달할 수 있고 출력으로 연상을 적용할 수 있으나, 다른 reactive 라이브러리와 사용하기 위해서는 출력을 조정해야한다. 어노테이션으로 이뤄진 컨트롤러와 같이 가능한 경우에는 WebFlux는 RxJava 등 다른 reactive 라이브러리 사용에도 잘 적용된다. 자세한 내용은 Reactive Libraries를 참조하라.
Reactive API 외에도 WebFlux는 명령형 프로그래밍을 제공하는 Kotlin의 Coroutines API와 함께 사용할 수도 있다.
spring-web
모듈은 reactive 기반으로써 Spring WebFlux를 포함한다. (이는 HTTP 추상체, 서버를 지원하는 Reactive Streams 어댑터, codecs, Servlet API와 유사하나 non-blocking contract를 제공하는 core WebHandler
API를 포함한다.)
이를 기반으로 Spring WebFlux는 두 종류의 프로그래밍 모델을 선택할 수 있다.
Annotated Controllers : Spring MVC와 일치하며 spring-web
모듈과 동일한 어노테이션을 기반으로한다. Spring MVC 및 WebFlux 컨트롤러는 모두 반응형(Reactor and RxJava) 반환 타입을 지원하기때문에 크게 다르지 않다. 차이점이라고 하면 WebFlux가 반응형 아규먼트인 @RequestBody
를 지원한다는 것이다.
Functional Endpoints : Lambda 기반의 경량 함수형 프로그래밍 모델이다. 어플리케이션이 요청을 라우팅하고 처리하는데 사용할 수 있는 작은 라이브러리 혹은 유틸리티 집합으로 생각할 수 있다. annotated Controller와의 큰 차이점은 어플리케이션이 처음부터 끝까지 요청 처리를 담당한다는 것과 어노테이션을 통해 의도를 선언하고 콜백을 수행한단 것이다.
🤔 Spring MVC or WebFlux ?
이분법적으로 이를 나누는 것은 좋지않다. 실제로 두 가지 모두 함께 작동함으로써 사용 가능한 옵션의 범위를 확장시킬 수 있다. 이 둘은 서로의 연속성과 일관성을 위해 설계되었으며 함께 사용가능하고 각자의 피드백이 양쪽 모두에 도움이 된다. 다음 다이어그램은 둘의 관계, 공통점, 고유한 기능을 보여준다.
다음과 같은 것들을 고려하도록 하라.
WebClient
를 사용하라. Spring MVC 컨트롤러 메소드는 반응형 타입(Reactor, RxJava 등)을 직접 반환할 수 있다. 호출 당 지연 시간이나 호출 간 상호의존성이 클수록 이득은 극적으로 나타난다. 스프링 MVC 컨트롤러는 다른 reactive component도 호출할 수 있다.WebClient
를 사용하는 것이다. 그 외에도 작은 규모로 변화를 시작하면서 이익을 측정해야한다. (광범위한 적용에 있어서는 그 변화가 불필요 할 것이라고 예상하기 때문이다.) 어떤 이점을 찾아야할지 확실하지 않은 경우, non-blocking I/O 작동 방식(e.g. 단일 스레드 Node.js의 동시성)과 그 효과에 대해 알아보면 도움이 될 것이다.Spring WebFlux는 Tomcat, Jetty, Servlet 3.1+ 컨테이너 뿐만 아니라 Netty와 Undertow와 같은 non-servlet 런타임에서도 지원된다. 모든 서버는 로우 레벨의 공통 API에 맞게 조정됨으로 서버 전체에서 하이 레벨의 프로그래밍 모델을 지원 할 수 있다.
Spring WebFlux는 내부적으로 서버를 시작하거나 종료하는 것은 지원하지 않는다. 그러나 Spring configuration과 WebFlux infrastructure로부터 어플리케이션을 조립하는 것은 간단하며 몇 줄의 코드로 이를 실행 시킬 수 있다.
Spring Boot에는 이러한 단계를 자동화하는 WebFlux starter가 있다. 기본적으로 starter는 Netty를 사용하지만 Maven/Gradle 종속성을 변경함으로써 Tomcat, Jetty, Undertow로 쉽게 전환 할 수 있다. Spring boot가 Netty를 기본값으로 사용하는 이유는 Netty가 비동기식, non-blocking 공간에서 널리 사용되며 클라이언트와 서버가 리소스를 공유 할 수 있도록 만들기 때문이다.
Tomcat과 Jetty는 Spring MVC와 WebFlux를 함께 사용할 수 있으나 사용되는 방식은 매우 다르다. Spring MVC는 Servlet blocking I/O에 의존하며 어플리케이션이 필요로 할 경우 Servlet API를 직접 사용할 수 있도록한다. 반면에 Spring WebFlux는 Servlet 3.1 non-blocking I/O에 의존하고 로우 레벨 어댑터 뒤에서 Servlet API를 사용한다. 이는 직접적인 사용을 위해 노출되지 않는다.
Undertow의 경우, Spring WebFlux는 Servlet API 없이 직업 Undertow API를 사용한다.
일반적으로 Reactive, non-blocking이 어플리케이션이 더 빨리 작동하도록 만들지는 않는다. WebClient
를 사용하여 원격 호출을 병렬로 실행하는 것과 같은 일부 경우에는 가능하나, 전반적으로 non-blocking 방식으로 작업을 수행하는데 더 많은 작업이 필요하므로 처리 시간이 약간 늘어날 수 있다.
reactive, non-blocking의 주된 이점은 적은 수의 고정된 스레드와 적은 메모리로도 확장이 가능하다는 것이다. 이는 어플리케이션이 보다 예측 가능한 방식으로 확장되기 때문에 어플리케이션을 더욱 탄력적(resilient)으로 만들어준다. 그러나 이러한 이점을 보기위해선 약간의 지연 시간(느리고 예측 불가능한 네트워크 I/O를 포함)이 필요하다. reactive stack이 강점을 보이기 시작하면 그 차이는 극적으로 벌어질 수 있다.
Spring MVC와 Spring WebFlux는 모두 어노테이션으로 구성된 컨트롤러를 지원하지만 동시성 모델과 blocking와 thread에 대한 기본 가정에는 몇 가지 주된 차이점이 있다.
Spring MVC (및 일반적으로 서블릿 어플리케이션)에서는 어플리케이션이 현재 스레드를 차단할 수 있다고 가정한다. (e.g. remote call) 따라서 서블릿 컨테이너는 요청 처리 도중 잠재적인 blocking을 흡수할 수 있도록 큰 스레드 풀을 사용한다.
Spring WebFlux (및 일반적으로 non-blocking 서버)에서는 어플리케이션이 차단되지 않는다고 가정한다. 따라서 non-blocking 서버는 작은 크기의 고정된 스레드 풀(이벤트 루프 워커)을 사용해 요청을 처리한다.
"규모 조정"과 "소수 스레드"는 모순되게 들릴 수 있으나 현재 스레드를 blocking하지 않고 콜백에 의존한단 것은 흡수할 blocking call이 없으므로 추가 스레드가 필요없음을 의미한다.
blocking 라이브러리를 사용해야하는 경우, Reactor와 RxJava는 다른 스레드에서 처리를 지속할 수 있는 publishOn
연산자를 제공한다. 즉, 쉽게 탈출할 수 있는 hatch(비상구)를 보유하고 있단 의미이다. 반면에 blocking API는 이런 동시성 모델에는 적합하지 않다.
Reactor와 RxJava에서는 연산자를 통해 로직을 선언한다. 런타임에서 별개의 단계에서 순차적으로 데이터를 처리하는 reactive pipeline이 형성된다. 이로인해 해당 파이프라인 내의 어플리케이션 코드는 동시에 호출되지 않으므로 어플리케이션은 변경 가능한 상태를 보호할 필요가 없다.
Spring WebFlux로 실행되는 서버에서 볼 수 있는 스레드는 어떤 것일까?
~생략~
spring-web
모듈은 reactive web 어플리케이션에 대해 다음과 같은 기본 지원이 포함된다.
WebHandler
API : 요청 처리를 위한 약간 더 높은 수준의 범용 웹 API. 어노테이션 구성 컨트롤러 및 함수형 엔드포인트와 같은 구체적인 프로그래밍 모델이 구축된다.ClientHttpConnector
Contract가 존재한다. 어플리케이션에서 사용되는 고수준의 WebClient는 이런 기본 Contract 위에 쌓아진다.HttpHandler는 요청과 응답을 처리하는 단일 메서드가 있는 간단한 contract이다. 유일한 목적은 다른 HTTP server API에 대한 최소화된 추상화이다.
지원되는 서버
서버 종속성
org.springframework.web.server
패키지는 HttpHandler
Contract를 기반으로 구축되어 multiple WebExceptionHandler
, multiple WebFilter
, 단일 WebHandler
컴포넌트의 체인을 통해 요청을 처리하기위한 범용 웹 API를 제공한다. 간편하게 컴포넌트가 자동으로 감지되는 Spring ApplicationContext
를 사용하거나 빌더에 컴포넌트를 등록하여 체인을 WebHttpHandlerBuilder
와 함께 사용할 수 있다.
HttpHandler
는 다른 HTTP 서버의 사용을 추상화한다는 단순한 목표를 가지고 있지만 WebHandler
API는 다음과 같은 웹 애플리케이션에서 일반적으로 사용되는 광범위한 기능들을 제공하는 것을 목표로 한다.
Locale
혹은 Principal
에 대한 해결