이전 글에서 Servlet이 뭔지, DispatcherServlet이 왜 필요한지를 얘기했다. 이번 글에서는 DispatcherServlet이 요청을 받은 후 실제로 어떤 순서로, 어떤 컴포넌트를 거쳐서 처리하는지를 단계별로 정리한다.
핵심 구조: Front Controller 패턴
Spring MVC는 Front Controller 패턴을 사용한다. 모든 HTTP 요청이 하나의 서블릿(DispatcherServlet)을 통과하고, 이 서블릿이 적절한 컴포넌트에 위임하는 구조다.
DispatcherServlet 하나만 두면, 나머지는 전부 스프링 빈(@Controller)으로 관리할 수 있다.
DispatcherServlet이 없다면?
URL마다 서블릿을 하나씩 만들고, web.xml에 매핑하고, 공통 로직(인증, 로깅, 예외처리)을 서블릿마다 중복 작성해야 한다.
해당 내용이 이해가 안간다면 이전 글을 참고하면 좋다!
전체 흐름 다이어그램

단계별 상세 설명
1단계: HTTP 요청 → DispatcherServlet
클라이언트(브라우저)가 GET /hello 같은 HTTP 요청을 보내면, 서블릿 컨테이너(Tomcat)가 DispatcherServlet에 전달한다.
이전 글에서 다뤘듯이 DispatcherServlet은 HttpServlet을 상속한 실제 서블릿이다. 핵심 메서드는 doDispatch()이고, 이 안에서 아래 2~8단계가 모두 실행된다.
HttpServlet (Java EE 표준)
└─ HttpServletBean (init-param → 빈 프로퍼티 매핑)
└─ FrameworkServlet (WebApplicationContext 초기화)
└─ DispatcherServlet (doDispatch() — 실제 요청 처리)
2단계: HandlerMapping - 핸들러 조회
DispatcherServlet은 등록된 HandlerMapping 구현체들을 순서대로 순회하며, 요청 URL에 매핑되는 핸들러(컨트롤러)를 찾는다.
// DispatcherServlet 내부 (의사 코드)
HandlerExecutionChain handler = handlerMapping.getHandler(request);
HandlerMapping이 반환하는 건 단순히 컨트롤러가 아니라 HandlerExecutionChain이다. 여기에는 핸들러(컨트롤러 메서드)와 함께 적용할 인터셉터 목록도 포함된다.
주요 구현체
- RequestMappingHandlerMapping (기본) : @RequestMapping, @GetMapping 등 어노테이션 기반
- BeanNameUrlHandlerMapping : 빈 이름이 URL 패턴인 경우
3단계: HandlerAdapter 조회
핸들러를 찾았으면, 이 핸들러를 실행할 수 있는 어댑터를 찾아야 한다.
HandlerAdapter adapter = getHandlerAdapter(handler);
핸들러의 종류가 다양하기 때문에 어댑터가 필요하다.
- @Controller 클래스의 메서드
- 옛날 Controller 인터페이스 구현체
- HttpRequestHandler 구현체
위와 같이 어댑터는 각각 호출 방식이 다른데, DispatcherServlet이 이걸 직접 분기 처리하면 코드가 복잡해진다. 어댑터 패턴을 써서 adapter.handle() 하나로 통일하는 것이다.
주요 구현체:
- RequestMappingHandlerAdapter (기본) : @RequestMapping 기반 핸들러 실행
- HttpRequestHandlerAdapter : HttpRequestHandler 실행
- SimpleControllerHandlerAdapter : 옛날 Controller 인터페이스 실행
4단계: 핸들러(컨트롤러) 실행
HandlerAdapter가 실제로 컨트롤러 메서드를 호출한다.
- interceptor.preHandle() : 인터셉터 전처리 (인증 체크, 로깅 등)
- ArgumentResolver : 메서드 파라미터 바인딩 (@RequestParam, @PathVariable, @RequestBody 등)
- 컨트롤러 메서드 실행 : 비즈니스 로직
- ReturnValueHandler : 반환값 처리
- interceptor.postHandle() : 인터셉터 후처리
@Controller
public class HelloController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "Hello, Spring MVC!"; // 여기가 실행됨
}
}
5단계: 결과 반환
컨트롤러 실행 결과가 HandlerAdapter를 거쳐 DispatcherServlet으로 돌아온다.
여기서 분기점이 생긴다.
- @ResponseBody가 있으면 → 4단계에서 이미 응답 작성 완료, ModelAndView는 null. 6~8단계 생략
- @ResponseBody가 없으면 → ModelAndView 객체가 반환되고, 6단계로 진행
6단계: ViewResolver 호출
ModelAndView에 담긴 뷰 이름(예: "hello")을 실제 View 객체로 변환한다.
// 예시: InternalResourceViewResolver 설정
// "hello" → /WEB-INF/views/hello.jsp
주요 구현체:
- InternalResourceViewResolver : JSP 뷰
- ThymeleafViewResolver : Thymeleaf 템플릿
- ContentNegotiatingViewResolver : Accept 헤더에 따라 다른 뷰 선택
7단계: View 반환
ViewResolver가 찾은 View 객체가 DispatcherServlet에 반환된다.
8단계: View.render(model)
DispatcherServlet이 View 객체의 render() 메서드를 호출한다. Model 데이터를 템플릿에 채워서 완성된 HTML을 response에 작성한다.
9단계: HTML 응답
완성된 HTML이 클라이언트(브라우저)에 전달된다.
공식 문서에는 있지만 다이어그램에는 없는 것들
위 9단계가 핵심 흐름이지만, Spring 공식 문서에는 사전 준비 단계가 존재한다.
| WebApplicationContext 바인딩 | request attribute에 컨텍스트를 바인딩하여 컨트롤러 등이 사용 가능 | 매 요청마다 실행 |
| LocaleResolver 바인딩 | 요청의 locale 결정 (뷰 렌더링, 데이터 준비에 사용) | 미사용 시 영향 없음 |
| MultipartResolver 검사 | 파일 업로드 요청인지 검사, MultipartHttpServletRequest로 래핑 | 설정 시에만 동작 |
| HandlerExceptionResolver | 요청 처리 중 예외 발생 시 처리 | @ExceptionHandler 등 |
시중에서 보이는 대부분의 Spring MVC 다이어그램이 이 부분을 생략하는 이유는, 핵심 라우팅 흐름에 직접적인 영향을 주지 않는 인프라 단계이기 때문이다.
@ResponseBody일 때의 흐름 (REST API)
위 다이어그램은 전통적인 서버 사이드 렌더링(SSR) 흐름이다.
요즘 많이 쓰는 REST API 방식에서는 6~8단계(ViewResolver → View)가 생략된다.
- @RestController는 @Controller + @ResponseBody를 합친 것이므로, 클래스의 모든 메서드가 자동으로 이 경로를 탄다.

