[Web on Reactive Stack] 1. 스프링 웹플럭스: 1.2. Reactive Core

한글로 번역한 Web on Reactive Stack, 1. Spring Webflux: 1.2. Reactive Core


1.2. 리액티브 코어(Reactive Core)

spring-web 모듈은 리액티브 웹 애플리케이션에 대한 다음과 같은 기본 지원이 포함한다.

  • 서버 요청 처리에는 두 가지 수준의 지원이 있다.
    • HttpHandler: Reactor Netty, Undertow, Tomcat, Jetty 및 모든 Servlet 3.1+ 컨테이너용 어댑터와 함께 동작하는 HTTP 요청 핸들링을 위한 논 블로킹 I/O 및 리액티브 스트림 기반의 기본 핸들러다.
    • WebHandler API: 약간 더 높은 수준의 요청 처리를 위한 범용적인 웹 API다. 어노테이션 컨트롤러 및 함수형 엔드포인트와 같은 구체적인 프로그래밍 모델 위에 위치한다.
  • 클라이언트 측의 경우, 리액터 네티(Reactor Netty) 및 리액티브 Jetty HttpClient용 어댑터와 함께 논 블로킹 I/O 및 리액티브 스트림 백프레셔로 HTTP 요청을 수행하는 기본 ClientHttpConnector 계약이 있다. 애플리케이션에서 사용되는 고수준(high-level)의 WebClient는 이 기본 계약을 기반으로 한다.

  • 클라이언트와 서버의 경우 HTTP 요청 및 응답 컨텐츠를 직렬화(serialization)와 역직렬화(deserialization)하기 위해 코덱(codecs)을 사용한다.


1.2.1. HttpHandler

HttpHandler는 요청과 응답을 처리하는 단일 메서드를 가진 간단한 계약이다. 의도적으로 최소한으로 만들어졌으며, 유일한 목적은 다른 HTTP 서버 API에 대한 최소한의 추상화이다.

다음 표는 지원되는 서버 API를 설명한다.

서버 이름 사용된 서버 API 리액티브 스트림 지원
Netty Netty API Reactor Netty
Undertow Undertow API spring-web: undertow to 리액티브 스트림 브릿지
Tomcat 서블릿 3.1 논 블로킹 I/O; byte[]에 대응하여 ByteBuffer를 읽고 쓰는 Tomcat API spring-web: 서블릿 3.1 논 블로킹 I/O to 리액티브 스트림 브릿지
Jetty 서블릿 3.1 논 블로킹 I/O; byte[]에 대응하여 ByteBuffer를 읽고 쓰는 Jetty API spring-web: 서블릿 3.1 논 블로킹 I/O to 리액티브 스트림 브릿지
Servlet 3.1+ 컨테이너 서블릿 3.1 논 블로킹 I/O spring-web: 서블릿 3.1 논 블로킹 I/O to 리액티브 스트림 브릿지

다음 표는 서버 의존성에 대해 설명한다(지원되는 버전도 참조할 것):

서버 이름 그룹 ID 아티팩트 이름
Reactor Netty io.projectreactor.netty reactor-netty
Underrtow io.undertow undertow-core
Tomcat org.apache.tomcat.embed tomcat-embed-core
Jetty org.eclipse.jetty jetty-server, jetty-servlet

아래 코드 스니펫은 각 서버 API로 HttpHandler 어댑터를 사용하는 것을 보여준다.

리액터 네티(Reactor Netty)

Java:

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

Kotlin:

val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()

언더토우(Undertow)

Java:

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

Kotlin:

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

톰캣(Tomcat)

Java:

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

Kotlin:

val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)

val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()

제티(Jetty)

Java:

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

Kotlin:

val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)

val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();

val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()

서블릿 3.1+ 컨테이너

서블릿 3.1+ 컨테이너에 WAR로 배포하기 위해 AbstractReactiveWebInitializer를 확장하여 WAR에 포함해야 한다. 이 클래스는 ServletHttpHandlerAdapterHttpHandler를 래핑하고 이를 서블릿으로 등록한다.


1.2.2. WebHandler API

