본문 바로가기
Spring

Spring Webflux - Reactive Core

by 즐겁게살자 2021. 7. 5.
728x90

 

Spring weblfus 공식 레퍼런스 번역 & 요약

 

1.2 Reactive Core

spring-web을 사용하면 다음과 같은 방법으로 리액티브 웹 어플리케이션을 만들 수 있다.

  • 서버 쪽 요청은 저수준과 고수준으로 나눠서 처리한다.
    • HttpHandler: 논블로킹 I/O와 리액티브 스트림 back pressure로 HTTP 요청을 처리한다. 리액터 Netty, Undertow, 톰캣, Jetty, 서블릿 3.1+ 컨테이너 어댑터와 함께 사용한다.
    • webHandler API : 약간 더 고수준으로, 어노테이션을 선언한 컨트롤러나 함수형 엔드포인트 같이 구체적인 프로그래밍 모델로 작성하는 범용 웹 API다.
  • 클라이언트 사이드에서는 기본적으로 ClientHttpConnector가 논블로킹 I/O와 리액티브 스트림 back pressure로 HTTP 요청을 처리한다. Reactor Netty, 리액티브 Jetty HttpClient 어댑터와 함께 사용하며, 어플리케이션에서 사용하는 고수준 WebClient는 이를 기반으로 동작하낟.
  • 클라이언트와 서버 사이드 모두, 코덱으로 HTTP 요청과 응답 컨텐츠를 직렬화/역직렬화 한다. 

1.2.1. HttpHandler

 

HttpHandler는 요청과 응답을 처리하는 메소드를 하나만 가지고 있다. 의도한 유일한 역할은 여러 HTTP 서버 API를 추상화하는 것이다.

지원하는 서버 API는 아래 표와 같다.

Server name Server API used Reactive Streams support
Netty Netty API Reactor Netty
Undertow Undertow API spring-web: Undertow to Reactive Streams bridge
Tomcat Servlet 3.1 non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[] spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
Jetty Servlet 3.1 non-blocking I/O; Jetty API to write ByteBuffers vs byte[] spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge
Servlet 3.1 container Servlet 3.1 non-blocking I/O spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge

다음은 server 디펜던시들이다.

Server name Group id Artifact name
Reactor Netty io.projectreactor.netty reactor-netty
Undertow io.undertow undertow-core
Tomcat org.apache.tomcat.embed tomcat-embed-core
Jetty org.eclipse.jetty jetty-server, jetty-servlet

다음은 HttpHandler 어댑터를 이용한 코드이다. (가이드에는 Netty, Undertow, Tomcat, Jetty 전부 나열 되어 있는데 여기서는 Netty와 Tomcat만 언급하겠다.)

 

Reactor Netty

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

 

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();

 

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


1.2.2. WebHandler API

org.springframework.web.server 패키지를 보면, Httphandler가 WebHandler와 여러 WebExceptionHandler, WebFilter로 체인을 형성해 요청을 처리하는 범용 웹 API를 제공한다. WebHttpHandlerBuilder에 컴포넌트를 등록 하거나, 스프링 ApplicationContext 위치만 알려주면 자동으로 컴포넌트를 체인에 추가한다.

 

HttpHandler는 서로 다른 HTTP 서버를 쓰기 위한 추상화가 전무인 반면, WebHandler API는 아래와 같이 웹 어플리케이션에서 흔히 쓰는 광범위한 기능을 제공하다.

 

  • User session과 Session attributes.
  • Request attributes.
  • Locale, Principal 리졸브
  • form 데이터 파싱, 캐시 조회.
  • multipart 데이터 추상화
  • 기타 등등

Special bean types

다음은 WebHttpHandlerBuilder 에 직접 등록하거나 어플리케이션 컨텐스트에서 자동으로 주입받을 수 있는 컴포넌트다.

Bean name Bean type Count Description
<any> WebExceptionHandler 0..N Provide handling for exceptions from the chain of WebFilter instances and the target WebHandler. For more details, see Exceptions.
<any> WebFilter 0..N Apply interception style logic to before and after the rest of the filter chain and the target WebHandler. For more details, see Filters.
webHandler WebHandler 1 The handler for the request.
webSessionManager WebSessionManager 0..1 The manager for WebSession instances exposed through a method on ServerWebExchange. DefaultWebSessionManager by default.
serverCodecConfigurer ServerCodecConfigurer 0..1 For access to HttpMessageReader instances for parsing form data and multipart data that is then exposed through methods on ServerWebExchange. ServerCodecConfigurer.create() by default.
localeContextResolver LocaleContextResolver 0..1 The resolver for LocaleContext exposed through a method on ServerWebExchange. AcceptHeaderLocaleContextResolver by default.
forwardedHeaderTransformer ForwardedHeaderTransformer 0..1 For processing forwarded type headers, either by extracting and removing them or by removing them only. Not used by default.

 

