Language & Framework/Spring Boot

[Spring Boot] Spring의 전체 흐름 알아보기 - 4. Servlet (HttpServlet, FrameworkServlet, DispatcherServlet)

코딩 기록하는 애기 개발자 2026. 4. 23. 19:00

목차

     

    이전 글에서는 Interceptor를 중심으로 요청 처리 흐름과 실행 구조를 살펴보았다.

    이를 통해 Spring MVC에서 요청이 어떻게 전·후 처리되는지 전반적인 흐름을 이해할 수 있었다.

     

    하지만 해당 흐름을 보다 깊이 이해하기 위해서는 그 기반이 되는 Servlet에 대한 이해가 필요하다.

    Spring MVC는 결국 Servlet 위에서 동작하는 구조이며, DispatcherServlet을 중심으로 요청을 처리한다.

     

    이번 글에서는 Servlet이 무엇인지 살펴보며 Spring MVC의 내부 동작 구조를 한 단계 더 깊이 이해해보려 한다.

     


    Servlet 이란 ?

    클라이언트의 요청을 받아 처리하고, 그 결과를 응답으로 반환하는 자바 기반의 웹 컴포넌트이다. 

    서블릿은 서버에서 대기하고 있다가 클라이언트 (브라우저)의 요청이 들어오면 해당 요청을 처리하고 그 결과를 응답으로 반환한다. 

     

    Servlet 의 동작과정

    클라이언트의 요청이 들어오면 WAS(Tomcat)가 이를 받아 적절한 Servlet을 실행하고, 요청을 처리한 뒤 그 결과를 응답으로 반환한다.  

    1. 클라이언트가 웹 서버에 요청을 보낸다.
    2. 웹 컨테이너 (Tomcat) 가 요청을 받아 내부에서 처리를 시작한다.
    3. Tomcat이 `HttpServletRequest`와 `HttpServletResponse` 객체를 생성한다.
    4. 요청 URL에 맞는 Servlet을 찾는다.
    5. 최초 요청 시 해당 Servlet이 생성되고 `init()`이 1회 호출된다.
    6. 이후 요청이 들어올 때 마다 `service()`가 호출된다.
    7. `doGet()` 또는 `doPost()`에서 실제 비즈니스 로직을 수행한다. 
    8. 처리 결과를 `HttpServletResponse`에 담아 클라이언트에 응답한다. 
    9. 서버 종료 시 `destroy()`를 실행해 자원을 정리한다. 

     


     

    앞선 글들에서 DispatcherServlet이 요청 처리의 핵심이라는 점을 간접적으로 언급했지만, 
    이를 제대로 이해하기 위해서는 그 기반이 되는 구조를 먼저 짚고 넘어갈 필요가 있다. 

     

    특히 DispatcherServlet은 단순히 독립적으로 존재하는 것이 아니라, 
    Java의 표전 Servlet인 HttpServlet을 기반으로, Spring이 확장한 FrameworkServlet을 거쳐 구현된 구조이다. 

     

    따라서 이번에는 DispatcherServlet을 바로 살펴보기보단, 

    그 상위 구조인 HttpServlet과 FrameworkServlet이 어떤 역할을 하는지 먼저 이해해보자. 

     

     

    HttpServlet 

    앞서 Servlet이 요청과  응답을 처리하는 웹 컴포넌트라는 점을 살펴봉ㅆ다. 

    하지만 실제 웹 애플리케이션에서는 단순한 요청 처리뿐 아니라, HTTP 프로토콜에 맞는 세부적인 데이터 처리도 필요하다. 

     

    이를 위해 등장한 것이 바로 HttpServlet이다. 

     

    HttpServlet은 Servlet을 HTTP 프로토콜 환경에 맞게 확장한 클래스이다. 

    즉, 웹 브라우저와 서버 간의 HTTP 통신을 보다 쉽게 처리할 수 있도록 기능이 추가된 Servlet 구현체라고 볼 수 있다. 

     

    일반적인 Servlet은 요청을 처리하는 구조만 정의되어 있지만, 

    HttpServlet은 HTTP 요청 방식에 따라 요청을 구분하여 처리할 수 있는 기능을 제공한다. 

     

    Tomcat과 같은 Servlet Container는 클라이언트 요청이 들어오면 `service()` 메서드를 호출하고,
    HttpServlet 내부에서는 요청 방식에 따라 적절한 메서드로 분기된다. 

     

    실제 코드

    사실 HttpServlet의 핵심은 하나다.

    " HTTP Method (Get, POST 등)에 따라 적절한 메서드로 분기하는 것 "

     

    그렇기 때문에 실제 코드에서 가장 중요한 코드는 `service()` 메서드다. 

    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
        String method = req.getMethod();
    
        switch (method) {
            case METHOD_GET -> {
                long lastModified = getLastModified(req);
                if (lastModified == -1) {
                    // servlet doesn't support if-modified-since, no reason
                    // to go through further expensive logic
                    doGet(req, resp);
                } else {
                    long ifModifiedSince;
                    try {
                        ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                    } catch (IllegalArgumentException iae) {
                        // Invalid date header - proceed as if none was set
                        ifModifiedSince = -1;
                    }
                    if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                        // If the servlet mod time is later, call doGet()
                        // Round down to the nearest second for a proper compare
                        // A ifModifiedSince of -1 will always be less
                        maybeSetLastModified(resp, lastModified);
                        doGet(req, resp);
                    } else {
                        resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                    }
                }
    
            }
            case METHOD_HEAD -> {
                long lastModified = getLastModified(req);
                maybeSetLastModified(resp, lastModified);
                doHead(req, resp);
    
            }
            case METHOD_POST -> doPost(req, resp);
            case METHOD_PUT -> doPut(req, resp);
            case METHOD_DELETE -> doDelete(req, resp);
            case METHOD_OPTIONS -> doOptions(req, resp);
            case METHOD_TRACE -> doTrace(req, resp);
            case METHOD_PATCH -> doPatch(req, resp);
            default -> {
                //
                // Note that this means NO servlet supports whatever
                // method was requested, anywhere on this server.
                //
    
                String errMsg = lStrings.getString("http.method_not_implemented");
                Object[] errArgs = new Object[1];
                errArgs[0] = method;
                errMsg = MessageFormat.format(errMsg, errArgs);
    
                resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
            }
        }
    }
    

     

    클라이언트 요청이 들어오면 Servlet Container (Tomcat)는 `service()`를 호출한다. 

    이후 HttpServlet은 요청의 HTTP Method를 확인하고, 해당 메서드에 맞는 메서드로 분기한다. 

     

    예를 들어, GET 요청이 들어오면, `doGet()` 을 호출하고, POST요청이 들어오면, `doPost()`를 호출하게 된다. 

     

    즉, Http Servlet은 단순히 요청을 처리하는 것이 아니라,
    HTTP 요청 종류에 따라 적절한 메서드를 연결해주는 Dispatcher 의 역할을 수행
    한다. 

     

     

    위 코드 중 GET 요청을 처리할 때의 로직이 궁금해 조금 더 살펴보았다. 

    case METHOD_GET -> {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            doGet(req, resp);
        } else {
            long ifModifiedSince;
            try {
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // Invalid date header - proceed as if none was set
                ifModifiedSince = -1;
            }
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }
    
    }

     

    일단, `lastModified` 라는 변수에 현재 요청된 리소스가 마지막으로 수정된 시간을 저장한다.

    예를 들어, 이미지 파일, HTML, JSON 데이터와 같은 리소스들의 최종 수정  시각을 저장하는 것이다. 

     

    만약, 수정 시간을 지원하지 않으면 바로 `doGet()`을 호출한다.

     

    이후, 브라우저가 가진 캐시 시간을 `getDateHedader(HEDAER_IFMODSINCE)` 를 통해 읽어온다. 

    이때, 브라우저는 요청을 보낼 때 캐시 시간을 함께 보낸다는 점을 알아야 한다. 

     

    서버 데이터가 더 최신인지 비교한 후,
    최신 데이터면 다시 응답하고, 최신 데이터가 아니면 304를 응답한다. 

     

    요약하면, GET 요청에서 브라우저 캐시와 서버 수정 시간을 비교하여 불필요한 응답을 막는 HTTP Cache Validation 로직이다 !!

     

    FrameworkServlet

    HttpServlet은 HTTP 요청을 분기하는 역할까지는 수행하지만, 
    Spring MVC가 동작하기 위해서는 단순히 HTTP 처리 이상의 기능이 필요하다. 

     

    FrameworkServlet은 Spring MVC 전용 Servlet 기반 클래스로,

    단순히 HTTP 요청만 처리하는 것이 아니라, Spring 기능을 Servlet에 연결하는 역할을 한다. 

     

    즉, Servlet과 Spring Container 를 연결해주는 역할이다.

     

    실제 코드

    @Override
    protected final void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
    
        processRequest(request, response);
    }

     

    위 코드는 `FrameworkServlet`의 실제 코드 중 일부이다. 

     

    HttpServlet에는 `doGet()`, `doPost()`가 각각 처리되었지만, 

    FrameworkServlet은 이 모든 요청을 최종적으로 `processRequest()` 메서드를 통해 처리한다. 

     

    processRequest ()

    모든 요청이 이 메서드로 처리되기 때문에 `FrameworkServlet`의 핵심은 ``processRequest()`` 메서드라고 할 수 있다. 

     

    protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
           throws ServletException, IOException {
    
        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
    
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        LocaleContext localeContext = buildLocaleContext(request);
    
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
    
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
    
        initContextHolders(request, localeContext, requestAttributes);
    
        try {
           doService(request, response);
        }
        catch (ServletException | IOException ex) {
           failureCause = ex;
           throw ex;
        }
        catch (Throwable ex) {
           failureCause = ex;
           throw new ServletException("Request processing failed: " + ex, ex);
        }
    
        finally {
           resetContextHolders(request, previousLocaleContext, previousAttributes);
           if (requestAttributes != null) {
              requestAttributes.requestCompleted();
           }
           logResult(request, response, failureCause, asyncManager);
           publishRequestHandledEvent(request, response, startTime, failureCause);
        }
    }

     

    위 코드는 `processRequest()` 메서드의 실제 코드이다. 중요한 부분만 살펴보자. 

     

    일단, LocaleContext 를 생성하게 되는데, 이는 사용자가 어떤 언어와 어떤 국가 환경으로 요청했는지를 담고 있는 context라고 생각하면 된다.  

     

    이후, 현재 요청에 대한 정보를 Spring 내부에서 사용할 수 있도록

    ``HttpServletRequest``와 ``HttpServletResponse`` 객체를 만들어 저장한다. 

     

    이렇게 저장된 정보는 요청이 처리되는 동안 유지되며, Spring 내부에서는 필요할 때 현재 요청 정보를 꺼내 사용할 수 있다. 

     

    이후 `doService()`가 호출되며, 실제 요청 처리가 시작된다. 

     

    이때 중요한 점은 `FrameworkServlet`이 직접 요청을 처리하는 것이 아니라,

    이를 상속한 DispatcherServlet이 실제 요청 흐름을 담당한다는 점이다.

     

     

    DispatcherServlet

    앞서 살펴본 것처럼 ``FrameworkServlet``은 요청을 준비하고 Spring Context를 연결하는 역할을 수행한다.

    하지만 실제로 요청을 어디로 보낼지 결정하고, Controller를 호출하며, 응답까지 만들어내는 핵심 역할은 ``DispatcherServlet``이 담당한다. 

     

    이는 단순히 요청을 전달하는 것이 아니라, 요청을 분석하고 적절한 Controller를 찾아 실행한 뒤, 최종 응답까지 만들어내는 역할을 수행한다. 

    실제 코드 기반의 내부 흐름 

    요청이 들어오면 DispatcherServletd은 내부적으로 `doDispatch()` 메서드를 실행한다. 

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response)

     

    이 메서드가 실제 Spring MVC 요청 처리의 중심이라고 볼수 있다. 

    `doDispatch()` 메서드 코드 중 중요한 부분만 살펴보자. 

    mappedHandler = getHandler(processedRequest);

     

    일단, 현재 요청 URL에 맞는 Controller를 찾는다.

    `Controller === Handler` 라고 생각하면 된다. 

     

    mappedHandler.applyPreHandle(processedRequest, response)

     

    Controller 실행 전에 interceptor가 먼저 실행된다. 

    이 과정에서 인증 처리, 권한 검사, 공통 로깅 등이 수행될 수 있다. 

     

    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

     

    실제 Controller 메서드가 실행되는 부분 즉, 비즈니스 로직이 수행되는 부분이다. 

     

    applyDefaultViewName(processedRequest, mv);

     

    Controller가 View 이름을 지정하지 않았다면, 기본 View를 설정한다. 

    다만 REST API처럼 JSON 응답을 반환하는 경우에는 View를 렌더링하지 않기 때문에 이 과정은 사실상 동작하지 않는다. 

     

    mappedHandler.applyPostHandle(processedRequest, response, mv);

     

    Controller 실행 후, interceptor의 `postHandle()`이 호출된다.

    이 시점에서는 Controller 로직은 이미 끝난 상태이며, Veiw가 렌더링 되기 전에 추가 작업을 수행할 수 있다. 

     

    processDispatchResult(...)

     

    Controller 실행 결과를 기반으로 최종 응답을 생성한다. 

     

    ViewResolver를 통해 View를 찾아 화면을 렌더링하거나, 

    REST API의 경우 객체를 JSON 형태로 변환하여 응답하게 된다. 

     

    triggerAfterCompletion(...)

     

    모든 요청처리가 끝난 뒤 interceptor의 `afterCompletion()`이 실행된다. 

     

    예외 발생 여부와 관계없이 마지막에 호출되며, 

    리소스 정리나 로그 기록과 같은 후처리 작업을 수행한다. 

     

    즉, DispatcherServlet은 단순히 Controller를  호출하는 역할만 하는 것이 아니라, 
    ``요청 → Handler 탐색 → interceptor 실행 → Controller 호출 → View/Response 생성 → 후처리 ``

    까지의 전체 요청 흐름을 조율하는 핵심 Dispatcher 역할을 수행한다. 

     


     

    생각보다 Servlet부터 DispatcherServlet 내부 흐름까지 살펴보니 내용이 꽤 깊어졌다.

    ( 파도 파도 끝이 없는 Spring의 세계란 .................... )

    처음에는 Handler까지 함께 정리하려 했지만, 

    요청 흐름을 제대로 이해하려면 먼저 Servlet 기반 구조를 충분히 이해하는 것이 중요하다고 느꼈다.  

    이번 글에서는 Servlet → HttpServlet → FrameworkServlet → DispatcherServlet으로 이어지는 구조와,  
    Spring MVC가 요청을 어떤 흐름으로 처리하는지 전체적인 기반을 정리해보았다.  

    다음 글에서는 이번 흐름 속에서 등장했던 Handler가 실제로 어떤 역할을 하는지,  
    그리고 HandlerMapping, HandlerAdapter와 어떤 관계로 동작하는지 이어서 정리해보려 한다.

     

    그리고 ... 꼭 코드를 다 살펴본 뒤 흐름을 다시 !!!!! 정리해야 할 것 같ㄷㅏ.. 너무 어려워요 !!!!!