jongkwan.dev
개발 · Essay №066

Spring MVC 요청 처리 흐름

HTTP 요청이 DispatcherServlet에서 핸들러 매핑, 어댑터, 뷰 리졸버를 거쳐 응답이 되기까지의 단계를 정리합니다.

이종관2026년 3월 21일12 min read
Contents

스프링 MVC의 모든 요청은 DispatcherServlet 하나로 모인 뒤 정해진 단계를 따라 응답이 됩니다.

프론트 컨트롤러라는 단일 진입점

스프링 MVC는 프론트 컨트롤러 패턴으로 동작합니다. 프론트 컨트롤러란 모든 요청을 한 객체가 먼저 받아 공통 처리를 끝낸 뒤, 알맞은 컨트롤러에 위임하는 구조를 말합니다. 이 진입점 객체가 디스패처 서블릿(DispatcherServlet)입니다.

DispatcherServlet은 HttpServlet을 상속한 평범한 서블릿입니다. 스프링 부트는 이 서블릿을 모든 경로(/)에 자동으로 매핑합니다. 요청이 오면 서블릿 표준 진입점인 service()가 호출됩니다. 부모 클래스 FrameworkServlet이 이를 오버라이드해 결국 핵심 메서드 doDispatch()로 이어집니다.

진입점이 하나라서 로깅, 인증, 예외 변환 같은 공통 관심사를 한곳에서 다룰 수 있습니다. 개별 컨트롤러는 자신의 비즈니스 로직에만 집중하면 됩니다. 흐름의 나머지 단계는 모두 이 doDispatch() 위에서 일어납니다.

doDispatch가 거치는 여덟 단계

요청 하나를 응답으로 바꾸는 일은 세 묶음으로 나뉩니다. 처리할 핸들러를 찾고, 그 핸들러를 실행하고, 실행 결과를 응답으로 변환합니다.

단계담당하는 일
1HandlerMapping요청 URL에 매핑된 핸들러(컨트롤러) 조회
2HandlerAdapter그 핸들러를 실행할 수 있는 어댑터 조회
3HandlerAdapter조회한 어댑터를 실행
4Controller어댑터가 실제 핸들러를 호출
5HandlerAdapter반환값을 ModelAndView로 변환해 반환
6ViewResolver뷰 이름으로 뷰 리졸버 실행
7ViewResolver논리 이름을 물리 이름으로 바꿔 View 반환
8View모델을 넣어 뷰 렌더링

이 표는 뷰를 그리는 전통적 경로입니다. REST 응답이라면 68단계 대신 메시지 컨버터가 반환 객체를 JSON 같은 본문으로 직렬화합니다. 두 경로 모두 15단계는 똑같이 거칩니다.

핸들러 매핑과 핸들러 어댑터

컨트롤러를 찾는 일과 실행하는 일이 따로 나뉜 이유는 컨트롤러 형태가 한 가지가 아니기 때문입니다. 핸들러 매핑은 '누가 이 요청을 처리하는가'를, 핸들러 어댑터는 '그 핸들러를 어떻게 실행하는가'를 책임집니다. 덕분에 애노테이션 컨트롤러와 과거 인터페이스 컨트롤러가 같은 흐름 위에서 공존할 수 있습니다.

스프링 부트는 여러 종류를 우선순위 순서로 자동 등록합니다. 핸들러 매핑은 먼저 매칭되는 것을 쓰고, 핸들러 어댑터는 supports()true를 돌려주는 첫 어댑터를 씁니다.

구분빈 이름처리 대상
HandlerMappingRequestMappingHandlerMapping@RequestMapping 기반 애노테이션 컨트롤러
HandlerMappingBeanNameUrlHandlerMapping스프링 빈 이름으로 찾는 핸들러
HandlerAdapterRequestMappingHandlerAdapter애노테이션 컨트롤러
HandlerAdapterHttpRequestHandlerAdapterHttpRequestHandler 구현체
HandlerAdapterSimpleControllerHandlerAdapter과거 Controller 인터페이스 구현체

실무 컨트롤러는 거의 다 @Controller@RequestMapping 기반입니다. 그래서 보통 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter 짝이 쓰입니다. 나머지 어댑터는 정적 리소스나 과거 방식 컨트롤러를 위한 호환 경로입니다.

인자 바인딩과 응답 변환

핸들러 어댑터가 컨트롤러 메서드를 실행할 때 두 협력자가 끼어듭니다. 파라미터를 채우는 일은 ArgumentResolver가, 반환값을 응답으로 바꾸는 일은 ReturnValueHandler가 맡습니다. 둘 다 필요하면 HTTP 메시지 컨버터(HttpMessageConverter)를 불러 본문을 객체로, 객체를 본문으로 바꿉니다.