org.springframework.web.server 패키지는 HttpHandler를 기반으로 다중 WebExceptionHandler**</a> 와 <a href="https://docs.spring.io/spring-framework/docs/5.2.7.RELEASE/javadoc-api/org/springframework/web/server/WebFilter.html" rel="nofollow" target="_blank">WebFilter</a> 그리고 단일 <a href="https://docs.spring.io/spring-framework/docs/5.2.7.RELEASE/javadoc-api/org/springframework/web/server/WebHandler.html" rel="nofollow" target="_blank">WebHandler</a> 컴포넌트의 체인을 통해 요청을 처리하기 위한 범용 웹 API 제공한다. 체인은 컴포넌트가 자동 감지<a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-web-handler-api-special-beans" rel="nofollow" target="_blank">(auto-detected)</a>되는 스프링 ApplicationContext`에 지정하거나 빌더에 컴포넌트를 등록하여 WebHttpHandlerBuilder와 함께 사용할 수 있다.

HttpHandler의 목적은 서로 다른 HTTP 서버에서의 사용을 추상화하는 것이지만, WebHandler API는 아래와 같이 웹 애플리케이션에서 일반적으로 사용되는 보다 더 광범위한 기능을 제공하는 것을 목표로 한다.

  • 속성이 있는 사용자 세션(User session with attributes)
  • 요청 속성(Request attributes)
  • 요청에 대한 리졸브된 Locale 또는 Principal(Resolved Locale or Principal for the request)
  • 구문 분석과 캐시된 폼 데이터에 대한 액세스(Access to parsed and cached form data)
  • 멀티파트 데이텅츼 추상화(Abstractions for multipart data)
  • 기타 등등.. (and more..)

특별한 빈 타입들(Special bean types)

아래 표는 WebHttpHandlerBuilder가 스프링 애플리케이션 컨텍스트에서 자동 감지하거나, 직접 등록할 수 있는 컴포넌트 목록이다.

빈 이름 빈 타입 개수 설명
<any> WebExceptionHandler 0..N WebFilter 인스턴스 체인과 대상 WebHandler에서 예외에 대한 처리를 제공한다. 자세한 내용은 예외(Exceptions)를 참조
<any> WebFilter 0..N 타겟 WebHandler 전후에 인터셉터 스타일의 처리를 제공한다. 자세한 내용은 필터(Filters) 참조
webHandler WebHandler 1 요청을 처리한다.
webSessionManager WebSessionManager 0..1 ServerWebExchange의 메서드를 통해 노출된 WebSession 인스턴스 관리자. 디폴트는 DefaultWebSessionManager
serverCodecConfigurer ServerCodecConfigurer 0..1 ServerWebExchange의 메서드를 통해 노출된 폼 데이터와 멀티파트 데이터를 구문 분석하기 위해 HttpMessageReader에 액세스. 기본적으로 ServerCodecConfigurer.create()
localeContextResolver LocaleContextResolver 0..1 ServerWebExchange의 메서드를 통해 노출되는 LocaleContext에 대한 리졸버
forwardedHeaderTransformer ForwardedHeaderTransformer 0..1 포워드 타입 헤더를 추출 및 제거 또는 제거만 한다. 디폴트는 사용하지 않음

폼 데이터(Form Data)

ServerWebExchange는 아래와 같은 폼 데이터 액세스 메서드를 제공한다.

Java:

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

Kotlin:

suspend fun getFormData(): MultiValueMap<String, String>

DefaultServerWebExchange는 설정된 HttpMessageReader를 사용하여 폼 데이터(application/x-www-form-urlencoded)를 MultiValueMap 으로 파싱한다. 기본적으로 FormHttpMessageReaderServerCodecConfigurer 빈에서 사용하도록 설정된다. (웹 핸들러 API 참조)

멀티파트 데이터(Multipart Data)

ServerWebExchange는 아래와 같은 멀티파트 데이터 액세스 메서드를 제공한다.

Java:

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

Kotlin:

suspend fun getMultipartData(): MultiValueMap<String, Part>

DefaultServerWebExchange는 설정된 HttpMessageReader <MultiValueMap<String, Part>>를 사용하여 multipart/form-data 컨텐츠를 MultiValueMap으로 파싱한다. 현재로서는 Synchronoss NIO Multipart가 유일하게 지원되는 써드파티 라이브러리며, 멀티파트 요청을 논 블로킹으로 파싱하는 유일한 라이브러리다. ServerCodecConfigurer 빈을 통해 활성화된다. (웹 핸들러 API 참조)

