Skip to content

Latest commit

 

History

History
160 lines (122 loc) · 11 KB

File metadata and controls

160 lines (122 loc) · 11 KB

02. HandlerMapping — 요청을 핸들러로 연결하기

무엇을 / 왜

doDispatch의 첫 결정은 "이 요청을 누가 처리하는가"다. HandlerMappingHTTP 요청 → 핸들러 객체 매핑을 책임진다. 여기서 "핸들러"는 의도적으로 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

HandlerExecutionChainhandler 하나 + HandlerInterceptor[]를 들고, applyPreHandle/applyPostHandle/triggerAfterCompletion 콜백을 제공한다(01장).

동작 흐름 ①: AbstractHandlerMapping.getHandler — 공통 골격

모든 매핑은 이 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로 정규화된다(WebRequestInterceptorWebRequestHandlerInterceptorAdapter로 감싼다). 또한 컨텍스트의 모든 MappedInterceptor 빈을 자동 탐지한다(detectMappedInterceptors). MappedInterceptor는 경로 패턴(include/exclude)을 들고 있어 matches(request)요청 경로에 맞을 때만 체인에 들어간다.

동작 흐름 ②: AbstractHandlerMethodMapping — 메서드 단위 매핑

@RequestMapping처럼 매핑 단위가 "빈"이 아니라 "메서드"인 경우의 공통 기반이다. 제네릭 <T>가 매핑 조건 타입이며, RequestMappingHandlerMapping에서는 T = RequestMappingInfo다.

시작 시: 핸들러 메서드 스캔 (afterPropertiesSetinitHandlerMethods)

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 와 조회의 동시성 보호

요청 시: 최적 매치 선택 (getHandlerInternallookupHandlerMethod)

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})이 섞이면 전체를 평가한다. 여러 개가 매치되면 비교자로 "가장 구체적인" 매핑을 고르고, 우열을 가릴 수 없으면 모호성 예외를 던진다.

핵심 메서드 — RequestMappingHandlerMapping

@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 와 조건들

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종(BeanNameUrlHandlerMappingRequestMappingHandlerMappingRouterFunctionMapping)이 한 컨텍스트에 공존하며 순서대로 시도된다.
  • 템플릿 메서드 패턴 : getHandler(공통 인프라) / getHandlerInternal(매핑 전략)의 분리. 인터셉터·CORS·기본 핸들러 같은 횡단 기능을 모든 매핑이 공짜로 얻는다.
  • PathPattern으로의 전환 : 7.0에서 AntPathMatcher/UrlPathHelper 기반 String 매칭이 deprecated 되고, 파싱된 PathPattern이 기본이다. usesPathPatterns()trueDispatcherServlet이 경로를 요청당 한 번만 파싱해 공유한다(01장의 parseRequestPath).
  • 런타임 등록 : registerMapping/unregisterMappingpublic이고 ReadWriteLock으로 보호돼, 기동 후에도 핸들러를 동적으로 추가/제거할 수 있다.
  • CORS 통합 : 글로벌 CorsConfigurationSource + 핸들러/메서드 레벨 @CrossOrigincombine하고, preflight(OPTIONS) 요청은 실제 핸들러를 찾되 no-op 핸들러로 응답하도록 체인을 구성한다.

정리

HandlerMapping은 요청을 핸들러 객체로 연결하고, 그 핸들러에 적용될 인터셉터까지 묶어 HandlerExecutionChain으로 돌려준다. AbstractHandlerMapping이 인터셉터·CORS·정렬 같은 공통 인프라를, AbstractHandlerMethodMapping이 메서드 단위 매핑 레지스트리와 2단계 조회(직접 경로 → 패턴)를 제공하며, RequestMappingHandlerMapping@RequestMappingRequestMappingInfo(여러 조건의 묶음)로 변환해 최적 매치를 고른다. 선택된 핸들러는 03장의 HandlerAdapter에게 넘겨진다.