Language & Framework/Spring Boot

[Spring Boot] Spring의 전체 흐름 알아보기 - 5. DispatcherServlet 내부의 HandlerMapping 과 HandlerAdapter 동작 원리

코딩 기록하는 애기 개발자 2026. 4. 26. 18:58

목차

     

    앞선 글들에서 정리했다시피 DispatcherServlet에서 요청이 전달되는 흐름을 따라가다 보면 자연스럽게 ``HandlerMapping``과 ``HandlerAdapter``라는 개념을 만나게 된다. 

     

    이전 글에서는 전체 요청 흐름을 중심으로 살펴보았기 때문에 DispatcherServlet 내 코드 역시 단순히 "Handler를 실행하기 위한 Adapter를 찾는 과정" 정도로 이해하고 넘어갔을 수 있다. 

     

    실젤 ``DispatcherServlet`` 내부에서의 흐름을 살펴보면, 

    `doService` 메서드 내에서 `doDispatch()` 라는 메서드를 호출하게된다. 

     

    `doDisptach()` 메서드 내 흐름은 다음과 같다.

    1.  실행할 Handler를 찾는다.
      클라이언트 요청이 들어오면 먼저 어떤 Controller가 요청을 처리할지 찾아야 한다.
      Spring은 요청된 URL과 Controller의 매핑 정보를 비교해 등록되어 있는 Controller 목록을 탐색한다. 이후, 매칭되는 Controller를 찾아 DispatcherServlet에게 반환하는데, 이 과정을 HandlerMapping이라고 한다. 
    2. Interceptor 의 메서드 중 `preHandle()` 메서드를 실행한다.
      Controller 가 실행되기 전에 실행된다. 보통 로그인 체크, 권한 검사, 요청 로깅, 인증 여부 판단 등을 처리한다. `preHandle()` 메서드를 통해 ``boolean``값을 반환하는데, 이를 기반으로 Controller를 실행할지 말지 판단된다.
    3. Handler를 실행할 HandlerAdapter 를 찾는다.
      이전 단계에서 반환된 Controller를 실행하기 위한 HandlerAdapter를 찾는다. 
    4. Controller를 실행하고, 응답을 생성한다. 
      위 단계에서 ``true``가 반환되면, 실제 비즈니스 로직이 실행되는 단게인 Controller가 실행된다. Controller가 성공적으로 실행되면, `ModelAndView` 형태로 응답이 생성된다.
    5. Interceptor 의 메서드 중 `postHandle()` 메서드를 실행한다.
      Controller 실행 후 View가 렌더링되기 전에 호출되며,페이지 공통 정보를 세팅하거나 Model 데이터를 추가하는 등을 수행한다. 
    6. Controller가 반환한 View 이름을 통해 ViewResolver를 조회해 렌더링 하여 HTML을 생성한다. 
    7. Interceptor 의 메서드 중 `afterCompletion()` 를 실행한다. 
      요청이 완전히 끝난 뒤, 리소스 정리, 예외 로깅 등을 처리하기 위해 사용된다. 

     

    위 흐름 중 눈에 띄는 부분은 

    Handler를 찾은 뒤 곧바로 실행하지 않고, 반드시 HandlerAdapter를 찾는 과정이 존재한다는 점이다. 

     

    단순히 Controller를 실행하는 것이라면 바로 호출해도 될 것 같지만,

    Spring MVC는 DispatcherServlet을 이용해 HandlerAdapter를 찾고, 이를 이용해 Handler를 실행한다. 

     

    그래서 이번 글에는 이 부분을 중심으로, HandlerAdapter는 무엇인지, 또 왜 필요한지 정리해보려 한다. 

     


     

    HandlerAdapter란 ?

    Spring MVC에서 요청이 들어오면,

    DispatcherServlet은 어떤 Handler가 실행되어야 하는지 먼저 찾고, 그 Handler를 실제로 실행해야 한다. 

     

    하지만 DispatcherServlet은 Handler를 직접 실행하지 않는다.

    그 이유는 Spring MVC는 하나의 Controller 방식만 지원하지 않기 때문이다.

     

    하나의 Controller 방식만 지원하지 않는다 ?  그래서 HandleAdapter가 필요하다고 ? 

    Spring MVC를 사용하면서 대부분 `@Controller`, `@RestController`, `@GetMapping`과 같은 방식만 사용하다 보니,

    "Contoroller 방식이 여러 개 존재한다"는 말이 처음에는 낯설게 느껴졌다.

     

    실제로 우리가 평소 개발을 할 때는 거의 하나의 방식만 사용하기 때문에 

    ' Controller 에도 여러 종류가 있었나 ? ' 라는 생각이 자연스럽게 들었던 것 같다. 

     

    하지만 과거에는 특정 인터페이스를 구현해 요청을 처리하는 방식도 존재했다. 

    예를 들어 `handleRequest()` 메서드를 직접 구현하는 Controller 인터페이스 기반 구조나, 서블릿과 유사하게 HTTP 요청과 응답 객체를 직접 다루는 방식도 사용되었다. 

     

    즉, Spring MVC 내부에서 Handler는 단순히 우리가 흔히 사용하는 Controller 클래스만을 의미하지 않는다. 요청을 처리할 수 있는 객체라면 모두 Handler가 될 수 있으며, Spring은 이러한 다양한 구조를 동일한 요청 처리 흐름 안에서 동작할 수 있도록 지원한다.

     

    그래서 DispatcherServlet 은 Handler를 직접 실행하지 않고,

    각 Handler 유형에 맞는 실행 방식을 연결해주는 HandlerAdapter 를 통해 요청을 처리하게 된다.

     

    실제 코드 (HandlerMapping)

    실제 코드를 보면서 이해해보자 .

    아래 코드는 실제 `doDispatch()` 메서드 내부에서의 중요 로직부 중 HandlerMapping 단계에 해당하는 로직부이다. 

    // Determine handler for the current request.
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }

     

    위 코드를 자세히 살펴보면, 가장 먼저 실행되는 메서드는 `getHandler()`이다.

    이 메서드는 현재 요청을 처리할 수 있는 Handler를 찾는 역할을 수행한다. 

     

    `getHandler()` 메서드 

    DispatcherServlet 의 `getHandler()`

    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들을 조회해 해당 요청을 처리할 수 있는 Handler를 찾는다. 

     

    HandlerMapping의 `getHandler()`

    @Override
    public final @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        ApiVersionHolder versionHolder = initApiVersion(request);
        Object handler = getHandlerInternal(request);
        if (handler == null) {
            handler = getDefaultHandler();
        }
        if (handler == null) {
            return null;
        }
    
        if (versionHolder.hasError() && !request.getDispatcherType().equals(DispatcherType.ERROR)) {
            throw versionHolder.getError();
        }
    
        // Bean name or resolved handler?
        if (handler instanceof String handlerName) {
            handler = obtainApplicationContext().getBean(handlerName);
        }
    
        // Ensure presence of cached lookupPath for interceptors and others
        if (!ServletRequestPathUtils.hasCachedPath(request)) {
            initLookupPath(request);
        }
    
        HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
    
        if (request.getAttribute(SUPPRESS_LOGGING_ATTRIBUTE) == null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Mapped to " + handler);
            }
            else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
                logger.debug("Mapped to " + executionChain.getHandler());
            }
        }
    
        if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
            CorsConfiguration config = getCorsConfiguration(handler, request);
            if (getCorsConfigurationSource() != null) {
                CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
                config = (globalConfig != null ? globalConfig.combine(config) : config);
            }
            if (config != null) {
                config.validateAllowCredentials();
                config.validateAllowPrivateNetwork();
            }
            executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
        }
    
        return executionChain;
    }

    위 메서드에서는 현재 요청을 처리할 Handler를 찾고 실행에 필요한 정보를 구성하는 역할을 한다.

     

    내부적으로 `getHandlerInternal()`을 호출해 요청 URL과 매핑 정보를 기반으로 Handler를 탐색하며, 매칭된 Handler가 없으면 기본 Handler를 확인한다. 

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

     

    `getHandlerInternal()` 메서드 내부를 살펴보면, 실제로 요청 URL을 기반으로 Handler를 탐색하는 과정이 이루어진다.
    이 과정에서 현재 요청 경로를 추출하고, Spring이 애플리케이션을 시작하는 시점에 따라 미리 저장해둔 매핑 정보와 비교하여 일치하는 Handler를 찾는다. 

     

    탐색 과정은 다음과 같이 이루어진다.

    1. `@RequestMapping`이 선언된 Controller와 메서드를 분석한다.
    2. 애플리케이션 초기화 과정에서 URL, HTTP Method, 파라미터 조건 등의 매핑 정보를 생성한다. 
    3. 생성된 매핑 정보는 내부 저장소인 ``Mapping Registry``를 조회하여 현재 요청과 일치하는 Handler를 탐색한다. 

    즉, 요청이 들어올 때 마다 Controller를 새로 탐색하는 것이 아니라, 
    이미 등록된 매핑 정보를 기반으로 현재 요청 조건과 일치하는 Handler를 조회하는 방식이다. 

    protected String initLookupPath(HttpServletRequest request) {
        if (usesPathPatterns()) {
            request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE);
            RequestPath requestPath = getRequestPath(request);
            String lookupPath = requestPath.pathWithinApplication().value();
            return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath);
        }
        else {
            return getUrlPathHelper().resolveAndCacheLookupPath(request);
        }
    }

     

    이렇게 Handler 탐색이 완료되면, 단순히 Controller 객체만 반환되는 것이 아니라, 

    해당 요청에 적용될 interceptor 정보까지 포함된 ``HandlerExecutionChain`` 이 생성되어 DispatcherServlet으로 전달된다. 

     

    만약, 일치하는 Handler가 존재하지 않는 경웨는 `null`을 반환한다.

    이후 DispatcherServlet은 해당 요청을 처리할 Handler가 없다고 판단하고, `notHandlerFound()`을 호출하여 `404(NOT FOUND)` 응답을 생성하게 된다. 

     

    실제 코드 (HandlerAdapter)

    아래는 interceptor의 `preHandle()` 메서드 호출 이후 요청에 맞는 HandlerAdapter를 찾는 과정이다.  

    // Determine handler adapter and invoke the handler.
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    if (asyncManager.isConcurrentHandlingStarted()) {
        return;
    }

     

    `getHandlerAdapter()` 메서드

    DispatcherServlet 의 `getHandlerAdapter()`

    protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
        if (this.handlerAdapters != null) {
            for (HandlerAdapter adapter : this.handlerAdapters) {
                if (adapter.supports(handler)) {
                    return adapter;
                }
            }
        }
        throw new ServletException("No adapter for handler [" + handler +
                "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
    }

    내부적으로 등록된 ``HandlerAdapter`` 목록을 순회하면서 각 Adapter의 `supports()` 메서드를 호출해 현재 Handler를 처리할 수 잇는지 확인한다. 

     

    Spring MVC에는 다양한 형태의 Handler를 지원하기 위해 여러 종류의 HandlerAdapter 가 존재한다. 

    • `RequestMappingHandlerAdapter` : `@RequestMapping` 어노테이션으로 구현한 Handler를 처리한다. 
    • `HandlerFunctionAdapter` : 함수형 Handler를 처리한다.
    • `HttpRequestHandlerAdapter` : HttpRequestHandler를 상속한 Handler를 처리한다.
    • `SimpleControllerHandlerAdapter` : Controller를 상속한 Handler를 처리한다. 

    현재 Spring MVC에서 가장 일반적으로 사용하는 방식은 `@Controller`, `@RestController`, `@RequestMapping` 기반의 Annotation 방식이다. 따라서 대부분의 요청은 ``RequestMappingHandlerAdapter`` 를 통해 처리된다.

     

    예를 들어 Annotation 기반 Controller가 Handler로 선택되었다면,

    ``RequestMappingHandlerAdapter``의 `supports()` 메서드가 ``true``를 반환하게 되고,

    DispatcherServlet은 해당 Adapter를 선택하고, 실제 요청 처리를 위해 `handle()` 메서드를 호출한다. 

     

    즉, HandlerAdapter는 단순히 Adapter를 선택하는 역할에서 끝나는 것이 아니라,

    선택된 Handler를 실제로 실행하는 진입 지점 역할까지 담당한다.

     

    이때 만약 어떠한 Adapter도 현재 Handler를 지원하지 않는다면, 최종적으로 예외가 발생한다.

    이는 실행할 Handler는 찾았지만, 이를 처리할 수 있는 Adapter가 존재하지 않는 상황을 의미한다. 

     

     


    지금까지 DispatcherServlet 내부 흐름을 따라가며 HandlerMapping과 HandlerAdatper가 어떤 방식으로 동작하는지 살폅왔다. 

     

    처음에는 단순히 "Controller를 찾고 실행하는 과정" 정도로 보일 수 있지만,

    실제 내부 구조를 따라가 보면 Spring MVC는 요청을 처리하기 위해 여러 역할을 명확하게 분리하고 있다는 것을 확인할 수 있다. 

     

    특히 HandlerMApping은 현재 요청을 처리할 Handler를 찾는 역할을 수행하고, HandlerAdapter는 해당 Handler를 실제로 실행할 수 있도록 연결해주는 역할을 담당한다. 

     

    이러한 구조 덕분에 Spring MVC는 다양한 Handler 구현 방식을 동일한 흐름 안에서 처리할 수 있으며, 내부 확장성과 유연성을 확보할 수 있다.

     

    그리고 실제 Handler가 실행되는 단계 내부로 들어가 보면,
    단순 메서드 호출이 아니라 파라미터 바인딩, Model 생성, Reflection 기반 메서드 호출, Proxy와 AOP까지 이어지는 흐름이 존재한다.

     

    다음 글에서는 이러한 실행 과정의 내부 구조를 따라가며, Spring이 Bean과 AOP를 기반으로 실제 요청을 어떻게 처리하는지 조금 더 깊게 정리해보려 한다.