HandlerMapping이 핸들러를 골랐지만(02장), 그 핸들러는 Object다. DispatcherServlet은 이 객체를 어떻게 실행하는지 모른다. HandlerAdapter가 그 간극을 메운다 — "이 핸들러를 내가 실행할 수 있나(supports)? 그럼 실행하고 ModelAndView를 돌려준다(handle)".
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}이 한 겹의 추상 덕분에 DispatcherServlet은 어떤 핸들러 타입에도 결합되지 않는다. 기본 등록된 어댑터는 4종이다.
| HandlerAdapter | 지원하는 핸들러 | 비고 |
|---|---|---|
HttpRequestHandlerAdapter |
HttpRequestHandler |
저수준 서블릿 스타일 |
SimpleControllerHandlerAdapter |
Controller 인터페이스 |
구식 MVC |
RequestMappingHandlerAdapter |
HandlerMethod(@RequestMapping) |
이 챕터의 주인공 |
HandlerFunctionAdapter |
HandlerFunction |
함수형(06장) |
이 챕터는 절대 다수의 애플리케이션이 쓰는 RequestMappingHandlerAdapter를 깊게 본다.
HandlerAdapter
▲
AbstractHandlerMethodAdapter supports() = HandlerMethod 인지 검사
▲ handle() → handleInternal() (추상)
RequestMappingHandlerAdapter @RequestMapping 실행의 본체
│
│ 보유:
├ messageConverters: List<HttpMessageConverter> 본문 ↔ 객체 변환
├ argumentResolvers: HandlerMethodArgumentResolverComposite 요청 → 인자
├ returnValueHandlers: HandlerMethodReturnValueHandlerComposite 반환값 → 응답
├ initBinderArgumentResolvers @InitBinder 용
└ modelAttribute/@ControllerAdvice 캐시
요청마다 만든다:
ServletInvocableHandlerMethod "리졸버로 인자 채워 메서드 호출 +
반환값을 핸들러로 처리" 한 번에
소스: mvc/method/annotation/RequestMappingHandlerAdapter.java, mvc/method/annotation/ServletInvocableHandlerMethod.java, mvc/method/AbstractHandlerMethodAdapter.java. HandlerMethodArgumentResolver/...ReturnValueHandler/InvocableHandlerMethod의 공통 추상은 spring-web(org.springframework.web.method)에 있고, webmvc는 Servlet 전용 구현을 더한다.
DispatcherServlet.doDispatch가 ha.handle(...)을 부르면 → handleInternal → invokeHandlerMethod로 들어온다.
invokeHandlerMethod(request, response, handlerMethod)
│ 비동기 인프라 준비(WebAsyncManager, AsyncWebRequest)
│ binderFactory = getDataBinderFactory(handlerMethod) @InitBinder 수집
│ modelFactory = getModelFactory(...) @ModelAttribute 수집
│
│ invocableMethod = ServletInvocableHandlerMethod(handlerMethod)
│ .setHandlerMethodArgumentResolvers(argumentResolvers)
│ .setHandlerMethodReturnValueHandlers(returnValueHandlers)
│ .setDataBinderFactory(binderFactory)
│
│ mavContainer = new ModelAndViewContainer()
│ mavContainer ← flash 속성
│ modelFactory.initModel(...) @ModelAttribute 메서드 실행해 모델 채움
│
▼ invocableMethod.invokeAndHandle(webRequest, mavContainer)
① invokeForRequest: 각 파라미터마다
argumentResolvers 에서 supportsParameter 인 리졸버 찾아 resolveArgument
→ args[] 완성 → method.invoke(controller, args)
② setResponseStatus: @ResponseStatus 있으면 상태코드 반영
③ returnValueHandlers 에서 반환값 타입을 지원하는 핸들러 찾아 handleReturnValue
→ 모델/뷰 채우거나(mavContainer), 본문 직접 기록(@ResponseBody)
│
▼ getModelAndView(mavContainer, ...)
requestHandled==true (응답 이미 다 씀) → null (렌더링 생략)
아니면 → ModelAndView(뷰이름/뷰 + 모델)
ModelAndViewContainer가 호출 전 과정을 잇는 누적 상태다. 인자 리졸버가 채운 모델, 반환값 핸들러가 정한 뷰/requestHandled 플래그가 모두 여기 모인다. 마지막에 이 컨테이너를 ModelAndView로 환원하거나, 이미 응답이 완성됐으면(@ResponseBody로 JSON 다 씀) null을 돌려 04장 렌더링을 건너뛴다.
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null
|| mavContainer.isRequestHandled()) {
mavContainer.setRequestHandled(true); // 처리 끝, 렌더링 없음
return;
}
}
...
mavContainer.setRequestHandled(false);
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);void/null을 반환해도 @ResponseStatus가 붙었거나, HttpServletResponse를 직접 인자로 받아 응답을 썼거나(isRequestHandled), Not-Modified면 "처리 완료"로 간주한다. 그 외에는 반환값을 핸들러 체인에 넘긴다.
RequestMappingHandlerAdapter의 진짜 능력은 기본 등록되는 30여 개의 전략에 있다. @RequestMapping 메서드의 "어떤 시그니처도 받아들이는" 유연함이 여기서 나온다.
[애너테이션 기반]
RequestParamMethodArgumentResolver @RequestParam
PathVariableMethodArgumentResolver @PathVariable (02장이 채운 URI 변수)
MatrixVariableMethodArgumentResolver @MatrixVariable
ServletModelAttributeMethodProcessor @ModelAttribute / 커맨드 객체 바인딩
RequestResponseBodyMethodProcessor @RequestBody ── 메시지 컨버터 사용
RequestPartMethodArgumentResolver @RequestPart (멀티파트)
RequestHeaderMethodArgumentResolver @RequestHeader
ServletCookieValueMethodArgumentResolver@CookieValue
SessionAttributeMethodArgumentResolver @SessionAttribute
RequestAttributeMethodArgumentResolver @RequestAttribute
[타입 기반]
ServletRequestMethodArgumentResolver HttpServletRequest, Locale, Principal...
ServletResponseMethodArgumentResolver HttpServletResponse, OutputStream...
HttpEntityMethodProcessor HttpEntity / RequestEntity
ModelMethodProcessor, MapMethodProcessor Model, Map
ErrorsMethodArgumentResolver Errors / BindingResult
UriComponentsBuilderMethodArgumentResolver
ApiVersionMethodArgumentResolver (7.0+)
[catch-all]
PrincipalMethodArgumentResolver
RequestParamMethodArgumentResolver(true) 단순 타입은 @RequestParam 으로 취급
ServletModelAttributeMethodProcessor(true) 그 외 객체는 모델 속성으로
이들은 HandlerMethodArgumentResolverComposite에 담겨, 파라미터마다 supportsParameter가 true인 첫 리졸버를 찾아(찾으면 캐시) resolveArgument로 값을 만든다. @RequestParam/@PathVariable 등은 내부적으로 WebDataBinder로 타입 변환·검증(@Valid)을 거친다.
[단일 목적]
ModelAndViewMethodReturnValueHandler ModelAndView
ModelMethodProcessor, ViewMethodReturnValueHandler
ResponseBodyEmitterReturnValueHandler SSE/스트리밍 emitter
StreamingResponseBodyReturnValueHandler
HttpHeadersReturnValueHandler HttpHeaders
HttpEntityMethodProcessor ResponseEntity ── 메시지 컨버터
...
[애너테이션 기반]
RequestResponseBodyMethodProcessor @ResponseBody ── 메시지 컨버터(JSON 등)
ViewNameMethodReturnValueHandler String → 뷰 이름
ModelAttributeMethodProcessor @ModelAttribute
@ResponseBody/ResponseEntity면 RequestResponseBodyMethodProcessor/HttpEntityMethodProcessor가 HttpMessageConverter로 객체를 본문에 직렬화하고 mavContainer.setRequestHandled(true)로 표시한다 → 뷰 렌더링 없음. String을 반환하면 그건 뷰 이름이 되어 04장으로 넘어간다.
메시지 컨버터(
messageConverters)는@RequestBody(읽기)와@ResponseBody(쓰기) 양쪽이 공유한다. 기본은 byte[]/String/Form 컨버터 + (클래스패스에 있으면) Jackson JSON 컨버터.RequestResponseBodyAdvice로 직렬화 전후를 가로챌 수 있다(@JsonView,@ControllerAdvice등).
getDataBinderFactory— 컨트롤러의@InitBinder메서드 + 글로벌@ControllerAdvice의 것을 모아WebDataBinderFactory를 만든다. 인자 바인딩 시 커스텀PropertyEditor/Formatter/검증기를 주입한다.getModelFactory/modelFactory.initModel—@ModelAttribute메서드를 핸들러 호출 전에 실행해 공통 모델 속성을 채우고,@SessionAttributes로 지정된 속성을 세션에서 복원한다.- 이 셋은 메서드/컨트롤러 단위로 캐시되어(
ConcurrentHashMap) 매 요청 리플렉션 비용을 줄인다.
- 합성(Composite) + 전략 : 어댑터 자체가 거대한 게 아니라, "작은 단일 책임 리졸버/핸들러 수십 개"의 합성이다.
@RequestMapping이 받아들이는 인자/반환 타입 목록은 곧 이 리스트다. 새 타입을 지원하려면 리졸버/핸들러를 추가한다. - 커스터마이징 3단계 :
setCustomArgumentResolvers(기본 앞이 아니라 catch-all 직전에 끼움) →setArgumentResolvers(전체 교체) →WebMvcConfigurer.addArgumentResolvers(07장, 가장 권장). 반환값 핸들러도 동일. requestHandled플래그 : "응답을 핸들러가 직접 다 썼는가"를 나타내, REST(JSON)와 뷰 렌더링을 같은 엔진에서 분기시키는 단일 신호다.- 비동기 일급 지원 : 반환값이
Callable/DeferredResult/WebAsyncTask/리액티브 타입/ResponseBodyEmitter면 전용 핸들러가WebAsyncManager에 위임하고, 디스패치 루프는 즉시 반환했다가 결과 준비 시 재진입한다(01장).
HandlerAdapter는 DispatcherServlet을 핸들러 타입에서 분리하는 SPI다. 핵심 구현 RequestMappingHandlerAdapter는 요청마다 ServletInvocableHandlerMethod를 만들어, 인자 리졸버 30여 종으로 메서드 파라미터를 채우고 메서드를 호출한 뒤, 반환값 핸들러로 결과를 처리한다. @ResponseBody면 메시지 컨버터로 본문에 직렬화하고(렌더링 생략), 뷰 이름/모델이면 ModelAndView로 환원해 04장으로 넘긴다. 모든 유연함은 작고 단일 책임인 전략 객체들의 합성에서 나온다.