Spring Boot로 API를 만들 때 우리는 @RestController에 메서드만 작성하면 된다.
그런데 HTTP 요청이 들어왔을 때 "이 요청은 저 Controller 메서드가 처리해"라고 누가 결정해주는 걸까?
그 역할을 하는 게 바로 DispatcherServlet이다.
DispatcherServlet은 Spring MVC의 Front Controller다.
모든 HTTP 요청이 이 서블릿을 통해 들어오고, 이 서블릿이 적절한 컴포넌트에 처리를 위임한다.
직접 뭔가를 처리하는 게 아니라, 전체 흐름을 조율하는 지휘자 역할을 한다.
클래스 계층을 보면 이렇다.
HttpServlet
└── HttpServletBean
└── FrameworkServlet
└── DispatcherServlet ← 우리가 말하는 바로 그것
결국 DispatcherServlet도 Servlet이다. Tomcat이 관리하는 평범한 서블릿인데,
그 안에서 Spring MVC 전체 흐름을 구현하고 있다.
DispatcherServlet이 없다면 어떻게 될지 생각해보자.
URL마다 별도의 Servlet을 만들어야 한다.
/api/users는 UserServlet, /api/orders는 OrderServlet... 이런 식으로.
URL이 100개면 Servlet도 100개다. 인증 처리, 로깅, 예외 처리 같은 공통 로직도
각 Servlet마다 중복으로 작성해야 한다.
Front Controller 패턴은 이 문제를 해결한다.
모든 요청을 하나의 진입점(DispatcherServlet)에서 받고,
공통 처리는 여기서 한 번만 하고, 실제 처리는 각 Controller에 위임한다.
HandlerMapping, HandlerAdapter 등)를 교체하거나 추가하기 쉽다
GET /api/users/1 요청이 들어왔을 때 DispatcherServlet 내부에서 어떤 일이 벌어지는지 보자.
HTTP Request: GET /api/users/1
|
↓
[DispatcherServlet.doDispatch()]
|
| ① HandlerMapping에 위임
| → "이 요청 처리할 핸들러 찾아줘"
↓
[HandlerMapping]
|
| URL + HTTP Method 분석
| → HandlerExecutionChain 반환
| (Handler 메서드 + 적용할 Interceptor 목록)
↓
[DispatcherServlet]
|
| ② 찾은 Handler에 맞는 HandlerAdapter 선택
↓
[HandlerAdapter 선택]
|
| ③ Interceptor preHandle() 순서대로 실행
| → false 반환 시 요청 차단, 이후 흐름 중단
↓
[HandlerInterceptor.preHandle()]
|
| ④ HandlerAdapter가 Controller 메서드 실행
| ArgumentResolver로 파라미터 바인딩 후 호출
↓
[Controller 메서드 실행]
|
| ⑤ Interceptor postHandle() 역순으로 실행
↓
[HandlerInterceptor.postHandle()]
|
| ⑥ 반환값 처리
| @ResponseBody → MessageConverter로 JSON 직렬화
| View 이름 → ViewResolver로 템플릿 렌더링
↓
[ReturnValueHandler + MessageConverter / ViewResolver]
|
| ⑦ Interceptor afterCompletion() 역순으로 실행
| (예외 발생 여부와 무관하게 항상 실행)
↓
[HandlerInterceptor.afterCompletion()]
|
↓
HTTP Response: 200 OK, JSON body
DispatcherServlet은 이 모든 흐름을 doDispatch() 메서드 하나에서 조율한다.
실제 Spring 소스코드를 보면 이 메서드가 얼마나 많은 걸 위임하는지 확인할 수 있다.
DispatcherServlet이 초기화될 때 아래 전략 컴포넌트들을 ApplicationContext에서 찾아서 세팅한다.
등록된 게 없으면 DispatcherServlet.properties에 정의된 기본값을 사용한다.
요청 URL과 HTTP Method를 보고 어떤 Handler(Controller 메서드)를 실행할지 결정한다.
@GetMapping, @PostMapping 같은 애노테이션 정보를 분석해서 매핑 테이블을 구성한다.
기본 구현체는 RequestMappingHandlerMapping이다.
선택된 Handler를 실제로 실행하는 역할이다.
Handler 타입이 다양하기 때문에 Adapter 패턴으로 일관된 방식으로 실행한다.
@RequestMapping 기반 Controller는 RequestMappingHandlerAdapter가 담당한다.
Handler 실행 중 발생한 예외를 처리하는 역할이다.
@ExceptionHandler, @ControllerAdvice가 이 레이어에서 동작한다.
예외가 발생하면 DispatcherServlet이 이 컴포넌트에 위임해서 응답을 만든다.
Controller가 View 이름(문자열)을 반환했을 때 실제 View 파일을 찾아서 렌더링하는 역할이다.
REST API 개발에서는 @ResponseBody를 쓰기 때문에 ViewResolver가 동작하지 않는다.
Thymeleaf, JSP 같은 서버사이드 렌더링을 쓸 때 관여한다.
Java 객체 ↔ HTTP 메시지 바디(JSON 등) 변환을 담당한다.
@ResponseBody가 있으면 반환 객체를 MessageConverter가 JSON으로 직렬화해서 응답 바디에 쓴다.
기본으로 MappingJackson2HttpMessageConverter가 등록되어 있다.
Spring Boot는 DispatcherServletAutoConfiguration을 통해 자동으로 DispatcherServlet을 등록한다.
별도 설정 없이 바로 동작하는 이유가 이것이다.
spring-boot-autoconfigure
└── DispatcherServletAutoConfiguration
├── DispatcherServlet 빈 생성
└── DispatcherServletRegistrationBean으로 "/" 경로에 등록
application.yml에서 경로를 바꿀 수도 있다.
spring:
mvc:
servlet:
path: /app # 기본값은 "/"
DispatcherServlet은 초기화 시점(init())에 WebApplicationContext에서
HandlerMapping, HandlerAdapter, HandlerExceptionResolver 등을 찾아서 내부에 세팅한다.
이때 등록된 구성 요소들이 이후 모든 요청 처리에 사용된다.
커스터마이징이 필요하면 WebMvcConfigurer를 구현해서 특정 컴포넌트만 추가/교체하면 된다.
@EnableWebMvc는 자동 설정을 전부 꺼버리므로 Spring Boot에서는 쓰지 않는 게 원칙이다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
// HandlerInterceptor 등록
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login", "/api/signup");
}
// 커스텀 ArgumentResolver 등록
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver());
}
// 커스텀 MessageConverter 추가
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 기존 컨버터 유지하면서 추가
}
}
Controller에서 예외가 터지면 DispatcherServlet이 HandlerExceptionResolver에 위임한다.
@RestControllerAdvice가 이 구조 위에 올라탄다.
// 전역 예외 처리기 — DispatcherServlet이 예외 발생 시 여기로 위임
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR", "서버 오류가 발생했습니다."));
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// DispatcherServlet → HandlerMapping이 이 메서드를 찾아줌
// HandlerAdapter가 파라미터 바인딩 후 실행
// 반환된 객체는 MessageConverter가 JSON으로 직렬화
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
UserResponse user = userService.findById(id);
return ResponseEntity.ok(user);
}
}
application.yml에 아래 설정을 추가하면 DispatcherServlet이 초기화될 때
등록된 매핑 정보를 로그로 확인할 수 있다. 개발 중에 유용하다.
logging:
level:
org.springframework.web.servlet.DispatcherServlet: DEBUG
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE
DispatcherServlet 인스턴스는 하나다. 그리고 멀티 스레드로 동시 요청을 처리한다.
인스턴스 변수에 상태를 저장하면 절대 안 된다.
요청 처리에 필요한 상태는 항상 메서드 파라미터 또는 HttpServletRequest 속성에 담아야 한다.
Spring MVC 구조에서는 ApplicationContext가 두 개 존재할 수 있다.
Spring Boot에서는 이 구분 없이 ApplicationContext 하나로 통합되어 있다.
직접 web.xml로 설정하던 시절의 구조라서, Spring Boot를 쓸 때는 크게 신경 쓰지 않아도 된다.
DispatcherServlet은 Filter 다음에 위치한다.
Filter에서 예외가 발생하면 DispatcherServlet에 도달하지 못하기 때문에
@ControllerAdvice로 처리할 수 없다.
Spring Security에서 인증 예외를 처리할 때 이 문제가 자주 등장한다.
Filter 예외는 AuthenticationEntryPoint, AccessDeniedHandler로 처리하거나,
별도 예외 처리 Filter를 체인 앞에 추가하는 방식을 쓴다.
HandlerMapping이 여러 개 등록되어 있을 수 있다.
DispatcherServlet은 order 값 기준으로 순서대로 매핑을 시도하고,
가장 먼저 매핑에 성공한 핸들러를 사용한다.
실무에서 ResourceHttpRequestHandler(정적 파일 서빙)와 Controller 매핑이 충돌할 때
이 우선순위 개념이 등장한다.
요청 URL에 매핑되는 Handler가 없으면 기본적으로 404 응답이 나간다.
이걸 @ControllerAdvice에서 잡아서 커스텀 에러 응답을 만들려면
application.yml에 아래 설정이 필요하다.
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<ErrorResponse> handleNoHandler(NoHandlerFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", e.getRequestURL() + " 경로를 찾을 수 없습니다."));
}
}
DispatcherServlet 레벨에서 요청 처리 시간을 측정하고 싶다면
HandlerInterceptor의 preHandle()과 afterCompletion()을 활용한다.
@Component
public class TimingInterceptor implements HandlerInterceptor {
private static final String START_TIME = "startTime";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute(START_TIME, System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
long start = (Long) request.getAttribute(START_TIME);
long elapsed = System.currentTimeMillis() - start;
System.out.printf("[%s %s] %dms%n",
request.getMethod(), request.getRequestURI(), elapsed);
}
}
DispatcherServlet은 Spring MVC의 Front Controller로 모든 HTTP 요청의 진입점이다HandlerMapping → HandlerAdapter → Controller → MessageConverter 순으로 위임한다DispatcherServletAutoConfiguration으로 자동 등록해준다HandlerExceptionResolver에 위임하고, @ControllerAdvice가 이 위에서 동작한다DispatcherServlet이 잡지 못한다 — 이 경계를 명확히 기억해야 한다WebMvcConfigurer로 한다. @EnableWebMvc는 Spring Boot에서 쓰지 않는다
DispatcherServlet이 뭔지 모를 때는 그냥 "Spring이 알아서 해주는 거겠지" 하고 넘겼다.
근데 Filter 예외가 @ControllerAdvice에서 안 잡히는 문제를 마주쳤을 때,
결국 이 흐름을 모르면 원인을 못 찾는다는 걸 깨달았다.
Spring Boot는 많은 걸 자동으로 해주지만, 그 자동화가 어떻게 이루어지는지 알아야
문제가 생겼을 때 어디를 봐야 하는지 알 수 있다.
DispatcherServlet은 그 흐름의 중심에 있는 클래스다.