@Controller vs @RestController
| @Controller | @RestController | |
| 반환값 해석 | 뷰 이름으로 해석 | HTTP 응답 본문으로 직접 작성 |
| ViewResolver | 거침 | 거치지 않음 |
| 주 용도 | SSR 웹 애플리케이션 (HTML) | REST API (JSON) |
| 내부 구조 | @Component | @Controller + @ResponseBody |
// @Controller — 뷰 이름을 반환
@Controller
public class PageController {
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("name", "Spring");
return "hello"; // → ViewResolver → hello.html
}
}
// @RestController — 객체를 직접 반환
@RestController
public class ApiController {
@GetMapping("/api/hello")
public Map<String, String> hello() {
return Map.of("message", "Hello!");
// → HttpMessageConverter → {"message":"Hello!"}
}
}
추가) SpringBoot의 편의성
SpringBoot를 쓰면 @SpringBootApplication 한 줄로 애플리케이션이 실행된다.
@SpringBootApplication가 내부적으로 해주는 일
- 내장 Tomcat 시작 : 서블릿 컨테이너를 직접 설치할 필요 없음
- DispatcherServlet 생성 및 등록 : / 경로에 자동 매핑
- WebApplicationContext 생성 : @ComponentScan으로 Bean 탐색
- HandlerMapping, HandlerAdapter, ViewResolver 등 자동 설정 : 기본 구현체 등록
SpringBoot 없이 직접 해보면 이런 코드가 필요하다.
public class Main {
public static void main(String[] args) throws Exception {
// 1. Spring ApplicationContext 생성
AnnotationConfigWebApplicationContext context =
new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// 2. DispatcherServlet 생성 + Context 연결
DispatcherServlet dispatcher = new DispatcherServlet(context);
// 3. 내장 Tomcat 설정
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
// 4. DispatcherServlet을 Tomcat에 등록
Context tomcatContext = tomcat.addContext("", null);
Tomcat.addServlet(tomcatContext, "dispatcher", dispatcher);
tomcatContext.addServletMappingDecoded("/", "dispatcher");
// 5. Tomcat 시작
tomcat.start();
tomcat.getServer().await();
}
}
단점은 내부에서 뭘 하는지 모르면 문제가 생겼을 때 디버깅이 어렵다는 것이다.
정리
Spring MVC의 요청 처리 흐름을 요약하자면, DispatcherServlet이 중앙에서 HandlerMapping → HandlerAdapter → Controller → ViewResolver → View 순으로 위임하는 구조이다.
이게 Spring Boot가 @SpringBootApplication 한 줄로 자동 설정해주는 것의 실체다. SpringBoot 없이 직접 DispatcherServlet을 등록하고 Tomcat에 올려보면(내장 톰캣 + DispatcherServlet 수동 등록) 흐름을 더 잘 이해하게 될 것이고, 이 부분은 추후 직접 해서 추가 포스팅 예정이다.
관련 포스팅
'Spring' 카테고리의 다른 글
| [Spring] Servlet와 DispatcherServlet (0) | 2026.03.31 |
|---|---|
| [Spring] Bean이란? (0) | 2026.01.24 |
| [Spring] DI(Dependency Injection) (0) | 2026.01.12 |
| [Spring] IoC(Inversion of Control) (0) | 2025.12.31 |
| [Spring] 스프링의 3계층 구조 : Controller, Service, Repository (1) | 2025.12.11 |