DispatcherServlet이란?
Spring MVC의 모든 HTTP 요청은 DispatcherServlet이라는 단일 진입점을 거칩니다. Front Controller 패턴을 구현한 이 서블릿은 요청을 적절한 핸들러(컨트롤러)에 라우팅하고, 뷰 렌더링까지 조율하는 오케스트레이터입니다. @GetMapping, @PostMapping이 자동으로 동작하는 이유가 바로 DispatcherServlet의 정교한 위임 구조 덕분입니다.
요청 처리 전체 흐름
HTTP 요청이 컨트롤러 메서드에 도달하기까지 거치는 단계를 순서대로 정리합니다.
HTTP 요청
│
▼
1. Servlet Container (Tomcat)
│
▼
2. Filter Chain (Spring Security, CORS 등)
│
▼
3. DispatcherServlet.doDispatch()
│
├─ 4. HandlerMapping → 핸들러(컨트롤러 메서드) 탐색
│
├─ 5. HandlerAdapter → 핸들러 실행 준비
│ ├─ ArgumentResolver → 파라미터 변환 (@RequestBody, @PathVariable)
│ ├─ HandlerInterceptor.preHandle()
│ ├─ 컨트롤러 메서드 실행
│ ├─ ReturnValueHandler → 반환값 처리 (@ResponseBody → JSON)
│ └─ HandlerInterceptor.postHandle()
│
├─ 6. ViewResolver → 뷰 이름 → 실제 뷰 변환 (REST API에서는 생략)
│
└─ 7. HandlerInterceptor.afterCompletion()
│
▼
HTTP 응답
HandlerMapping: 핸들러 탐색
DispatcherServlet은 등록된 HandlerMapping 구현체를 순서대로 순회하며, 요청 URL에 매칭되는 핸들러를 찾습니다.
// Spring Boot가 등록하는 HandlerMapping (우선순위 순)
1. RequestMappingHandlerMapping → @RequestMapping, @GetMapping 등
2. WelcomePageHandlerMapping → index.html
3. BeanNameUrlHandlerMapping → 빈 이름이 URL인 경우
4. RouterFunctionMapping → 함수형 라우팅
5. SimpleUrlHandlerMapping → 정적 리소스
// 어떤 HandlerMapping이 선택되었는지 디버깅
@Component
public class MappingDebugger implements ApplicationRunner {
@Autowired
private RequestMappingHandlerMapping mapping;
@Override
public void run(ApplicationArguments args) {
mapping.getHandlerMethods().forEach((info, method) -> {
System.out.printf("%-40s → %s.%s()%n",
info.getPatternValues(),
method.getBeanType().getSimpleName(),
method.getMethod().getName());
});
}
}
// [/api/users] → UserController.findAll()
// [/api/users/{id}] → UserController.findById()
RequestMappingHandlerMapping은 @Controller의 모든 @RequestMapping 메서드를 스캔하여 URL 패턴과 HTTP 메서드를 매핑합니다. 동일 URL에 GET과 POST가 있으면 HTTP 메서드로 구분합니다.
HandlerAdapter: 핸들러 실행
핸들러를 찾았으면 HandlerAdapter가 실제 실행을 담당합니다. 컨트롤러 메서드의 파라미터 변환과 반환값 처리가 여기서 이루어집니다.
// RequestMappingHandlerAdapter 내부 동작 (단순화)
public class RequestMappingHandlerAdapter {
// 1. ArgumentResolver로 파라미터 변환
private List<HandlerMethodArgumentResolver> argumentResolvers;
// 2. ReturnValueHandler로 반환값 처리
private List<HandlerMethodReturnValueHandler> returnValueHandlers;
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod) {
// 파라미터 해석
Object[] args = resolveArguments(handlerMethod, request);
// 메서드 실행
Object returnValue = handlerMethod.invoke(args);
// 반환값 처리
handleReturnValue(returnValue, response);
}
}
주요 ArgumentResolver들:
| Resolver | 처리 대상 | 동작 |
|---|---|---|
RequestParamMethodArgumentResolver |
@RequestParam | 쿼리 파라미터 바인딩 |
PathVariableMethodArgumentResolver |
@PathVariable | URL 경로 변수 추출 |
RequestResponseBodyMethodProcessor |
@RequestBody | HttpMessageConverter로 JSON → 객체 |
ModelAttributeMethodProcessor |
@ModelAttribute | 폼 데이터 바인딩 |
doDispatch() 소스 코드 분석
DispatcherServlet의 핵심인 doDispatch() 메서드를 단순화하여 분석합니다.
protected void doDispatch(HttpServletRequest request,
HttpServletResponse response) throws Exception {
HandlerExecutionChain mappedHandler = null;
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 1. 핸들러 탐색
mappedHandler = getHandler(request);
if (mappedHandler == null) {
noHandlerFound(request, response); // 404
return;
}
// 2. 핸들러에 맞는 어댑터 찾기
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. Interceptor preHandle 실행
if (!mappedHandler.applyPreHandle(request, response)) {
return; // preHandle이 false 반환하면 중단
}
// 4. 핸들러(컨트롤러) 실행
mv = ha.handle(request, response, mappedHandler.getHandler());
// 5. Interceptor postHandle 실행
mappedHandler.applyPostHandle(request, response, mv);
} catch (Exception ex) {
dispatchException = ex;
}
// 6. 결과 처리 (뷰 렌더링 또는 예외 처리)
processDispatchResult(request, response, mappedHandler,
mv, dispatchException);
// 7. Interceptor afterCompletion 실행 (finally에서)
mappedHandler.triggerAfterCompletion(request, response, null);
}
이 흐름을 이해하면 @ExceptionHandler가 6단계에서 동작하는 이유, HandlerInterceptor의 세 메서드가 각각 3·5·7단계에서 호출되는 이유를 알 수 있습니다.
커스텀 HandlerMapping 구현
특수한 라우팅 로직이 필요할 때 커스텀 HandlerMapping을 만들 수 있습니다.
// API 버전별 라우팅: Accept 헤더의 버전으로 핸들러 선택
@Component
public class ApiVersionHandlerMapping
extends RequestMappingHandlerMapping {
@Override
protected RequestMappingInfo getMappingForMethod(
Method method, Class<?> handlerType) {
RequestMappingInfo info =
super.getMappingForMethod(method, handlerType);
if (info == null) return null;
ApiVersion version =
AnnotatedElementUtils.findMergedAnnotation(
method, ApiVersion.class);
if (version == null) {
version = AnnotatedElementUtils.findMergedAnnotation(
handlerType, ApiVersion.class);
}
if (version != null) {
// Accept 헤더 조건 추가
// Accept: application/vnd.api.v2+json
RequestMappingInfo versionInfo = RequestMappingInfo
.paths("")
.produces("application/vnd.api.v"
+ version.value() + "+json")
.build();
return info.combine(versionInfo);
}
return info;
}
@Override
public int getOrder() {
return super.getOrder() - 1; // 기본보다 높은 우선순위
}
}
// 사용
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
int value();
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
@ApiVersion(1)
public List<UserV1Dto> getUsersV1() { ... }
@GetMapping
@ApiVersion(2)
public List<UserV2Dto> getUsersV2() { ... }
}
디버깅·로깅 설정
# application.yml
logging:
level:
# 핸들러 매핑 과정
org.springframework.web.servlet.DispatcherServlet: DEBUG
# 어떤 핸들러가 선택되었는지
org.springframework.web.servlet.handler: TRACE
# 파라미터 바인딩 과정
org.springframework.web.method.support: DEBUG
# DispatcherServlet 상세 로깅 활성화
spring:
mvc:
log-request-details: true
DEBUG 레벨에서 DispatcherServlet은 매 요청마다 선택된 핸들러, 사용된 ArgumentResolver, 반환값 처리 방식을 로그로 출력합니다.
관련 글
- Spring HandlerInterceptor 심화 — DispatcherServlet 흐름 내에서 Interceptor의 정확한 동작 위치
- Spring ArgumentResolver 심화 — HandlerAdapter가 파라미터를 변환하는 내부 메커니즘