
Spring MVC로 개발하다 보면 HandlerMapping, HandlerAdapter, DispatcherServlet 같은 용어를 자주 마주하게 됩니다.
하지만 DispatcherServlet이 컨트롤러를 호출하기까지 왜 두 단계를 거쳐야 하는지는 구조를 직접 들여다보기 전에는 쉽게 이해하기 어렵습니다.
최근 진행하고 있는 기술 스터디에서 Spring Web MVC의 요청 처리 흐름을 살펴보던 중
“HandlerMapping이 이미 컨트롤러를 찾았는데, 왜 바로 호출하지 않을까?”,
“그렇다면 HandlerAdapter는 어떤 역할 때문에 필요한 걸까?”
라는 궁금증이 생겼습니다.
이 글을 그 질문에 대한 답을 정리하면서, Spring MVC 요청 처리 구조를 이해하는 데 도움이 되고자 작성해보았습니다.
이 글을 읽으면 다음 내용을 명확하게 이해할 수 있습니다.
- Spring MVC의 요청 처리 흐름
- HandlerMapping과 HandlerAdapter가 맡는 역할
- 두 컴포넌트를 분리해야 하는 이유
- Spring MVC가 다양한 컨트롤러 타입을 지원할 수 있는 구조적 원리
- Plug & Play 관점에서 살펴본 Spring MVC의 설계 의도
읽고 나면 왜 Spring MVC가 이런 방식으로 설계되었는지를 다른 사람에게도 자연스럽게 설명할 수 있을거에요.
Spring MVC 요청 처리 흐름 요약

Spring MVC는 프론트 컨트롤러 패턴을 기반으로 동작합니다.
모든 요청은 단일 진입점인 DispatcherServlet을 거쳐 들어오며, 이후 여러 컴포넌트가 협력해 응답을 만들어 냅니다.
요청 흐름은 다음과 같은 단계로 진행됩니다.
- 클라이언트 요청 → 서블릿 컨테이너 → DispatcherServlet
- DispatcherServlet은 HandlerMapping을 사용해 “이 URL을 처리할 핸들러(컨트롤러)가 무엇인지” 조회합니다.
- HandlerMapping은 HandlerExecutionChain을 반환합니다.
→ 실제 컨트롤러 객체 + 적용할 인터셉터 목록 - DispatcherServlet은 해당 핸들러를 실행할 수 있는 HandlerAdapter를 찾고, 이를 통해 컨트롤러를 호출합니다.
- 컨트롤러는 비즈니스 로직을 처리한 뒤 ModelAndView 를 반환합니다.
- ViewResolver가 뷰 이름을 실제 뷰 객체로 변환합니다.
- 뷰가 렌더링되어 최종 응답이 클라이언트로 전달됩니다.
그리고 여기서 자연스럽게 핵심 질문이 생깁니다.
왜 DispatcherServlet은 컨트롤러를 찾은 뒤 바로 호출하지 않고, 반드시 HandlerAdapter를 거쳐야 할까?
다음 섹션에서는 이 질문에 대한 이유를 구조적·설계적 관점에서 풀어보겠습니다.
그냥 바로 호출하면 되지 않을까?

요청 흐름만 보면 쉽게 이렇게 떠올릴 수 있습니다.
- HandlerMapping: “컨트롤러 찾았다!”
- DispatcherServlet: “오 좋네? 그럼 바로 실행!”
- 끝
하지만 Spring MVC는 여기서 한 단계 더 나아가, HandlerAdapter라는 별도의 컴포넌트를 두어
‘컨트롤러를 찾는 과정’과 ‘실행하는 과정’을 명확히 분리해 두었습니다.
왜 그럴까요?
정답은 간단합니다.
Spring MVC가 지원해야 하는 컨트롤러의 종류가 하나가 아니기 때문입니다.
Spring MVC에는 다양한 컨트롤러 타입이 존재한다
Spring MVC라고 해서 컨트롤러가 항상 @Controller 기반만 있는 것은 아닙니다.
대표적인 컨트롤러 타입만 살펴봐도 아래처럼 서로 다른 방식들이 존재합니다.
어노테이션 기반 컨트롤러
@Controller
public class MyController {
@RequestMapping("/myPath")
public ModelAndView handle() {
// 비즈니스 로직
return new ModelAndView("viewName");
}
}
HttpRequestHandler 기반 컨트롤러
public class MyHttpRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request,
HttpServletResponse response) {
// 서블릿 API 직접 활용
}
}
이 두 컨트롤러는 호출 방식도 다르고, 반환 타입도 완전히 다릅니다.
MyController
→ 메서드를 리플렉션으로 찾아 실행하고,ModelAndView를 반환MyHttpRequestHandler
→handleRequest()를 직접 호출하고, 반환값이 없음
즉, DispatcherServlet이 하나의 방식으로 모든 컨트롤러를 실행할 수는 없습니다.
그렇다면 어떻게 다양한 컨트롤러 타입을 처리할 수 있을까요?
바로 여기에서 HandlerAdapter의 존재 이유가 드러납니다.
HandlerAdapter: 컨트롤러 호출 방식의 차이를 감추는 어댑터 패턴

