doDispatch의 첫 결정은 "이 요청을 누가 처리하는가"다. HandlerMapping은 HTTP 요청 → 핸들러 객체 매핑을 책임진다. 여기서 "핸들러"는 의도적으로 Object다 — @RequestMapping 메서드(HandlerMethod)일 수도, 옛 Controller 빈일 수도, HandlerFunction일 수도, 심지어 다른 프레임워크의 객체일 수도 있다. 매핑은 핸들러를 고를 뿐, 실행 방법은 모른다(그건 HandlerAdapter의 몫, 03장).
핵심은 매핑이 **단순히 핸들러만이 아니라 인터셉터까지 묶은 HandlerExecutionChain**을 돌려준다는 것이다.
HandlerMapping (인터페이스) getHandler(request): HandlerExecutionChain
▲
AbstractHandlerMapping 인터셉터·CORS·기본핸들러·정렬 공통 처리
│ getHandler() = 템플릿 메서드, getHandlerInternal() = 추상
├──────────────────────────────┬───────────────────────────────┐
▼ ▼ ▼
AbstractUrlHandlerMapping AbstractHandlerMethodMapping<T> RouterFunctionMapping
│ URL→핸들러 빈 │ 메서드 단위 매핑 레지스트리 (06장: 함수형)
├ SimpleUrlHandlerMapping ▼
└ BeanNameUrlHandlerMapping RequestMappingInfoHandlerMapping
(디폴트 매핑) ▼
RequestMappingHandlerMapping ← @RequestMapping 의 주인공
소스: handler/AbstractHandlerMapping.java, handler/AbstractHandlerMethodMapping.java, mvc/method/RequestMappingInfoHandlerMapping.java, mvc/method/annotation/RequestMappingHandlerMapping.java
HandlerExecutionChain은 handler 하나 + HandlerInterceptor[]를 들고, applyPreHandle/applyPostHandle/triggerAfterCompletion 콜백을 제공한다(01장).
모든 매핑은 이 final 템플릿 메서드를 거친다. 서브클래스는 getHandlerInternal만 구현한다.
getHandler(request)
│ initApiVersion(request) API 버전 파싱(7.0+) → 요청 속성에 저장
│ handler = getHandlerInternal(request) ◀── 서브클래스 책임
│ handler == null → getDefaultHandler() (기본 핸들러 폴백)
│ handler == null → return null (이 매핑은 처리 못 함 → 다음 매핑 시도)
│
│ handler 가 String(빈 이름)이면 → 컨텍스트에서 실제 빈 조회
│ initLookupPath(request) 매칭/인터셉터용 lookupPath 캐시 보장
│
│ chain = getHandlerExecutionChain(handler, request)
│ ├ adaptedInterceptors 순회
│ ├ MappedInterceptor 면 matches(request) 인 것만 체인에 추가
│ └ 그 외 인터셉터는 무조건 추가
│
│ CORS 설정이 있으면(또는 preflight 요청이면)
│ → CorsInterceptor 를 체인 맨 앞에 끼움
▼ return chain
핵심 발췌:
Object handler = getHandlerInternal(request);
if (handler == null) handler = getDefaultHandler();
if (handler == null) return null; // 다음 HandlerMapping 으로
if (handler instanceof String handlerName)
handler = obtainApplicationContext().getBean(handlerName); // 지연 빈 조회
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = getCorsConfiguration(handler, request);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;이렇게 인터셉터 적용·CORS·기본 핸들러·빈 지연 조회가 전부 추상 기반에 모여 있고, 서브클래스는 "이 요청에 맞는 핸들러가 무엇인가"(getHandlerInternal)에만 집중한다.
setInterceptors(...)로 받은 인터셉터는 initApplicationContext 시점에 HandlerInterceptor로 정규화된다(WebRequestInterceptor면 WebRequestHandlerInterceptorAdapter로 감싼다). 또한 컨텍스트의 모든 MappedInterceptor 빈을 자동 탐지한다(detectMappedInterceptors). MappedInterceptor는 경로 패턴(include/exclude)을 들고 있어 matches(request)로 요청 경로에 맞을 때만 체인에 들어간다.
@RequestMapping처럼 매핑 단위가 "빈"이 아니라 "메서드"인 경우의 공통 기반이다. 제네릭 <T>가 매핑 조건 타입이며, RequestMappingHandlerMapping에서는 T = RequestMappingInfo다.
initHandlerMethods()
for 각 빈 이름 in 컨텍스트
beanType = 컨텍스트.getType(name)
isHandler(beanType)? ◀── @Controller 타입인가? (서브클래스)
detectHandlerMethods(beanName)
MethodIntrospector.selectMethods(userType, m -> getMappingForMethod(m, userType))
▲ 각 메서드의 @RequestMapping → RequestMappingInfo (null이면 핸들러 아님)
각 (method, mapping) 마다 registerHandlerMethod
→ MappingRegistry.register(mapping, handler, method)
MappingRegistry(내부 클래스)는 매핑을 여러 인덱스로 보관한다.
registry: Map<T, MappingRegistration>— 전체 매핑pathLookup: MultiValueMap<String, T>— 패턴이 아닌 직접 경로 → 매핑 (빠른 1차 조회)nameLookup— 매핑 이름(TC#getFoo) → HandlerMethod (리다이렉트 빌더용)corsLookup— HandlerMethod → CORS 설정ReentrantReadWriteLock— 런타임 동적 (un)register 와 조회의 동시성 보호
lookupHandlerMethod(lookupPath, request)
① directPathMatches = pathLookup.get(lookupPath) 직접 경로 후보 (빠른 길)
있으면 그것들만 매칭 시도
② 비었으면 → 등록된 전체 매핑을 매칭 시도 (느린 길: 패턴 매칭 포함)
각 매핑마다 getMatchingMapping(mapping, request) ◀── 조건 평가 (서브클래스)
→ 매치된 것만 Match 리스트에
③ 매치 0개 → handleNoMatch (404/405/415 판단)
④ 매치 ≥1 → getMappingComparator(request) 로 정렬, 0번이 best
best 와 2등이 동점이면 → IllegalStateException("Ambiguous handler methods")
⑤ BEST_MATCHING_HANDLER_ATTRIBUTE 등 요청 속성 채우고 best 반환
즉 2단계 조회다. URL이 정적 경로면 해시 조회 한 방으로 후보를 좁히고, 패턴(/users/{id})이 섞이면 전체를 평가한다. 여러 개가 매치되면 비교자로 "가장 구체적인" 매핑을 고르고, 우열을 가릴 수 없으면 모호성 예외를 던진다.
@RequestMapping을 실제 매핑으로 바꾸는 구체 클래스다.
isHandler(beanType)— 타입에@Controller(또는 그걸 메타로 단@RestController)가 있으면 핸들러 빈으로 본다.
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);getMappingForMethod(method, handlerType)— 메서드의@RequestMapping/@HttpExchange로 메서드 레벨RequestMappingInfo를 만들고, 타입 레벨 정보와combine으로 합친다(클래스@RequestMapping("/api")+ 메서드@GetMapping("/x")=/api/x). 매핑 애너테이션이 없으면null(= 핸들러 메서드 아님).
RequestMappingInfo는 여러 RequestCondition의 묶음이다. 각 조건은 요청에 대해 매치 여부를 판단하고 자기들끼리 우열을 비교할 수 있다.
RequestMappingInfo
├ PathPatternsRequestCondition 경로 패턴 (/users/{id})
├ RequestMethodsRequestCondition HTTP 메서드 (GET, POST)
├ ParamsRequestCondition 쿼리 파라미터 (?type=a)
├ HeadersRequestCondition 헤더
├ ConsumesRequestCondition Content-Type (consumes)
├ ProducesRequestCondition Accept (produces)
└ VersionRequestCondition API 버전 (7.0+)
소스: mvc/condition/. getMatchingMapping은 각 조건의 getMatchingCondition(request)를 호출해 "이 요청에 적용되는 부분만 남긴 새 RequestMappingInfo"를 만든다 — 모든 조건이 매치돼야 전체가 매치다. getMappingComparator는 경로 구체성 → 메서드 → 파라미터 ... 순으로 비교해 best를 정한다.
handleMatch(RequestMappingInfoHandlerMapping)는 매치 후 URI 템플릿 변수({id} → 실제 값), 매트릭스 변수, producible 미디어 타입을 요청 속성에 채워 03장의 인자 리졸버가 쓰게 한다. handleNoMatch는 "경로는 맞는데 메서드만 안 맞음" → 405, "consumes 안 맞음" → 415 같은 세분화된 예외를 던진다.
- 핸들러 =
Object: 매핑이 실행 방식을 모르게 하여 어떤 핸들러 모델도 끼울 수 있다.DispatcherServlet.properties의 기본 3종(BeanNameUrlHandlerMapping→RequestMappingHandlerMapping→RouterFunctionMapping)이 한 컨텍스트에 공존하며 순서대로 시도된다. - 템플릿 메서드 패턴 :
getHandler(공통 인프라) /getHandlerInternal(매핑 전략)의 분리. 인터셉터·CORS·기본 핸들러 같은 횡단 기능을 모든 매핑이 공짜로 얻는다. - PathPattern으로의 전환 : 7.0에서
AntPathMatcher/UrlPathHelper기반 String 매칭이 deprecated 되고, 파싱된PathPattern이 기본이다.usesPathPatterns()가true면DispatcherServlet이 경로를 요청당 한 번만 파싱해 공유한다(01장의parseRequestPath). - 런타임 등록 :
registerMapping/unregisterMapping이public이고ReadWriteLock으로 보호돼, 기동 후에도 핸들러를 동적으로 추가/제거할 수 있다. - CORS 통합 : 글로벌
CorsConfigurationSource+ 핸들러/메서드 레벨@CrossOrigin을combine하고, preflight(OPTIONS) 요청은 실제 핸들러를 찾되 no-op 핸들러로 응답하도록 체인을 구성한다.
HandlerMapping은 요청을 핸들러 객체로 연결하고, 그 핸들러에 적용될 인터셉터까지 묶어 HandlerExecutionChain으로 돌려준다. AbstractHandlerMapping이 인터셉터·CORS·정렬 같은 공통 인프라를, AbstractHandlerMethodMapping이 메서드 단위 매핑 레지스트리와 2단계 조회(직접 경로 → 패턴)를 제공하며, RequestMappingHandlerMapping이 @RequestMapping을 RequestMappingInfo(여러 조건의 묶음)로 변환해 최적 매치를 고른다. 선택된 핸들러는 03장의 HandlerAdapter에게 넘겨진다.