멀티파트 데이터를 스트리밍 방식으로 파싱하려면 HttpMessageReader<Party>에서 반환된 Flux<Part>를 대신 사용할 수 있다. 예를 들어, 어노테이션 컨트롤러에서 @RequestPart를 사용하면 이름 별로 개별 파트에 대한 Map과 같은 액세스를 의미한다. 따라서, 멀티파트 데이터를 전체적으로 파싱해야 한다. 반면에 @RequestBody를 사용하여 MultiValueMap으로 모으지 않고 Flux<Part>로 컨텐츠를 디코딩할 수 있다.

전달된 헤더(Forwarded Headers)

요청이 프록시(예를 들면 로드 밸런서)를 통과하면 호스트, 포트 그리고 체계(scheme)가 변경될 수 있다. 따라서 클라이언트 관점에서 올바른 호스트, 포트 그리고 체계가 가리키는 링크를 만드는 것은 쉽지 않다.

RFC 7239(링크)는 원래 요청에 대한 정보를 제공하는데 사용할 수 있는 Forwarded HTTP 헤더를 정의한다. X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl 그리고 X-Forwarded-Prefix 를 포함한 다른 비표준 헤더도 있다.

ForwardedHeaderTransformer는 ForwardedHeader를 기반으로 요청의 호스트, 포트 그리고 체계(scheme)를 수정한 후에 해당 헤더를 제거하는 컴포넌트다. 이름이 forwardedHeaderTransformer인 빈으로 선언하면 감지되어 사용된다.

애플리케이션은 헤더가 프록시에 의해 추가되었는지 또는 악의적인 클라이언트에 의해 의도적으로 추가되었는지 알 수 없으므로 전달된 헤더(forwarded headers)의 보안 고려사항이 있다. 이것이 신뢰의 경계에 있는 프록시를 구성하여 외부에서 들어오는 신뢰할 수 없는 트래픽을 제거하도록 설정해야 하는 이유다. removeOnly=true 옵션으로 ForwardedHeaderTransformer를 구성할 수도 있다. 이 경우 헤더는 제거하지만 사용하지 않는다.

5.1 버전에서는 ForwardedHeaderFilter가 deprecated 되었고 ForwardedHeaderTransformer로 대체되었다. 그렇기 때문에 전달된 헤더(forwarded headers)는 exchange의 생성되기 전에 더 일찍 처리될 수 있다. 필터가 설정된 경우라면 필터 목록에서 제거되고 대신 ForwardedHeaderTransformer가 사용된다.


1.2.3. 필터

WebHandler API에서 WebFilter를 사용하여 인터셉터 스타일의 로직을 WebHandler의 전후에 체이닝 방식으로 적용할 수 있다. Webflux Config를 사용하는 경우, WebFilter를 등록하는 것은 스프링 빈을 등록하는 것만큼 간단하며 빈 선언에 @Order를 사용하거나 Ordered 인터페이스를 구현하여 우선순위를 표시할 수 있다.

CORS

스프링 웹플럭스는 컨트롤러의 어노테이션을 통해 CORS 설정을 세부적으로 지원한다. 그러나 스프링 시큐리티(Spring Security)와 함께 사용하는 경우 내장 CorsFilter를 사용하는 것을 권장한다. 이 필터는 스프링 시큐리티의 필터 체인보다 먼저 적용되어야 한다.

더 자세한 내용은 CORSwebflux-cors를 참조하라.


1.2.4. 예외

WebHandler API에서 WebExceptionHandler를 사용하여 WebFilter 인스턴스와 타겟 WebHandler의 예외를 처리할 수 있다. WebFlux Config를 사용하는 경우, WebExceptionHandler를 등록하는 것은 스프링 빈을 등록하는 것만큼 간단하며 빈 선언에 @Order를 사용하거나 Ordered 인터페이스를 구현하여 우선순위를 표시할 수 있다.

아래 표는 사용 가능한 WebExceptionHandler 구현체에 대한 설명이다.

예외 핸들러 설명
ResponseStatusExceptionHandler 예외의 HTTP 상태 코드에 대한 응답을 설정하여 ResponseStatusException 유형의 예외를 처리한다.
WebFluxResponseStatusExceptionHandler 예외 유형에 상관없이 @ResponseStatus의 상태 코드를 결정할 수 있는 ResponseStatusExceptionHandler의 확장 버전이다. 이 핸들러는 Webflux Config에 선언한다.


1.2.5. 코덱(Codecs)

spring-webspring-core 모듈은 리액티브 스트림 백프레셔와 논 블로킹 I/O를 통하여 고수준 객체와 바이트 컨텐츠를 직렬화(serialization)하고 역직렬화(deserialization)하는 기능을 지원한다. 다음은 지원하는 기능에 대한 설명이다.

  • EncoderDecoder는 HTTP와 무관하게 컨텐츠를 인코딩하고 디코딩하는 저수준(low level) 기능이다.
  • HttpMessageReaderHttpMessageWriter는 HTTP 메시지 컨텐츠를 인코딩하고 디코딩하기 위해 사용된다.
  • 인코더는 EncoderHttpMessageWriter로 래핑되어 웹 애플리케이션에서 사용할 수 있도록 조정할 수 있고, 디코더는 DecoderHttpMessageReader로 래핑될 수 있다.

  • DataBuffer는 다른 바이트 버퍼 표현(예를 들면 Netty ByteBuf, java.nio.ByteBuffer 등)을 추상화하며 모든 코덱은 여기서 동작한다. 관련하여 자세한 내용은 “Spring Core”의 Data Buffers and Codecs를 참고하라.

spring-core 모듈은 byte[], ByteBuffer, Resource, String 인코더 및 디코더 구현체를 제공한다. spring-web 모듈은 Jackson JSON, Jackson Smile, JAXB2, Protocol Buffer 그리고 기타 다른 인코더와 디코더와 함께 폼 데이터, 멀티파트 컨텐츠, 서버 전송 이벤트 및 기타 처리를 위한 웹 전용 HTTP 메시지 reader/writer 구현체를 제공한다.

ClientCodecConfigurerServerCodecConfigurer는 일반적으로 애플리케이션에서 사용할 코덱을 설정하고 사용자 맞춤 설정(customize)을 위해 사용된다. 이 부분은 HTTP 메시지 코덱 설정에 대한 섹션을 참조하라.

잭슨(Jackson) JSON

잭슨(Jackson) 라이브러리가 있으면, JSON과 이진 JSON(Smile)이 모두 지원된다.

Jackson2Decoder는 아래와 같이 동작한다:

  • Jackson의 비동기, 논 블로킹 파서는 바이트 청크 스트림을 각각 JSON 객체를 나타내는 TokenBuffer로 수집하기 위해 사용된다.
  • TokenBuffer는 Jackson의 ObjectMapper로 전달되어 더 높은 수준의 객체를 만든다.
  • 단일값 퍼블리셔(Mono)로 디코딩할 때는, 하나의 TokenBuffer가 존재한다.
  • 다중값 퍼블리셔(Flux)로 디코딩할 때는, 각 TokenBuffer는 완전히 포맷팅된 객체가 될 정도의 충분한 바이트가 수신되는 즉시 ObjectMapper로 전달된다. 입력 컨텐츠는 JSON 배열이거나, 컨텐츠 유형이 application/stream+json인 경우 라인 구분된 JSON(line-delimited JSON)일 수 있다.

Jackson2Encoder는 아래와 같이 동작한다:

  • 단일값 퍼블리셔(Mono)의 경우, 간단히 ObectMapper로 직렬화(serialization)한다.
  • application/json을 사용하는 다중값 퍼블리셔의 경우, 기본적으로 Flux#collectToList()로 값을 모은 후에 그 결과를 직렬화한다.
  • application/stream+json 또는 application/stream+x-jackson-smile과 같은 스트리밍 미디어 타입의 다중값 퍼블리셔의 경우 라인 구분된 JSON(line-delimited JSON) 포맷을 이용하여 각 값을 개별적으로 인코딩, 쓰기 그리고 플러싱한다.
  • SSE(Server-Sent Events)의 경우 Jackson2Encoder가 이벤트마다 호출되며 출력(output)은 지연 없이 전달되도록 플러싱된다.

기본적으로 Jackson2EncoderJackson2Decoder 모두 문자열(String) 타입의 요소를 지원하지 않는다. 대신에 기본 가정은 문자열 또는 문자열 시퀀스가 직렬화된 JSON 컨텐츠를 나타내며 CharSequenceEncoder에 의해 렌더링된다는 것이다. Flux<String>에서 JSON 배열을 렌더링해야 하는 경우, Flux#collectToList()를 사용하고 Mono<List<String>>을 인코딩하라.

폼 데이터(Form Data)

FormHttpMessageReaderFormHttpMessageWriterapplication/x-www-form-urlencoded 컨텐츠의 디코딩과 인코딩을 지원한다.