Spring MVC는 컨트롤러마다 호출 방식이 서로 다른 문제를 어댑터 패턴(Adapter Pattern) 으로 해결합니다.
핵심 아이디어는 매우 단순합니다.
컨트롤러 타입이 여러 가지라면,
각 타입의 호출 차이를 감춰주는 어댑터(HandlerAdapter)를 두자.
즉, 컨트롤러가 어떤 형태로 작성되어 있든, DispatcherServlet 입장에서는 모든 컨트롤러를 동일한 방식으로 호출할 수 있게 만드는 것입니다.
예를 들어, 어노테이션 기반 컨트롤러를 위한 어댑터는 다음과 같은 구조로 동작합니다.
public class MyControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof MyController;
}
@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
return ((MyController) handler).handle();
}
@Override
public long getLastModified(HttpServletRequest request,
Object handler) {
return -1;
}
}
이 구조를 통해 DispatcherServlet은 다음과 같은 흐름으로 동작할 수 있습니다.
- HandlerMapping이 핸들러(컨트롤러) 객체를 반환
- 여러 HandlerAdapter 중
supports() == true인 어댑터를 선택 - 해당 어댑터의
handle()을 호출
→ 컨트롤러를 어떻게 실행할지는 어댑터가 모두 알아서 처리
결과적으로,
DispatcherServlet은 컨트롤러의 구체 타입이나 호출 규칙을 알 필요가 없고, 언제나 일관된 방식으로 요청을 처리할 수 있게 됩니다.
이 점이 Spring MVC가 다양한 컨트롤러 스타일을 수용하면서도 핵심 구조를 깔끔하게 유지할 수 있는 이유입니다.
HandlerMapping과 HandlerAdapter를 분리한 이유
정리해 보면, Spring MVC가 두 컴포넌트를 분리한 이유는 다음과 같습니다.
역할을 명확하게 분리하기 위해 — “누가 처리할까” vs “어떻게 처리할까”
- HandlerMapping: 어떤 핸들러(컨트롤러)가 이 요청을 처리할지 결정
- HandlerAdapter: 해당 핸들러를 어떤 방식으로 실행할지 결정
이 둘을 하나로 묶어버리면 DispatcherServlet이 모든 컨트롤러 타입에 대해 “if → else → instanceof” 같은 조건문을 직접 관리해야 합니다.
결과적으로 구조가 복잡하게 얽히고 유지보수성이 크게 떨어집니다.
다양한 컨트롤러 타입을 유연하게 지원하기 위해
Spring MVC는 오랜 기간 여러 컨트롤러 스타일을 지원해 왔습니다.
@Controller기반 컨트롤러- SimpleControllerHandlerAdapter 기반의 컨트롤러
HttpRequestHandler- 과거 방식의
Controller인터페이스
이들은 호출 방식도, 반환 타입도 제각각입니다.
하나의 규칙으로 모두 처리하는 것은 사실상 불가능하죠.
HandlerAdapter가 존재하기 때문에 다양한 컨트롤러를 무리 없이 받아들일 수 있습니다.
Plug & Play 아키텍처를 구현하기 위해
Spring MVC는 다음 철학을 지향합니다.
새로운 컨트롤러 타입을 만들고 싶다면, 필요한 컴포넌트만 추가하라.
즉,
- 새로운 컨트롤러 타입 추가 → HandlerMapping + HandlerAdapter 추가
DispatcherServlet수정 → 필요 없음- 기존 로직과 충돌 → 발생하지 않음
이 구조 덕분에 Spring MVC는 높은 확장성을 갖게 되었고,
결국 Plug & Play 방식의 유연한 웹 프레임워크로 자리 잡을 수 있었습니다.
'Backend > Spring 🌱' 카테고리의 다른 글
| Spring SSE와 Redis Pub/Sub으로 구현하는 실시간 알림 (다중 서버 환경까지 스케일링하기) (0) | 2024.03.19 |
|---|---|
| 스프링 컨테이너 이해하기: BeanFactory와 ApplicationContext (0) | 2024.03.12 |
| 이벤트 기반으로 파일 업로드 기능 구현하기 (0) | 2024.02.07 |
| 전략 패턴으로 확장성 있게 소셜 로그인 설계하기 (0) | 2024.02.07 |
| Spring Boot 프로젝트에 PostGIS 적용하기: 위치 정보 관리를 위한 DB (0) | 2024.02.07 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