Spring WebFlux
Spring portfolio에서 Spring WebFlux에 대해 간단히 정리해보았는데, 이번 글에서는 한 단계 더 들어가 WebFlux가 내부적으로 어떻게 요청을 처리하는지, 그리고 어떤 컴포넌트들이 이를 구성하고 있는지 중심으로 깊게 알아보려고 합니다.

Spring WebFlux는 HTTP 요청을 처리할 때 특정 서버 구현체에 종속되지 않고, Netty든 Tomcat이든 어떤 서버를 사용하더라도 동일한 처리 방식으로 애플리케이션을 실행할 수 있는 추상화 계층을 제공합니다. 실제 소켓 I/O 처리와 HTTP 프로토콜 레벨의 작업은 서버가 담당하며, WebFlux는 그 위에서 애플리케이션 로직만 관여하는 구조입니다. WebFlux에서 가장 많이 사용되는 서버 구현체는 Netty 기반의 Reactive 서버 라이브러리인 Reactor Netty입니다.

Reactor Netty는 Netty의 저수준 네트워크 API를 Reactor의 Reactive Streams 모델에 맞게 추상화한 계층으로, WebFlux가 Netty 기반으로 동작할 때는 Reactor Netty가 제공하는 HttpServer를 통해 서버를 구동합니다. 클라이언트의 요청 흐름은 다음과 같이 진행됩니다.
- Netty 파이프라인에 등록된 NIO 소켓 채널이 HTTP 요청을 수신
- HTTP Decoder가 바이트 스트림을 HttpMessage로 파싱
- Reactor Netty가 이를 HttpServerRequest로 래핑
- HttpServer.handle(...)에 전달된 사용자 처리 로직으로 요청이 흘러감
문제는 WebFlux가 Netty뿐 아니라 Tomcat, Jetty 같은 Servlet 기반 서버에서도 동일한 WebFlux 애플리케이션을 실행할 수 있어야 한다는 점입니다. 서버마다 요청 처리 방식은 다르고, 함수 시그니처도 서로 호환되지 않습니다.
이를 해결하기 위해 WebFlux는 서버 독립적인 공통 요청 처리 지점으로 HttpHandler 인터페이스를 정의했습니다. 그리고 Netty 환경에서는 ReactorHttpHandlerAdapter, Servlet 환경에서는 ServletHttpHandlerAdapter가 HttpHandler를 각 서버가 요구하는 처리 방식에 맞게 변환해 연결합니다.
HttpHandler
앞서 살펴본 Reactor Netty의 HttpServer는 요청 처리 로직을 handle(...) 메서드를 통해 전달받는데, 이 메서드는 다음과 같은 함수형 시그니처를 요구합니다.
public final HttpServer handle(
BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> handler
)
반면 WebFlux는 Netty뿐 아니라 Tomcat, Jetty 같은 Servlet 기반 서버 등 여러 서버 환경에서 동일한 방식으로 애플리케이션을 실행해야 하므로, 서버 구현체에 종속되지 않는 공통 처리 지점으로 HttpHandler 인터페이스를 정의합니다.
public interface HttpHandler {
/**
* Handle the given request and write to the response.
* @param request current request
* @param response current response
* @return indicates completion of request handling
*/
Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
}
HttpHandler는 ServerHttpRequest와 ServerHttpResponse를 입력받아 응답 처리가 완료되는 시점을 Mono로 비동기 반환하는 WebFlux의 핵심 진입점입니다.
문제는 두 타입의 시그니처가 서로 다르다는 점입니다.
이를 연결하기 위해 WebFlux는 ReactorHttpHandlerAdapter를 제공하며, 이 어댑터가 HttpHandler를 Reactor Netty에서 요구하는 함수 형태로 변환해 HttpServer.handle(...)에 전달할 수 있도록 해줍니다.
public class ReactorHttpHandlerAdapter
implements BiFunction<HttpServerRequest, HttpServerResponse, Mono<Void>> {
private final HttpHandler httpHandler;
public ReactorHttpHandlerAdapter(HttpHandler httpHandler) {
this.httpHandler = httpHandler;
}
...
}
HttpHandler httpHandler = (request, response) ->
response.writeWith(
Mono.just(
response.bufferFactory()
.wrap("hello".getBytes())
)
);
ReactorHttpHandlerAdapter adapter =
new ReactorHttpHandlerAdapter(httpHandler);
HttpServer.create()
.port(8080)
.handle(adapter)
.bindNow()
.onDispose()
.block();
Spring WebFlux에서 사용되는 요청 객체와 응답 객체인 ServerHttpRequest와 ServerHttpResponse에 대해 살펴보겠습니다.
ServerHttpRequest