여러 곳에서 폼 컨텐츠에 접근해야 하는 서버 측에서는 ServerWebExchange가 제공하는 getFormData() 메서드로 파싱한다. FormHttpMessageReader 통해 내용을 파싱한 후 반복적인 액세스를 위해 결과를 캐싱한다. WebHandler API 섹션의 폼 데이터(Form Data)를 참조하라.

getFormData() 메서드가 호출되면, 더 이상 요청 본문에서 원래의 원본 컨텐츠는 읽을 수 없다. 이러한 이유로 애플리케이션은 요청 본문에서 원본 컨텐츠를 읽는 것 대신에 ServerWebExchange를 통해 캐싱된 폼 데이터에 접근하도록 한다.

멀티파트(Multipart)

MultipartHttpMessageReaderMultipartHttpMessageWritermultipart/form-data 컨텐츠의 디코딩과 인코딩을 지원한다. 결과적으로 MultipartHttpMessageReaderFlux<Part>로의 파싱 작업은 HttpMessageReader에게 위임한 후에 결과를 MultiValueMap에 수집한다. 현재는 Synchronoss NIO Multipart가 실제 파싱에 사용된다.

여러 곳에서 멀티파트 컨텐츠에 접근해야 하는 서버 측에서는 ServerWebExchange가 제공하는 getMultipartData() 메서드로 파싱한다. MultipartHttpMessageReader를 통해 내용을 파싱한 후 반복적인 액세스를 위해 결과를 캐싱한다. WebHandler API 섹션의 멀티파트 데이터(Multipart Data)를 참조하라.

getMultipartData() 메서드가 호출되면, 더 이상 요청 본문에서 원래의 원본 컨텐츠는 읽을 수 없다. 이로 인해 애플리케이션은 반복적인 맵과 같은 액세스에 대해서 getMultipartData() 메서드를 지속적으로 사용해야 하며, Flux<Part>로의 일회성 접근에는 SynchronossPartHttpMessageReader를 사용한다.

제한(Limits)

입력 스트림의 일부 또는 전부를 버퍼링하는 DecoderHttpMessageReader 구현체는 메모리에서 버퍼링할 최대 바이트 사이즈를 지정할 수 있다. 입력 버퍼링이 발생하는 경우가 있다. 예를 들면 @RequestBody byte[], x-www-form-urlencoded 등의 데이터를 다루는 컨트롤러 메서드처럼 입력이 합쳐져 단일 객체로 표현되는 경우가 있다. 또한, 구분된 텍스트(delimited text), JSON 객체의 스트림 등과 같은 입력 스트림을 분리할 때 스트리밍에서에서 버퍼링이 발생할 수 있다. 이러한 스트리밍 경우, 버퍼 바이트 사이즈 제한은 스트림에서 하나의 객체와 연결된 바이트 수에 적용된다.

버퍼 사이즈를 설정하기 위해서, 지정된 Decoder 또는 HttpMessageReadermaxInMemorySize 설정이 가능한지 확인하고, Javadoc에 기본값에 대한 세부 사항이 있는지 확인할 수 있다. 서버 측에서 ServerCodecConfigurer은 모든 코덱을 설정할 수 있는 단일 위치를 제공한다. 관련 내용은 HTTP 메시지 코덱을 참조하라. 클라이언트 쪽에서는 모든 코덱에 대한 제한을 WebClient.Builder에서 변경할 수 있다.

maxInMemorySize 속성은 멀티파트 파싱에 적용되는 non-file 파트의 크기를 제한한다. 파일 파트의 경우 파트가 디스크에 기록되는 임계값을 결정한다. 디스크에 기록된 파일 파트의 경우 파트 당 디스크 공간의 양을 제한하는 maxDiskUsagePerPart 속성이 추가적으로 있다. 또한 maxParts 속성은 멀티파트 요청의 전체 사이즈를 제한한다. 웹플럭스에서 이 세가지 속성을 모두 설정하려면 미리 설정된 MultipartHttpMessageReader 인스턴스를 ServerCodecConfigurer에 설정해야 한다.

스트리밍(Streaming)

text/event-stream, application/stream+json과 같은 HTTP 응답으로 스트리밍 할 때는 연결이 끊어진 클라이언트를 보다 빨리 감지할 수 있도록 주기적으로 데이터를 보내야한다. 이러한 전송은 코멘트만 있거나, 빈 SSE(Server Sent Events) 또는 심장박동(heartbeat) 역할을 하는 다른 어떠한 “동작없음(no-op)” 데이터일 수 있다.

