Skip to content

Latest commit

 

History

History
125 lines (93 loc) · 7.91 KB

File metadata and controls

125 lines (93 loc) · 7.91 KB

06. 함수형 엔드포인트 — RouterFunction 와 HandlerFunction

무엇을 / 왜

@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에 둘 다 기본 등록돼 있어, 애너테이션 컨트롤러와 함수형 라우트가 한 애플리케이션에 공존한다(매핑 우선순위로 조정).

RouterFunctionMapping.getHandlerInternal

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를 적용한다.

HandlerFunctionAdapter.handle

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, StreamingServerResponseWebAsyncManager와 연동해 동시 처리/SSE/스트리밍을 지원한다.
  • 확장 : 직접 RouterFunction 빈을 등록하면 RouterFunctionMapping이 자동 수집한다. 커스텀 RequestPredicate/HandlerFilterFunction으로 매칭·횡단 관심사를 추가한다.

정리

함수형 엔드포인트는 RequestPredicate(조건) + HandlerFunction(처리) = RouterFunction(라우트)을 람다로 조립하는 모델이다. RouterFunctionMapping(HandlerMapping)이 라우트를 찾고 HandlerFunctionAdapter(HandlerAdapter)가 핸들러 함수를 실행해 ServerResponse.writeTo로 응답을 쓴다 — 즉 01장 디스패치 루프에 끼워지는 전략 한 쌍이다. 메시지 컨버터·비동기·렌더링 같은 토대는 애너테이션 모델과 공유하며, 둘은 한 애플리케이션에 공존할 수 있다.