public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
// 서버가 생성한 요청의 고유 식별자 반환 (Request ID, 로깅/트래킹 용)
String getId();
// query를 포함하지 않는 구조화된 path 정보 제공
// contextPath와 applicationPath를 분리하여 제공
RequestPath getPath();
// decoded된 쿼리 파라미터가 담긴 read-only map
MultiValueMap<String, String> getQueryParams();
// 클라이언트가 전달한 쿠기가 담긴 read-only map
MultiValueMap<String, HttpCookie> getCookies();
// request의 속성을 변경할 수 있는 Builder를 반환
default ServerHttpRequest.Builder mutate() {
return new DefaultServerHttpRequestBuilder(this);
}
}
public interface HttpRequest extends HttpMessage {
// HTTP Method
HttpMethod getMethod();
// request URI
URI getURI();
// request 속성이 담긴 변경가능한 map
Map<String, Object> getAttributes();
}
public interface ReactiveHttpInputMessage extends HttpMessage {
// request body를 Flux 형태로 반환
Flux<DataBuffer> getBody();
}
public interface HttpMessage {
HttpHeaders getHeaders();
}
ServerHttpResponse
public interface ServerHttpResponse extends ReactiveHttpOutputMessage {
boolean setStatusCode(@Nullable HttpStatusCode status);
void addCookie(ResponseCookie cookie);
}
public interface ReactiveHttpOutputMessage extends HttpMessage {
// response body를 생성할 때 사용하는 DataBufferFactory 반환
DataBufferFactory bufferFactory();
// response가 클라이언트로 전송되기 직전에 수행
void beforeCommit(Supplier<? extends Mono<Void>> action);
// reponse body 쓰기
Mono<Void> writeWith(Publisher<? extends DataBuffer> body);
// response body를 chunk 단위로 전송
Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body);
// response 완료 처리 (자동 호출)
Mono<Void> setComplete();
}
HttpHandler의 handle() 메서드가 Mono<Void>를 반환하는 이유는, 내부에서 응답 처리를 담당하는 writeWith() 또는 자동으로 호출되는 setComplete() 메서드가 모두 Mono<Void>를 반환하기 때문입니다.
WebHandler
Spring은 WebFlux를 설계하면서 HTTP 요청 처리 구조를 기존 서블릿 방식과 동일하게 가져가지 않기로 결정했습니다. 비동기·논블로킹 환경에서는 네트워크 I/O와 애플리케이션 로직이 동일 계층에서 처리될 경우 구조가 복잡해지고 처리 파이프라인의 확장성이 떨어지기 때문입니다.
그래서 Spring은 이 지점에서 책임을 분리했습니다.
- HTTP 요청 수신 및 응답 생성을 담당하는 계층 → HttpHandler
- 실제 웹 애플리케이션 로직을 실행하는 계층 → WebHandler
HttpHandler는 어디까지나 서버(Reactor Netty, Tomcat 등)와 WebFlux 사이의 공통 진입점 역할을 하고, 그 아래에서 WebHandler가 라우팅, 핸들러 실행, 필터 체인, 예외 처리 등을 담당합니다.
public interface WebHandler {
/**
* Handle the web server exchange.
* @param exchange the current server exchange
* @return {@code Mono<Void>} to indicate when request handling is complete
*/
Mono<Void> handle(ServerWebExchange exchange);
}

