DispatcherServlet 심화 — HTTP 요청 하나가 컨트롤러에 도달하기까지
브라우저에서 URL을 입력하고 엔터를 누르면, 그 HTTP 요청이 스프링 컨트롤러의 메서드까지 어떤 경로로 도달할까요? 중간에 거치는 모든 단계를 추적해봅시다.
개념 정의
DispatcherServlet 은 스프링 MVC의 프론트 컨트롤러입니다. 모든 HTTP 요청이 이 하나의 서블릿을 거쳐서 적절한 컨트롤러로 분배됩니다. 요청 수신부터 응답 생성까지의 전체 파이프라인을 조율하는 오케스트레이터입니다.
왜 필요한가
프론트 컨트롤러 없이 각 URL마다 서블릿을 만들면 다음 문제가 생깁니다.
- 공통 로직(인코딩, 보안, 로깅)을 모든 서블릿에 중복 작성해야 합니다
- URL과 서블릿의 매핑을 web.xml에 일일이 등록해야 합니다
- 요청 처리 흐름을 통합적으로 관리할 수 없습니다
DispatcherServlet이 중앙 허브 역할을 하면서, 공통 관심사를 한 곳에서 처리하고 개별 컨트롤러는 비즈니스 로직에만 집중합니다.
내부 동작
전체 요청 처리 흐름
1. 클라이언트 → 서블릿 컨테이너 (톰캣)
2. → Filter 체인 (서블릿 필터)
3. → DispatcherServlet.service()
4. → HandlerMapping: 요청에 맞는 핸들러 검색
5. → HandlerExecutionChain: 핸들러 + 인터셉터 반환
6. → HandlerInterceptor.preHandle()
7. → HandlerAdapter: 핸들러 실행
8. → ArgumentResolver: 파라미터 바인딩
9. → 컨트롤러 메서드 실행
10. → ReturnValueHandler: 반환값 처리
11. → HandlerInterceptor.postHandle()
12. → ViewResolver: 뷰 해석 (필요 시)
13. → View 렌더링 또는 HttpMessageConverter 변환
14. → HandlerInterceptor.afterCompletion()
15. → Filter 체인 (응답)
16. → 클라이언트
HandlerMapping
여러 HandlerMapping이 순서대로 실행되며, 첫 번째로 매칭되는 핸들러를 사용합니다.
// 등록된 HandlerMapping 순서 (기본)
// 1. RequestMappingHandlerMapping → @RequestMapping 기반
// 2. BeanNameUrlHandlerMapping → 빈 이름으로 URL 매핑
// 3. RouterFunctionMapping → 함수형 엔드포인트
// 4. SimpleUrlHandlerMapping → 정적 리소스 등
RequestMappingHandlerMapping이 가장 먼저 실행되며, @GetMapping("/users") 같은 어노테이션 기반 매핑을 처리합니다.
HandlerAdapter
HandlerMapping이 찾은 핸들러를 실제로 실행하는 역할입니다.
// HandlerAdapter 인터페이스
public interface HandlerAdapter {
boolean supports(Object handler); // 이 핸들러를 처리할 수 있는가?
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler); // 핸들러 실행
}
RequestMappingHandlerAdapter 가 가장 많이 사용됩니다. 이 어댑터가 하는 일은 많습니다.
RequestMappingHandlerAdapter
├── ArgumentResolver로 파라미터 바인딩
│ ├── @RequestParam → RequestParamMethodArgumentResolver
│ ├── @PathVariable → PathVariableMethodArgumentResolver
│ ├── @RequestBody → RequestResponseBodyMethodProcessor
│ └── @ModelAttribute → ModelAttributeMethodProcessor
├── 컨트롤러 메서드 호출
└── ReturnValueHandler로 반환값 처리
├── @ResponseBody → RequestResponseBodyMethodProcessor
├── ModelAndView → ModelAndViewMethodReturnValueHandler
└── String (뷰 이름) → ViewNameMethodReturnValueHandler
doDispatch() 핵심 로직
DispatcherServlet의 핵심 메서드를 개념적으로 정리하면 다음과 같습니다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
// 1. 핸들러 검색
HandlerExecutionChain mappedHandler = getHandler(request);
if (mappedHandler == null) {
noHandlerFound(request, response); // 404
return;
}
// 2. 핸들러 어댑터 검색
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
이어서 나머지 구현 부분입니다.
// 3. 인터셉터 preHandle
if (!mappedHandler.applyPreHandle(request, response)) {
return; // 인터셉터가 false 반환하면 중단
}
// 4. 핸들러 실행 (컨트롤러 메서드 호출)
ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());
// 5. 인터셉터 postHandle
mappedHandler.applyPostHandle(request, response, mv);
// 6. 뷰 렌더링 또는 응답 작성
processDispatchResult(request, response, mappedHandler, mv, null);
}
코드 예제
REST API에서의 흐름
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
// 이 메서드가 호출되기까지의 과정:
// 1. HandlerMapping: GET /api/users/123 → 이 메서드 매핑
// 2. ArgumentResolver: "123" → Long 123으로 변환
// 3. 메서드 실행
// 4. ReturnValueHandler: UserResponse → JSON 변환
User user = userService.findById(id);
return UserResponse.from(user);
}
}
흐름 디버깅 — 로그 레벨 설정
logging:
level:
org.springframework.web.servlet.DispatcherServlet: DEBUG
org.springframework.web.servlet.handler.AbstractHandlerMapping: TRACE
org.springframework.web.servlet.mvc.method.annotation: TRACE
DEBUG 레벨 로그 출력 예시:
DEBUG DispatcherServlet - GET "/api/users/1"
DEBUG RequestMappingHandlerMapping - Mapped to UserController#getUser(Long)
DEBUG RequestResponseBodyMethodProcessor - Writing [UserResponse{id=1, name='홍길동'}]
DEBUG DispatcherServlet - Completed 200 OK
HandlerMapping 커스터마이징
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 정적 리소스 매핑 (SimpleUrlHandlerMapping)
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCachePeriod(3600);
}
}
커스텀 ArgumentResolver
// 커스텀 어노테이션
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {}
// ArgumentResolver 구현
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class);
}
이어서 나머지 어노테이션 기반 구현부입니다.
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
// JWT 토큰에서 사용자 정보 추출
String token = request.getHeader("Authorization");
return jwtService.parseUser(token);
}
}
이어서 나머지 구현 부분입니다.
// 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver());
}
}
// 사용
@GetMapping("/profile")
public UserResponse profile(@CurrentUser User user) {
return UserResponse.from(user);
}
ViewResolver (SSR 방식)
REST API에서는 ViewResolver를 거치지 않지만, 서버 사이드 렌더링에서는 ViewResolver가 뷰 이름을 실제 뷰 템플릿으로 변환합니다.
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("message", "Hello");
return "home"; // → ViewResolver → /templates/home.html
}
}
주의할 점
1. HandlerMapping 순서를 모르면 의도한 컨트롤러가 호출되지 않는다
DispatcherServlet은 등록된 HandlerMapping을 순서대로 탐색하여 첫 번째로 매칭되는 핸들러를 사용합니다. @RequestMapping과 SimpleUrlHandlerMapping이 동시에 같은 경로를 처리하면 우선순위가 높은 것만 동작합니다. 특히 정적 리소스 핸들러가 API 경로와 겹치면 API 대신 정적 파일이 반환되는 문제가 발생합니다.
2. 여러 핸들러가 같은 URL 패턴에 매핑되면 Ambiguous mapping 에러가 발생한다
두 개의 @GetMapping("/users")가 서로 다른 컨트롤러에 있으면 IllegalStateException: Ambiguous handler methods mapped로 애플리케이션이 시작되지 않습니다. 컨트롤러 간 URL 매핑이 겹치지 않도록 @RequestMapping의 prefix를 명확히 분리해야 합니다.
3. @Controller에서 @ResponseBody 없이 객체를 반환하면 뷰를 찾으려 한다
@RestController가 아닌 @Controller에서 @ResponseBody 없이 객체를 반환하면, DispatcherServlet이 ViewResolver를 통해 뷰를 찾습니다. 뷰를 찾지 못하면 404가 발생하고, Thymeleaf 등이 설정되어 있으면 엉뚱한 HTML이 반환됩니다. REST API 컨트롤러에는 반드시 @RestController를 사용하세요.
정리
- DispatcherServlet은 ** 프론트 컨트롤러 패턴 **으로 모든 요청의 진입점 역할을 합니다
- 요청 처리 흐름: HandlerMapping → HandlerAdapter → Controller → ViewResolver 순서입니다
- REST API에서는 ViewResolver 대신 HttpMessageConverter 가 JSON 변환을 처리합니다
- ArgumentResolver 로 컨트롤러 파라미터 바인딩을, ReturnValueHandler 로 반환값 처리를 커스터마이징할 수 있습니다
- 로그 레벨을 DEBUG로 설정하면 전체 요청 처리 흐름을 추적할 수 있습니다