Skip to content

Latest commit

 

History

History
171 lines (139 loc) · 10.9 KB

File metadata and controls

171 lines (139 loc) · 10.9 KB

03. HandlerAdapter — 애너테이션 컨트롤러 실행 엔진

무엇을 / 왜

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 전용 구현을 더한다.

동작 흐름: 한 요청의 컨트롤러 호출 (invokeHandlerMethod)

DispatcherServlet.doDispatchha.handle(...)을 부르면 → handleInternalinvokeHandlerMethod로 들어온다.

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장 렌더링을 건너뛴다.

invokeAndHandle — null 반환의 의미

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 메서드의 "어떤 시그니처도 받아들이는" 유연함이 여기서 나온다.

getDefaultArgumentResolvers (요청 → 메서드 인자)

[애너테이션 기반]
 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에 담겨, 파라미터마다 supportsParametertrue인 첫 리졸버를 찾아(찾으면 캐시) resolveArgument로 값을 만든다. @RequestParam/@PathVariable 등은 내부적으로 WebDataBinder로 타입 변환·검증(@Valid)을 거친다.

getDefaultReturnValueHandlers (반환값 → 응답)

[단일 목적]
 ModelAndViewMethodReturnValueHandler     ModelAndView
 ModelMethodProcessor, ViewMethodReturnValueHandler
 ResponseBodyEmitterReturnValueHandler    SSE/스트리밍 emitter
 StreamingResponseBodyReturnValueHandler
 HttpHeadersReturnValueHandler            HttpHeaders
 HttpEntityMethodProcessor                ResponseEntity ── 메시지 컨버터
 ...
[애너테이션 기반]
 RequestResponseBodyMethodProcessor       @ResponseBody  ── 메시지 컨버터(JSON 등)
 ViewNameMethodReturnValueHandler         String → 뷰 이름
 ModelAttributeMethodProcessor            @ModelAttribute

@ResponseBody/ResponseEntityRequestResponseBodyMethodProcessor/HttpEntityMethodProcessorHttpMessageConverter로 객체를 본문에 직렬화하고 mavContainer.setRequestHandled(true)로 표시한다 → 뷰 렌더링 없음. String을 반환하면 그건 뷰 이름이 되어 04장으로 넘어간다.

메시지 컨버터(messageConverters)는 @RequestBody(읽기)와 @ResponseBody(쓰기) 양쪽이 공유한다. 기본은 byte[]/String/Form 컨버터 + (클래스패스에 있으면) Jackson JSON 컨버터. RequestResponseBodyAdvice로 직렬화 전후를 가로챌 수 있다(@JsonView, @ControllerAdvice 등).

보조 모델: @InitBinder, @ModelAttribute, @SessionAttributes

  • 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장).

정리

HandlerAdapterDispatcherServlet을 핸들러 타입에서 분리하는 SPI다. 핵심 구현 RequestMappingHandlerAdapter는 요청마다 ServletInvocableHandlerMethod를 만들어, 인자 리졸버 30여 종으로 메서드 파라미터를 채우고 메서드를 호출한 뒤, 반환값 핸들러로 결과를 처리한다. @ResponseBody면 메시지 컨버터로 본문에 직렬화하고(렌더링 생략), 뷰 이름/모델이면 ModelAndView로 환원해 04장으로 넘긴다. 모든 유연함은 작고 단일 책임인 전략 객체들의 합성에서 나온다.