이 글에서 다루는 것
"Spring 소스 파헤치기" 시리즈의 스물네 번째 글입니다. 21편, 22편, 23편에서는 서블릿 위에서 도는 Spring MVC가 요청 하나를 어떻게 처리하는지 봤습니다. 이번 글은 같은 일을 리액티브 방식으로 하는 Spring WebFlux를 읽습니다. 1편 지도에서는 Phase 5(웹)의 U22입니다.
답하려는 질문은 네 가지입니다.
- 서버가 받은 요청은 어떤 층을 거쳐 우리 코드에 닿는가 —
HttpHandler,WebFilter,WebExceptionHandler,WebHandler - MVC의
DispatcherServlet.doDispatch()에 해당하는DispatcherHandler.handle()은 무엇이 같고 무엇이 다른가 @GetMapping을 붙인 컨트롤러와RouterFunctions.route()로 만든 함수형 엔드포인트는 어떻게 한 디스패처 안에서 같이 도는가- HTTP 요청을 보내는 쪽인
WebClient는 안에서 어떤 순서로 일하는가
| 항목 | 내용 |
|---|---|
| 기준 소스 | Spring Framework 7.1.0-SNAPSHOT |
| 커밋 | f447f3c310 (2026-10-01, main 브랜치) |
| 주로 읽는 모듈 | spring-webflux, spring-web (http.server.reactive, web.server, http.codec) |
| 주로 읽는 클래스 | WebHttpHandlerBuilder, HttpWebHandlerAdapter, DispatcherHandler, RequestMappingHandlerAdapter, RouterFunctions, RouterFunctionMapping, DefaultWebClient |
글에 나오는 줄 번호와 세부 동작은 모두 이 커밋 기준입니다. 다른 버전에서는 줄 번호가 다르고 세부 내용도 달라질 수 있습니다. 같은 커밋의 소스를 받는 방법은 1편에 있습니다.
WebFlux는 넓습니다. 이 글은 Spring을 처음 보는 분이 따라올 수 있는 큰 줄기만 골랐고, 빼놓은 것은 글 끝의 "이 글에서 뺀 것"에 이름만 적어 두었습니다.
먼저 알아 둘 말
리액티브와 Mono·Flux. 보통의 메서드는 결과가 준비될 때까지 기다렸다가 값을 돌려줍니다. 리액티브 방식의 메서드는 값을 돌려주는 대신 "나중에 값이 흘러올 통로"를 바로 돌려줍니다. Reactor라는 라이브러리의 Mono(값이 0개 또는 1개)와 Flux(값이 여러 개)가 그 통로입니다. 19편 5절에서 리액티브 트랜잭션을 볼 때 처음 만났습니다.
구독해야 실행됩니다. Mono를 돌려받았다는 것은 일이 끝났다는 뜻도, 시작됐다는 뜻도 아닙니다. Mono는 "이렇게 하겠다"는 계획서이고, 누군가 구독(subscribe)해야 그때부터 실제로 실행됩니다. 이 글의 예제에서 이것을 두 번 눈으로 확인합니다. 예제는 block()을 쓰는데, 이것은 "구독하고 끝날 때까지 이 스레드에서 기다리기"입니다.
세 가지 신호. 통로로 흘러오는 것은 값(onNext), 정상 종료(onComplete), 오류(onError) 세 가지입니다. 리액티브 코드에서 예외는 throw로 호출 스택을 거슬러 올라가는 대신 오류 신호로 통로를 따라 흘러갑니다. 그래서 try/catch 대신 onErrorResume 같은 연산자로 받습니다. Mono<Void>는 값 없이 "끝났다" 또는 "실패했다"만 알려 주는 통로입니다.
연산자. 통로에 이어 붙여 계획을 늘려 가는 메서드입니다. 이 글의 소스에 나오는 것만 적습니다.
| 연산자 | 뜻 |
|---|---|
map / flatMap |
값이 오면 바꾼다 / 값이 오면 그것으로 또 다른 통로를 만들어 잇는다 |
concatMap |
Flux의 값마다 통로를 만들되 앞의 것이 끝나야 다음 것을 시작한다 |
next |
Flux에서 첫 값만 받아 Mono로 만들고 나머지는 취소한다 |
switchIfEmpty |
값 없이 끝나면 다른 통로로 바꾼다 |
onErrorResume |
오류 신호가 오면 다른 통로로 바꾼다 (catch에 해당) |
then |
앞의 것이 끝나면 다음 것을 실행한다 |
Mono.defer |
구독될 때에야 안의 코드를 실행해 통로를 만든다 |
**ServerWebExchange**는 요청 하나와 그 응답, 그리고 요청 동안 쓰는 속성 맵을 한데 묶은 객체입니다(ServerWebExchange 44행, 65–75행). MVC에서 HttpServletRequest와 HttpServletResponse 두 개를 들고 다니던 것을 WebFlux는 이것 하나로 들고 다닙니다.
**핸들러, HandlerMapping, HandlerAdapter**는 MVC와 같은 말입니다. 핸들러는 요청을 실제로 처리하는 우리 코드, HandlerMapping은 "이 요청은 누가 처리하나"를 찾는 쪽, HandlerAdapter는 그 핸들러를 부를 줄 아는 쪽입니다. 21편과 22편에서 다뤘습니다. WebFlux에는 같은 이름의 인터페이스가 org.springframework.web.reactive 패키지에 따로 있습니다.
1. 바닥: 모든 것은 HttpHandler 하나로 모입니다
WebFlux의 가장 아래에는 메서드가 하나뿐인 인터페이스가 있습니다.
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);
}
spring-web/src/main/java/org/springframework/http/server/reactive/HttpHandler.java 43–53행
요청과 응답을 받고, "다 끝나면 알려 주겠다"는 Mono<Void>를 돌려줍니다. 서블릿의 service(request, response)와 닮았지만 반환이 void가 아니라는 점이 전부를 바꿉니다. 메서드가 돌아온 시점에는 아무 일도 끝나 있지 않습니다.
눈여겨볼 것은 이 인터페이스가 spring-webflux가 아니라 spring-web 모듈에 있다는 점입니다. WebFilter, WebHandler, ServerWebExchange도 그렇습니다. 2편의 모듈 지도에서 spring-web이 MVC와 WebFlux 양쪽의 공통 바닥이었던 이유입니다. spring-webflux에는 그 위의 디스패처와 프로그래밍 모델이 있습니다.
파란 상자 둘이 우리가 끼어들 수 있는 자리(예외 처리기와 필터)입니다. 빨간 상자는 마지막 안전망입니다.
서버 어댑터 — 서버마다 하나씩
HttpHandler는 특정 서버를 모릅니다. 서버와 이어 주는 것은 서버마다 하나씩 있는 어댑터입니다. spring-web의 http.server.reactive 패키지에 ReactorHttpHandlerAdapter(Reactor Netty), TomcatHttpHandlerAdapter, JettyCoreHttpHandlerAdapter, ServletHttpHandlerAdapter가 있습니다. 하는 일은 같습니다. 서버가 준 요청·응답 객체를 Spring의 ServerHttpRequest·ServerHttpResponse로 감싸 httpHandler.handle(request, response)를 부릅니다(ReactorHttpHandlerAdapter 55–76행).
그러면 구독은 누가 할까요. 서블릿 쪽 어댑터는 직접 합니다 — this.httpHandler.handle(httpRequest, httpResponse).subscribe(subscriber)(ServletHttpHandlerAdapter 197행). Reactor Netty 쪽 어댑터는 Mono를 그대로 돌려주고(65–67행), 그 뒤의 구독은 이 저장소 밖인 Reactor Netty가 맡습니다.
WebHttpHandlerBuilder — 세 겹으로 감싸기
우리가 직접 HttpHandler를 구현하는 일은 드뭅니다. 한 단계 위의 인터페이스 셋으로 일하고, WebHttpHandlerBuilder가 그것들을 HttpHandler 하나로 엮어 줍니다.
| 인터페이스 | 메서드 | 역할 |
|---|---|---|
WebHandler |
Mono<Void> handle(ServerWebExchange) |
요청을 처리하는 본체. 보통 DispatcherHandler (35–44행) |
WebFilter |
Mono<Void> filter(ServerWebExchange, WebFilterChain) |
본체 앞뒤에 끼어드는 필터 (32–43행) |
WebExceptionHandler |
Mono<Void> handle(ServerWebExchange, Throwable) |
오류 신호를 응답으로 바꾸는 쪽 (27–39행) |
엮는 코드는 build()에 있습니다. (// ... 줄은 이 글에서 줄인 곳이고, 나머지는 소스 그대로입니다.)
public HttpHandler build() {
WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
decorated = new ExceptionHandlingWebHandler(decorated, this.exceptionHandlers);
HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
// ...
adapted.afterPropertiesSet();
return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted);
}
spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java 427–459행
안쪽부터 세 겹입니다.
FilteringWebHandler— 본체를 필터 사슬로 감쌉니다. 사슬은DefaultWebFilterChain인데, 필터 목록을 뒤에서부터 훑으며 "이 필터 + 그 다음 고리"를 하나씩 만들어 앞에 붙이므로(71–78행), 실행은 등록한 순서대로입니다. 고리 하나의filter()는 남은 필터가 있으면 그것을 부르고 없으면 본체를 부릅니다(103–108행).ExceptionHandlingWebHandler— 그 바깥을 감싸서 오류 신호를 받습니다. 바로 아래에서 봅니다.HttpWebHandlerAdapter—WebHandler를HttpHandler로 바꿔 주는 어댑터입니다. 요청마다ServerWebExchange를 새로 만들고(306행), 안쪽에 넘긴 뒤, 끝에서 응답을 마무리합니다(321–326행).
ExceptionHandlingWebHandler의 본문은 짧아서 그대로 읽을 만합니다.
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Mono<Void> completion;
try {
completion = super.handle(exchange);
}
catch (Throwable ex) {
completion = Mono.error(ex);
}
for (WebExceptionHandler handler : this.exceptionHandlers) {
completion = completion.doOnError(error -> exchange.getAttributes().put(HANDLED_WEB_EXCEPTION, error))
.onErrorResume(ex -> handler.handle(exchange, ex));
}
return completion;
}
두 가지를 봅니다. 첫째, try/catch가 있습니다. 안쪽 코드가 Mono를 돌려주기도 전에 그냥 throw 해 버린 경우를 오류 신호(Mono.error)로 바꿔 한 길로 모으는 것입니다. 둘째, for 루프는 예외 처리기를 실행하지 않습니다. onErrorResume을 처리기 수만큼 이어 붙일 뿐입니다. 오류가 나면 첫 처리기가 받고, 그 처리기가 다시 오류 신호를 돌려주면(= "나는 못 한다") 다음 onErrorResume이 받습니다. WebExceptionHandler의 주석도 완료 신호는 "처리 끝", 오류 신호는 "아직 처리 안 됨"이라고 적어 두었습니다(30–32행).
끝까지 아무도 처리하지 못하면 맨 바깥 HttpWebHandlerAdapter의 handleUnresolvedError가 받아 상태 코드를 500으로 정하고 끝냅니다(323행, 359–382행).
예제 1: 필터와 예외 처리기의 순서
서버를 띄우지 않고 WebHttpHandlerBuilder로 HttpHandler를 만들어, spring-test의 가짜 요청·응답(MockServerHttpRequest, MockServerHttpResponse)으로 직접 불러 봅니다. ex/u22/FilterChainExample.java입니다. 이 글의 예제 코드는 핵심만 남기고 줄여 실었습니다(임포트와 클래스·main 선언은 빼고 들여쓰기를 당겼습니다. 그 밖에 줄인 곳은 // ... 주석으로 적었고, 짧은 메서드는 한 줄로 붙였습니다).
// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
/** 앞뒤로 이름을 찍는 필터. */
static WebFilter named(String name) {
return (exchange, chain) -> {
System.out.println(" " + name + " before");
return chain.filter(exchange)
.doOnSuccess(v -> System.out.println(" " + name + " after (completed)"))
.doOnError(ex -> System.out.println(" " + name + " after (error " + ex.getClass().getSimpleName() + ")"));
};
}
static void send(HttpHandler httpHandler) {
MockServerHttpRequest request = MockServerHttpRequest.get("/hello").build();
MockServerHttpResponse response = new MockServerHttpResponse();
Mono<Void> completion = httpHandler.handle(request, response);
System.out.println(" handle() returned a Mono. status so far = " + response.getStatusCode());
completion.block(); // 여기서 구독한다
System.out.println(" status = " + response.getStatusCode()
+ ", body = \"" + response.getBodyAsString().block() + "\"");
}
// ... 여기부터 main 안. hello 는 200 과 "hello" 를 쓰는 WebHandler,
// notFound 는 Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)) 를 돌려주는 WebHandler
System.out.println("[1] two filters, then the handler");
send(WebHttpHandlerBuilder.webHandler(hello)
.filter(named("filter A"), named("filter B"))
.build());
System.out.println("[2] the handler fails, a WebExceptionHandler is registered");
send(WebHttpHandlerBuilder.webHandler(notFound)
.filter(named("filter A"))
.exceptionHandler(new ResponseStatusExceptionHandler())
.build());
System.out.println("[3] the handler fails, no WebExceptionHandler");
send(WebHttpHandlerBuilder.webHandler(notFound)
.filter(named("filter A"))
.build());
이 예제는 릴리스 버전인 Spring Framework 7.0.9로 실행해 아래 출력을 확인했습니다(이 시리즈가 읽는 7.1.0-SNAPSHOT을 직접 빌드해 돌린 것은 아닙니다). 이 글의 나머지 예제도 같습니다.
[1] two filters, then the handler
handle() returned a Mono. status so far = null
filter A before
filter B before
handler
filter B after (completed)
filter A after (completed)
status = 200 OK, body = "hello"
[2] the handler fails, a WebExceptionHandler is registered
handle() returned a Mono. status so far = null
filter A before
handler (signals ResponseStatusException 404)
filter A after (error ResponseStatusException)
status = 404 NOT_FOUND, body = ""
[3] the handler fails, no WebExceptionHandler
handle() returned a Mono. status so far = null
filter A before
handler (signals ResponseStatusException 404)
filter A after (error ResponseStatusException)
status = 500 INTERNAL_SERVER_ERROR, body = ""
- 세 경우 모두
handle()이 돌아온 직후에는 필터도 핸들러도 찍힌 것이 없습니다.block()으로 구독한 뒤에야 실행됩니다. - [1]에서 필터는 등록한 순서(A, B)로 들어가고 반대 순서로 나옵니다.
- [2]에서 필터 A는 오류 신호를 봅니다. 예외 처리기가 필터보다 바깥 겹이기 때문입니다. 필터를 지나간 뒤에
ResponseStatusExceptionHandler가 오류를 받아 상태를 404로 정합니다(65–68행). - [3]은 같은 404 예외인데 응답이 500입니다.
헷갈리기 쉬운 곳
ResponseStatusException(404)를 던졌다고 저절로 404가 되지 않습니다. 그 예외를 응답으로 옮기는 것은 등록된WebExceptionHandler입니다. 하나도 없으면 [3]처럼 마지막 안전망이 500으로 끝냅니다.@EnableWebFlux를 쓰면 이 일을 하는 처리기가 빈으로 등록됩니다(3절).- 필터의 "뒤" 코드는
chain.filter(exchange)다음 줄에 쓰면 안 됩니다. 그 줄은 계획서를 받았을 뿐입니다. 뒤에 할 일은 예제처럼 돌려받은Mono에 연산자로 이어 붙여야 합니다. - 서블릿
Filter와WebFilter는 다른 것입니다. 서블릿 필터(jakarta.servlet.Filter)는 서블릿 컨테이너가 부르는 것이고,WebFilter는 서블릿 API와 관계없이 위의DefaultWebFilterChain이 부르는 Spring의 인터페이스입니다. 이름만 비슷합니다.
2. DispatcherHandler.handle — doDispatch의 리액티브 판
1절의 맨 안쪽 WebHandler 자리에 보통 들어가는 것이 DispatcherHandler입니다. 클래스 선언부터 WebHandler를 구현합니다(72행). MVC의 DispatcherServlet이 네 층의 상속을 가진 서블릿이었던 것과 달리 220줄짜리 클래스 하나이고, 요청을 처리하는 handle()은 열네 줄입니다.
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return handlePreFlight(exchange);
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.onErrorResume(ex -> handleResultMono(exchange, Mono.error(ex)))
.flatMap(handler -> handleRequestWith(exchange, handler));
}
spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java 137–151행
마지막 여섯 줄이 전부입니다. 한 줄씩 읽습니다.
Flux.fromIterable(this.handlerMappings)—HandlerMapping목록을 통로에 올립니다..concatMap(mapping -> mapping.getHandler(exchange))— 하나씩 차례로 "이 요청의 핸들러를 아느냐"고 묻습니다.getHandler는Mono<Object>를 돌려주고, 모르면 값 없이 끝납니다(HandlerMapping107행)..next()— 처음 나온 핸들러 하나만 받고 나머지 매핑에게는 묻지 않습니다..switchIfEmpty(createNotFoundError())— 아무도 모르면ResponseStatusException(HttpStatus.NOT_FOUND)오류 신호로 바꿉니다(153–158행)..onErrorResume(...)— 여기까지 오는 동안 생긴 오류(404 포함)를 예외 처리 쪽으로 한 번 넘겨 봅니다..flatMap(handler -> handleRequestWith(exchange, handler))— 핸들러를 찾았으면 실행하러 갑니다.
MVC의 getHandler()가 for 루프로 하던 "차례로 물어서 처음 답하는 쪽을 쓴다"가 concatMap과 next가 됐습니다. 루프와 return이 연산자로 바뀌었을 뿐 뜻은 같습니다.
실선이 정상 흐름, 점선이 오류 신호의 길입니다.
핸들러를 부르고, 결과를 처리합니다
private Mono<Void> handleRequestWith(ServerWebExchange exchange, Object handler) {
if (ObjectUtils.nullSafeEquals(exchange.getResponse().getStatusCode(), HttpStatus.FORBIDDEN)) {
return Mono.empty(); // CORS rejection
}
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
Mono<HandlerResult> resultMono = adapter.handle(exchange, handler);
return handleResultMono(exchange, resultMono);
}
}
}
return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
}
spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java 196–209행
supports(handler)가 참인 첫 HandlerAdapter가 핸들러를 실행합니다. 여기까지는 MVC와 같습니다. 다른 것은 돌아오는 것입니다. MVC의 어댑터는 ModelAndView를 돌려줬고, WebFlux의 어댑터는 Mono<HandlerResult> 를 돌려줍니다(HandlerAdapter 71행). HandlerResult는 "어떤 핸들러가, 무엇을, 어떤 선언 타입으로 돌려줬는가"를 담은 그릇입니다(HandlerResult 32–40행).
그 그릇을 응답으로 바꾸는 일은 세 번째 전략인 HandlerResultHandler 가 합니다. handleResult()가 목록을 돌며 supports(result)가 참인 첫 처리기에게 넘깁니다(181–194행). MVC에는 이 이름의 인터페이스가 없습니다. MVC에서는 반환값 처리가 어댑터 안쪽(HandlerMethodReturnValueHandler)에 들어 있었는데, WebFlux는 그것을 디스패처가 직접 고르는 전략으로 꺼내 놓았습니다. 그래서 WebFlux의 디스패처는 전략이 세 종류입니다.
핸들러 실행 중의 오류는 handleResultMono()가 다룹니다(160–179행). 어댑터가 DispatchExceptionHandler도 구현하고 있으면 그쪽에 먼저 기회를 주고(@ExceptionHandler 메서드가 여기로 불립니다, 3절), 그래도 남은 오류 신호는 DispatcherHandler 밖으로 흘러 나가 1절의 WebExceptionHandler들이 받습니다.
MVC와 나란히 놓고 보기
| Spring MVC (21편) | Spring WebFlux (이 글) | |
|---|---|---|
| 디스패처 | DispatcherServlet (서블릿) |
DispatcherHandler (WebHandler) |
| 중심 메서드 | void doDispatch(request, response) |
Mono<Void> handle(exchange) |
| 요청·응답 | HttpServletRequest, HttpServletResponse |
ServerWebExchange 하나 |
| 핸들러 찾기 | HandlerMapping → HandlerExecutionChain (핸들러 + 인터셉터) |
HandlerMapping → Mono<Object> (핸들러만) |
| 핸들러 부르기 | HandlerAdapter → ModelAndView |
HandlerAdapter → Mono<HandlerResult> |
| 결과 처리 | 어댑터 안의 반환값 처리기, 그리고 뷰 렌더링 | HandlerResultHandler |
| 앞뒤에 끼어들기 | HandlerInterceptor, 서블릿 Filter |
WebFilter |
| 예외 | HandlerExceptionResolver |
DispatchExceptionHandler, 그 바깥의 WebExceptionHandler |
| 본문 변환 | HttpMessageConverter |
HttpMessageReader / HttpMessageWriter |
| 기다리는 방식 | 메서드가 돌아오면 끝난 것 | 돌려준 Mono가 완료 신호를 내야 끝난 것 |
표의 "핸들러만"이 눈에 띕니다. WebFlux의 HandlerMapping은 인터셉터 묶음을 돌려주지 않고, spring-webflux 모듈에는 HandlerInterceptor라는 타입이 아예 없습니다. 핸들러 앞뒤에 끼어드는 일은 1절의 WebFilter가 맡습니다.
헷갈리기 쉬운 곳
handle()은 요청을 처리하지 않습니다. 처리하는 계획을 조립해 돌려줍니다. 위 코드 어디에도 "기다리는" 줄이 없습니다.HandlerMapping에게 묻는 일조차 구독된 뒤에 일어납니다.- 핸들러를 못 찾은 것도 오류 신호입니다. MVC는
noHandlerFound()라는 별도 갈래가 있었지만 여기서는ResponseStatusException하나로 합쳐져 예외 처리 길을 탑니다. - 같은 이름, 다른 타입입니다.
org.springframework.web.servlet.HandlerMapping과org.springframework.web.reactive.HandlerMapping은 서로 관계없는 인터페이스입니다. IDE의 자동 임포트가 틀린 쪽을 고르기 쉽습니다.
3. 전략 빈은 어디서 오나, 그리고 어노테이션 컨트롤러
@EnableWebFlux가 등록하는 것
DispatcherHandler는 세 목록을 스스로 만들지 않습니다. ApplicationContext를 받으면 initStrategies()가 HandlerMapping, HandlerAdapter, HandlerResultHandler 타입의 빈을 전부 찾아 순서대로 정렬해 둡니다(115–134행). MVC에는 빈이 없을 때 쓰는 기본값 파일(DispatcherServlet.properties)이 있었지만 DispatcherHandler에는 그런 대비책이 없습니다. 빈이 없으면 목록이 빕니다.
그 빈들을 등록하는 것이 @EnableWebFlux입니다. 이 어노테이션은 DelegatingWebFluxConfiguration을 @Import하고(EnableWebFlux 90행), 그 부모 WebFluxConfigurationSupport에 @Bean 메서드가 모여 있습니다. @Import가 설정 클래스를 끌어오는 방법은 9편에서 봤습니다. 이 글과 관계있는 것만 고르면 이렇습니다.
| 빈 | 종류 | 위치 |
|---|---|---|
webHandler → DispatcherHandler |
디스패처 자신 | 125–128행 |
WebFluxResponseStatusExceptionHandler |
WebExceptionHandler |
130–134행 |
RouterFunctionMapping (순서 -1) |
HandlerMapping |
248–258행 |
RequestMappingHandlerMapping (순서 0) |
HandlerMapping |
136–142행 |
RequestMappingHandlerAdapter, HandlerFunctionAdapter, SimpleHandlerAdapter |
HandlerAdapter |
305행, 482–490행 |
ResponseEntityResultHandler, ResponseBodyResultHandler, ViewResolutionResultHandler, ServerResponseResultHandler |
HandlerResultHandler |
521–562행 |
디스패처 빈의 이름이 webHandler인 것은 우연이 아닙니다. WebHttpHandlerBuilder.applicationContext(context)가 바로 그 이름으로 본체를 찾고(WEB_HANDLER_BEAN_NAME, 72행), WebFilter와 WebExceptionHandler 빈은 타입으로 모아 정렬합니다(174–189행). 1절에서 손으로 넣던 것을 컨테이너에서 꺼내 오는 것입니다.
어노테이션 컨트롤러가 지나는 길
@GetMapping이 붙은 메서드로 가는 길은 MVC와 이름까지 같습니다. 구현만 리액티브 판입니다.
RequestMappingHandlerMapping— 요청에 맞는HandlerMethod를 찾습니다. 상속도 MVC와 같은 모양입니다(RequestMappingHandlerMapping→RequestMappingInfoHandlerMapping→AbstractHandlerMethodMapping→AbstractHandlerMapping, 75행, 64행, 67행). 찾는 규칙은 22편에서 본 것과 같은 생각입니다.RequestMappingHandlerAdapter—handler instanceof HandlerMethod이면 자기 차례입니다(247–250행). 핵심은 이 다섯 줄입니다.
Mono<HandlerResult> resultMono = this.modelInitializer
.initModel(handlerMethod, bindingContext, exchange)
.then(Mono.defer(() -> invocableMethod.invoke(exchange, bindingContext)))
.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
.onErrorResume(ex -> exceptionHandler.handleError(exchange, ex));
모델을 준비하고, 그 다음에(then) 메서드를 부르고, 결과에 예외 처리기를 달아 두고, 오류가 나면 그 처리기로 넘깁니다. 여기의 exceptionHandler가 같은 클래스의 handleException()을 부르는 람다이고(267–268행), 그 안에서 맞는 @ExceptionHandler 메서드를 찾아 실행합니다(294–295행). 메서드를 실제로 부르는 것은 InvocableHandlerMethod.invoke()이고, 인자가 다 준비된 뒤에 리플렉션으로 method.invoke(getBean(), args)를 합니다(InvocableHandlerMethod 181–205행). 반환값은 무엇이든 HandlerResult에 담겨 나옵니다(239–240행).
ResponseBodyResultHandler— 클래스나 메서드에@ResponseBody가 있으면(@RestController는 그것을 품고 있습니다) 자기 차례이고(98–104행), 반환값을writeBody()에 넘깁니다(118행).
HttpMessageWriter — 반환값이 바이트가 되는 곳
writeBody()는 부모 클래스 AbstractMessageWriterResultHandler에 있습니다(169행부터). 하는 일은 세 가지입니다.
- 반환값이
Mono나Flux같은 리액티브 타입이면 그대로Publisher로 쓰고, 평범한 객체면Mono.justOrEmpty(body)로 감쌉니다(179–199행). 그래서 WebFlux 컨트롤러가String을 그냥 돌려줘도 됩니다. - 응답의 미디어 타입을 고릅니다(207행).
HttpMessageWriter목록을 돌며canWrite(타입, 미디어 타입)이 참인 첫 번째에게 쓰게 합니다(231–237행).
HttpMessageWriter와 HttpMessageReader는 MVC의 HttpMessageConverter(20편)에 대응합니다. MVC에서는 읽기와 쓰기가 한 인터페이스였지만 여기서는 둘로 나뉘어 있고, 무엇보다 주고받는 것이 통로입니다. 쓰는 쪽은 객체 하나가 아니라 Publisher를 받고(HttpMessageWriter 89행), 읽는 쪽은 Flux<T>나 Mono<T>를 돌려줍니다(HttpMessageReader 86행, 96행). 본문이 다 도착할 때까지 스레드를 붙잡고 기다리지 않으려는 것입니다.
이름은 MVC와 같고, 단계마다 Mono로 이어진다는 점만 다릅니다. 이 절의 1~3번(그림의 ①②③)을 한 장에 모았습니다.
4. 함수형 엔드포인트 — 어노테이션 없이 함수로
WebFlux에는 컨트롤러를 쓰는 방법이 하나 더 있습니다. 어노테이션 대신 함수 세 개로 조립합니다.
| 인터페이스 | 메서드 | 한 줄 뜻 |
|---|---|---|
HandlerFunction<T> |
Mono<T> handle(ServerRequest) |
요청을 받아 응답을 만드는 함수 (30–39행) |
RequestPredicate |
boolean test(ServerRequest) |
"이 요청이 맞는가"를 답하는 함수 (33–40행) |
RouterFunction<T> |
Mono<HandlerFunction<T>> route(ServerRequest) |
요청을 받아 그것을 처리할 HandlerFunction을 돌려주는 함수 (36–44행) |
셋 다 @FunctionalInterface라 람다로 쓸 수 있습니다. ServerRequest와 ServerResponse는 ServerWebExchange를 함수형 모델에 맞게 감싼 요청·응답 타입입니다.
가장 작은 RouterFunction은 "조건 하나 + 핸들러 하나"입니다.
@Override
public Mono<HandlerFunction<T>> route(ServerRequest request) {
if (this.predicate.test(request)) {
// ...
return Mono.just(this.handlerFunction);
}
else {
return Mono.empty();
}
}
조건이 맞으면 핸들러를, 아니면 빈 Mono를 돌려줍니다. RouterFunctions.route(predicate, handlerFunction)이 이 DefaultRouterFunction을 만듭니다(116–120행).
경로가 여럿이면 빌더를 씁니다. RouterFunctions.route()(인자 없는 쪽)는 RouterFunctionBuilder를 돌려주고(97–99행), 빌더의 GET("/path", handler)는 RequestPredicates.GET(pattern) 조건으로 위의 작은 RouterFunction을 하나 만들어 목록에 넣습니다(RouterFunctionBuilder 84–86행, 57–67행). build()는 그 목록을 BuiltRouterFunction 하나로 묶습니다(414–428행).
@Override
public Mono<HandlerFunction<ServerResponse>> route(ServerRequest request) {
return Flux.fromIterable(this.routerFunctions)
.concatMap(routerFunction -> routerFunction.route(request))
.next();
}
2절의 DispatcherHandler.handle()과 같은 모양입니다. 목록을 차례로 물어 처음 답하는 것을 씁니다. 그래서 함수형 엔드포인트에서는 등록한 순서가 곧 우선순위입니다. 어노테이션 쪽이 여러 후보 가운데 가장 구체적인 것을 골라 주는 것과 다릅니다.
초록 상자에서 멈춥니다. 뒤의 경로는 물어보지도 않습니다.
DispatcherHandler에 끼우는 세 조각
함수형 엔드포인트는 별도의 디스패처를 쓰지 않습니다. 2절의 세 전략 자리에 하나씩 끼워져 있을 뿐입니다.
RouterFunctionMapping(HandlerMapping) — 컨테이너의RouterFunction빈을 전부 모아andOther로 하나로 합쳐 두고(122–134행), 요청이 오면ServerRequest를 만들어routerFunction.route(request)를 부릅니다. 나온HandlerFunction이 핸들러이고, 방금 만든ServerRequest는 exchange의 속성에 넣어 둡니다(158–172행).HandlerFunctionAdapter(HandlerAdapter) — 전부 옮겨도 열두 줄입니다.
@Override
public boolean supports(Object handler) {
return handler instanceof HandlerFunction;
}
@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;
ServerRequest request = exchange.getRequiredAttribute(RouterFunctions.REQUEST_ATTRIBUTE);
return handlerFunction.handle(request)
.map(response -> new HandlerResult(handlerFunction, response, HANDLER_FUNCTION_RETURN_TYPE));
}
매핑이 넣어 둔 ServerRequest를 속성에서 꺼내 함수를 부르고, 돌아온 ServerResponse를 HandlerResult에 담습니다. 어노테이션 쪽 어댑터가 인자 해석, 모델, 검증을 하느라 길었던 것과 견주면, 함수형 모델에서는 그 일을 우리 함수가 ServerRequest로 직접 하기 때문에 어댑터가 할 일이 없습니다.
3. ServerResponseResultHandler (HandlerResultHandler) — 반환값이 ServerResponse이면 자기 차례이고(104–107행), response.writeTo(exchange, context)를 불러 응답이 스스로 쓰게 합니다(109–123행). writeTo는 먼저 상태 코드와 헤더를 옮기고(DefaultServerResponseBuilder 327–344행) 본문은 건네받은 HttpMessageWriter 목록으로 씁니다. 3절과 같은 writer들입니다.
파란 쪽이 함수형, 회색 쪽이 어노테이션입니다. 맨 아래에서 다시 만납니다.
예제 2: 두 모델이 같은 디스패처를 지납니다
ex/u22/DispatcherHandlerExample.java입니다. @EnableWebFlux 설정에 컨트롤러 하나와 RouterFunction 하나를 같이 등록하고, 일부러 /both라는 같은 경로를 양쪽에 다 두었습니다.
// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
@RestController
static class HelloController {
@GetMapping("/hello")
Mono<String> hello() { return Mono.just("hello from @GetMapping"); }
@GetMapping("/both")
Mono<String> both() { return Mono.just("both from @GetMapping"); }
}
@Configuration
@EnableWebFlux
static class Config {
@Bean
HelloController helloController() { return new HelloController(); }
@Bean
RouterFunction<ServerResponse> routes() {
return RouterFunctions.route()
.GET("/fn/hello", request -> ServerResponse.ok().bodyValue("hello from HandlerFunction"))
.GET("/both", request -> ServerResponse.ok().bodyValue("both from HandlerFunction"))
.build();
}
/** 체인이 끝난 뒤, HandlerMapping 이 고른 핸들러의 종류를 찍는 필터. */
@Bean
WebFilter handlerTracer() {
return (exchange, chain) -> chain.filter(exchange).doFinally(signal -> {
Object handler = exchange.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
// ... HandlerMethod 인지 HandlerFunction 인지 출력
});
}
}
// ... main: 세 전략 빈을 DispatcherHandler.initStrategies 와 같은 방법으로 모아 찍은 뒤 아래를 부른다.
// send 는 예제 1 처럼 가짜 요청을 보내고 상태와 본문을 찍는다
HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(context).build();
send(httpHandler, "/hello");
send(httpHandler, "/fn/hello");
send(httpHandler, "/both");
send(httpHandler, "/nope");
[1] bean "webHandler" is a DispatcherHandler
same list as getHandlerMappings()? true
HandlerMapping:
RouterFunctionMapping
RequestMappingHandlerMapping
EmptyHandlerMapping
HandlerAdapter:
WebSocketHandlerAdapter
RequestMappingHandlerAdapter
HandlerFunctionAdapter
SimpleHandlerAdapter
HandlerResultHandler:
ResponseEntityResultHandler
ServerResponseResultHandler
ResponseBodyResultHandler
ViewResolutionResultHandler
[2] requests through WebHttpHandlerBuilder.applicationContext(context)
GET /hello
handler kind = HandlerMethod
status = null, body = "hello from @GetMapping"
GET /fn/hello
handler kind = HandlerFunction
status = 200 OK, body = "hello from HandlerFunction"
GET /both
handler kind = HandlerFunction
status = 200 OK, body = "both from HandlerFunction"
GET /nope
handler kind = null
status = 404 NOT_FOUND, body = ""
(실행하면 맨 위에 Hibernate Validator가 버전을 알리는 로그 두 줄이 먼저 찍히는데, 이 글과 관계없어 뺐습니다.)
HandlerMapping목록의 맨 앞이RouterFunctionMapping입니다. 소스의mapping.setOrder(-1); // go before RequestMappingHandlerMapping그대로입니다(253행). 세 번째EmptyHandlerMapping은 정적 리소스 핸들러를 하나도 등록하지 않았을 때 그 자리를 채우는 빈 매핑입니다(287행).- 그래서
/both는 함수형 쪽이 이깁니다.concatMap과next가 첫 답에서 멈추므로RequestMappingHandlerMapping은 물어보지도 않습니다. /nope는 1절의 예제와 달리 404입니다.@EnableWebFlux가 등록한WebFluxResponseStatusExceptionHandler가 디스패처의 404 오류 신호를 받아 응답으로 옮겼습니다.
헷갈리기 쉬운 곳
/hello의status = null은 오류가 아닙니다. 어노테이션 쪽은@ResponseStatus가 없으면 상태 코드를 건드리지 않습니다(InvocableHandlerMethod221–224행). 가짜 응답 객체는 아무도 정하지 않은 상태를null로 보여 줍니다(getStatusCode()는@Nullable입니다,AbstractServerHttpResponse106–108행). 함수형 쪽은ServerResponse.ok()로 만든 상태를writeTo가 항상 응답에 옮기므로 200이 찍혔습니다. 실제 서버가 상태를 정하지 않은 응답을 200으로 내보내는지는 서버 쪽의 일이라 이 글에서 소스로 확인하지 않았습니다.- 함수형 엔드포인트만 쓸 때는 디스패처 없이도 됩니다.
RouterFunctions.toHttpHandler(routerFunction)은DispatcherHandler를 거치지 않고, 라우팅·실행·쓰기를 한 번에 하는 작은WebHandler를 만들어 1절의WebHttpHandlerBuilder에 넣습니다(318–325행, 1472–1488행). 아래 예제 3의 서버가 이 방식입니다.
5. WebClient — 요청을 보내는 쪽
WebClient는 spring-webflux에 들어 있는 리액티브 HTTP 클라이언트입니다. 서버 쪽과 방향만 반대이고 생각은 같습니다. 필터 사슬이 있고, 맨 끝에 실제 HTTP 라이브러리와 이어 주는 어댑터가 있고, 본문은 같은 HttpMessageReader·HttpMessageWriter가 읽고 씁니다.
| 서버 쪽 (1절) | 클라이언트 쪽 | 메서드 |
|---|---|---|
WebHandler |
ExchangeFunction |
Mono<ClientResponse> exchange(ClientRequest) (42–55행) |
WebFilter |
ExchangeFilterFunction |
Mono<ClientResponse> filter(ClientRequest, ExchangeFunction next) (35–51행) |
| 서버 어댑터 | ClientHttpConnector |
Mono<ClientHttpResponse> connect(method, uri, requestCallback) (34–49행) |
빌더가 조립하는 것
WebClient.create()와 WebClient.builder()는 둘 다 DefaultWebClientBuilder에서 시작합니다(WebClient 147–149행, 164–166행). build()의 앞부분이 위 표의 세 가지를 준비합니다.
public WebClient build() {
ClientHttpConnector connectorToUse =
(this.connector != null ? this.connector : initConnector());
ExchangeFunction exchange = (this.exchangeFunction == null ?
ExchangeFunctions.create(connectorToUse, initExchangeStrategies()) :
this.exchangeFunction);
ExchangeFilterFunction filterFunctions = (this.filters != null ? this.filters.stream()
.reduce(ExchangeFilterFunction::andThen)
.orElse(null) : null);
// ...
}
- 커넥터. 따로 주지 않으면
initConnector()가 클래스패스에 있는 라이브러리를 보고 고릅니다. Reactor Netty, Jetty 클라이언트, Apache HttpComponents 순으로 확인하고, 아무것도 없으면 JDK의HttpClient를 쓰는JdkClientHttpConnector입니다(329–342행). ExchangeFunction.ExchangeFunctions.create(...)가 만드는DefaultExchangeFunction은connector.connect(...)를 부르고, 돌아온 저수준 응답을ClientResponse로 감쌉니다(ExchangeFunctions97–119행).- 필터. 여러 필터를
andThen으로 접어 하나로 만듭니다.a.andThen(b)는 "a를 실행하되,a가next를 부르면b가 실행된다"는 뜻이라(59–63행) 등록한 순서대로 들어갑니다.
요청 한 번
client.get().uri(...)로 요청을 적어 나가는 객체는 DefaultWebClient 안의 DefaultRequestBodyUriSpec입니다(207행). 실제로 보내는 일은 그 안의 exchange()가 합니다(442–480행). 첫 줄(443행)에서 요청 빌더를 준비하는 것만 빼면 본문 전체가 Mono.deferContextual(...) 안에 들어 있어서, 구독되기 전에는 ClientRequest조차 만들어지지 않습니다(requestBuilder.build()가 456행에 있습니다). 구독되면 필터를 ExchangeFunction에 씌워 부릅니다 — finalFilterFunction.apply(exchangeFunction).exchange(request)(462–463행).
파란 상자가 필터입니다. 응답은 같은 길을 거꾸로 돌아옵니다.
retrieve()와 exchangeToMono()
둘 다 같은 exchange()를 부릅니다. 다른 것은 돌아온 ClientResponse를 누가 다루느냐입니다.
retrieve()는DefaultResponseSpec을 돌려줍니다(408–411행). 여기에는 기본 상태 처리기가 하나 들어 있습니다.
private static final StatusHandler DEFAULT_STATUS_HANDLER =
new StatusHandler(code -> code.value() >= 400, ClientResponse::createException);
bodyToMono(...)로 본문을 꺼낼 때 상태 처리기를 먼저 적용하므로(594–598행, 677–681행), 상태가 400 이상이면 본문 대신 WebClientResponseException 오류 신호가 옵니다. onStatus(...)로 처리기를 더하면 기본 처리기보다 앞에 놓입니다(572–580행).
exchangeToMono(함수)는ClientResponse를 우리 함수에 그대로 넘깁니다(413–426행). 상태가 무엇이든 예외로 바꾸지 않습니다. 대신 함수가 끝난 뒤 읽지 않은 본문을 치워 주는releaseIfNotConsumed가 값·빈 값·오류 세 갈래 모두에 붙어 있습니다.
예제 3: 서버를 잠깐 띄워 불러 보기
ex/u22/WebClientExample.java입니다. Reactor Netty 서버를 임의의 빈 포트에 띄우고(port(0)), 1절의 ReactorHttpHandlerAdapter로 함수형 엔드포인트를 물린 다음, WebClient로 부릅니다. 끝나면 서버를 닫습니다.
// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
/** 요청이 나갈 때와 응답이 돌아올 때 이름을 찍는 필터. */
static ExchangeFilterFunction named(String name) {
return (request, next) -> {
System.out.println(" " + name + " -> " + request.method() + " " + request.url().getPath());
return next.exchange(request)
.doOnNext(response -> System.out.println(" " + name + " <- " + response.statusCode().value()));
};
}
// ... 여기부터 main 안
AtomicInteger hits = new AtomicInteger(); // 서버가 요청을 받은 횟수
RouterFunction<ServerResponse> routes = RouterFunctions.route()
.GET("/greet/{name}", request -> {
hits.incrementAndGet();
return ServerResponse.ok().bodyValue("hi, " + request.pathVariable("name"));
})
.build();
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routes);
DisposableServer server = HttpServer.create()
.host("localhost")
.port(0) // 빈 포트를 아무거나
.handle(new ReactorHttpHandlerAdapter(httpHandler))
.bindNow(TIMEOUT);
// ... 아래는 try 블록 안이고, 서버를 닫는 마지막 세 줄은 finally 블록 안이다
WebClient client = WebClient.builder()
.baseUrl("http://localhost:" + server.port())
.filter(named("filter A"))
.filter(named("filter B"))
.build();
System.out.println("[1] retrieve(): nothing is sent until someone subscribes");
Mono<String> greeting = client.get().uri("/greet/{name}", "spring")
.retrieve()
.bodyToMono(String.class);
System.out.println(" Mono built. server hits = " + hits.get());
System.out.println(" body = \"" + greeting.block(TIMEOUT) + "\", server hits = " + hits.get());
System.out.println("[2] subscribing to the same Mono again sends a second request");
System.out.println(" body = \"" + greeting.block(TIMEOUT) + "\", server hits = " + hits.get());
System.out.println("[3] retrieve() on a 404");
try {
client.get().uri("/nope").retrieve().bodyToMono(String.class).block(TIMEOUT);
}
catch (WebClientResponseException ex) {
System.out.println(" " + ex.getClass().getSimpleName() + ", status = " + ex.getStatusCode().value());
}
System.out.println("[4] exchangeToMono() on a 404");
String seen = client.get().uri("/nope")
.exchangeToMono(response -> Mono.just("my function saw status " + response.statusCode().value()))
.block(TIMEOUT);
System.out.println(" " + seen);
server.disposeNow(TIMEOUT);
HttpResources.disposeLoopsAndConnections();
System.out.println("server disposed: " + server.isDisposed());
[1] retrieve(): nothing is sent until someone subscribes
Mono built. server hits = 0
filter A -> GET /greet/spring
filter B -> GET /greet/spring
filter B <- 200
filter A <- 200
body = "hi, spring", server hits = 1
[2] subscribing to the same Mono again sends a second request
filter A -> GET /greet/spring
filter B -> GET /greet/spring
filter B <- 200
filter A <- 200
body = "hi, spring", server hits = 2
[3] retrieve() on a 404
filter A -> GET /nope
filter B -> GET /nope
filter B <- 404
filter A <- 404
NotFound, status = 404
[4] exchangeToMono() on a 404
filter A -> GET /nope
filter B -> GET /nope
filter B <- 404
filter A <- 404
my function saw status 404
server disposed: true
- [1]
retrieve().bodyToMono(...)까지 불러도 서버는 요청을 받지 않았습니다(hits = 0).block()으로 구독하자 필터가 A, B 순서로 실행되고 응답은 B, A 순서로 돌아옵니다. - [3]의
NotFound는WebClientResponseException.NotFound입니다. 자주 쓰는 상태 코드에는 하위 클래스가 따로 있고, 그 밖의 코드는WebClientResponseException그대로 만들어집니다(WebClientResponseException310행, 385행). - [4]는 같은 404인데 예외 없이 우리 함수가 상태를 받았습니다.
- 서버에는 예외 처리기를 따로 넣지 않았는데도 500이 아니라 404가 나옵니다.
toHttpHandler가 쓰는 기본HandlerStrategies에WebFluxResponseStatusExceptionHandler가 들어 있기 때문입니다(DefaultHandlerStrategiesBuilder60행).
헷갈리기 쉬운 곳
- 같은
Mono를 두 번 구독하면 요청도 두 번 나갑니다. [2]에서hits가 2가 됐습니다.Mono는 결과가 아니라 계획서이고, 구독할 때마다exchange()안의defer가 처음부터 다시 실행됩니다. 결과를 여러 곳에서 쓰려면 한 번 구독한 값을 넘기거나 Reactor의 캐시 연산자를 써야 합니다. retrieve()만 부르고 구독하지 않으면 아무 일도 일어나지 않습니다. 컴파일도 되고 예외도 없어서 찾기 어렵습니다. WebFlux 컨트롤러 안이라면 그Mono를 반환값에 이어 붙여야 프레임워크가 구독해 줍니다.- 예제의
block()은 보여 주기 위한 것입니다.main스레드에서 결과를 눈으로 보려고 썼습니다. 요청을 처리하는 리액티브 코드 안에서는block()으로 기다리지 않고Mono를 이어 붙여 돌려주는 것이 원칙으로 알려져 있는데, 이것은 Reactor 쪽의 규칙이라 이 글에서 소스로 확인한 내용은 아닙니다.
이 글에서 뺀 것
큰 줄기를 위해 이름만 적고 넘어간 것들입니다. 요청 본문과 인자를 풀어 주는 리액티브 판 HandlerMethodArgumentResolver들과 ReactiveAdapterRegistry(RxJava, 코루틴 같은 다른 리액티브 타입을 Publisher로 바꿔 주는 등록소). 실제 바이트를 다루는 Encoder/Decoder와 DataBuffer, 그리고 그것들을 모아 두는 ServerCodecConfigurer(WebFluxConfigurationSupport 351–356행). 받는 쪽이 처리할 수 있는 만큼만 보내게 하는 배압(backpressure). CORS 사전 요청 처리(DispatcherHandler 211–218행, AbstractHandlerMapping 186–218행). 뷰를 그리는 ViewResolutionResultHandler와 ResponseEntity를 다루는 ResponseEntityResultHandler. 함수형 엔드포인트의 필터인 HandlerFilterFunction과 경로를 묶는 nest. WebSession, 로케일, Forwarded 헤더 처리, 관측(Observation) 연동(HttpWebHandlerAdapter 312–324행). 블로킹으로 판단된 컨트롤러 메서드를 별도 스케줄러에서 구독하게 하는 설정(RequestMappingHandlerAdapter 276–279행). 그리고 WebSocketHandlerAdapter는 다음 글의 주제와 이어집니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
Mono / Flux |
값이 아니라 계획서. 구독해야 실행되고, 구독할 때마다 다시 실행된다 |
HttpHandler |
WebFlux의 바닥. 요청·응답을 받아 Mono<Void>를 돌려주는 메서드 하나. spring-web에 있다 |
| 서버 어댑터 | 서버의 요청·응답을 Spring 타입으로 감싸 HttpHandler를 부른다 |
WebHttpHandlerBuilder |
WebHandler를 필터 사슬, 예외 처리, HttpWebHandlerAdapter 순으로 세 겹 감싼다 |
WebFilter |
등록한 순서로 들어가고 반대로 나온다. MVC의 인터셉터와 서블릿 필터 자리를 맡는다 |
WebExceptionHandler |
필터보다 바깥에서 오류 신호를 응답으로 바꾼다. 없으면 500 |
DispatcherHandler.handle |
매핑에게 차례로 묻고(concatMap·next), 어댑터로 부르고, 결과 처리기로 쓴다 |
HandlerResultHandler |
MVC에 없는 세 번째 전략. HandlerResult를 응답으로 바꾼다 |
HttpMessageReader / Writer |
HttpMessageConverter의 리액티브 짝. 객체가 아니라 통로를 주고받는다 |
| 함수형 엔드포인트 | RequestPredicate + HandlerFunction = RouterFunction. 등록한 순서가 우선순위 |
RouterFunctionMapping |
순서 -1이라 어노테이션 매핑보다 먼저 묻는다 |
WebClient |
필터(ExchangeFilterFunction) → ExchangeFunction → ClientHttpConnector |
retrieve() / exchangeToMono() |
400 이상을 예외로 바꿔 준다 / 응답을 내 함수가 직접 본다 |
다음에 볼 것
서블릿 위의 MVC(21편)와 리액티브 위의 WebFlux가 같은 뼈대(매핑 → 어댑터 → 결과)를 서로 다른 방식으로 채운다는 것을 봤습니다.
지금까지의 웹은 전부 "요청 하나에 응답 하나"였습니다. 다음 글은 연결을 열어 둔 채 양쪽이 메시지를 주고받는 세계로 갑니다. WebSocket 연결 위에서 STOMP 메시지가 어떤 채널을 타고 @MessageMapping 메서드까지 가는지를 25편: WebSocket과 메시징 — STOMP 메시지는 어떻게 흐르나에서 봅니다.