ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot - HandlerMapping의 동작 방식
    Framework & Library/Spring Boot 2023. 1. 30. 14:49

    HandlerMapping의 동작방식

    HandlerMapping의 역할

    HandlerMapping은 클라이언트로부터 받은 요청을 처리하기 위해 적절한 handler를 찾아온다는 것을 들어보았을 것이다. 구체적으로 HandlerMapping은 사용자 요청의 URL과 매칭되는 handler를 선택하는 역할을 수행한다. 이번 게시글에서는 HandlerMapping이 어떠한 방식으로 요청에 대한 handler를 찾는지 알아보겠다.

     

    Spring MVC의 요청 처리 흐름

    HandlerMapping의 역할을 살펴보기 전에, Spring MVC에서 요청이 어떠한 순서로 처리되는지 알아보겠다.

     

     

    1. 우선, front-controller의 역할을 하는 DispatcherServlet이 요청을 받는다.

    2. DispatcherServlet은 적절한 Controller를 선택하는 일을 HandlerMapping에게 요청한다.

    3. HandlerMapping은 적합한 Controller를 선택한다.

    4. DispatcherServlet은 선택된 Controller의 비즈니스 로직 실행 작업을 HandlerAdapter에게 위임한다.

    5. HandlerAdapterController의 비즈니스 로직을 호출하고, 결과를 ModelAndView 객체에 담아서 DispathcerServlet에게 넘겨준다.

    6. DispatcherServletViewResolver를 이용하여 결과를 보여줄 View를 가져온다.

    7. View 객체에게 DispathcerServlet이 응답 결과 생성을 요청한다.

     

    위와 같은 과정 속에서 이번 게시글에서 살펴볼 과정은 2, 3번이다.

     

    DispatcherServlet이란?
    public abstract class HttpServlet extends GenericServlet
    				↑
    public abstract class HttpServletBean extends HttpServlet
    				↑
    public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware
    				↑
    public class DispatcherServlet extends FrameworkServlet

    DispatcherServlet의 상속구조를 살펴보면 위와 같다. 상속구조를 보면 알 수 있듯이, DispatcherServlet은 결국 HttpServlet을 상속함을 알 수 있다. 따라서, DispatcherServletServlet의 생명주기와 비슷하게 흘러감을 알 수 있다.

     

    1. DispatcherServlet 클래스

    public class DispatcherServlet extends FrameworkServlet {
        ...
        
        protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
            try {
                doDispatch(request, response);
            }
        }
        
        ...
    }

    DispatcherServlet 클래스를 살펴보면 위 코드와 같이 doService() 메서드가 호출된다. 그 후, doService() 메서드 내에 있는 doDispatch() 메서드를 호출한다.

     

    Process the actual dispatching to the handler. The handler will be obtained by applying the servlet's HandlerMappings in order. The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters to find the first that supports the handler class.

    doDispatch() 메서드의 javadoc을 보면, Servlet의 HandlerMapping을 순서대로 처리하여 handler를 가져온다고 되어있다.

     

    2. doDispatch() 메서드

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        
        ...
        
        try {
            mappedHandler = getHandler(processedRequest);
        }
        
        ...
    }

    doDispatch() 메서드의 실제 코드를 보면 위와 같이 요청에 대한 handler를 가져오는  getHandler() 메서드를 호출하고 있다.

     

    3. getHandler() 메서드

    @Nullable
    protected 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;
    }

    위 코드는 DispatcherServlet 클래스 내에 존재하는 getHandler() 메서드이다. DispatcherServlet은 처음 초기화되는 과정에서 여러 가지 HandlerMapping들을 등록하고 handlerMappings라는 이름의 List를 통해 관리하고 있다.

     

    if (this.handlerMappings != null)

    코드를 하나씩 분석해 보자면, 위 코드는 DispatcherServlet 안에 HandlerMapping들이 등록되어 있는지 확인한다.

     

    for (HandlerMapping mapping : this.handlerMappings)

    HandlerMapping이 등록되어 있다면, 등록되어 있는 HandlerMapping들을 순회하는 작업을 수행한다.

     

    HandlerExecutionChain handler = mapping.getHandler(request);
    if (handler != null)
        return handler;

    HandlerMapping으로부터 요청에 맞는 handler를 가져오고, handler를 정상적으로 가져왔다면 해당 handler를 반환하는 것이다.

     

    HandlerMapping이 handler를 가져오는 과정
    public interface HandlerMapping {
        HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
    }

    HandlerMapping은 위와 같이 함수의 선언부만 가지고 있는 인터페이스이다.

    실제로 handler를 가져오는 getHandler() 메서드는 추상 클래스인 AbstractHandlerMapping에 정의되어 있다. 우리가 흔히 아는 RequestMappingHandlerMapping, SimpleUrlHandlerMapping과 같은 것들의 부모가 AbstractHandlerMapping이다.

     

    public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        Object handler = getHandlerInternal(request);
        
        ...
        
        HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
        
        ...
        
        return executionChain;
    }

    위 코드는 AbstractHandlerMappinggetHandler() 메서드이다. getHandlerInternal() 메서드를 통해서 handler를 찾아오고, HandlerExecutionChain을 반환한다. HadnlerExecutionChain은 handler와 handler interceptor들을 모아놓은 것이라고 보면 된다.

    우리가 관심 있는 것은 handler를 찾아오는 방식이기 때문에, getHandlerInternal() 메서드를 살펴보겠다.

     

     

    getHandlerInternal() 메서드는 AbstractHandlerMapping 클래스를 상속한 AbstractHandlerMethodMapping 클래스에 정의되어 있다. 위 사진은 AbstractHandlerMethodMapping 클래스를 상속하는 클래스들에 대한 구조를 표현한 것이다.

     

    public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping {
        ...
        
        protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
            String lookupPath = initLookupPath(request);
            this.mappingRegistry.acquireReadLock();
            try {
                HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
                return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
            }
            finally {
                this.mappingRegistry.releaseReadLock();
            }
        }
        
        ...
    }

    위 코드는 AbstractHandlerMethodMapping 클래스 내에 정의된 getHandlerInternal() 메서드의 코드이다.

     

    Look up a handler method for the given request.

    getHandlerInternal() 메서드에 대한 javadoc을 살펴보면, 위와 같이 주어진 요청에 대한 handler method를 찾는다고 되어있다. 즉, 어떠한 방식으로 요청에 대한 handler를 찾는지에 대한 핵심적인 부분인 것이다.

     

    String lookupPath = initLookupPath(request);
    this.mappingRegistry.acquireReadLock();

    코드를 좀 더 자세히 살펴보겠다. lookupPath 변수는 현재 Servlet Mapping 안에서의 검색 경로인데, 요청을 분석해서 얻을 수 있다. 그리고 maapingRegistry에 대한 ReadLock을 가져오는 것을 확인할 수 있다.

     

    try {
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }

    lookupPathrequest를 인자로 받는 lookupHandlerMethod() 메서드를 통해서 요청에 대한 handlerMethod를 가져온 후 반환한다. 이 handlerMethod가 바로 우리가 직접 Controller 안에 정의한 메서드인 것이다.

     

     

    사용자 요청 URL과 매핑되는 handler를 가져오는 과정을 그림으로 표현해 본다면 위와 같다. DispatcherServlet 함수 안에서 handlerMapping이 여러 과정을 거쳐서 적절한 handlerMethod를 가져온다는 것을 알 수 있다.

     

    MappingRegistry이란?

    MappingRegistry는 바로 이전에서 살펴본 AbstractHandlerMethodMapping의 내부 클래스이다. MappingRegistry handlerMethod에 대한 모든 mapping을 유지 및 관리한다.

     

    class MappingRegistry {
    
        private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
    
        private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();
    
        private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
    
        private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
    
        private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
        ...
    }

    MappingRegistry 클래스 안에서는 Map 자료구조를 가진 멤버 변수들이 존재한다. 그중에서 LinkedMultiValueMap이라는 자료구조는 하나의 key에 여러 value들을 저장하는 MultiValueMap LinkedHashMap으로 감싼 자료구조이다.

     

    pathLookup() 메서드는 LinkedMultiValueMap 자료구조이며, key url을 가지고 value RequestMappingInfo를 가진다. LinkedMultiValueMap을 쓰는 이유는 하나의 url에 여러 handlerMethod들에 대한 정보가 담기기 때문이다.

     

    key : "/app/user/"
    value : [GET /app/user, POST /app/user]

    예를 들어, /app/user/라는 url에 user에 대한 정보를 조회하는 GET, user를 추가하는 POST가 매핑될 때, 위와 같은 RequestMappingInfo가 들어가는 것이다. 위와 같은 구조를 통해 MappingRegistry url에 해당하는 handlerMethod를 구별할 수 있게 된다.

     

    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()) {
            if (matches.size() > 1) {
                matches.sort(comparator);
                bestMatch = matches.get(0);
                    
                ...
            }
            
            request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
            handleMatch(bestMatch.mapping, lookupPath, request);
            return bestMatch.getHandlerMethod();
        }
    }

    위 코드는 이전에서 잠깐 언급된 lookupHandlerMethod() 메서드이다. 적절한 handlerMethod를 가져온 후 반환하는 역할을 한다.

     

    List<Match> matches = new ArrayList<>();

    코드를 좀 더 구체적으로 살펴보겠다. 우선, Match를 담는 matches라는 리스트가 있다.

     

    private class Match {
        private final T mapping;
        
        private final HandlerMethod handlerMethod;
    }

    Match에 대한 정보는 위와 같다.

     

    List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);

    현재 url에 매핑되는 handler method들의 RequestMappingInfo들을 directPathMatches 변수에 저장하는 부분이다. 예를 들어 url /app/user라면, directPathMatches 변수에는 [GET /app/user, POST /app/user]와 같은 정보가 들어갈 것이다.

     

    if (directPathMatches != null) {
        addMatchingMappings(directPathMatches, matches, request);
    }

    그 후, [GET /app/user, POST /app/user] 중에서 request 정보와 일치하는 것들을 addMatchingMappings() 메서드를 통해서 matches에 추가한다.

     

    if (matches.size() > 1) {
        matches.sort(comparator);
        bestMatch = matches.get(0);
                    
        ...
    }

    mathces에 저장된 정보가 2건 이상일 경우 우선순위에 맞게 정렬 작업을 진행하고, 요청과 가장 일치하는 첫 번째 Match bestMatch 변수에 저장한다.

     

    request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
    handleMatch(bestMatch.mapping, lookupPath, request);
    return bestMatch.getHandlerMethod();

    bestMatch의 멤버 메서드인 getHandlerMethod()를 통해 최종적으로 적합한 handlerMethod를 찾게 된다.


    출처

     http://becomeweasel.tistory.com/entry/Spring-MVC-HandlerMapping%EC%9D%98-%EB%8F%99%EC%9E%91%EB%B0%A9%EC%8B%9D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-1%ED%8E%B8

     http://becomeweasel.tistory.com/entry/Spring-MVC-HandlerMapping%EC%9D%98-%EB%8F%99%EC%9E%91%EB%B0%A9%EC%8B%9D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-2%ED%8E%B8

     

    728x90

    댓글

Designed by Tistory.