데이터 버퍼(Data Buffer)

DataBuffer는 웹플럭스의 바이트 버퍼를 나타낸다. 스프링 코어의 데이터 버퍼와 코덱 섹션에서 더 자세히 확인할 수 있다. 중요한 점은 네티(Netty)와 같은 일부 서버에서 바이트 버퍼가 풀링되고 참조 카운트되며 메모리 누수(leak)를 방지하기 위해 소비될 때 해제되어야 한다는 것이다.

데이터 버퍼를 직접 소비하거나 생산하지 않는 한, 더 높은 수준의 개체로 변환하거나 사용자 지정 코덱을 만들어 사용하거나 또는 코덱을 사용하여 고수준 객체들로/로부터 변환하는 작업을 하지 않는 이상, 웹플럭스 애플리케이션은 일반적으로 이러한 이슈에 대해서 걱정할 필요가 없다. 이러한 경우에 대해서는 데이터 버퍼와 코덱, 특히 데이터 버퍼 사용에 대한 섹션을 참조하라.


1.2.6. 로깅(Logging)

스프링 웹플럭스의 DEBUG 레벨 로깅은 가볍고, 최소화되며, 인간 친화적으로 설계되었다. 특정 문제를 디버깅할 때만 유용한 다른 정보에 비해 계속해서 가치가 있는 정보에 중점을 둔다.

TRACE 레벨 로깅은 일반적으로 DEBUG와 동일한 원칙을 따르지만(예를 들어, firehose가 되어선 안된다.) 어떠한 디버깅에도 사용될 수 있다. 또한 일부 로그 메시지는 TRACE와 DEBUG 레벨에서 서로 다른 수준의 세부 정보를 표시할 수 있다.

좋은 로깅은 사용 경험에서 비롯된다. 명시된 목표를 충족하지 못하는 것이 발견되면 제보하라.

로그 아이디(Log Id)

웹플럭스에서는 단일 요청을 여러 스레드에서 실행할 수 있기 때문에, 특정 요청에 대한 로그 메시지의 연관성을 찾는데 스레드 ID는 유용하지 못하다. 이것이 웹플럭스 로그 메시지 앞에 기본적으로 요청별 ID가 접두사로 붙는 이유다.

서버 측에서 로그 ID는 ServerWebExchange 속성(LOG_ID_ATTRIBUTE)으로 저장되며 ServerWebExchange#getLogPrefix() 메서드를 통해 해당 ID를 기반으로 한 완전히 포맷팅된 접두사를 얻을 수 있다. 클라이언트 측에서 로그 ID는 ClientRequest 속성(LOG_ID_ATTRIBUTE)로 저장되며 ClientRequest#logPrefix() 메서드를 통해 완전히 포맷팅된 접두사를 얻을 수 있다.

민감한 데이터(Sensitive Data)

DEBUG와 TRACE 로깅은 민감한 정보를 기록할 수 있다. 그렇기 때문에 폼 파라미터와 헤더는 기본적으로 마스킹되어야 하고, 전체 로깅은 명시적으로 활성화돼야 한다.

다음 예제는 서버 측 요청에 대한 로깅 설정 방법이다:

Java:

@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

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

Kotlin

@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true)
    }
}

다음 예제는 클라이언트 측 요청에 대한 로깅 설정 방법이다:

Java:

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

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

Kotlin:

val consumer: (ClientCodecConfigurer) -> Unit  = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }

val webClient = WebClient.builder()
        .exchangeStrategies({ strategies -> strategies.codecs(consumer) })
        .build()

사용자 지정 코덱(Custom codecs)

애플리케이션은 추가적인 미디어 유형을 지원하거나 기본 코덱에서 지원하지 않는 특정 동작을 지원하기 위해 사용자 지정 코덱을 등록할 수 있다.

개발자가 설정할 수 있는 일부 옵션은 기본 코덱에 적용된다. 사용자 지정 코덱은 버퍼링 제한(enforcing buffering limits) 또는 민감한 데이터 로깅(logging sensitive data)과 같은 설정을 필요로 할 수 있다.

아래 예제는 클라이언트측 요청에 대한 설정 방법이다.

Java:

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

Kotlin:

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

목차 가이드


댓글을 남기시려면 Github 로그인을 해주세요 :D


Hi, there!

Thanks for visiting my blog.
Please let me know if there are any mistakes in my post.