Spring Web MVC, doDispatch부터 코드로 따라가보기
Spring Web MVC를 사용하다 보면 DispatcherServlet, HandlerMapping, HandlerAdapter 같은 개념을 자연스럽게 접하게 됩니다. 그런데 막상 "HandlerMapping이 직접 컨트롤러를 실행하면 안 되나요? 왜 HandlerAdapter가 따로 필요하죠?"처럼 조금만 더 파고들면, 명확하게 답하기 어려운 질문들이 등장합니다.
저도 Spring Web MVC를 학교에서 공부하면서, 사이드 프로젝트를 진행하면서 사용해봤지만 명확한 답을 하기가 어려웠습니다. 그래서 이번에는 단순한 개념 나열에서 멈추지 않고, 실제 소스코드를 열어서 코드 레벨에서 한 줄씩 따라가 보기로 했습니다.
Spring Web MVC의 내부 구조
Spring Web MVC는 Servlet API를 기반으로 만들어진 웹 프레임워크라고 볼 수 있습니다.

Spring Web MVC는 내부에 여러 가지 요소를 가지고 있는데요, 하나씩 살펴보겠습니다.
DispatcherServlet
Spring Web MVC의 프론트 컨트롤러로, 모든 HTTP 요청을 받아 적절한 핸들러에게 위임하고 응답을 렌더링하는 중앙 디스패처입니다.
Spring Web MVC는 프론트 컨트롤러 패턴을 중심으로 설계되어 있으며, 중앙의 서블릿인 DispatcherServlet이 요청 처리를 위한 공유 알고리즘을 제공하고, 실제 작업은 구성 가능한 위임 컴포넌트들이 수행합니다. Spring Web MVC에서 가장 핵심적인 역할을 합니다.
HandlerMapping
들어오는 웹 요청을 적절한 핸들러(및 인터셉터 목록)에 매핑하는 역할을 하는 인터페이스입니다.
요청을 핸들러에 매핑하고, 전처리 및 후처리를 위한 인터셉터 목록을 함께 지정합니다. 주요 구현체로는 RequestMappingHandlerMapping(@RequestMapping 어노테이션이 지정된 메서드를 지원)과 SimpleUrlHandlerMapping(URI 경로 패턴과 핸들러 간의 명시적 등록을 관리)이 있습니다.
HandlerAdapter
DispatcherServlet이 핸들러가 어떤 형태이든 상관없이 일관된 방식으로 호출할 수 있게 해주는 어댑터입니다.
DispatcherServlet이 요청에 매핑된 핸들러를 호출할 수 있도록 돕습니다. HandlerAdapter의 주요 목적은 DispatcherServlet이 세부 사항에 신경 쓰지 않도록 역할을 분리하는 것입니다.
ViewResolver
컨트롤러가 반환한 문자열 형태의 논리적 뷰 이름을 실제 View 객체로 변환(매핑)해주는 인터페이스입니다.
핸들러에서 반환된 논리적 문자열 기반 뷰 이름을 응답에 렌더링할 실제 뷰로 변환합니다.
내부 구현
이제 코드 레벨에서 구현이 어떻게 되어 있는지 살펴보겠습니다. 코드는 spring-webmvc 7.0.6 버전 기준입니다.
doDispatch
DispatcherServlet의 doDispatch() 메서드부터 시작합니다.
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);processedRequest는 원본 request를 담아두되, 요청이 멀티파트라면 MultipartHttpServletRequest로 래핑된 객체로 교체됩니다.
mappedHandler는 finally/catch 블록에서도 접근해야 해서 try 바깥에서 미리 선언되어 있습니다.
asyncManager는 비동기 처리 여부를 추적하기 위한 객체입니다.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}핸들러를 못 찾으면 즉시 return으로 메서드를 종료합니다. 이 지점 이후 코드는 핸들러가 반드시 존재한다는 전제 하에 실행됩니다.
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}applyPreHandle()이 false를 반환하면 인터셉터 중 하나가 요청을 가로챈 경우에 해당하며, 이 시점에서 요청 처리는 즉시 종료됩니다.
applyPreHandle()은 HandlerExecutionChain 클래스의 메서드로, 등록된 인터셉터들의 preHandle() 메서드를 순서대로 호출합니다.
- 반환값이
true: 다음 인터셉터 또는 핸들러 자체(컨트롤러 메서드)로 요청이 넘어갑니다. - 반환값이
false: 그 인터셉터가 이미 응답을 처리했다고 간주하고DispatcherServlet은 더 이상 아무것도 하지 않습니다.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());HandlerAdapter가 실제 컨트롤러를 호출하고 ModelAndView를 반환받는 부분입니다.
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}컨트롤러가 비동기 처리를 시작했다면, 아직 최종 결과가 나오지 않았으므로 여기서 리턴하고 이후 뷰 렌더링을 하지 않습니다.
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);컨트롤러가 뷰 이름을 명시적으로 지정하지 않고 반환할 때가 있습니다. 이럴 때 applyDefaultViewName()이 요청 정보를 기반으로 기본 뷰 이름을 대신 채워 넣습니다.
그 다음 실행되는 applyPostHandle()은 등록된 인터셉터들의 postHandle()을 호출합니다. preHandle()이 요청이 들어올 때(핸들러 실행 전) 순서대로 실행됐다면, postHandle()은 핸들러 실행 후 뷰 렌더링 전에 그 순서를 거꾸로 풀어내며 실행됩니다.
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}여기서 주목할 점은 예외를 즉시 던지지 않고 dispatchException 변수에 담아둔다는 것입니다.
Throwable도 잡아서 ServletException으로 래핑하는데, 주석에 따르면 이는 핸들러 메서드에서 던져진 Error도
@ExceptionHandler가 처리할 수 있게 하기 위함이라고 합니다.
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
asyncManager.setMultipartRequestParsed(multipartRequestParsed);
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
cleanupMultipart(processedRequest);
}
}
}동기/비동기 여부에 따라 완전히 다른 작업이 진행됩니다.
- 비동기 처리 중: 아직 요청이 끝난 게 아니므로 멀티파트 정리(
cleanupMultipart)를 하지 않고, 대신multipartRequestParsed상태를asyncManager에 넘겨서 실제 비동기 처리가 끝났을 때 정리하도록 위임합니다. - 동기 처리: 요청이 이미 끝났으므로 멀티파트로 처리된 요청이었다면 즉시
cleanupMultipart()로 리소스를 정리합니다.
getHandler
이어서 doDispatch()에 등장했던 getHandler() 메서드를 살펴보려고 합니다. 그에 앞서 HandlerMapping 클래스의 상속 관계를 먼저 보고 가는 게 이해에 도움이 될 것 같습니다.

