DispatcherServlet 역할 — Spring MVC의 교통정리 담당자

최병현·2026년 6월 5일

spring boot

목록 보기
11/34

1. 개념 소개

Spring Boot로 API를 만들 때 우리는 @RestController에 메서드만 작성하면 된다. 그런데 HTTP 요청이 들어왔을 때 "이 요청은 저 Controller 메서드가 처리해"라고 누가 결정해주는 걸까?

그 역할을 하는 게 바로 DispatcherServlet이다.

DispatcherServletSpring MVC의 Front Controller다. 모든 HTTP 요청이 이 서블릿을 통해 들어오고, 이 서블릿이 적절한 컴포넌트에 처리를 위임한다. 직접 뭔가를 처리하는 게 아니라, 전체 흐름을 조율하는 지휘자 역할을 한다.

클래스 계층을 보면 이렇다.

HttpServlet
    └── HttpServletBean
            └── FrameworkServlet
                    └── DispatcherServlet  ← 우리가 말하는 바로 그것

결국 DispatcherServlet도 Servlet이다. Tomcat이 관리하는 평범한 서블릿인데, 그 안에서 Spring MVC 전체 흐름을 구현하고 있다.


2. 왜 필요한가

DispatcherServlet이 없다면 어떻게 될지 생각해보자.

URL마다 별도의 Servlet을 만들어야 한다. /api/usersUserServlet, /api/ordersOrderServlet... 이런 식으로. URL이 100개면 Servlet도 100개다. 인증 처리, 로깅, 예외 처리 같은 공통 로직도 각 Servlet마다 중복으로 작성해야 한다.

Front Controller 패턴은 이 문제를 해결한다. 모든 요청을 하나의 진입점(DispatcherServlet)에서 받고, 공통 처리는 여기서 한 번만 하고, 실제 처리는 각 Controller에 위임한다.

  • URL별 Servlet을 만들지 않아도 된다
  • 인증, 로깅, 예외 처리 같은 공통 관심사를 한 곳에서 처리할 수 있다
  • Controller는 순수 비즈니스 로직에만 집중할 수 있다
  • 확장 포인트(HandlerMapping, HandlerAdapter 등)를 교체하거나 추가하기 쉽다

3. 전체 동작 흐름

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 소스코드를 보면 이 메서드가 얼마나 많은 걸 위임하는지 확인할 수 있다.


4. 핵심 구성 요소

DispatcherServlet이 초기화될 때 아래 전략 컴포넌트들을 ApplicationContext에서 찾아서 세팅한다. 등록된 게 없으면 DispatcherServlet.properties에 정의된 기본값을 사용한다.

HandlerMapping

요청 URL과 HTTP Method를 보고 어떤 Handler(Controller 메서드)를 실행할지 결정한다. @GetMapping, @PostMapping 같은 애노테이션 정보를 분석해서 매핑 테이블을 구성한다. 기본 구현체는 RequestMappingHandlerMapping이다.

HandlerAdapter

선택된 Handler를 실제로 실행하는 역할이다. Handler 타입이 다양하기 때문에 Adapter 패턴으로 일관된 방식으로 실행한다. @RequestMapping 기반 Controller는 RequestMappingHandlerAdapter가 담당한다.

HandlerExceptionResolver

Handler 실행 중 발생한 예외를 처리하는 역할이다. @ExceptionHandler, @ControllerAdvice가 이 레이어에서 동작한다. 예외가 발생하면 DispatcherServlet이 이 컴포넌트에 위임해서 응답을 만든다.

ViewResolver

Controller가 View 이름(문자열)을 반환했을 때 실제 View 파일을 찾아서 렌더링하는 역할이다. REST API 개발에서는 @ResponseBody를 쓰기 때문에 ViewResolver가 동작하지 않는다. Thymeleaf, JSP 같은 서버사이드 렌더링을 쓸 때 관여한다.

MessageConverter

Java 객체 ↔ HTTP 메시지 바디(JSON 등) 변환을 담당한다. @ResponseBody가 있으면 반환 객체를 MessageConverter가 JSON으로 직렬화해서 응답 바디에 쓴다. 기본으로 MappingJackson2HttpMessageConverter가 등록되어 있다.


5. Spring Boot에서 어떻게 연결되는가

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) {
        // 기존 컨버터 유지하면서 추가
    }
}

6. 간단한 예제 코드

6-1. DispatcherServlet이 예외를 잡는 흐름

Controller에서 예외가 터지면 DispatcherServletHandlerExceptionResolver에 위임한다. @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", "서버 오류가 발생했습니다."));
    }
}

6-2. 요청 처리 전체 흐름 — Controller 레벨 코드

@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);
    }
}

6-3. DispatcherServlet 초기화 확인 (디버깅 팁)

application.yml에 아래 설정을 추가하면 DispatcherServlet이 초기화될 때 등록된 매핑 정보를 로그로 확인할 수 있다. 개발 중에 유용하다.

logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: DEBUG
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE

7. 자주 헷갈리는 부분

DispatcherServlet은 싱글턴이다

DispatcherServlet 인스턴스는 하나다. 그리고 멀티 스레드로 동시 요청을 처리한다. 인스턴스 변수에 상태를 저장하면 절대 안 된다. 요청 처리에 필요한 상태는 항상 메서드 파라미터 또는 HttpServletRequest 속성에 담아야 한다.

DispatcherServlet의 WebApplicationContext vs Root WebApplicationContext

Spring MVC 구조에서는 ApplicationContext가 두 개 존재할 수 있다.

  • Root WebApplicationContext — Service, Repository, 공통 빈들. 여러 Servlet이 공유
  • Servlet WebApplicationContext — Controller, HandlerMapping 등 MVC 관련 빈들. DispatcherServlet 전용

Spring Boot에서는 이 구분 없이 ApplicationContext 하나로 통합되어 있다. 직접 web.xml로 설정하던 시절의 구조라서, Spring Boot를 쓸 때는 크게 신경 쓰지 않아도 된다.

Filter에서 발생한 예외는 DispatcherServlet이 잡지 못한다

DispatcherServlet은 Filter 다음에 위치한다. Filter에서 예외가 발생하면 DispatcherServlet에 도달하지 못하기 때문에 @ControllerAdvice로 처리할 수 없다. Spring Security에서 인증 예외를 처리할 때 이 문제가 자주 등장한다. Filter 예외는 AuthenticationEntryPoint, AccessDeniedHandler로 처리하거나, 별도 예외 처리 Filter를 체인 앞에 추가하는 방식을 쓴다.


8. 실무에서 중요한 포인트

HandlerMapping 우선순위

HandlerMapping이 여러 개 등록되어 있을 수 있다. DispatcherServletorder 값 기준으로 순서대로 매핑을 시도하고, 가장 먼저 매핑에 성공한 핸들러를 사용한다.

실무에서 ResourceHttpRequestHandler(정적 파일 서빙)와 Controller 매핑이 충돌할 때 이 우선순위 개념이 등장한다.

NoHandlerFoundException 처리

요청 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 레벨에서 요청 처리 시간을 측정하고 싶다면 HandlerInterceptorpreHandle()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);
    }
}

9. 정리

  • DispatcherServletSpring MVC의 Front Controller로 모든 HTTP 요청의 진입점이다
  • 직접 처리하지 않고 HandlerMappingHandlerAdapter → Controller → MessageConverter 순으로 위임한다
  • 초기화 시 ApplicationContext에서 전략 컴포넌트들을 찾아 세팅하고, 없으면 기본값을 사용한다
  • Spring Boot는 DispatcherServletAutoConfiguration으로 자동 등록해준다
  • 예외 처리는 HandlerExceptionResolver에 위임하고, @ControllerAdvice가 이 위에서 동작한다
  • Filter에서 발생한 예외는 DispatcherServlet이 잡지 못한다 — 이 경계를 명확히 기억해야 한다
  • 커스터마이징은 WebMvcConfigurer로 한다. @EnableWebMvc는 Spring Boot에서 쓰지 않는다

10. 느낀 점

DispatcherServlet이 뭔지 모를 때는 그냥 "Spring이 알아서 해주는 거겠지" 하고 넘겼다. 근데 Filter 예외가 @ControllerAdvice에서 안 잡히는 문제를 마주쳤을 때, 결국 이 흐름을 모르면 원인을 못 찾는다는 걸 깨달았다.

Spring Boot는 많은 걸 자동으로 해주지만, 그 자동화가 어떻게 이루어지는지 알아야 문제가 생겼을 때 어디를 봐야 하는지 알 수 있다. DispatcherServlet은 그 흐름의 중심에 있는 클래스다.

profile
Develop

0개의 댓글