Form Data

ServerWebExchange는 form 데이터 (application/w-www-form-urlencoded)에 접근할 수 있는 다음 메소드를 제공한다. 

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

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

 

Multipart Data

(Web MVC)

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

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

DefaultServerWebExchange는 설정에 있는 HttpMessageReader<MultiValueMap<String, Part>>를 사용해 multipart/form-data 컨텐츠를 MultiValueMap으로 파싱한다. 현재로써는 SynchronossNIO Multipart가 유일하게 지원하는 서드파티 라이브러리이며, 논블로킹으로 multipart 요청을 파싱하는 유일한 라이브러리다. ServerCodecConfigurer 빈으로 활성화 할 수 있다. 

 

스트리밍 방식으로 multipart 데이터를 파싱하려면 HttpMessageReader<Part>가 리턴하는 Flux<Part>를 사용하면 된다. 예를 들어 컨트롤러에서 @RequestPart를 선언하면 Map처럼 이름으로 각 파트에 접근하겠다는 뜻이므로, multipart 데이터를 한 번에 파싱해야 한다. 반대로 Flux<Part> 타입에 @RequestBody를 사용하면 컨텐츠를 디코딩할 때 MultiValueMap에 수집하지 않는다. 

 

Forwarded Headers

(Web MVC)

프록시를 경유한 요청은(로드 밸런서 같은) 호스트, 포트, Scheme이 변경 될 수 있기 때문에, 클라이언트 입장에서는 원래 url 정보를 알아내기 어렵다.

 

RFC 7239에 따르면 Forwarded HTTP 헤더는 프록시가 원래 요청에 대한 정보를 추가하는 헤더다. 물론 X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, X-Forwarded-Prefix 같은 비표준 헤더도 있다.

 

ForwardedHeaderTransformer는 forwarded 헤더를 보고 요청의 호스트, 포트, scheme을 바꿔준 다음, 헤더를 제거하는 컴포넌트다. forwardedHeaderTransformer라는 이름으로 빈을 정의하면 자동으로 체인에 추가 된다.

 

forwarded 헤더는 보안에 신경 써야 할 요소가 있는데, 프록시가 헤더를 추가한 건지, 클라이언트가 악의적으로 추가한 것인지 어플리케이션에서는 알 수 없기 때문이다. 이 때문에 외부에서 들어오는 신뢰할 수 없는 프록시 요청을 제거하고 싶을 수도 있다. ForwardedHeaderTransformer를 removeOnly=true로 설정하면 헤더 정보를 사용하지 않고 제거해 준다.

 

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

1.2.3. Filters

(Web MVC)

 

WebHandler API에서 WebFilter를 사용하여 필터의 나머지 처리 체인과 대상 WebHandler 전후에 가로채기 스타일 논리를 적용할 수 있다. WebFlux Config를 사용할 때 WebFilter 등록은 Spring 빈으로 선언하고 (선택적으로) 빈 선언에서 @Order를 사용하거나 Ordered를 구현하여 우선 순위를 표현하는 것처럼 간단하다.

 

CORS

(Web MVC)

 

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

 

자세한 내용은 CORS와 webflux-cors를 참고하라.


1.2.4. Exceptions

(Web MVC)

 

WebHandler API에서 WebExceptionHandler를 사용하여 WebFilter 인스턴스 체인과 대상 WebHandler의 예외를 처리할 수 있다. WebFlux Config를 사용할 때 WebExceptionHandler를 등록하는 것은 Spring 빈으로 선언하고 (선택적으로) 빈 선언에 @Order를 사용하거나 Ordered를 구현하여 우선 순위를 표현하는 것처럼 간단하다.

 

다음은 바로 사용할 수 있는 WebExceptionHandler 구현체다.

Exception Handler Description
ResponseStatusExceptionHandler Provides handling for exceptions of type ResponseStatusException by setting the response to the HTTP status code of the exception.
WebFluxResponseStatusExceptionHandler Extension of ResponseStatusExceptionHandler that can also determine the HTTP status code of a @ResponseStatus annotation on any exception.
This handler is declared in the WebFlux Config.

 


1.2.5. Codecs

(Web MVC)

 

