티스토리 뷰

반응형

DispatcherServlet과 그 내부를 살펴보며 어떤 순서로 요청이 처리되는지, 그리고 어디서 어떻게 최적의 핸들러를 탐색하는지 알아보았다. 내부 구현은 Spring의 버전에 따라 달라지므로 세부 코드보다는 클래스 간 흐름이 어떤 식으로 이루어지는지 보는 것을 추천한다. 설명에 사용된 Spring 버전은 6.0.7이다.


DispatcherServlet의 처리 순서

 

DispatcherServlet 은 우리가 만들어놓은 Handler(컨트롤러), Interceptor, 에 대한 정보를 리스트의 형태로 가지고 있다.

ApplicationContext가 완성된 뒤 HandlerMapping 등 각 단계에 필요한 빈 타입을 순차적으로 찾아 등록한다.

// org.springframework.web.servlet의 DispatcherServlet, Spring 6.0.7
private void initHandlerMappings(ApplicationContext context) {
   this.handlerMappings = null;

   if (this.detectAllHandlerMappings) {
      // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
      Map<String, HandlerMapping> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
      if (!matchingBeans.isEmpty()) {
         this.handlerMappings = new ArrayList<>(matchingBeans.values());
         // We keep HandlerMappings in sorted order.
         AnnotationAwareOrderComparator.sort(this.handlerMappings);
      }
   }
   . . .
}

 

각 요청(HttpServletRequest)이 외부에서 들어오면 그 처리는 DispatcherServlet의 doService()와 doDispatch()에서 일어난다. 이 중 doDispatch()에서 Spring MVC의 주요한 개념들에 대한 처리들을 담당한다.

 

  1. DispatcherServlet이 가진 HandlerMapping의 리스트를 순회, 요청을 처리할 수 있는 HandlerMapping을 찾는다.
  2. 해당 HandlerMapping을 통해 Handler와 Interceptor의 리스트가 포함된 HandlerExecutionChain을 얻는다.
  3. HandlerMapping에서 Handler를 가져오고, DispatcherServlet이 가진 HandlerAdaptor 중 해당 Handler의 응답 처리가 가능한 HandlerAdaptor를 선택한다.
  4. ExecutionChain에 포함된 Interceptor들의 preHandle() 처리를 요청, preHandle()을 통해 요청이 모두 처리되었다 판단되면 doDispatch()를 종료한다.
  5. Handler와 HandlerAdaptor의 실질적인 기능을 실행시키고, 응답으로 ModelAndView를 얻는다.
  6. ExecutionChain의 Interceptor들의 postHandle() 처리를 요청한다.
  7. 결과인 ModelAndView를 렌더링 하거나 발생한 예외를 처리하는 processDispatchResult() 메서드를 호출한다
  8. 멀티파트 요청의 경우 사용한 자원을 정리한다.

 

 

HandlerMapping은 어떤 우선순위로 핸들러를 선택하는가?

 

요청에 대해 처리가 가능한지는 어디서 어떻게 판단하는 걸까?

DispatcherServlet에서 사용하는 HandlerMapping은 인터페이스이며 요청을 받아 처리가 가능한 Handler, Interceptor들을 포함한 HandlerExecutionChain을 반환하기만 한다.

 

@Nullable
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

 

HandlerMapping의 구현체는 매우 많으며 이 중 RequestMappingHandlerMapping이 가장 일반적으로 쓰인다.

SimpleUrl, BeanNameUrl, DefaultAnnotationHandlerMapping 등의 구현체가 있으나 ReqestMapping으로 탐색된 핸들러가 가장 우선순위가 높게 처리된다. (내부의 필드로 Order를 가지며, Order가 낮은 HandlerMapping이 우선시 된다.)

 

 

RequestMappingHandlerMapping은 어노테이션 기반의 컨트롤러에 대한 매핑을 처리하는 클래스이다.

 

RequestMappingHandlerMapping - HandlerMapping, Spring 6.0.7

 

여러 추상화 단계 중 AbstractHandlerMapping 클래스에서 getHandler()를 구현하며, 주요 기능인 getHandlerInternal() 메서드는 다시 AbstractHandlerMethodMapping에서 구현된다.

 

AbstractHandlerMethodMapping의 lookupHandlerMethod() 메서드는 요청에 대한 최적의 핸들러 메서드를 가져온다.

 

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<>();
    List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
    if (directPathMatches != null) {
       addMatchingMappings(directPathMatches, matches, request);
    }
    if (matches.isEmpty()) {
       addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
    }
    if (!matches.isEmpty()) {
    	if (!matches.isEmpty()) {
		Match bestMatch = matches.get(0);
		if (matches.size() > 1) {
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			matches.sort(comparator);
			bestMatch = matches.get(0);
    		. . .
    }
}

 

addMatchMapping()에서 실행되는 getMatchingMapping()의 구현은 RequestMappingHandlerMapping에 존재하며,

이 메서드의 입력인 RequestMappingInfo를 살펴보면 비로소 실질적인 우선순위가 어떻게 관리되는지 볼 수 있다.

 

 

RequestMappingInfo

 

RequestMappingInfo의 compareTo() 메서드를 보면 실제 우선순위가 어떻게 결정되는지 알 수 있다.

최종적으로는 아래의 우선순위로 여러 핸들러들을 판단, 가장 우선순위가 높은 것을 선택한다.

  1. URL Path를 기반으로 최대한 일치하는 핸들러를 우선한다.
  2. HTTP 메서드가 매칭되는 핸들러를 우선한다.
  3. 요청 파라미터가 일치하는 핸들러를 우선한다
  4. 헤더가 일치하는 핸들러를 우선한다
  5. 요청의 Media-Type이 일치하는 핸들러를 우선한다.
  6. 응답의 Content-Type이 일치하는 핸들러를 우선한다.
  7. 이외에 사용자가 설정한 조건이 일치하는 핸들러를 우선한다.

 

조금 더 살펴보면 RequestMappingInfo는 필드로 Http 요청의 Method, Path 등에 대한 조건들을 모두 들고 있으며 각각의 조건들은 TreeSet을 통해 우선순위를 관리하는 것을 볼 수 있다.

 

public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
	
    . . .
    @Nullable
    private final PathPatternsRequestCondition pathPatternsCondition;

    @Nullable
    private final PatternsRequestCondition patternsCondition;

    private final RequestMethodsRequestCondition methodsCondition;
    . . .
}

 

RequestMappingInfo의 getMatchingCondition()
Checks if all conditions in this request mapping info match the provided request and returns a potentially new request mapping info with conditions tailored to the current request.
For example the returned instance may contain the subset of URL patterns that match to the current request, sorted with best matching patterns on top.
Returns: a new instance in case of a match; or null otherwise

 

public final class PathPatternsRequestCondition extends AbstractRequestCondition<PathPatternsRequestCondition> {

   private static final SortedSet<PathPattern> EMPTY_PATH_PATTERN =
         new TreeSet<>(Collections.singleton(new PathPatternParser().parse("")));
     . . .
}

 

 

여러 컨트롤러의 메서드 중 스프링이 어떤 조건을 우선시하여 핸들러를 선택하는지 찾을 수 있었다.

다른 버전의 Spring은 세부 구현 내역이 추상화 계층 간에 이동되는 경우가 있으므로 필요하다면 주요 클래스들 바탕으로 찾아보자.

 

반응형