HandlerMapping 클래스는 다음과 같이 상속을 하고 있습니다. 이러한 부모-자식 관계를 미리 알아두고 이어나가겠습니다.
HandlerMapping에 getHandler()가 정의되어 있는데요, 해당 메서드 상단에 적힌 주석을 보면 다음과 같은 역할을 한다는 것을 알 수 있습니다.
요청에 대한 핸들러와 인터셉터들을 반환한다. 반환되는 HandlerExecutionChain은 핸들러 Object를 포함하는데, 이는 핸들러가 어떤 방식으로도 제약받지 않도록 하기 위함이다. 일치하는 것을 찾지 못하면 null을 반환한다. 이것은 에러가 아니다. DispatcherServlet은 등록된 모든 HandlerMapping 빈을 조회하여 일치하는 것을 찾으며, 어느 것도 핸들러를 찾지 못했을 때에만 비로소 에러로 판단한다.
protected @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}Spring Boot가 REST API 애플리케이션을 띄우면, RequestMappingHandlerMapping 클래스의 인스턴스가 빈으로 생성되어 handlerMappings 리스트에 등록됩니다.
여기서 getHandler()가 호출되는데, mapping의 실제 타입인 RequestMappingHandlerMapping에는 getHandler()를 오버라이드한 게 없으므로 상속 계층을 따라 올라가 AbstractHandlerMapping.getHandler()가 실행됩니다.
AbstractHandlerMapping.getHandler()의 코드 일부는 살펴보겠습니다.
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}여기서 실제 핸들러가 조회됩니다. 다만 여기서도 직접 조회하지는 않습니다. 하위 클래스가 오버라이드한 getHandlerInternal()이 호출됩니다.
핸들러를 찾지 못하면 HandlerMapping에 설정된 기본 핸들러(defaultHandler)로 대체합니다. 그래도 없으면 null을 반환하고, DispatcherServlet.getHandler()의 반복문에서 다음 HandlerMapping으로 넘어가게 됩니다.
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);여기서 HandlerExecutionChain을 조립합니다. HandlerExecutionChain은 핸들러 실행 체인으로, 핸들러 객체와 핸들러 인터셉터들로 구성됩니다.
이 메서드 내부에서 이 HandlerMapping에 등록된 공통 인터셉터들과, 현재 요청 URL에 매칭되는 MappedInterceptor들을 체인에 추가합니다.
AbstractHandlerMapping.getHandler()에 등장했던 getHandlerInternal()을 다시 살펴보겠습니다. AbstractHandlerMapping.getHandlerInternal()을 보면 다음과 같이 추상 메서드로 선언돼있습니다.
protected abstract @Nullable Object getHandlerInternal(HttpServletRequest request) throws Exception;따라서 동적 바인딩에 의해 RequestMappingHandlerMapping 인스턴스의 상속 체인을 따라 실제로 오버라이드가 존재하는 가장 가까운 RequestMappingInfoHandlerMapping의 getHandlerInternal()을 호출하게 됩니다.
해당 메서드는 아래와 같습니다.
@Override
protected @Nullable HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
try {
return super.getHandlerInternal(request);
}
finally {
ProducesRequestCondition.clearMediaTypesAttribute(request);
}
}여기서 보이는 것처럼 super.getHandlerInternal(request)로 부모의 메서드를 호출합니다. 이는 AbstractHandlerMethodMapping.getHandlerInternal()로 이어집니다.
AbstractHandlerMethodMapping.getHandlerInternal()에서 핵심적인 부분만 보겠습니다.
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);실제로 등록된 매핑들 중 이 요청과 가장 잘 일치하는 것을 찾는 핵심 로직인 lookupHandlerMethod()를 호출합니다.
그 후 찾은 HandlerMethod가 아직 빈 이름(String) 상태로만 들고 있다면, 이 시점에 실제 컨트롤러 빈 인스턴스로 완전히 해석(resolve)해서 돌려줍니다.
여기서도 마찬가지로 매핑의 탐색을 lookupHandlerMethod()로 위임하고 있습니다. 이 메서드를 이어서 살펴보겠습니다.
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}getMappingsByDirectPath()는 URL 패턴이 아니라 고정된 문자열 경로(예: /users/list)로 등록된 매핑들만 빠르게 찾는 캐시 조회입니다.
여기서 매치되는 게 있으면 굳이 전체 매핑(getRegistrations().keySet())을 순회하지 않고 끝냅니다.
즉 이건 성능 최적화용 분기이고, /users/{id}처럼 경로 변수가 있는 패턴은 이 분기에 걸리지 않으므로 두 번째 블록(전체 순회)으로 넘어갑니다.
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
for (T mapping : mappings) {
T match = getMatchingMapping(mapping, request);
if (match != null) {
matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
}
}
}addMatchingMappings() 메서드 안에서 getMatchingMapping() 메서드가 호출됩니다. getMatchingMapping()은 추상 메서드로, 실제로 이 요청과 매치되는지 판단하는 로직을 가지고 있습니다.
RequestMappingInfoHandlerMapping에서 종합적으로 판단하는 실제 구현이 여기로 연결됩니다.
다시 lookupHandlerMethod()로 돌아가겠습니다.
if (matches.size() > 1) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);여러 컨트롤러 메서드가 동시에 매치될 수 있다는 가정 하에 조건문으로 처리를 합니다.
동시에 매치된 컨트롤러 메서드 중 더 나은 것을 선정하기 위해 정렬하는 과정이 getMappingComparator()로 위임됩니다.
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.getHandlerMethod();
}
else {
return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
}- 매치 성공 시:
setAttribute()를 통해BEST_MATCHING_HANDLER_ATTRIBUTE값을 저장하고handleMatch()메서드를 호출한 뒤 리턴합니다. - 매치 실패 시: 그냥
null을 반환하는 게 아니라handleNoMatch()라는 훅으로 위임합니다. 404, 405 같은 구체적인 에러 판단 로직으로 이어지는 지점입니다.
이러한 과정을 통해 doDispatch()의 mappedHandler로 매치되는 핸들러를 받아오게 됩니다.
그러면 DispatchServlet.doDispatch()에서 mappedHandler를 바로 실행시키면 될 것 같은데, 그러지 않고 HandlerAdapter로 어댑터를 받아오는 것을 볼 수 있습니다. 이미 매치된 핸들러를 가져왔는데 왜 실행시키지 않고 어댑터를 또 찾아오려는 걸까요?
mappedHandler.getHandler()가 반환하는 값의 타입은 Object입니다. HandlerMapping.getHandler()의 공식 Javadoc에서도 "핸들러가 어떤 방식으로도 제약받지 않도록" Object로 남겨둔다고 명시하고 있습니다.
REST API 컨트롤러라면 실제로는 HandlerMethod가 들어있지만, 코드 레벨에서는 그게 HandlerMethod인지, Controller 인터페이스 구현체인지, HttpRequestHandler인지 알 수 없는 채로 넘어옵니다. Object 타입에는 "이걸 실행하라"는 공통 메서드가 없습니다. mappedHandler.getHandler().handle()처럼 곧바로 호출하고 싶어도, 컴파일 시점에 Object에는 그런 메서드가 존재하지 않으므로 애초에 코드가 성립하지 않습니다.
그래서 필요한 게 HandlerAdapter입니다. 핸들러의 실제 타입이 무엇이든, HandlerAdapter는 supports(Object handler)로 "내가 이걸 처리할 수 있는지" 판단하고, handle(request, response, Object handler)로 "그 안에서 알아서 캐스팅한 뒤 실행"합니다. 즉 DispatcherServlet은 핸들러의 구체적인 타입을 몰라도, HandlerAdapter라는 중간 계층을 통해 일관된 방식으로 "실행해줘"라고 요청만 하면 됩니다.
다시 말해 Handler와 Adapter를 분리한 의도는, 각자가 자신의 역할만 하고 서로의 구체적인 구현을 몰라도 전체 시스템이 동작하도록 역할을 나눠 분산시킨 것입니다. 그 결과, 새로운 핸들러 타입이 추가되어도 DispatcherServlet이나 기존 구현체들을 수정할 필요 없이, 그 타입 전용의 HandlerAdapter 하나만 새로 추가하면 되는 확장성이 생깁니다.
adapter가 필요한 이유를 알았으니 이어서 HandlerAdapter에 대해서도 알아보도록 하겠습니다.
getHandlerAdapter
HandlerAdapter도 클래스 간 상속 관계를 미리 확인하고 시작하겠습니다.