HttpWebHandlerAdapter
앞에서 httpHandler를 호출 가능한 형태로 변환해주는 어댑터가 있듯이, 마찬가지로 webHandler를 호출 가능한 형태로 변환해주는 어댑터로 HttpWebHandlerAdapter가 있습니다.
public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler {
public HttpWebHandlerAdapter(WebHandler delegate) {
super(delegate);
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
...
ServerWebExchange exchange = createExchange(request, response);
return getDelegate().handle(exchange);
}
}
public class WebHandlerDecorator implements WebHandler {
private final WebHandler delegate;
public WebHandlerDecorator(WebHandler delegate) {
this.delegate = delegate;
}
public WebHandler getDelegate() {
return this.delegate;
}
}
그런데 ReactorHttpHandlerAdapter와 달리 HttpWebHandlerAdapter는 직접 생성하지 않습니다. 그 이유는 단순히 WebHandler를 감싸는 것만 하는 게 아니라 WebFlux 요청 처리 체인을 구성하는 핵심 설정을 함께 묶어 생성하기 때문입니다.

public final class WebHttpHandlerBuilder {
/**
* Build the {@link HttpHandler}.
*/
public HttpHandler build() {
WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
decorated = new ExceptionHandlingWebHandler(decorated, this.exceptionHandlers);
HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
if (this.sessionManager != null) {
adapted.setSessionManager(this.sessionManager);
}
if (this.codecConfigurer != null) {
adapted.setCodecConfigurer(this.codecConfigurer);
}
if (this.localeContextResolver != null) {
adapted.setLocaleContextResolver(this.localeContextResolver);
}
if (this.forwardedHeaderTransformer != null) {
adapted.setForwardedHeaderTransformer(this.forwardedHeaderTransformer);
}
if (this.observationRegistry != null) {
adapted.setObservationRegistry(this.observationRegistry);
}
if (this.observationConvention != null) {
adapted.setObservationConvention(this.observationConvention);
}
if (this.applicationContext != null) {
adapted.setApplicationContext(this.applicationContext);
}
adapted.afterPropertiesSet();
return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted);
}
...
}
따라서 위와 같이 WebHttpHandlerBuilder 를 사용하여 주어진 Filter, ExceptionHandlers, CodecConfigurer, ApplicationContext 등을 이용하나 하나의 HttpWebHandlerAdapter를 생성합니다.
ServerWebExchange
WebHandler는 더 이상 ServerHttpRequest와 ServerHttpResponse를 따로 전달받지 않고, 둘을 하나의 컨텍스트로 묶은 ServerWebExchange를 인자로 받습니다.
public interface ServerWebExchange {
// ServerHttpRequest 반환
ServerHttpRequest getRequest();
// ServerHttpResponse 반환
ServerHttpResponse getResponse();
// 요청 처리 중에 변경할 수 있는 Map
Map<String, Object> getAttributes();
// WebSession 반환
Mono<WebSession> getSession();
// 인증 정보를 담고 있는 Principal 반환
<T extends Principal> Mono<T> getPrincipal();
// Content-Type이 application/x-www-form-urlencoded인 경우
// 요청 본문에서 폼 데이터를 MultiValueMap 형태로 반환
Mono<MultiValueMap<String, String>> getFormData();
// Content-Type이 multipart/form-data인 경우
// 요청 본문에서 multipart 파트들을 MultiValueMap 형태로 반환
Mono<MultiValueMap<String, Part>> getMultipartData();
// Spring 환경에서 구동된 경우 applicationContext 반환
ApplicationContext getApplicationContext();
...
}
다음과 같이 webHandler를 사용하여 Reactor Netty의 HttpServer로 서버를 실행할 수 있습니다.
WebHandler webHandler = exchange -> {
final ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
DataBuffer buffer = response.bufferFactory()
.wrap("hello".getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
};
HttpHandler httpHandler = WebHttpHandlerBuilder
.webHandler(webHandler)
.build();
ReactorHttpHandlerAdapter adapter =
new ReactorHttpHandlerAdapter(httpHandler);
HttpServer.create()
.port(8080)
.handle(adapter)
.bindNow()
.onDispose()
.block();
Codec
Reactor Netty에서 요청 바이트(ByteBuf)가 Flux<DataBuffer>로 변환되어 ServerHttpRequest.getBody()에 담겨 전달됩니다. 그리고 애플리케이션에서 Body가 실제로 필요해지는 시점에 HttpMessageReader가 이 DataBuffer를 읽어 타입에 맞게 디코딩하며, 이때 사용되는 MessageReader들은 CodecConfigurer가 미리 등록·구성해 둔 것입니다.

HttpMessageReader

HttpMessageReader는 모든 Body Reader의 상위 인터페이스이며, 아래와 같이 크게 세 계열로 나눌 수 있습니다.
- Default Server Type Reader (form-data / multipart 처리)
- exchange.formData()
- exchange.multipartData()
- @RequestPart
- Default Type Reader (String, byte, raw body)
- exchange.getRequest().getBody()
- @RequestBody String
- bodyToMono(String.class)
- Default Object Reader (JSON/XML → DTO)
- @RequestBody User
- bodyToMono(User.class)
formDataMono , multipartDataMono
대부분의 변환은 애플리케이션에서 Body가 실제로 필요해지는 시점에 변환되지만, form-data나 multipart-data로의 변환은 Body를 실제로 읽는 시점이 아니라 ServerWebExchange 생성 시 미리 준비됩니다.
ServerWebExchange는 HttpWebHandlerAdapter.handle() 과정에서 createExchange()를 통해 생성되고, 생성된 Exchange 객체가 이후 WebHandler(DispatcherHandler)로 전달됩니다.
protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
return new DefaultServerWebExchange(request, response, this.sessionManager,
getCodecConfigurer(), getLocaleContextResolver(), this.applicationContext);
}
여기서 ServerHttpRequest, ServerHttpResponse, 그리고 CodecConfigurer가 함께 전달되며 아래와 같이 DefaultServerWebExchange 내부에서 form-data와 multipart-data 처리가 사전 준비됩니다.
protected DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer,
LocaleContextResolver localeContextResolver, @Nullable ApplicationContext applicationContext) {
this.attributes.put(ServerWebExchange.LOG_ID_ATTRIBUTE, request.getId());
this.request = request;
this.response = response;
this.sessionMono = sessionManager.getSession(this).cache();
this.localeContextResolver = localeContextResolver;
this.formDataMono = initFormData(request, codecConfigurer, getLogPrefix());
this.multipartDataMono = initMultipartData(codecConfigurer, getLogPrefix());
this.applicationContext = applicationContext;
...
}
ServerWebExchange 생성 시점에 formData/multipartData를 위한 Mono 파이프라인(formDataMono, multipartDataMono)을 미리 만들어두고, exchange.getFormData()나 exchange.getMultipartData()가 호출되어 구독되는 순간 그 Mono가 실행되면서 Codec(HttpMessageReader)이 동작해 실제 변환이 일어납니다.
private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpRequest request,
ServerCodecConfigurer configurer, String logPrefix) {
MediaType contentType = getContentType(request);
if (contentType == null || !contentType.isConcrete() || !contentType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) {
return EMPTY_FORM_DATA;
}
HttpMessageReader<MultiValueMap<String, String>> reader = getReader(configurer, contentType, FORM_DATA_TYPE);
if (reader == null) {
return Mono.error(new IllegalStateException("No HttpMessageReader for " + contentType));
}
return reader
.readMono(FORM_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
.switchIfEmpty(EMPTY_FORM_DATA)
.cache();
}
private Mono<MultiValueMap<String, Part>> initMultipartData(ServerCodecConfigurer configurer, String logPrefix) {
MediaType contentType = getContentType(this.request);
if (contentType == null || !contentType.isConcrete() || !contentType.getType().equalsIgnoreCase("multipart")) {
return EMPTY_MULTIPART_DATA;
}
HttpMessageReader<MultiValueMap<String, Part>> reader = getReader(configurer, contentType, MULTIPART_DATA_TYPE);
if (reader == null) {
return Mono.error(new IllegalStateException("No HttpMessageReader for " + contentType));
}
return reader
.readMono(MULTIPART_DATA_TYPE, this.request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
.doOnNext(ignored -> this.multipartRead = true)
.switchIfEmpty(EMPTY_MULTIPART_DATA)
.cache();
}
코드를 보면 reader 중에 `application/x-www-form-urlencoded`와 `multipart`를 처리할 수 있는 HttpMessageReader를 찾아 변환 파이프라인(Mono)을 미리 구성해두고, 결과에는 cache()가 적용되어 있습니다. 이 Mono는 구독되기 전까지는 단순히 준비된 파이프라인일 뿐 실제 body를 파싱하지 않으며, 최초 구독 시에만 Reader가 Flux<DataBuffer>를 읽어 변환을 수행하고, 이후 호출부터는 캐싱된 결과가 반환됩니다.
WebFilter

Spring WebFlux에서 WebFilter는 Servlet 기반의 Filter와 가장 유사한 역할을 합니다. 요청이 WebHandler까지 도달하기 전/후에 개입하여 공통 처리 로직을 적용할 수 있는 구조입니다.
public interface WebFilter {
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link WebFilterChain}.
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
}
public class DefaultWebFilterChain implements WebFilterChain {
...
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() ->
this.currentFilter != null && this.chain != null ?
invokeFilter(this.currentFilter, this.chain, exchange) :
this.handler.handle(exchange));
}
}
다음 filter 또는 handler를 실행시키기 위한 WebFilterChain을 사용합니다.
아래는 filter 전, 후의 로깅을 남기는 예시입니다.
WebFilter preFilter = new WebFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
log.info("pre filter");
return chain.filter(exchange);
}
};
WebFilter afterFilter = new WebFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.doOnSuccess(v -> {
log.info("after filter");
});
}
};
final HttpHandler httpHandler = WebHttpHandlerBuilder
.webHandler(webHandler)
.filter(preFilter, afterFilter)
.build();
mutate()
앞서 살펴보았던 ServerHttpRequest와 ServerWebExchange는 모두 불변 객체(immutable)로 설계되어 있습니다. 즉, 생성된 후 필드를 직접 변경할 수 없으며, 값을 수정하고 싶다면 새로운 객체를 기반으로 복사하여 재구성해야 합니다.
이때 사용되는 API가 mutate()입니다.
mutate()는 기존 객체를 변경하는 것이 아니라, 기존 정보를 복사하고 원하는 속성만 변경한 뒤 새로운 immutable 객체를 생성하는 빌더(builder) 역할을 합니다.
public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
...
default ServerHttpRequest.Builder mutate() {
return new DefaultServerHttpRequestBuilder(this);
}
interface Builder {
Builder method(HttpMethod httpMethod);
Builder uri(URI uri);
Builder path(String path);
Builder contextPath(String contextPath);
Builder header(String headerName, String... headerValues);
Builder headers(Consumer<HttpHeaders> headersConsumer);
Builder sslInfo(SslInfo sslInfo);
Builder remoteAddress(InetSocketAddress remoteAddress);
ServerHttpRequest build();
}
}
public interface ServerWebExchange {
...
default Builder mutate() {
return new DefaultServerWebExchangeBuilder(this);
}
interface Builder {
Builder request(Consumer<ServerHttpRequest.Builder> requestBuilderConsumer);
Builder request(ServerHttpRequest request);
Builder response(ServerHttpResponse response);
Builder principal(Mono<Principal> principalMono);
ServerWebExchange build();
}
}
따라서 WebFilter에서 request를 가공하고 싶은 경우 아래와 같이 mutate()를 사용하여 새로운 객체를 생성해야합니다.
WebFilter preFilter = new WebFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest newRequest = exchange.getRequest()
.mutate()
.header("X-Test", "1234")
.build();
ServerWebExchange newExchange = exchange.mutate()
.request(newRequest)
.build();
return chain.filter(newExchange);
}
};
WebExceptionHandler

