목차

Reactive Core

설명

스프링-웹 모듈에는 반응형 웹 애플리케이션을 위한 다음과 같은 기본 지원이 포함되어 있다.

HttpHandler

HttpHandler는 요청과 응답을 처리하는 메소드를 하나만 가지고 있다. 의도적으로 최소한의 기능을 제공하며, 주된 목적은 다양한 HTTP 서버 API에 대한 최소한의 추상화이다.
지원하는 서버의 API는 아래 표와 같다.

서버 이름 사용하는 Servlet API 리액티브 스트림 지원
NettyNetty APIReactor Netty
UndertowUndertow API APIspring-web: Undertow to 리액티브 스트림 브릿지
Tomcat서블릿 3.1 논블로킹 I/O; ByteBuffers로 byte[]를 읽고 쓰는 Tomcat APIspring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지
Jetty서블릿 3.1 논블로킹 I/O; ByteBuffers로 byte[]를 읽고 쓰는 Jetty APIspring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지
서블릿 3.1 컨테이너서블릿 3.1 논블로킹 I/Ospring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지


서버 Dependency는 아래 표와 같다(지원 버전 참고)

서버 이름 Group ID Artifact Name
Reactor Nettyio.projectreactor.nettyreactor-netty
Undertowio.undertowundertow-core
Tomcatorg.apache.tomcat.embedtomcat-embed-core
Jettyorg.eclipse.jettyjetty-server, jetty-servlet


다음은 각 서버의 API 어댑터를 활용하는 HttpHandler이다.

리액터 Netty

HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();

Undertow

HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();

Tomcat

HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);
 
Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();

Jetty

HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
 
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
 
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();

서블릿 3.1+ 컨테이너

서블릿 3.1+ 컨테이너에 WAR를 배포하려면 WAR에 AbstractReactiveWebInitializer를 확장하여 추가하면 된다. 이 클래스는 HttpHandler, ServletHttpHandlerAdapter를 감싸고 있으며, 이 핸들러를 서블릿으로 등록한다.

WebHandler API

org.springframework.web.server 패키지를 보면 HttpHandler가 WebHandler와 여러 WebExceptionHandler, WebFilter로 체인을 형성해 요청을 처리하는 범용 웹 API를 제공한다.
WebHttpHandlerBuilder에 컴포넌트를 등록하거나 스프링 ApplicationContext 위치만 알려주면 자동으로 컴포넌트 체인에 추가한다.
HttpHandler는 서로 다른 HTTP 서버를 쓰기 위한 추상화가 전부인 반면, WebHandler API는 아래와 같이 웹 애플리케이션에서 흔히 쓰는 광범위한 기능을 제공한다.

Form Data

ServerWebExchange는 form 데이터에 접근할 수 있는 메소드를 제공한다.

Mono<MultiValueMap<String, String>> getFormData();

DefaultServerWebExchange는 설정에 있는 HttpMessageReader를 사용해 form 데이터를 MultiValueMap으로 파싱한다. 디폴트로 사용하는 리더는 ServerCodecConfigurer 빈에 있는 FormHttpMessageReader이다.

Multipart Data

ServerWebExchange는 multipart 데이터에 접근할 수 있는 메소드를 제공한다.

Mono<MultiValueMap<String, Part>> getMultipartData();

DefaultServerWebExchange는 설정에 있는 HttpMessageReader<MultiValueMap<String, Part»를 사용해 multipart/form-data 컨텐츠를 MultiValueMap으로 파싱한다. 현재로써는 Synchronoss NIO Multipart가 유일하게 지원하는 서브파티 라이브러리이며, 논블로킹으로 multipart 요청을 파싱하는 유일한 라이브러리이다. ServerCodecConfigurer 빈으로 활성화할 수 있다.
스트리밍 방식으로 multipart 데이터를 파싱하려면 HttpMessageReader<Part>가 리턴하는 Flux<Part>를 사용한다. 예를 들어 컨트롤러에서 @RequestPart를 선언하면 Map처럼 이름으로 각 파트에 접근하겠다는 뜻이므로 multipart 데이터를 한번에 파싱해야 한다. 반대로 Flux<Part> 타입에 @RequestPart를 사용허면 컨텐츠를 디코딩할 때 MultiValueMap에 수집하지 않는다.

Forwarded Headers

Load Balancers와 같이 프톡시를 경유한 요청은 호스트, 포트, URL 스킴ㅇ이 변경될 수 있어서 클라이언트 입장에서는 원래의 URL 정보를 알아내기 어렵다.
RFC 7239 정의에 따르면 Forwarded HTTP 헤더는 프록시가 원래 요청에 대한 정보를 추가하는 헤다이다. X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, and X-Forwarded-Prefix 같은 비표준 헤더도 존재한다.
ForwardedHeaderTransformer는 forwarded 헤더를 보고 요청의 호스트, 포트, 스킴을 바꿔준 다음, 헤더를 제거하는 컴포넌트이다. ForwardedHeaderTransformer라는 이름으로 빈을 정의하면 자동으로 체인에 추가된다.
forwarded 헤더는 보안에 신경써야 할 요소가 있는데 프록시가 헤더를 추가한건지, 클라이언트가 악의적으로 추가한 것인지 애플리케이션에서는 알 수 없기 때문이다.
따라서 외부에서 들어오는 신뢰할 수 없는 프록시 요청을 제거할 때 ForwardedHeaderTransformer를 removeOnly=true로 설정하여 헤더 정보를 사용하지 않고 제거할 수 있다.

5.1 버전부터 ForwardedHeaderFilter는 제거대상(deprecated)이 되어 ForwardedHeaderTransformer롤 대신한다. 따라서 exchange(http 요청/응답과 세션 정보 등의 컨테이너)를 만들기 전에 forwarded 헤더를 처리할 수 있다. 필터를 사용하더라도 이 필터는 전체 필터 리스트에서 제외되며 그 대신 ForwardedHeaderTransformer를 사용한다.

Filters

WebHandler API에서는 WebFilter를 사용하면 다른 필터 체인과 WebHandler 전후에 요청을 가로채서 원하는 로직을 넣을 수 있다. WebFilter를 등록하려면 스프링 빈으로 만들어 빈 위에 @Order를 선언아거나 Ordered를 구현해 순서를 정해도 되고, WebFlux Config를 사용해도 된다.

CORS

CORS는 컨트롤러에 어노테이션을 선언하는 것으로 잘 동작한다. Spring Security와 사용하면 CorsFilter를 사용해서 Spring Security 필터 체인보다 먼저 처리하도록 해야 한다.

Exceptions

WebHandler API에서는 WebFilter 체인과 WebHandler에서 발생한 예외를 WebExceptionHandler로 처리한다. WebExceptionHandler를 등록하려면 스프링 빈으로 빈 위에 @Order를 선언아거나 Ordered를 구현해 순서를 정해도 되고, WebFlux Config를 사용해도 된다.
아래 표는 바로 사용할 수 있는 WebExceptionHandler 구현체이다.

Exception Handler Description
ResponseStatusExceptionHandlerHttp status code를 지정할 수 있는 ResponseStatusException을 처리한다.
WebFluxResponseStatusExceptionHandlerResponseStatusExceptionHandler를 확장하는 것으로 다른 exception 타입도 @ResponseStatus를 선언해서 HTTP status code를 정할 수 있다.
이 핸들러는 WebFlux Config 안에 선언되어 있다.

참고자료