위와 같은 상속 관계를 알아두고 DispatcherServlet.getHandlerAdapter()의 로직을 살펴보겠습니다.
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}이 구조는 앞에서 살펴본 DispatcherServlet.getHandler()의 반복문과 정확히 같은 패턴입니다.
handlerMappings를 순회하며 "이 요청을 처리할 수 있는지"를 각 HandlerMapping에게 물었던 것처럼, 여기서는 handlerAdapters를 순회하며 "이 핸들러를 실행할 수 있는지"를 각 HandlerAdapter에게 묻습니다. adapter.supports(handler)가 해당 부분입니다. 판단은 전적으로 각 어댑터에게 위임되고, DispatcherServlet은 true를 반환하는 첫 번째 어댑터를 그대로 쓰기만 합니다.
REST API(@RestController) 기준이라면 adapter 객체의 런타임 타입은 RequestMappingHandlerAdapter이지만, supports()는 부모인 AbstractHandlerMethodAdapter에서 이미 final로 구현되어 있어 RequestMappingHandlerAdapter가 다시 오버라이드할 수 없습니다. 따라서 adapter.supports(handler)를 호출하면 AbstractHandlerMethodAdapter.supports()가 그대로 실행됩니다.
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");getHandler()가 핸들러를 못 찾았을 때 null을 반환하고 다음 HandlerMapping으로 넘어갔던 것과 달리, 여기서는 지원하는 어댑터를 못 찾으면 곧바로 예외를 던집니다.
"핸들러를 못 찾은 것"은 요청 URL이 잘못됐을 수도 있는 정상적인 상황이지만, "핸들러는 찾았는데 실행할 어댑터가 없는 것"은 애플리케이션 설정 자체의 문제로 받아들여지는 것 같습니다. 에러 메시지에 적힌 "The DispatcherServlet configuration needs to include..."라는 문구도 이 상황을 설정 오류로 다루고 있다는 걸 보여줍니다.
그러면 이제 AbstractHandlerMethodAdapter.supports() 메서드를 이어서 살펴보겠습니다.
@Override
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod handlerMethod && supportsInternal(handlerMethod));
}이 메서드는 두 단계로 나뉩니다. 먼저 handler가 HandlerMethod 타입인지 검사하고, 아니라면 && 연산자에 의해 supportsInternal()은 호출조차 되지 않고 곧바로 false가 반환됩니다. 타입 검사를 통과한 경우에만 supportsInternal(handlerMethod)로 세부 판단을 위임합니다.
supports() 자체는 final로 고정되어 하위 클래스가 건드릴 수 없다는 점에서, 앞서 getHandler 섹션에서 봤던 AbstractHandlerMapping.getHandler()와 같은 역할을 합니다. 그리고 실제 세부 판단 로직은 abstract 메서드인 supportsInternal()로 위임되는데, 이 역시 getHandlerInternal()과 같은 역할입니다.
AbstractHandlerMethodAdapter에서 supportsInternal() 메서드는 다음과 같이 abstract 메서드로 선언되어 있습니다.
protected abstract boolean supportsInternal(HandlerMethod handlerMethod);실제 구현되어 있는 곳은 AbstractHandlerMethodAdapter를 상속한 RequestMappingHandlerAdapter입니다.
RequestMappingHandlerAdapter에 구현된 supportsInternal() 메서드는 아래와 같습니다.
@Override
protected boolean supportsInternal(HandlerMethod handlerMethod) {
return true;
}이름과 달리 실제로는 아무 조건 없이 true를 반환합니다. 공식 Javadoc에 따르면, RequestMappingHandlerAdapter는 인식하지 못하는 메서드 인자나 반환값이 있더라도 요청 파라미터나 모델 속성으로 대신 처리하는 fallback 로직을 갖고 있어서, 처리하지 못할 HandlerMethod가 사실상 없다고 보는 것입니다.
소스코드에서 해당 메서드의 주석을 보면 이렇게 명시되어 있습니다.
어떤 메서드 인자 타입이나 반환 값 타입이든 어떤 방식으로든지 처리되므로 항상 true를 반환한다. HandlerMethodArgumentResolver에 의해 인식되지 않은 메서드 인자는, 단순 타입(simple type)이라면 요청 파라미터로 해석되고, 그렇지 않다면 모델 속성(model attribute)으로 해석된다. HandlerMethodReturnValueHandler에 의해 인식되지 않은 반환 값은 모델 속성으로 해석될 것이다.
앞서 RequestMappingInfoHandlerMapping.getMatchingMapping()이 URL•HTTP메서드•헤더 조건을 까다롭게 비교했던 것과 대비됩니다. "이 요청을 누가 처리할지" 정교하게 고르는 일은 이미 HandlerMapping 단계에서 끝났고, HandlerAdapter 단계는 그렇게 정해진 핸들러를 "어떻게 실행할지"만 신경 쓰면 되는 거죠.
이 부분이 위에서 얘기했던 "왜 HandlerMapping과 HandlerAdapter가 분리되어 있는가"라는 질문에 대한 코드 레벨의 답이 되는 셈입니다.
이렇게 해서 DispatcherServlet.doDispatch()의 ha에 핸들러 어댑터가 담기게 됩니다. 그 후 ha.handle(processedRequest, response, mappedHandler.getHandler())를 통해 핸들러가 실행됩니다.
그러면 마지막으로 handle() 메서드를 살펴보겠습니다.
handle
handle()은 HandlerAdapter에 선언되어 있는 메서드로, 위에서 봤던 supports()와 동일하게 AbstractHandlerMethodAdapter.handle()이 실행됩니다.
@Override
public final @Nullable ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}supports()가 handler instanceof HandlerMethod로 타입을 안전하게 검사했던 것과 달리, handle()은 검사 없이 (HandlerMethod) handler로 강제 캐스팅합니다.
doDispatch()에서 getHandlerAdapter()가 먼저 supports()로 검증을 마친 어댑터만 골라서 반환하고, 그 결과로 얻은 어댑터에 대해서만 handle()이 나중에 호출됩니다. 이 순서가 지켜지는 한 handle()은 별도 검증 없이 캐스팅해도 문제가 없는 거죠.
또한 리턴문을 보면 하위 메서드인 handleInternal()로 위임하는 것을 볼 수 있습니다. handleInternal()은 AbstractHandlerMethodAdapter에 abstract 메서드로 선언되어 있고, 해당 클래스를 상속하는 RequestMappingHandlerAdapter에서 구현하고 있습니다.
checkRequest(request);컨트롤러 실행 전에 지원하는 HTTP 메서드인지, 필요한 세션이 있는지를 검사합니다.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}synchronizeOnSession이 켜져 있다면, 같은 세션에서 오는 요청들이 동시에 컨트롤러를 실행하지 못하도록 세션 단위로 락을 겁니다.
WebUtils.getSessionMutex(session)으로 세션별 락(mutex) 객체를 얻어와서, 같은 세션에서 들어온 여러 요청이 동시에 컨트롤러를 실행하지 못하도록 순차적으로 처리합니다. 반대로 세션이 없거나 이 옵션이 꺼져 있으면, 그냥 synchronized 없이 바로 invokeHandlerMethod()를 호출합니다.
분기로 나뉘기는 하지만 결국에는 invokeHandlerMethod(request, response, handlerMethod) 메서드를 호출합니다. 이 곳이 바로 컨트롤러 메서드가 호출되는 지점입니다.
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}실행이 끝나면, 아직 Cache-Control 헤더가 없는 경우에 한해 캐시 정책을 자동으로 채워 넣습니다. 이때 @SessionAttributes를 사용하는 핸들러는 일반 컨트롤러와 다른 캐시 정책이 적용됩니다.
이제 invokeHandlerMethod()를 이어서 살펴보겠습니다.
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);컨트롤러가 비동기 반환 타입을 쓸 가능성에 대비해, 실행 전에 WebAsyncManager를 미리 세팅해두는 부분입니다.
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);지금까지 봐온 HandlerMethod를, ServletInvocableHandlerMethod로 감싸는 지점입니다. ServletInvocableHandlerMethod는 실제로 파라미터를 채워서 호출까지 할 수 있는 객체로 볼 수 있습니다.
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);컨트롤러 메서드가 Model 파라미터를 받거나 @ModelAttribute를 쓸 수 있으므로, 실제 호출 전에 모델 객체를 미리 준비해두는 부분입니다.
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
Object[] resultContext = asyncManager.getConcurrentResultContext();
Assert.state(resultContext != null && resultContext.length > 0, "Missing result context");
mavContainer = (ModelAndViewContainer) resultContext[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}비동기 처리가 끝난 뒤, Servlet 컨테이너가 다시 요청을 디스패치했을 때를 위한 분기입니다.
invocableMethod.invokeAndHandle(webRequest, mavContainer);실제로 컨트롤러가 실행되는 부분입니다. 이 한 줄 안에서 실제 메서드 호출과 반환값 처리가 함께 일어납니다.
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);컨트롤러 실행 결과 비동기 처리가 막 시작된 상태라면 아직 결과가 없으므로 null을 반환합니다. 그렇지 않다면 mavContainer에 쌓인 정보를 바탕으로 최종 ModelAndView를 만들어 반환합니다.
@ResponseBody였다면 이 시점에 이미 invokeAndHandle() 안에서 응답이 다 쓰였으므로, 결과적으로 null에 가까운 ModelAndView가 반환됩니다.
마치며
지금까지 DispatcherServlet.doDispatch()에서 시작해서, getHandler()로 요청에 맞는 컨트롤러를 찾고, getHandlerAdapter()로 그 컨트롤러를 실행할 방법을 찾고, 최종적으로 invokeHandlerMethod()까지 이어지는 흐름을 코드 레벨로 따라가봤습니다.
지금까지의 과정을 한 문장으로 요약하면 이렇습니다.
"누가 처리할지"는
HandlerMapping이 정하고, "어떻게 실행할지"는HandlerAdapter가 정한다.
실제 소스코드를 열어보면서 느낀 건, Spring Web MVC 전체를 관통하는 하나의 패턴이 있다는 것이었습니다. AbstractHandlerMapping.getHandler()와 getHandlerInternal(), AbstractHandlerMethodAdapter.supports()와 supportsInternal(), handle()과 handleInternal()까지, 공통 로직은 final로 고정해서 상위 클래스가 책임지고, 실제 판단이나 실행은 abstract 메서드로 하위 클래스에 위임하는 구조가 계속 반복됩니다.
이 패턴 하나만 눈에 들어오면, Spring Web MVC의 다른 확장 지점을 처음 마주쳐도 훨씬 빠르게 이해할 수 있을 것 같습니다.
댓글0