WebExceptionHandler는 Spring WebFlux에서 요청 처리 중 발생한 예외를 가로채어 처리하는 컴포넌트입니다.
public interface WebExceptionHandler {
/**
* Handle the given exception. A completion signal through the return value
* indicates error handling is complete while an error signal indicates the
* exception is still not handled.
* @param exchange the current exchange
* @param ex the exception to handle
* @return {@code Mono<Void>} to indicate when exception handling is complete
*/
Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
}
WebHandler 단계에서 예외가 발생하면 WebFlux는 exchange와 Throwable을 WebExceptionHandler로 전달합니다.
ExceptionHandler는 이 정보를 사용해 상태 코드, 헤더, Body 등을 직접 작성해 즉시 응답할 수 있으며, 처리 대상이 아니라면 예외를 다시 흘려보내 다음 ExceptionHandler가 처리하도록 만들 수도 있습니다.
WebExceptionHandler webExceptionHandler = new WebExceptionHandler() {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
final ServerHttpResponse response = exchange.getResponse();
if (ex instanceof CustomException) {
response.setStatusCode(HttpStatus.BAD_REQUEST);
DataBuffer buffer = response.bufferFactory().wrap(ex.getMessage().getBytes());
return response.writeWith(Mono.just(buffer));
} else {
return Mono.error(ex);
}
}
};
final HttpHandler webHttpHandler = WebHttpHandlerBuilder
.webHandler(webHandler)
.exceptionHandler(webExceptionHandler)
.build();
만약 같은 exception을 처리하는 WebExceptionHandler가 여러 개라면 먼저 등록된 handler가 예외를 처리합니다.
handleUnresolvedError()
만약 exceptionHandler에서도 에러를 처리하지 못한다면, HttpWebHandlerAdapter에서 handleUnresolvedError() 메서드에 의해서 500 에러 응답을 주도록 처리됩니다.
private Mono<Void> handleUnresolvedError(
ServerWebExchange exchange, ServerRequestObservationContext observationContext, Throwable ex) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String logPrefix = exchange.getLogPrefix();
if (response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR)) {
logger.error(logPrefix + "500 Server Error for " + formatRequest(request), ex);
return Mono.empty();
}
...
}
DispatcherHandler
Spring WebFlux에서 HttpHandler를 자동으로 구성해주는 Auto Configuration 클래스인 HttpHandlerAutoConfiguration가 있습니다.
public class HttpHandlerAutoConfiguration {
...
@Bean
public HttpHandler httpHandler(ObjectProvider<WebFluxProperties> propsProvider, ObjectProvider<WebHttpHandlerBuilderCustomizer> handlerBuilderCustomizers) {
WebHttpHandlerBuilder handlerBuilder = WebHttpHandlerBuilder
.applicationContext(this.applicationContext);
...
HttpHandler httpHandler = handlerBuilder.build();
...
return httpHandler;
}
}
여기서 applicationContext를 이용하여 WebHttpHandlerBuilder를 build() 하는 것을 확인할 수 있습니다.
다음은 WebHttpHandlerBuilder.applicationContext() 메서드입니다.
public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
WebHttpHandlerBuilder builder = new WebHttpHandlerBuilder(
context.getBean(WEB_HANDLER_BEAN_NAME, WebHandler.class), context);
List<WebFilter> webFilters = context
.getBeanProvider(WebFilter.class)
.orderedStream()
.toList();
builder.filters(filters -> filters.addAll(webFilters));
List<WebExceptionHandler> exceptionHandlers = context
.getBeanProvider(WebExceptionHandler.class)
.orderedStream()
.toList();
builder.exceptionHandlers(handlers -> handlers.addAll(exceptionHandlers));
context.getBeanProvider(HttpHandlerDecoratorFactory.class)
.orderedStream()
.forEach(builder::httpHandlerDecorator);
context.getBeanProvider(ObservationRegistry.class).ifUnique(builder::observationRegistry);
context.getBeanProvider(ServerRequestObservationConvention.class).ifAvailable(builder::observationConvention);
try {
builder.sessionManager(
context.getBean(WEB_SESSION_MANAGER_BEAN_NAME, WebSessionManager.class));
}
catch (NoSuchBeanDefinitionException ex) {
// Fall back on default
}
try {
builder.codecConfigurer(
context.getBean(SERVER_CODEC_CONFIGURER_BEAN_NAME, ServerCodecConfigurer.class));
}
catch (NoSuchBeanDefinitionException ex) {
// Fall back on default
}
try {
builder.localeContextResolver(
context.getBean(LOCALE_CONTEXT_RESOLVER_BEAN_NAME, LocaleContextResolver.class));
}
catch (NoSuchBeanDefinitionException ex) {
// Fall back on default
}
try {
builder.forwardedHeaderTransformer(
context.getBean(FORWARDED_HEADER_TRANSFORMER_BEAN_NAME, ForwardedHeaderTransformer.class));
}
catch (NoSuchBeanDefinitionException ex) {
// Fall back on default
}
return builder;
}
applicationContext()에서 하는 일은 다음과 같습니다.
- Context로부터 WEB_HANDLER_BEAN_NAME("webHandler")라는 이름을 갖는 빈을 찾아서 WebHandler로 등록
- Context로부터 WebFilter.class 타입의 빈들을 찾아서 정렬 후 filters로 등록
- Context로부터 WebExceptionHandler.class 타입의 빈들을 찾아서 정렬 후 exceptionHandlers로 등록
- Context로부터 WebSessionManager, ServerCodecConfigurer, LocaleContextResolver, ForwardedHeaderTransformer 빈을 찾아서 builder에 등록
Spring은 WebFluxConfigurationSupport(Auto-configuration 기반 설정 클래스)를 통해서 DispatcherHandler를 생성하고 이를 "webHandler"라는 이름의 Bean으로 등록합니다.
public class WebFluxConfigurationSupport implements ApplicationContextAware {
...
@Bean
public DispatcherHandler webHandler() {
return new DispatcherHandler();
}
}
WebSessionManager
WebSessionManager는 WebFlux에서 HTTP 세션을 저장·조회·갱신하는 전략을 정의하는 컴포넌트입니다.
public interface WebSessionManager {
Mono<WebSession> getSession(ServerWebExchange exchange);
}
public class DefaultServerWebExchange implements ServerWebExchange {
this.sessionMono = sessionManager.getSession(this).cache();
...
}
WebSession은 ServerWebExchange에서 getSession() 메서드를 통해서 접근 가능합니다. ServerWebExchange는 sessionManager로부터 getSession() 한 후 이를 cache()하여 제공합니다.
sessionManager는 기본적으로 InMemoryWebSessionStore를 사용하기 때문에 모든 session을 메모리에 저장합니다.
public class DefaultWebSessionManager implements WebSessionManager {
private WebSessionStore sessionStore = new InMemoryWebSessionStore();
...
}
ForwardedHeaderTranformer
ForwardedHeaderTransformer는 Reverse proxy(nginx, AWS ALB, ingress, Istio 등) 뒤에 WebFlux 서버가 있을 때
X-Forwarded-*, Forwarded 헤더를 기반으로 원래 클라이언트 요청 주소 스킴/호스트/포트/IP를 복원하는 컴포넌트입니다.
내부적으로 mutate()를 통해 ServerHttpRequest를 수정하면서 X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port 헤더 값을 읽어 원래 클라이언트 요청 정보를 기반으로 URI를 재구성합니다.
public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, ServerHttpRequest> {
...
@Override
public ServerHttpRequest apply(ServerHttpRequest request) {
ServerHttpRequest.Builder builder = request.mutate();
URI originalUri = request.getURI();
HttpHeaders headers = request.getHeaders();
URI uri = adaptFromForwardedHeaders(originalUri, headers);
builder.uri(uri);
...
removeForwardedHeaders(builder);
request = builder.build();
return request;
}
}
public static UriComponentsBuilder adaptFromForwardedHeaders(URI uri, HttpHeaders headers) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri);
...
String protocolHeader = headers.getFirst("X-Forwarded-Proto");
if (StringUtils.hasText(protocolHeader)) {
uriComponentsBuilder.scheme(StringUtils.tokenizeToStringArray(protocolHeader, ",")[0]);
uriComponentsBuilder.port(null);
}
else if (isForwardedSslOn(headers)) {
uriComponentsBuilder.scheme("https");
uriComponentsBuilder.port(null);
}
String hostHeader = headers.getFirst("X-Forwarded-Host");
if (StringUtils.hasText(hostHeader)) {
adaptForwardedHost(uriComponentsBuilder, StringUtils.tokenizeToStringArray(hostHeader, ",")[0]);
}
String portHeader = headers.getFirst("X-Forwarded-Port");
if (StringUtils.hasText(portHeader)) {
uriComponentsBuilder.port(Integer.parseInt(StringUtils.tokenizeToStringArray(portHeader, ",")[0]));
}
uriComponentsBuilder.resetPortIfDefaultForScheme();
return uriComponentsBuilder;
}
'Spring > Webflux' 카테고리의 다른 글
| WebClient (1) | 2025.12.11 |
|---|---|
| DispatcherHandler (1) | 2025.12.10 |
| Reactor operators (0) | 2025.12.08 |
| Netty - ByteBuf (2) | 2025.12.03 |
| Netty Server (0) | 2025.12.03 |