요청 본문 처리를 예로 들면, @RequestBody는 RequestResponseBodyMethodProcessor가 담당합니다. HttpEntity는 HttpEntityMethodProcessor가 맡으며, 둘 다 ArgumentResolver입니다. 이들이 메시지 컨버터를 호출해 JSON 본문을 자바 객체로 만듭니다. 응답에서는 @ResponseBody를 처리하는 ReturnValueHandler가 같은 컨버터로 객체를 직렬화합니다.

java
@RestController
public class MemberController {
 
    @PostMapping("/members")
    public MemberResponse create(@RequestBody MemberRequest request) {
        // request: 메시지 컨버터가 JSON 본문을 객체로 변환한 결과
        // 반환값: ReturnValueHandler가 다시 JSON 본문으로 직렬화
        return new MemberResponse(request.getName());
    }
}

반환값이 객체가 아니라 뷰 이름이라면 6단계의 뷰 리졸버 경로를 탑니다. 뷰 리졸버는 new-form 같은 논리 이름을 받아 실제 파일 경로로 바꿉니다. 스프링 부트가 자동 등록하는 InternalResourceViewResolver가 이 일을 합니다. 설정값 spring.mvc.view.prefixspring.mvc.view.suffix를 앞뒤에 붙여 물리 경로를 완성합니다.

yaml
spring:
  mvc:
    view:
      prefix: /WEB-INF/views/
      suffix: .jsp

위 설정에서 컨트롤러가 new-form을 반환하면 뷰 리졸버가 /WEB-INF/views/new-form.jsp로 해석합니다. 이렇게 컨트롤러는 논리 이름만 알면 되고, 실제 경로 규칙은 설정에 맡깁니다. 화면 기술이 바뀌어도 컨트롤러 코드는 그대로 둘 수 있습니다.

필터와 인터셉터의 위치

공통 관심사를 끼워 넣는 지점은 둘입니다. 필터는 서블릿 계층에서 DispatcherServlet 바깥을 감싸고, 인터셉터는 스프링 MVC 안쪽에서 컨트롤러 앞뒤를 감쌉니다. 같은 일을 하더라도 다룰 수 있는 정보와 위치가 다릅니다.

필터는 서블릿 표준 기술이라 request, response만 다룹니다. 필터 인터페이스는 메서드가 단순합니다. doFilter 안에서 chain.doFilter()를 호출해야 다음 필터나 서블릿으로 넘어가고, 호출하지 않으면 거기서 요청이 끝납니다.

java
public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {}
 
    void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;
 
    default void destroy() {}
}

인터셉터는 스프링 MVC가 제공하므로 어떤 핸들러가 호출되는지, 어떤 ModelAndView가 반환되는지까지 받습니다. HandlerInterceptor는 세 시점으로 나뉩니다. preHandle은 핸들러 어댑터 호출 전에, postHandle은 호출 후에, afterCompletion은 뷰 렌더링까지 끝난 뒤에 실행됩니다.

java
public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception {
        return true;
    }
 
    default void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {}
 
    default void afterCompletion(HttpServletRequest request,
            HttpServletResponse response, Object handler,
            Exception ex) throws Exception {}
}

세 메서드의 호출 규칙이 인터셉터 설계의 핵심입니다. preHandlefalse를 반환하면 다음 인터셉터와 핸들러 어댑터가 모두 호출되지 않아 거기서 흐름이 끝납니다. 그래서 로그인 여부 검사처럼 요청을 차단해야 하는 로직에 잘 맞습니다.

예외가 나면 동작이 갈립니다. 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않지만, afterCompletion은 항상 호출되며 예외 객체 ex를 파라미터로 받습니다. 따라서 예외 여부와 무관하게 끝내야 하는 공통 처리는 afterCompletion에 둡니다.

구분필터인터셉터
제공 주체서블릿 컨테이너스프링 MVC
위치DispatcherServlet 바깥컨트롤러 앞뒤
접근 정보request, responsehandler, ModelAndView 포함
차단 방법chain.doFilter 미호출preHandle에서 false 반환

표를 한 줄로 요약하면, 인코딩이나 보안 필터 체인처럼 MVC 바깥 관심사는 필터가, 컨트롤러 문맥이 필요한 처리는 인터셉터가 맞습니다. 스프링 시큐리티가 필터 기반으로 동작하는 것도 같은 이유입니다.

정리

스프링 MVC의 요청 흐름은 DispatcherServlet이라는 단일 진입점에서 시작합니다. doDispatch()는 핸들러 매핑으로 컨트롤러를 찾고, 핸들러 어댑터로 실행한 뒤, 뷰 리졸버나 메시지 컨버터로 응답을 만듭니다. 인자 바인딩과 반환값 변환은 ArgumentResolver와 ReturnValueHandler가, 본문 직렬화는 메시지 컨버터가 맡습니다. 공통 관심사는 서블릿 계층의 필터와 MVC 안쪽의 인터셉터로 나뉘어 붙고, 인터셉터의 세 시점은 차단과 예외 처리의 기준이 됩니다. 이 단계 구분을 머릿속에 두면 검증, 예외 변환, 인증이 각각 어디에서 일어나는지 따라가기 쉬워집니다.