Loading...
Spring Framework Reference Documentation 7.0.2의 Reactive Core의 한국어 번역본입니다.
아래의 경우에 피드백에서 신고해주신다면 반영하겠습니다.
감사합니다 :)
spring-web module은 reactive web 애플리케이션을 위한 다음과 같은 기초적인 지원을 포함합니다:
WebHandler API: 요청 처리를 위한 약간 더 높은 수준의 범용 web API로,
이 위에 어노테이션 기반 컨트롤러와 함수형 엔드포인트 같은 구체적인 프로그래밍 모델이 구축됩니다.ClientHttpConnector contract가 있으며, 여기에
Reactor Netty, reactive
Jetty HttpClient
및 Apache HttpComponents에 대한 adapter가 있습니다.
애플리케이션에서 사용하는 상위 수준의 WebClient는
이 기본 contract 위에 구축됩니다.HttpHandlerHttpHandler는 요청과 응답을 처리하기 위한 단일 메서드를 가진 간단한 contract입니다. 의도적으로 최소한으로 설계되었으며, 그 주된 그리고 유일한 목적은 서로 다른 HTTP 서버 API 위에 최소한의 추상화를 제공하는 것입니다.
다음 표는 지원되는 서버 API를 설명합니다:
| Server name | Server API used | Reactive Streams support |
|---|---|---|
| Netty | Netty API | Reactor Netty |
| Tomcat | Servlet non-blocking I/O; Tomcat API를 사용하여 byte[] 대신 ByteBuffer를 read/write | spring-web: Servlet non-blocking I/O를 Reactive Streams로 bridge |
| Jetty | Servlet non-blocking I/O; Jetty API를 사용하여 byte[] 대신 ByteBuffer를 write | spring-web: Servlet non-blocking I/O를 Reactive Streams로 bridge |
| Servlet container | Servlet non-blocking I/O | spring-web: Servlet non-blocking I/O를 Reactive Streams로 bridge |
다음 표는 서버 의존성을 설명합니다 (supported versions도 참고하세요):
| Server name | Group id | Artifact name |
|---|---|---|
| Reactor Netty | io.projectreactor.netty | reactor-netty |
| Tomcat | org.apache.tomcat.embed | tomcat-embed-core |
| Jetty | org.eclipse.jetty | jetty-server, jetty-servlet |
아래 코드 스니펫은 각 서버 API에서 HttpHandler adapter를 사용하는 방법을 보여줍니다.
Reactor Netty
1HttpHandler handler = ... 2ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); 3HttpServer.create().host(host).port(port).handle(adapter).bindNow();
1val handler: HttpHandler = ... 2val adapter = ReactorHttpHandlerAdapter(handler) 3HttpServer.create().host(host).port(port).handle(adapter).bindNow()
Tomcat
1HttpHandler handler = ... 2Servlet servlet = new TomcatHttpHandlerAdapter(handler); 3 4Tomcat server = new Tomcat(); 5File base = new File(System.getProperty("java.io.tmpdir")); 6Context rootContext = server.addContext("", base.getAbsolutePath()); 7Tomcat.addServlet(rootContext, "main", servlet); 8rootContext.addServletMappingDecoded("/", "main"); 9server.setHost(host); 10server.setPort(port); 11server.start();
1val handler: HttpHandler = ... 2val servlet = TomcatHttpHandlerAdapter(handler) 3 4val server = Tomcat() 5val base = File(System.getProperty("java.io.tmpdir")) 6val rootContext = server.addContext("", base.absolutePath) 7Tomcat.addServlet(rootContext, "main", servlet) 8rootContext.addServletMappingDecoded("/", "main") 9server.host = host 10server.setPort(port) 11server.start()
Jetty
1HttpHandler handler = ... 2JettyCoreHttpHandlerAdapter adapter = new JettyCoreHttpHandlerAdapter(handler); 3 4Server server = new Server(); 5server.setHandler(adapter); 6 7ServerConnector connector = new ServerConnector(server); 8connector.setHost(host); 9connector.setPort(port); 10server.addConnector(connector); 11 12server.start();
1val handler: HttpHandler = ... 2val adapter = JettyCoreHttpHandlerAdapter(handler) 3 4val server = Server() 5server.setHandler(adapter) 6 7val connector = ServerConnector(server) 8connector.host = host 9connector.port = port 10server.addConnector(connector) 11 12server.start()
Spring Framework 6.2에서
JettyHttpHandlerAdapter는<br>Servlet 레이어 없이 Jetty 12 API와 직접 통합하는<br>JettyCoreHttpHandlerAdapter로 대체되면서 deprecated되었습니다.
대신 Servlet 컨테이너에 WAR로 배포하려면
AbstractReactiveWebInitializer를 사용하여,
ServletHttpHandlerAdapter를 통해 HttpHandler를 Servlet에 적응시켜야 합니다.
WebHandler APIorg.springframework.web.server 패키지는
HttpHandler contract 위에 구축되어,
여러 개의
WebExceptionHandler, 여러 개의
WebFilter, 그리고 하나의
WebHandler 컴포넌트로 이루어진 체인을 통해
요청을 처리하기 위한 범용 web API를 제공합니다.
이 체인은 Spring
ApplicationContext를 가리키기만 하면
auto-detected되는 컴포넌트와,
builder에 직접 등록된 컴포넌트를 사용하여 WebHttpHandlerBuilder로 조합할 수 있습니다.
HttpHandler가 서로 다른 HTTP 서버 사용을 추상화하는 단순한 목표를 가지는 반면,
WebHandler API는 web 애플리케이션에서 일반적으로 사용되는 보다 폭넓은 기능을 제공하는 것을 목표로 합니다.
예를 들어 다음과 같습니다:
Locale 또는 Principal resolution.아래 표는 WebHttpHandlerBuilder가 Spring ApplicationContext에서 auto-detect하거나,
또는 직접 등록할 수 있는 컴포넌트를 나열합니다:
| Bean name | Bean type | Count | Description |
|---|---|---|---|
<any> | WebExceptionHandler | 0..N | WebFilter 인스턴스 체인과 target<br>WebHandler에서 발생하는 예외를 처리합니다. 자세한 내용은 Exceptions를 참고하세요. |
<any> | WebFilter | 0..N | 나머지 필터 체인과 target WebHandler의<br>앞뒤에 인터셉션 스타일의 로직을 적용합니다. 자세한 내용은 Filters를 참고하세요. |
webHandler | WebHandler | 1 | 요청을 위한 handler입니다. |
webSessionManager | WebSessionManager | 0..1 | ServerWebExchange의 메서드를 통해 노출되는 WebSession 인스턴스를 관리하는 매니저입니다.<br>기본값은 DefaultWebSessionManager입니다. |
serverCodecConfigurer | ServerCodecConfigurer | 0..1 | Form data와 multipart data를 파싱하기 위한 HttpMessageReader 인스턴스에 대한 접근을 제공하며,<br>그 결과는 ServerWebExchange의 메서드를 통해 노출됩니다. 기본값은 ServerCodecConfigurer.create()입니다. |
localeContextResolver | LocaleContextResolver | 0..1 | ServerWebExchange의 메서드를 통해 노출되는 LocaleContext를 위한 resolver입니다.<br>기본값은 AcceptHeaderLocaleContextResolver입니다. |
forwardedHeaderTransformer | ForwardedHeaderTransformer | 0..1 | Forwarded 유형 header를 처리하며, 이를 추출하고 제거하거나 제거만 합니다.<br>기본적으로는 사용되지 않습니다. |
ServerWebExchange는 form data에 접근하기 위한 다음 메서드를 노출합니다:
1Mono<MultiValueMap<String, String>> getFormData();
1suspend fun getFormData(): MultiValueMap<String, String>
DefaultServerWebExchange는 구성된 HttpMessageReader를 사용하여 form data
(application/x-www-form-urlencoded)를 MultiValueMap으로 파싱합니다. 기본적으로
FormHttpMessageReader가 ServerCodecConfigurer bean에 의해 사용되도록 구성되어 있습니다
(Web Handler API를 참고하세요).
ServerWebExchange는 multipart data에 접근하기 위한 다음 메서드를 노출합니다:
1Mono<MultiValueMap<String, Part>> getMultipartData();
1suspend fun getMultipartData(): MultiValueMap<String, Part>
DefaultServerWebExchange는 구성된
HttpMessageReader<MultiValueMap<String, Part>>를 사용하여 multipart/form-data,
multipart/mixed, 그리고 multipart/related content를 MultiValueMap으로 파싱합니다.
기본적으로 이는 DefaultPartHttpMessageReader이며, 어떤 서드파티
의존성도 가지지 않습니다.
대신, Synchronoss NIO Multipart 라이브러리를 기반으로 하는
SynchronossPartHttpMessageReader를 사용할 수도 있습니다.
둘 다 ServerCodecConfigurer bean을 통해 구성됩니다
(Web Handler API를 참고하세요).
Streaming 방식으로 multipart data를 파싱하려면, @RequestPart 대신
PartEventHttpMessageReader에서 반환되는 Flux<PartEvent>를 사용할 수 있습니다. @RequestPart는
이름별로 개별 part에 대한 Map과 유사한 접근을 의미하므로, multipart data를 전체적으로 파싱해야 합니다.
반대로, @RequestBody를 사용하여 MultiValueMap으로 수집하지 않고 content를 Flux<PartEvent>로
디코딩할 수 있습니다.
요청이 로드 밸런서 같은 프록시를 통과하면서 host, port, scheme이 변경될 수 있고, 이는 클라이언트 관점에서 올바른 host, port, scheme을 가리키는 링크를 생성하는 데 어려움을 줍니다.
RFC 7239는 프록시가 원래 요청에 대한 정보를 제공하기 위해
사용할 수 있는 Forwarded HTTP header를 정의합니다.
X-Forwarded-Host, X-Forwarded-Port,
X-Forwarded-Proto, X-Forwarded-Ssl, X-Forwarded-Prefix, X-Forwarded-For와 같은
기타 비표준 header도 있습니다.
표준은 아니지만, X-Forwarded-Host: <host>는
원래 host를 downstream 서버에 전달하는 데 사용되는 사실상의 표준 header입니다. 예를 들어,
example.com/resource에 대한 요청이 프록시로 전송되고 프록시가 이 요청을
localhost:8080/resource로 전달하는 경우, X-Forwarded-Host: example.com header를
전송하여 서버에 원래 host가 example.com이었음을 알릴 수 있습니다.
표준은 아니지만, X-Forwarded-Port: <port>는 원래 port를 downstream 서버에 전달하는 데
사용되는 사실상의 표준 header입니다. 예를 들어,
example.com/resource에 대한 요청이 프록시로 전송되고 프록시가 이 요청을
localhost:8080/resource로 전달하는 경우, X-Forwarded-Port: 443 header를 전송하여
서버에 원래 port가 443이었음을 알릴 수 있습니다.
표준은 아니지만, X-Forwarded-Proto: (https|http)는
원래 프로토콜(예: https / http)을 downstream 서버에 전달하는 데 사용되는 사실상의 표준 header입니다.
예를 들어, example.com/resource에 대한 요청이 프록시로 전송되고 프록시가 이 요청을
localhost:8080/resource로 전달하는 경우, X-Forwarded-Proto: https header를 전송하여
서버에 원래 프로토콜이 https였음을 알릴 수 있습니다.
표준은 아니지만, X-Forwarded-Ssl: (on|off)는 원래 프로토콜(예: https / https)을 downstream 서버에
전달하는 데 사용되는 사실상의 표준 header입니다. 예를 들어,
example.com/resource에 대한 요청이 프록시로 전송되고 프록시가 이 요청을
localhost:8080/resource로 전달하는 경우, X-Forwarded-Ssl: on header를 전송하여
서버에 원래 프로토콜이 https였음을 알릴 수 있습니다.
표준은 아니지만, X-Forwarded-Prefix: <prefix>는
원래 URL path prefix를 downstream 서버에 전달하는 데 사용되는 사실상의 표준 header입니다.
X-Forwarded-Prefix의 사용은 배포 시나리오에 따라 달라질 수 있으며, target 서버의 path prefix를
대체, 제거 또는 앞에 추가할 수 있도록 유연해야 합니다.
Scenario 1: Override path prefix
1https://example.com/api/{path} -> http://localhost:8080/app1/{path}
Prefix는 capture group {path} 앞의 path 시작 부분입니다. 프록시에 대해
prefix는 /api이고, 서버에 대해 prefix는 /app1입니다. 이 경우, 프록시는
X-Forwarded-Prefix: /api를 전송하여 원래 prefix /api가 서버 prefix /app1을
override하도록 할 수 있습니다.
Scenario 2: Remove path prefix
때때로 애플리케이션은 prefix가 제거되기를 원할 수 있습니다. 예를 들어, 다음과 같은 프록시에서 서버로의 매핑을 고려해 보세요:
1https://app1.example.com/{path} -> http://localhost:8080/app1/{path} 2https://app2.example.com/{path} -> http://localhost:8080/app2/{path}
프록시에는 prefix가 없지만, 애플리케이션 app1과 app2에는 각각
/app1 및 /app2 path prefix가 있습니다. 프록시는 X-Forwarded-Prefix:를 전송하여
비어 있는 prefix가 서버 prefix /app1 및 /app2를 override하도록 할 수 있습니다.
이 배포 시나리오의 일반적인 사례는 production 애플리케이션 서버당 라이선스 비용을 지불해야 하고,<br>비용을 줄이기 위해 여러 애플리케이션을 하나의 서버에 배포하는 것이 더 바람직한 경우입니다.<br>또 다른 이유는 서버를 실행하는 데 필요한 리소스를 공유하기 위해 동일한 서버에서 더 많은 애플리케이션을<br>실행하기 위한 것입니다.<br>이러한 시나리오에서, 동일한 서버에 여러 애플리케이션이 있기 때문에 애플리케이션은 비어 있지 않은 context root가<br>필요합니다. 그러나 이는 public API의 URL path에는 드러나지 않아야 하며, 여기서 애플리케이션은 다음과 같은<br>이점을 제공하는 서로 다른 subdomain을 사용할 수 있습니다:<br>- 추가적인 보안, 예를 들어 same origin policy<br><br>- 애플리케이션의 독립적인 scaling(서로 다른 domain이 서로 다른 IP address를 가리킴)
Scenario 3: Insert path prefix
다른 경우에는 prefix를 앞에 추가해야 할 수도 있습니다. 예를 들어, 다음과 같은 프록시에서 서버로의 매핑을 고려해 보세요:
1https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path}
이 경우, 프록시에는 /api/app1 prefix가 있고 서버에는 /app1 prefix가 있습니다.
프록시는 X-Forwarded-Prefix: /api/app1를 전송하여 원래 prefix /api/app1이
서버 prefix /app1을 override하도록 할 수 있습니다.
X-Forwarded-For: <address>는
원래 클라이언트의 InetSocketAddress를 downstream 서버에 전달하는 데 사용되는 사실상의 표준 header입니다.
예를 들어, 클라이언트가 [fd00:fefe:1::4]에서 프록시 192.168.0.1로 요청을 전송하는 경우,
HTTP 요청에 포함된 "remote address" 정보는 프록시가 아니라 클라이언트의 실제 address를 반영합니다.
ForwardedHeaderTransformer는 forwarded header를 기반으로 요청의 host, port, scheme을
수정한 다음 해당 header를 제거하는 컴포넌트입니다. 이를 forwardedHeaderTransformer라는
이름의 bean으로 선언하면
detected되어 사용됩니다.
애플리케이션은 header가 의도한 대로 프록시에 의해 추가된 것인지, 아니면 악의적인 클라이언트에 의해
추가된 것인지 알 수 없기 때문에 forwarded header에는 보안상의 고려 사항이 있습니다. 이 때문에
신뢰 경계의 프록시는 외부에서 들어오는 신뢰할 수 없는 forwarded 트래픽을 제거하도록 구성해야 합니다.
또한 ForwardedHeaderTransformer를 removeOnly=true로 구성할 수도 있는데, 이 경우 header를
사용하지 않고 제거만 합니다.
WebHandler API에서는 WebFilter를 사용하여 필터 체인과 target
WebHandler의 나머지 처리 체인 앞뒤에 인터셉션 스타일의 로직을 적용할 수 있습니다.
WebFlux Config를 사용할 때,
WebFilter를 등록하는 것은 Spring bean으로 선언하고, (선택적으로) bean 선언에 @Order를 사용하거나
Ordered를 구현하여 우선순위를 표현하는 것만큼 간단합니다.
Spring WebFlux는 컨트롤러에 대한 어노테이션을 통해 CORS 구성을 세밀하게 지원합니다.
그러나 이를 Spring Security와 함께 사용할 때는, Spring Security의 필터 체인보다 먼저
실행되도록 순서를 지정해야 하는 내장 CorsFilter에 의존하는 것이 좋습니다.
자세한 내용은 CORS와
CORS WebFilter 섹션을 참고하세요.
컨트롤러 엔드포인트가 URL path의 trailing slash 유무와 상관없이 라우트와 매칭되도록
하고 싶을 수 있습니다.
예를 들어, "GET /home"과 "GET /home/" 모두가 @GetMapping("/home")으로 어노테이션된
컨트롤러 메서드에 의해 처리되어야 합니다.
모든 매핑 선언에 trailing slash 변형을 추가하는 것은 이 use case를 처리하는 최선의 방법이
아닙니다. 이 목적을 위해 UrlHandlerFilter web 필터가 설계되었습니다. 이는 다음과 같이
구성할 수 있습니다:
다음은 블로그 애플리케이션을 위해 UrlHandlerFilter를 생성하고 구성하는 방법입니다:
1UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter 2 // will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post" 3 .trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT) 4 // will mutate the request to "/admin/user/account/" and make it as "/admin/user/account" 5 .trailingSlashHandler("/admin/**").mutateRequest() 6 .build();
1val urlHandlerFilter = UrlHandlerFilter 2 // will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post" 3 .trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT) 4 // will mutate the request to "/admin/user/account/" and make it as "/admin/user/account" 5 .trailingSlashHandler("/admin/**").mutateRequest() 6 .build()
WebHandler API에서는 WebExceptionHandler를 사용하여
WebFilter 인스턴스 체인과 target WebHandler에서 발생하는 예외를 처리할 수 있습니다.
WebFlux Config를 사용할 때,
WebExceptionHandler를 등록하는 것은 Spring bean으로 선언하고, (선택적으로) bean 선언에
@Order를 사용하거나 Ordered를 구현하여 우선순위를 표현하는 것만큼 간단합니다.
다음 표는 사용 가능한 WebExceptionHandler 구현을 설명합니다:
| Exception Handler | Description |
|---|---|
ResponseStatusExceptionHandler | ResponseStatusException<br>유형의 예외에 대한 handling을 제공하며, 예외의 HTTP 상태 코드를 응답에 설정합니다. |
WebFluxResponseStatusExceptionHandler | @ResponseStatus 어노테이션이 있는 어떤 예외에 대해서도<br>HTTP 상태 코드를 결정할 수 있는 ResponseStatusExceptionHandler의 확장입니다.<br>이 handler는 WebFlux Config에 선언되어 있습니다. |
spring-web 및 spring-core module은 non-blocking I/O와
Reactive Streams back pressure를 통해 byte content를 상위 수준 객체로
직렬화 및 역직렬화하는 것을 지원합니다. 다음은 이 지원에 대한 설명입니다:
Encoder와
Decoder는
HTTP와 무관하게 content를 인코딩 및 디코딩하기 위한 저수준 contract입니다.HttpMessageReader와
HttpMessageWriter는
HTTP 메시지 content를 인코딩 및 디코딩하기 위한 contract입니다.Encoder는 EncoderHttpMessageWriter로 wrapping되어 web
애플리케이션에서 사용할 수 있도록 적응될 수 있으며, Decoder는 DecoderHttpMessageReader로 wrapping될 수 있습니다.DataBuffer는
서로 다른 byte buffer 표현(Netty ByteBuf, java.nio.ByteBuffer 등)을 추상화하며,
모든 codec이 이 위에서 동작합니다. 이 주제에 대한 자세한 내용은 "Spring Core" 섹션의
Data Buffers and Codecs를 참고하세요.spring-core module은 byte[], ByteBuffer, DataBuffer, Resource, 그리고
String encoder 및 decoder 구현을 제공합니다. spring-web module은 Jackson
JSON, Jackson Smile, JAXB2, Protocol Buffers 및 기타 encoder와 decoder를 제공하며,
form data, multipart content, server-sent event 등 web 전용 HTTP 메시지 reader 및
writer 구현도 제공합니다.
ClientCodecConfigurer와 ServerCodecConfigurer는 일반적으로 애플리케이션에서 사용할
codec을 구성하고 커스터마이즈하는 데 사용됩니다. Codec 구성에 대한 내용은
HTTP message codecs 섹션을 참고하세요.
JSON과 binary JSON(Smile)은 Jackson 라이브러리가 존재할 때 모두 지원됩니다.
JacksonJsonDecoder는 다음과 같이 동작합니다:
TokenBuffer로 aggregate합니다.TokenBuffer는 Jackson의 JsonMapper에 전달되어 상위 수준 객체를 생성합니다.Mono)로 디코딩할 때는 TokenBuffer가 하나입니다.Flux)로 디코딩할 때는, 완전한 객체를 만들기에 충분한 byte가 수신되는 즉시
각 TokenBuffer가 JsonMapper에 전달됩니다.
입력 content는 JSON array이거나 NDJSON, JSON Lines, JSON Text Sequences 같은
line-delimited JSON 형식일 수 있습니다.JacksonJsonEncoder는 다음과 같이 동작합니다:
Mono)의 경우, 이를 JsonMapper를 통해 단순히 직렬화합니다.application/json을 사용하는 다중 값 publisher의 경우, 기본적으로 Flux#collectToList()로 값을
수집한 다음 resulting collection을 직렬화합니다.application/x-ndjson 또는 application/stream+x-jackson-smile과 같은 streaming media type을 사용하는
다중 값 publisher의 경우, 각 값을 개별적으로 인코딩, write, flush하며
line-delimited JSON 형식을 사용합니다.
다른 streaming media type도 encoder에 등록될 수 있습니다.JacksonJsonEncoder는 이벤트마다 호출되며, 지연 없이 전달되도록 출력을 flush합니다.기본적으로
JacksonJsonEncoder와JacksonJsonDecoder는String타입 element를 지원하지 않습니다.<br>대신, string 또는 string sequence는 직렬화된 JSON content를 나타내는 것으로 간주되며,<br>CharSequenceEncoder에 의해 렌더링됩니다. 만약Flux<String>에서 JSON array를 렌더링해야 한다면,<br>Flux#collectToList()를 사용하고,Mono<List<String>>를 인코딩하세요.
FormHttpMessageReader와 FormHttpMessageWriter는
application/x-www-form-urlencoded content의 디코딩과 인코딩을 지원합니다.
Form content를 여러 위치에서 접근해야 하는 서버 측에서는,
ServerWebExchange가 FormHttpMessageReader를 통해 content를 파싱하고
결과를 반복 접근을 위해 캐시하는 전용 getFormData() 메서드를 제공합니다.
WebHandler API 섹션의
Form Data를 참고하세요.
getFormData()가 사용되면, 원래 raw content는 더 이상 요청 body에서 읽을 수 없습니다.
이러한 이유로, 애플리케이션은 raw 요청 body를 읽는 대신 캐시된 form data에 접근하기 위해
항상 ServerWebExchange를 통해 접근해야 합니다.
MultipartHttpMessageReader와 MultipartHttpMessageWriter는
"multipart/form-data", "multipart/mixed", "multipart/related" content의 디코딩과
인코딩을 지원합니다.
MultipartHttpMessageReader는 실제로 Flux<Part>로 파싱하기 위해 다른 HttpMessageReader에
위임하고, 단순히 part를 MultiValueMap으로 수집합니다.
기본적으로 DefaultPartHttpMessageReader가 사용되지만, 이는
ServerCodecConfigurer를 통해 변경할 수 있습니다.
DefaultPartHttpMessageReader에 대한 자세한 내용은
DefaultPartHttpMessageReader의 javadoc을 참고하세요.
Multipart form content를 여러 위치에서 접근해야 할 수 있는 서버 측에서는,
ServerWebExchange가 MultipartHttpMessageReader를 통해 content를 파싱하고
결과를 반복 접근을 위해 캐시하는 전용 getMultipartData() 메서드를 제공합니다.
WebHandler API 섹션의
Multipart Data를 참고하세요.
getMultipartData()가 사용되면, 원래 raw content는 더 이상 요청 body에서 읽을 수 없습니다.
이러한 이유로 애플리케이션은 반복적이고 map과 같은 방식으로 part에 접근할 때
항상 getMultipartData()를 사용해야 하며, 그렇지 않으면
Flux<Part>에 대한 단 한 번의 접근을 위해 SynchronossPartHttpMessageReader에 의존해야 합니다.
ProtobufEncoder와 ProtobufDecoder는 com.google.protobuf.Message 타입에 대해
"application/x-protobuf", "application/octet-stream",
"application/vnd.google.protobuf" content의 디코딩과 인코딩을 지원합니다.
또한, content type에 "delimited" parameter가 함께 제공되는 경우
(예: "application/x-protobuf;delimited=true") 값의 stream도 지원합니다.
이를 위해서는 3.29 이상의 "com.google.protobuf:protobuf-java" 라이브러리가 필요합니다.
ProtobufJsonDecoder와 ProtobufJsonEncoder variant는 Protobuf message와 JSON document 간의
read/write를 지원합니다.
이들은 "com.google.protobuf:protobuf-java-util" 의존성을 필요로 합니다.
JSON variant는 message stream read를 지원하지 않는다는 점에 유의하세요.
자세한 내용은 ProtobufJsonDecoder의 javadoc을 참고하세요.
애플리케이션은 Google Gson 라이브러리를 통해
GsonEncoder와 GsonDecoder를 사용하여 JSON document를 직렬화 및 역직렬화할 수 있습니다.
이 codec은 JSON media type과 streaming을 위한 NDJSON 형식을 모두 지원합니다.
Gson은 non-blocking parsing을 지원하지 않으므로,GsonDecoder는Flux<*>타입으로의<br>역직렬화를 지원하지 않습니다. 예를 들어, 이 decoder가 JSON stream이나 element list를<br>Flux<*>로 역직렬화하는 데 사용되면, runtime에UnsupportedOperationException이 발생합니다.<br>애플리케이션은 대신 유한한 collection 역직렬화에 집중하고, target 타입으로Mono<List<*>>를 사용해야 합니다.
입력 stream의 일부 또는 전부를 버퍼링하는 Decoder와 HttpMessageReader 구현은
메모리에 버퍼할 수 있는 최대 byte 수에 대한 limit를 구성할 수 있습니다.
일부 경우에는 입력이 aggregate되어 단일 객체로 표현되기 때문에 버퍼링이 발생합니다.
예를 들어, @RequestBody byte[]를 사용하는 컨트롤러 메서드,
x-www-form-urlencoded data 등이 해당됩니다. Streaming에서도 입력을 분할할 때
버퍼링이 발생할 수 있습니다. 예를 들어, 구분된 텍스트, JSON 객체 stream 등입니다.
이러한 streaming 경우에 limit는 stream에서 하나의 객체와 관련된 byte 수에 적용됩니다.
버퍼 크기를 구성하려면, 주어진 Decoder 또는 HttpMessageReader가
maxInMemorySize property를 노출하는지 확인하고, 그렇다면 Javadoc에서 기본값에 대한
세부 정보를 확인할 수 있습니다. 서버 측에서는 ServerCodecConfigurer가
모든 codec을 설정할 수 있는 단일 위치를 제공하며,
HTTP message codecs를 참고하세요.
클라이언트 측에서는 모든 codec에 대한 limit를
WebClient.Builder에서 변경할 수 있습니다.
Multipart parsing의 경우,
maxInMemorySize property는 파일이 아닌 part의 크기를 제한합니다. 파일 part의 경우,
part가 디스크에 write되는 임계값을 결정합니다. 디스크에 write되는 파일 part에 대해서는
part당 디스크 공간을 제한하는 추가적인 maxDiskUsagePerPart property가 있습니다.
또한 multipart 요청에서 전체 part 수를 제한하는 maxParts property도 있습니다.
이 세 가지를 WebFlux에서 구성하려면, 사전 구성된 MultipartHttpMessageReader 인스턴스를
ServerCodecConfigurer에 제공해야 합니다.
HTTP 응답으로 streaming할 때(예: text/event-stream,
application/x-ndjson), 연결이 끊어진 클라이언트를 가능한 한 빨리 안정적으로
감지하기 위해 주기적으로 data를 전송하는 것이 중요합니다.
이러한 전송은 주석만 있는 비어 있는 SSE 이벤트이거나, heartbeat 역할을 하는
기타 "no-op" data일 수 있습니다.
DataBufferDataBuffer는 WebFlux에서 byte buffer를 나타내는 표현입니다. Spring Core 부분의
참고 문서에는 Data Buffers and Codecs 섹션에 이에 대한
더 많은 내용이 있습니다. 핵심은 Netty 같은 일부 서버에서는 byte buffer가
pool되고 reference count되며, 메모리 leak을 방지하기 위해 소비 시 release되어야 한다는 점입니다.
WebFlux 애플리케이션은 codec에 의존하여 상위 수준 객체로 변환하거나, custom codec을 생성하지 않는 한, 일반적으로 이러한 문제를 고려할 필요가 없습니다. 이러한 경우에는 Data Buffers and Codecs의 정보를, 특히 Using DataBuffer 섹션을 검토해야 합니다.
Spring WebFlux의 DEBUG 수준 logging은 compact하고 최소한이며
사람이 읽기 쉬운 형태로 설계되었습니다. 이는 특정 issue를 debug할 때만 유용한 정보보다,
반복적으로 유용한 고가치 정보에 초점을 맞춥니다.
TRACE 수준 logging은 일반적으로 DEBUG와 동일한 원칙을 따르며(예를 들어,
firehose가 되어서는 안 됩니다), 어떤 issue든 debug하는 데 사용할 수 있습니다.
또한 일부 log message는 DEBUG와 비교했을 때 TRACE에서 다른 수준의 세부 정보를
표시할 수 있습니다.
좋은 logging은 log를 사용하는 경험에서 나옵니다. 명시된 목표를 충족하지 못하는 부분을 발견하면 알려주시기 바랍니다.
WebFlux에서는 하나의 요청이 여러 thread에서 실행될 수 있으며, thread ID는 특정 요청에 속하는 log message를 상호 연관시키는 데 유용하지 않습니다. 이 때문에 WebFlux log message는 기본적으로 요청별 ID가 prefix로 붙습니다.
서버 측에서는 log ID가 ServerWebExchange attribute
(LOG_ID_ATTRIBUTE)에 저장되며,
해당 ID를 기반으로 완전히 formatting된 prefix는
ServerWebExchange#getLogPrefix()에서 사용할 수 있습니다.
WebClient 측에서는 log ID가 ClientRequest attribute
(LOG_ID_ATTRIBUTE)에 저장되며,
완전히 formatting된 prefix는 ClientRequest#logPrefix()에서 사용할 수 있습니다.
DEBUG 및 TRACE logging은 민감한 정보를 log로 남길 수 있습니다. 이 때문에
form parameter와 header는 기본적으로 masking되며, 전체를 log로 남기려면 명시적으로
활성화해야 합니다.
다음 예제는 서버 측 요청에 대해 이를 수행하는 방법을 보여줍니다:
1@Configuration 2@EnableWebFlux 3class MyConfig implements WebFluxConfigurer { 4 5 @Override 6 public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { 7 configurer.defaultCodecs().enableLoggingRequestDetails(true); 8 } 9}
1@Configuration 2@EnableWebFlux 3class MyConfig : WebFluxConfigurer { 4 5 override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { 6 configurer.defaultCodecs().enableLoggingRequestDetails(true) 7 } 8}
다음 예제는 클라이언트 측 요청에 대해 이를 수행하는 방법을 보여줍니다:
1Consumer<ClientCodecConfigurer> consumer = configurer -> 2 configurer.defaultCodecs().enableLoggingRequestDetails(true); 3 4WebClient webClient = WebClient.builder() 5 .exchangeStrategies(strategies -> strategies.codecs(consumer)) 6 .build();
1val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) } 2 3val webClient = WebClient.builder() 4 .exchangeStrategies({ strategies -> strategies.codecs(consumer) }) 5 .build()
SLF4J 및 Log4J 2와 같은 logging 라이브러리는 blocking을 피하는 asynchronous logger를 제공합니다. 이들은 log를 위해 queue에 넣을 수 없었던 message를 drop할 수도 있다는 단점이 있지만, 현재 reactive, non-blocking 애플리케이션에서 사용할 수 있는 최선의 옵션입니다.
애플리케이션은 추가적인 media type을 지원하거나, 기본 codec에서 지원하지 않는 특정 동작을 위해 custom codec을 등록할 수 있습니다.
개발자가 표현한 일부 구성 옵션은 기본 codec에 적용됩니다. Custom codec은 버퍼링 limit 강제나 민감한 data logging과 같이 이러한 선호도와 일치하도록 조정할 수 있어야 할 수 있습니다.
다음 예제는 클라이언트 측 요청에 대해 이를 수행하는 방법을 보여줍니다:
1WebClient webClient = WebClient.builder() 2 .codecs(configurer -> { 3 CustomDecoder decoder = new CustomDecoder(); 4 configurer.customCodecs().registerWithDefaultConfig(decoder); 5 }) 6 .build();
1val webClient = WebClient.builder() 2 .codecs({ configurer -> 3 val decoder = CustomDecoder() 4 configurer.customCodecs().registerWithDefaultConfig(decoder) 5 }) 6 .build()
Overview
DispatcherHandler