spring-web, spring-core 모듈을 사용하면 리액티브 논블로킹 방식으로 바이트 컨텐츠를 고수준 객체로 직렬화, 역지렬화 할 수 있다. 다음과 같은 내용을 지원한다. 

  • Encoder, Decorder는 HTTP와는 관계없는 컨텐츠를 인코딩, 디코딩한다.
  • HttpMessageReader, HttpMessageWriter는 HTTP 메세지를 인코딩, 디코딩한다.
  • 웹 어플리케이션에선 Encoder를 감싸고 있는 EncoderHttpMessageWriter와 Decoder를 감싸고 있는 DecoderHttpMessageReader를 사용할 수 있다.
  • 모든 코덱은 라이브러리마다 다른 바이트 버퍼(e.g Netty ByteBuf, java.nio.ByteBuffer 등)를 추상화한 DataBuffer로 처리한다. 자세한 내용은 스프링 코어의 Data Buffers and Codesc를 참고하라.

spring-core 모듈에는 byte[], ByteBuffer, DataBuffer, Resources, String 인코더/디코더 구현체가 있다. spring-web 모듈은 Jackson Json, Jackson Smile, JAXB2, Protocol Buffers등의 인코더/디코더와 form 데이터, multipart 데이터, 서버 전송 이벤트(SSE)등을 처리하는 웹 전용 HTTP 메세지 reader/wrtier를 제공한다.

 

ClientCodecConfigurerServerCodecConfigurer로 기본 코덱을 설정하거나 커스텀 코덱을 등록 할 수 있다. HTTP message codecs를 참고하라.

 

(이하 개별 자료형별 처리 설명은 생략한다. 시간이 나는대로 정리..)

 


1.2.6. Logging

(Web MVC)

 

스프링 웹플럭스는 debug 레벨 로그에 꼭 필요한 정보만 최소한으로 담았기 때문에 읽기 편할 것이다. 어떤 이슈에서도 유용할만한 가치 있는 정보만 추렸다.

 

trace 레벨도 debug와 원론적으로 동일하지만 (예를 들어 trace도 불필요한 정보를 잔뜩 쏟아내선 안된다), 이슈를 디버깅할 때 좀 더 유용할 만한 정보를 담았다. 일부 trace, debug 레벨 로그는 디테일한 정도가 다를 것이다. 

 

어떤 로그가 좋은 로그인지는 사용해 봐야 알 수 있다. 각 레벨과 어울리지 않는 로그를 발견하면 제보 바란다.

 

Log Id

웹플럭스에선 요청 하나를 여러 쓰레드로 처리할 수 있기 때문에, 쓰레드 ID만 보고는 어떤 요청인지 파악하기 어렵다. 그렇기 때문에 웹플럭스는 기본적으로 로그 메세지마다 앞에 요청 ID를 붙인다.

 

서버 사이드에선 이 로그 ID를 ServerWebExchage attribute(LOG_ID_ATTRIBUTE)에 저장하며, ServerWebExchange#getLogPrefix()로 포맷팅된 로그 프리픽스를 확인할 수 있다.

 

WebClient에선 ClientRequest attribute (LOG_ID_ATTRIBUTE)에 저장하고 포맷팅된 로그 프리픽스는 ClientRequest$logPrefix()로 확인할 수 있다.

 

Sensitive Data

(Web MVC)

 

debug, trace 로그는 민감한 정보를 포함할 수 있다. 따라서 form 파라미터와 헤더를 로깅하지 않는 게 디폴트며, 원한다면 직접 활성화 시켜야 한다.

 

다음은 서버 로그를 활성화 시키는 코드다.

@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true);
    }
}

다음은 클라이언트 로그를 활성화 시키는 코드다.

Consumer<ClientCodecConfigurer> consumer = configurer ->
        configurer.defaultCodecs().enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
        .exchangeStrategies(strategies -> strategies.codecs(consumer))
        .build();

 

Custom codecs

다른 미디어 타입이나 디폴트 코덱이 지원하지 않는 기능을 추가하고 싶으면 커스텀 코덱을 사용한다.

 

커스텀 코덱에서도 버퍼 제한이나 form 데이터/헤더 로깅같은 설정을 그대로 사용하고 싶을 수 있는데, 그럴 땐 디폴트 코덱에 설정한 일부 옵션을 재사용할 수 있다.

다음은 클라이언트 사이드 예제로, 커스텀 코덱에 디폴트 코덱 설정을 등록한다.

WebClient webClient = WebClient.builder()
        .codecs(configurer -> {
                CustomDecoder decoder = new CustomDecoder();
                configurer.customCodecs().registerWithDefaultConfig(decoder);
        })
        .build();

 

'Spring' 카테고리의 다른 글

WebFlux - Functional Endpoints  (0) 2021.09.12
Spring Webflux - DispatcherHandler  (0) 2021.07.12
Spring Webflux - Overview  (0) 2021.07.04

댓글