Deep Dive /Java Web /9

9. DispatcherServlet → HandlerMapping: getHandler()

DispatcherServlet의 doDispatch() 안에서 어떤 컨트롤러를 실행할지 결정하는 첫 번째 핵심 단계가 getHandler() 입니다. 이 단계는 단순히 컨트롤러 하나를 찾는 것이 아니라, 등록된 여러 HandlerMapping 중에서 현재 요청을 처리할 수 있는 매핑 전략을 순서대로 탐색하고, 그 결과를 HandlerExecutionChain 으로 감싸 반환합니다.


전체 흐름 요약

  1. DispatcherServlet 초기화 시점에 HandlerMapping 빈들을 수집한다.
  2. 수집된 HandlerMapping 들은 정렬되어 우선순위를 가진다.
  3. 요청이 들어오면 DispatcherServlet.getHandler() 가 목록을 순서대로 순회한다.
  4. 가장 먼저 매칭된 HandlerMappingHandlerExecutionChain 을 반환한다.
  5. 이 체인 안에는 실제 핸들러와 함께 실행할 인터셉터 목록이 들어 있다.

실제 코드 흐름 분석

1) DispatcherServlet 초기화 시 HandlerMapping 목록 준비

// DispatcherServlet.java
private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;

    if (this.detectAllHandlerMappings) {
        Map<String, HandlerMapping> matchingBeans =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }

    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
    }
}

핵심은 HandlerMapping 이 하나가 아니라 List<HandlerMapping> 이라는 점입니다. 그리고 Spring은 이 목록을 정렬해 우선순위 순서대로 유지합니다.

2) 실제 요청 시 getHandler()가 모든 HandlerMapping을 순회

// DispatcherServlet.java
protected @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

이 메서드는 매우 단순하지만 설계적으로 중요합니다.

  • 모든 HandlerMapping 을 순서대로 확인한다.
  • 각 매핑 전략은 현재 요청을 처리할 수 있으면 HandlerExecutionChain 을 반환한다.
  • 가장 먼저 null 이 아닌 값을 반환한 매핑이 승리한다.
  • 끝까지 못 찾으면 null 이고, 이후 noHandlerFound() 흐름으로 간다.

3) HandlerMapping의 역할은 “요청 → 핸들러 체인” 매핑

// HandlerMapping.java
public interface HandlerMapping {

    @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

HandlerMapping 인터페이스의 핵심은 “현재 요청을 처리할 수 있는 핸들러와 인터셉터 묶음”을 찾는 것입니다.

주석에도 이런 의도가 드러납니다.

  • Spring은 BeanNameUrlHandlerMapping, RequestMappingHandlerMapping 같은 여러 구현체를 제공한다.
  • 구현체는 Ordered 를 통해 우선순위를 가질 수 있다.
  • 반환값은 단순 handler가 아니라 HandlerExecutionChain 이다.

4) 반환값이 handler가 아니라 HandlerExecutionChain인 이유

// HandlerExecutionChain.java
public class HandlerExecutionChain {

    private final Object handler;

    private final List<HandlerInterceptor> interceptorList = new ArrayList<>();

    public Object getHandler() {
        return this.handler;
    }

    public List<HandlerInterceptor> getInterceptorList() {
        return (!this.interceptorList.isEmpty() ? Collections.unmodifiableList(this.interceptorList) :
                Collections.emptyList());
    }

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        for (int i = 0; i < this.interceptorList.size(); i++) {
            HandlerInterceptor interceptor = this.interceptorList.get(i);
            if (!interceptor.preHandle(request, response, this.handler)) {
                triggerAfterCompletion(request, response, null);
                return false;
            }
            this.interceptorIndex = i;
        }
        return true;
    }
}

즉 Step 9에서 DispatcherServlet이 받는 것은 단순한 컨트롤러 메서드가 아닙니다.

  • 실제 실행 대상 handler
  • 그 앞뒤에서 동작할 interceptor 목록

이 둘을 하나로 묶은 실행 단위가 HandlerExecutionChain 입니다.


왜 이렇게 설계했을까?

1) HandlerMapping을 하나로 고정하지 않기 위해

Spring MVC는 요청 매핑 전략이 하나가 아닙니다.

  • @RequestMapping 기반 매핑
  • 빈 이름 기반 URL 매핑
  • 정적 리소스 매핑
  • 사용자 정의 매핑

이런 여러 전략을 동시에 등록하고, 우선순위에 따라 선택할 수 있어야 합니다. 그래서 DispatcherServlet 은 특정 구현체 하나를 알지 않고 List<HandlerMapping> 만 순회합니다.

2) 인터셉터 적용을 매핑 결과에 포함하기 위해

DispatcherServlet 입장에서 필요한 것은 단순히 “누가 처리하느냐” 만이 아닙니다. 실제로는:

  1. 어떤 핸들러가 실행되는가?
  2. 그 전에 어떤 인터셉터들이 실행되는가?
  3. 이후 어떤 postHandle/afterCompletion 이 붙는가?

그래서 반환값이 handler 가 아니라 HandlerExecutionChain 인 것입니다.

3) first match wins 구조를 만들기 위해

getHandler() 구현을 보면 첫 번째 매칭 결과를 즉시 반환합니다.

for (HandlerMapping mapping : this.handlerMappings) {
    HandlerExecutionChain handler = mapping.getHandler(request);
    if (handler != null) {
        return handler;
    }
}

이 구조 덕분에 Spring MVC는 여러 매핑 전략을 조합하면서도 충돌 시 우선순위를 명확히 유지할 수 있습니다.


시퀀스 다이어그램

sequenceDiagram participant DispatcherServlet participant HM1 as HandlerMapping #1 participant HM2 as HandlerMapping #2 participant Chain as HandlerExecutionChain DispatcherServlet->>HM1: getHandler(request) HM1-->>DispatcherServlet: null DispatcherServlet->>HM2: getHandler(request) HM2-->>Chain: handler + interceptors 생성 Chain-->>DispatcherServlet: HandlerExecutionChain

여기서 꼭 잡아야 할 포인트

  • DispatcherServlet 은 컨트롤러를 직접 찾지 않는다.
  • 실제 탐색 책임은 각 HandlerMapping 구현체에 있다.
  • Step 9의 결과물은 handler 하나가 아니라 HandlerExecutionChain 이다.
  • Step 11에서 preHandle() 이 바로 실행될 수 있는 이유도, Step 9에서 이미 인터셉터 목록이 같이 반환되었기 때문이다.

탐구 질문

  1. 왜 Spring은 HandlerMapping 을 하나의 구현으로 고정하지 않고 List 로 관리했을까요?
  2. 왜 반환 타입이 단순 Object handler 가 아니라 HandlerExecutionChain 일까요?
  3. 만약 RequestMappingHandlerMapping 이 매칭되기 전에 다른 HandlerMapping 이 먼저 매칭된다면, 전체 요청 흐름은 어떻게 달라질까요?

← 이전: 8. FrameworkServlet → DispatcherServlet | 다음: 10. HandlerMapping → DispatcherServlet