Spring DispatcherServlet 동작 원리

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, 반환값 처리 방식을 로그로 출력합니다.

관련 글

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux