@Controller 애너테이션 모델(02·03장)이 "선언적"이라면, 함수형 엔드포인트는 라우팅과 처리를 코드(람다)로 명시적으로 조립하는 대안이다. 같은 DispatcherServlet 위에서 동작하며, spring-webflux의 함수형 모델과 거의 동일한 API를 Servlet 환경에 제공한다.
세 가지 함수형 구성요소가 핵심이다.
HandlerFunction<T>:ServerRequest → ServerResponse(요청을 처리하는 람다).@RequestMapping메서드의 함수형 대응.RequestPredicate:ServerRequest → boolean(이 요청을 이 핸들러로 보낼지 판단). 매핑 조건의 함수형 대응.RouterFunction<T>:ServerRequest → Optional<HandlerFunction<T>>(조건이 맞으면 핸들러를 돌려줌).RequestPredicate+HandlerFunction의 결합.
RouterFunctions (정적 팩토리/진입점)
route(predicate, handlerFn) → RouterFunction
nest(predicate, routerFn) → 중첩 라우팅
route() → Builder → 빌더 스타일 DSL
RouterFunction<T> route(request): Optional<HandlerFunction<T>>
│ .and(other) / .andRoute(...) / .filter(...) 로 조합
▼
HandlerFunction<T> handle(ServerRequest): T (T extends ServerResponse)
RequestPredicates (정적 팩토리)
GET("/path"), POST(...), path(...), accept(...), contentType(...),
queryParam(...), headers(...) → RequestPredicate (and/or/negate 조합)
ServerRequest 요청 추상 (path, headers, body(Class), pathVariable...)
ServerResponse 응답 추상 + writeTo(req,res,ctx): ModelAndView
├ EntityResponse 본문 객체 (메시지 컨버터로 직렬화)
├ RenderingResponse 뷰 이름 + 모델 (04장 렌더링)
└ (Sse/Streaming/AsyncServerResponse)
소스: function/RouterFunctions.java, function/RequestPredicates.java, function/HandlerFunction.java, function/ServerRequest.java, function/ServerResponse.java, function/support/RouterFunctionMapping.java, function/support/HandlerFunctionAdapter.java
함수형 모델은 별도 디스패처가 아니라, 01장 디스패치 루프에 끼워지는 한 쌍의 전략으로 구현된다.
DispatcherServlet.doDispatch
│ getHandler → RouterFunctionMapping (HandlerMapping)
│ routerFunction.route(serverRequest) → HandlerFunction
│ 핸들러 = HandlerFunction
│ getHandlerAdapter → HandlerFunctionAdapter (supports: handler instanceof HandlerFunction)
▼ ha.handle → handlerFunction.handle(serverRequest) → ServerResponse
serverResponse.writeTo(req, res, ctx) → ModelAndView?
즉 RouterFunctionMapping이 02장의 HandlerMapping 자리에, HandlerFunctionAdapter가 03장의 HandlerAdapter 자리에 들어간다. DispatcherServlet.properties에 둘 다 기본 등록돼 있어, 애너테이션 컨트롤러와 함수형 라우트가 한 애플리케이션에 공존한다(매핑 우선순위로 조정).
ServerRequest request = ServerRequest.create(servletRequest, this.messageConverters);
HandlerFunction<?> handlerFunction = this.routerFunction.route(request).orElse(null);
servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, request); // 어댑터가 재사용
return handlerFunction; // null이면 다음 HandlerMapping 시도RouterFunction이 라우트를 찾으면 그 HandlerFunction을 핸들러로 반환하고, 만든 ServerRequest를 요청 속성에 저장한다. 시작 시 RouterFunctionMapping은 컨텍스트의 모든 RouterFunction 빈을 모아 하나로 합치고(andOther), PathPatternParser를 적용한다.
HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;
ServerResponse serverResponse = handlerFunction.handle(serverRequest); // 사용자 람다 실행
if (serverResponse != null) {
return serverResponse.writeTo(servletRequest, servletResponse, new ServerRequestContext(serverRequest));
}핸들러 함수를 호출해 ServerResponse를 얻고, writeTo로 응답을 쓴다. writeTo의 반환이 ModelAndView면(예: RenderingResponse) 04장 렌더링으로 이어지고, null이면(예: EntityResponse가 본문을 직접 씀) 렌더링을 생략한다 — 03장의 requestHandled와 같은 분기다.
RouterFunction<ServerResponse> route =
RouterFunctions.route(GET("/users"), handler::listUsers)
.andRoute(POST("/users"), handler::createUser)
.filter((request, next) -> { /* 인터셉터 역할 */ return next.handle(request); });
// 또는 빌더 스타일 + 중첩
RouterFunctions.route()
.path("/users", builder -> builder
.GET("/{id}", handler::getUser)
.POST(handler::createUser))
.build();route(predicate, handlerFn)— 가장 기본. 술어가 참이면 핸들러로 라우팅하는RouterFunction생성.nest(predicate, routerFn)— 공통 접두 경로/조건을 공유하는 중첩 라우트.path("/users", ...)가 대표.RouterFunction.filter(HandlerFilterFunction)— 핸들러 호출 전후를 감싼다(인증·로깅 등). 인터셉터의 함수형 대응.
ServerResponse listUsers(ServerRequest request) {
List<User> users = service.findAll();
return ServerResponse.ok().contentType(APPLICATION_JSON).body(users); // EntityResponse
}
ServerResponse getUser(ServerRequest request) {
String id = request.pathVariable("id"); // PathPattern 변수
return ServerResponse.ok().body(service.find(id));
}ServerRequest로 경로 변수/쿼리/헤더/본문(request.body(User.class), 메시지 컨버터 사용)에 접근하고, ServerResponse의 빌더(ok()/status(...)/created(uri) ...)로 응답을 조립한다. 본문 직렬화/역직렬화는 애너테이션 모델과 동일한 HttpMessageConverter 를 쓴다.
- 같은 인프라, 다른 표면 : 함수형 모델은
DispatcherServlet·메시지 컨버터·비동기·콘텐츠 협상을 그대로 재사용한다. 새 디스패처가 아니라HandlerMapping+HandlerAdapter한 쌍의 추가 구현일 뿐이다. - 불변·조합 가능 :
RouterFunction/RequestPredicate/HandlerFunction이 모두 함수형 인터페이스라and/or/filter로 합성된다. 라우팅 규칙이 코드로 표현돼 테스트·재사용이 쉽다(라이브러리성 모듈에서 선호). - 명시적 등록 : 컨트롤러 스캔(
@Controller탐지) 대신,RouterFunction을 반환하는@Bean메서드로 라우트를 명시 등록한다. 어떤 URL이 어디로 가는지 한 곳에서 보인다. - 비동기/스트리밍 :
AsyncServerResponse,SseServerResponse,StreamingServerResponse가WebAsyncManager와 연동해 동시 처리/SSE/스트리밍을 지원한다. - 확장 : 직접
RouterFunction빈을 등록하면RouterFunctionMapping이 자동 수집한다. 커스텀RequestPredicate/HandlerFilterFunction으로 매칭·횡단 관심사를 추가한다.
함수형 엔드포인트는 RequestPredicate(조건) + HandlerFunction(처리) = RouterFunction(라우트)을 람다로 조립하는 모델이다. RouterFunctionMapping(HandlerMapping)이 라우트를 찾고 HandlerFunctionAdapter(HandlerAdapter)가 핸들러 함수를 실행해 ServerResponse.writeTo로 응답을 쓴다 — 즉 01장 디스패치 루프에 끼워지는 전략 한 쌍이다. 메시지 컨버터·비동기·렌더링 같은 토대는 애너테이션 모델과 공유하며, 둘은 한 애플리케이션에 공존할 수 있다.