Spring Dispatcher Servlet 정의와 동작 원리
Spring MVC의 핵심인 Dispatcher Servlet은 모든 HTTP 요청을 가장 먼저 받아 적합한 컨트롤러로 위임하는 Front Controller 역할을 합니다. Spring 개발자라면 반드시 이해해야 할 핵심 개념입니다.
이 글에서는 Dispatcher Servlet의 정확한 정의부터 동작 원리, 정적 리소스 처리 방법, 그리고 8단계로 나누어지는 상세한 요청 처리 과정까지 모두 다룹니다. Spring MVC의 내부 동작을 깊이 이해하고 싶은 개발자에게 필수적인 내용입니다.
Dispatcher Servlet은 Spring MVC의 중심에 있는 Front Controller로, 모든 HTTP 요청을 최초로 받아 처리하는 핵심 컴포넌트입니다.
Dispatcher Servlet이라는 이름에서 'Dispatch'는 "보내다"라는 의미를 가지고 있습니다. 즉, 들어오는 요청을 적절한 곳으로 보내주는(위임하는) 역할을 담당합니다.
① 요청 수신: HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받음
② 공통 처리: 인증, 로깅, 인코딩 등 공통적인 전처리 작업 수행
③ 컨트롤러 탐색: 요청을 처리할 적합한 컨트롤러 찾기
④ 작업 위임: 찾은 컨트롤러에게 요청 처리 위임

클라이언트 → Servlet Container(Tomcat) → Dispatcher Servlet → Controller → Service → Repository
클라이언트로부터 요청이 오면 Tomcat과 같은 서블릿 컨테이너가 요청을 받고, 이 모든 요청을 Dispatcher Servlet이 가장 먼저 받아 처리합니다.
Front Controller는 디자인 패턴의 하나로, 서블릿 컨테이너의 제일 앞에서 모든 요청을 받아 처리하는 컨트롤러입니다. MVC 구조에서 함께 사용되는 대표적인 패턴입니다.
① 중앙 집중식 처리: 모든 요청이 하나의 진입점을 통과
② 공통 로직 분리: 인증, 권한 검사 등 공통 기능을 한 곳에서 처리
③ 코드 중복 제거: 각 컨트롤러마다 반복되는 코드 제거
④ 유지보수 용이: 공통 기능 수정 시 한 곳만 변경
| 구분 | 기존 Servlet 방식 | Dispatcher Servlet 방식 |
|---|---|---|
| URL 매핑 | web.xml에 모든 서블릿 등록 | 하나의 Dispatcher Servlet만 등록 |
| 공통 처리 | 각 서블릿마다 중복 구현 | Dispatcher Servlet에서 일괄 처리 |
| 컨트롤러 구현 | Servlet 상속 필수 | POJO 클래스로 구현 가능 |
| 설정 복잡도 | 높음 (web.xml 비대화) | 낮음 (어노테이션 기반) |
Spring MVC 이전에는 모든 서블릿을 web.xml에 일일이 등록해야 했습니다.
<servlet>
<servlet-name>userServlet</servlet-name>
<servlet-class>com.example.UserServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>userServlet</servlet-name>
<url-pattern>/user/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>productServlet</servlet-name>
<servlet-class>com.example.ProductServlet</servlet-class>
</servlet>
// 수십, 수백 개의 서블릿 등록 필요...
Dispatcher Servlet 도입 후에는 단 하나의 서블릿만 등록하면 됩니다.
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
컨트롤러를 구현만 해두면 Dispatcher Servlet이 자동으로 인식하고 적합한 컨트롤러로 위임합니다.
@RequestMapping("/users")
public class UserController {
// Dispatcher Servlet이 자동으로 이 메서드를 찾아 호출
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// 비즈니스 로직
return ResponseEntity.ok(userService.findById(id));
}
}
Front Controller 패턴을 더 깊이 이해하기 위해 필터와 인터셉터의 관계를 살펴보겠습니다.
클라이언트 요청은 다음 순서로 처리됩니다.
① 클라이언트 요청
↓
② Servlet Container (Tomcat 등)
↓
③ Filter Chain (서블릿 콘텍스트)
↓
④ Dispatcher Servlet (스프링 콘텍스트)
↓
⑤ Interceptor Chain
↓
⑥ Controller
| 구분 | Filter | Interceptor |
|---|---|---|
| 관리 주체 | Servlet Container | Spring Container |
| 실행 시점 | Dispatcher Servlet 이전/이후 | Controller 이전/이후 |
| 용도 | 인코딩, 보안, 로깅 | 인증, 권한 검사, 로깅 |
| Spring Bean 접근 | 불가 | 가능 |
Dispatcher Servlet은 서블릿 콘텍스트와 스프링 콘텍스트의 경계에 위치합니다.
① 서블릿 콘텍스트: 웹 애플리케이션 전체를 관리하는 컨테이너
② 스프링 콘텍스트: Spring Bean들을 관리하는 컨테이너
③ Dispatcher Servlet: 두 콘텍스트를 연결하는 다리 역할
Dispatcher Servlet이 모든 요청을 가로채면서 정적 리소스(이미지, CSS, JavaScript)를 불러오지 못하는 문제가 발생했습니다. 이를 해결하기 위한 2가지 전략을 살펴보겠습니다.
Dispatcher Servlet은 모든 요청(/)을 처리하도록 설정되어 있어, 정적 파일 요청까지 가로챕니다.
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern> <-- 모든 요청을 가로챔
</servlet-mapping>
// 다음 요청들이 모두 Dispatcher Servlet으로 가버림
/images/logo.png <-- 이미지 요청도
/css/style.css <-- CSS 파일도
/js/app.js <-- JavaScript 파일도
첫 번째 방법은 애플리케이션 요청과 정적 리소스 요청의 URL을 구분하는 것입니다.
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
// 정적 리소스: /resources/* (Dispatcher Servlet이 처리 안 함)
http://example.com/app/users <-- Dispatcher Servlet 처리
http://example.com/resources/css/style.css <-- 서블릿 컨테이너가 직접 처리
① 모든 URL에 /app 접두사 필요: 코드가 지저분해짐
② 직관적이지 않은 설계: URL 구조가 복잡해짐
③ 기존 코드 수정 필요: 레거시 시스템에 적용 어려움
두 번째 방법이 현재 Spring에서 권장하는 방식입니다. Dispatcher Servlet이 먼저 컨트롤러를 찾고, 없으면 정적 리소스를 찾습니다.
① 요청 수신: /images/logo.png 요청
↓
② 컨트롤러 탐색: @RequestMapping("/images/logo.png") 찾기
↓
③ 컨트롤러 없음: 매핑되는 컨트롤러 없음
↓
④ 정적 리소스 탐색: 설정된 리소스 경로에서 파일 찾기
↓
⑤ 파일 반환: logo.png 파일을 클라이언트에 반환
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// /resources/** 패턴 요청을 /static/ 폴더에서 찾음
registry.addResourceHandler("/resources/**")
.addResourceLocations("/static/");
}
}
<mvc:resources mapping="/resources/**" location="/static/" />
<!-- Spring Boot는 자동 설정 -->
# application.properties
spring.web.resources.static-locations=classpath:/static/
① 효율적인 리소스 관리: 명확한 역할 분리
② 확장 용이성: 리소스 경로 추가가 간편
③ 직관적인 URL: 깔끔한 URL 구조 유지
④ 캐싱 지원: 정적 리소스에 대한 브라우저 캐싱 최적화
Dispatcher Servlet이 요청을 처리하는 과정을 8단계로 나누어 상세히 분석합니다.

① 클라이언트 요청 수신
② Handler Mapping으로 컨트롤러 찾기
③ Handler Adapter 찾기
④ Handler Adapter가 컨트롤러로 요청 위임
⑤ 비즈니스 로직 처리
⑥ 컨트롤러가 반환값 반환
⑦ Handler Adapter가 반환값 처리
⑧ 클라이언트에 응답 반환
Dispatcher Servlet은 Front Controller로서 모든 요청을 최초로 수신합니다.
① 클라이언트가 HTTP 요청 전송
② Servlet Container(Tomcat)가 요청 수신
③ 필터 체인(Filter Chain) 통과
④ Dispatcher Servlet이 요청을 받음
Handler Mapping은 요청 URL과 HTTP 메서드를 기반으로 처리할 컨트롤러를 찾습니다.
// 1. 애플리케이션 시작 시 모든 @Controller 스캔
@Controller
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// ...
}
}
// 2. HashMap에 (요청 정보, HandlerMethod) 저장
// Key: RequestMappingInfo (HTTP Method + URI)
// Value: HandlerMethod (컨트롤러 + 메서드 정보)
Map<RequestMappingInfo, HandlerMethod> handlerMethods = ...
// 3. 요청이 오면 HashMap에서 HandlerMethod 찾기
// GET /users/123 → UserController.getUser() 메서드
중요: 찾은 HandlerMethod는 HandlerExecutionChain으로 감싸져 반환됩니다. 이는 인터셉터를 포함하기 위함입니다.
Dispatcher Servlet은 컨트롤러를 직접 호출하지 않고 Handler Adapter를 통해 호출합니다.
왜 Adapter를 사용할까?
컨트롤러 구현 방식이 다양하기 때문입니다.
@Controller
public class UserController {
@GetMapping("/users")
public String list() { return "userList"; }
}
// 방식 2: Controller 인터페이스 구현
public class OldStyleController implements Controller {
@Override
public ModelAndView handleRequest(...) {
// ...
}
}
// Handler Adapter 패턴으로 모든 방식을 통일되게 처리
주요 Handler Adapter:
- RequestMappingHandlerAdapter: @RequestMapping 방식 처리
- SimpleControllerHandlerAdapter: Controller 인터페이스 방식 처리
- HttpRequestHandlerAdapter: HttpRequestHandler 방식 처리
Handler Adapter는 컨트롤러를 호출하기 전에 전처리 작업을 수행합니다.
① 인터셉터 실행
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(...) {
// 인증 체크
return true; // true면 계속 진행
}
}
② ArgumentResolver로 파라미터 처리
@GetMapping("/users/{id}")
public User getUser(
@PathVariable Long id, // PathVariableArgumentResolver
@RequestParam String name, // RequestParamArgumentResolver
@RequestBody UserDto dto // RequestBodyArgumentResolver
) {
// ArgumentResolver가 자동으로 값을 바인딩해줌
}
③ 리플렉션으로 메서드 호출
Method method = handlerMethod.getMethod();
Object controller = handlerMethod.getBean();
// 실제 컨트롤러 메서드 실행
Object result = method.invoke(controller, args);
컨트롤러가 Service 계층을 호출하여 실제 비즈니스 로직을 수행합니다.
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// Service → Repository → DB 조회
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
}
컨트롤러는 처리 결과를 반환합니다. 반환 타입은 다양할 수 있습니다.
| 반환 타입 | 용도 | 예시 |
|---|---|---|
| ResponseEntity | RESTful API 응답 | ResponseEntity.ok(user) |
| String | View 이름 반환 | return "userList"; |
| ModelAndView | View + Model 함께 반환 | new ModelAndView("user", model) |
| @ResponseBody + 객체 | JSON 직렬화 | return user; |
Handler Adapter는 ReturnValueHandler를 사용하여 반환값을 후처리합니다.
① ResponseEntity 처리
ResponseEntity<User> response = ...
// 1. HTTP Status 설정 (200, 404, 500 등)
httpResponse.setStatus(response.getStatusCodeValue());
// 2. MessageConverter로 객체 → JSON 변환
String json = objectMapper.writeValueAsString(response.getBody());
// 3. Response Body에 JSON 작성
httpResponse.getWriter().write(json);
② View 이름 처리
String viewName = "userList";
// ViewResolver로 실제 View 찾기
View view = viewResolver.resolveViewName(viewName, locale);
// View 렌더링
view.render(model, request, response);
주요 MessageConverter
- MappingJackson2HttpMessageConverter: 객체 ↔ JSON
- StringHttpMessageConverter: String ↔ text/plain
- ByteArrayHttpMessageConverter: byte[] ↔ application/octet-stream
처리된 응답이 필터 체인을 역순으로 통과하여 클라이언트에게 반환됩니다.
Dispatcher Servlet → Filter3 → Filter2 → Filter1 → Servlet Container → Client
각 필터에서 응답에 대한 후처리(로깅, 압축 등)가 가능합니다.
A: 네, Dispatcher Servlet은 싱글톤입니다. 서블릿 컨테이너는 서블릿 인스턴스를 하나만 생성하여 모든 요청을 처리합니다. 따라서 멀티스레드 환경에서 안전하게 설계되어야 하며, 인스턴스 변수 사용 시 주의가 필요합니다.
A: 애플리케이션 시작 시점에 초기화됩니다. RequestMappingHandlerMapping은 모든 @Controller 빈을 스캔하여 @RequestMapping 정보를 추출하고 HashMap에 저장합니다. 따라서 런타임에 동적으로 매핑을 추가할 수는 없습니다.
A: 가장 큰 차이는 관리 주체와 실행 시점입니다.
필터(Filter)
- Servlet Container가 관리
- Dispatcher Servlet 이전/이후 실행
- Spring Bean에 접근 불가
- web.xml 또는 @WebFilter로 설정
인터셉터(Interceptor)
- Spring Container가 관리
- Controller 이전/이후 실행
- Spring Bean에 접근 가능
- WebMvcConfigurer로 설정
A: @RestController = @Controller + @ResponseBody
@Controller
public class ViewController {
@GetMapping("/page")
public String showPage() {
return "userPage"; // ViewResolver가 처리
}
}
// @RestController: JSON 반환
@RestController
public class ApiController {
@GetMapping("/api/users")
public List<User> getUsers() {
return userList; // MessageConverter가 JSON으로 변환
}
}
A: Spring은 가장 구체적인 매핑을 선택합니다.
public User getUser(@PathVariable Long id) { ... }
@GetMapping("/users/new") // 2. 구체적
public String newUserForm() { ... }
// GET /users/new 요청 시 → 2번 메서드 실행
// GET /users/123 요청 시 → 1번 메서드 실행
구체적인 경로가 변수를 포함한 경로보다 우선순위가 높습니다.
A: 각 파라미터 타입과 어노테이션에 맞는 Resolver가 값을 바인딩합니다.
@PostMapping("/users")
public User createUser(
@RequestBody UserDto dto, // RequestResponseBodyMethodProcessor
@RequestParam String name, // RequestParamMethodArgumentResolver
@PathVariable Long id, // PathVariableMethodArgumentResolver
@RequestHeader("Auth") String token // RequestHeaderMethodArgumentResolver
) {
// 모든 값이 자동으로 바인딩됨
}
커스텀 ArgumentResolver를 만들어 특수한 파라미터 처리도 가능합니다.
A: 다음 방법들로 성능을 최적화할 수 있습니다.
① 정적 리소스 캐싱
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/static/")
.setCachePeriod(3600); // 1시간 캐싱
}
② 비동기 요청 처리
public CompletableFuture<User> getUser() {
return CompletableFuture.supplyAsync(() -> {
// 비동기로 처리
return userService.findUser();
});
}
③ HTTP 압축 활성화
server.compression.enabled=true
server.compression.mime-types=application/json,text/html
Dispatcher Servlet은 Spring MVC의 핵심 컴포넌트로, Front Controller 패턴을 구현한 대표적인 예입니다.
핵심 요약
① Dispatcher Servlet은 모든 요청을 최초로 받는 Front Controller
② Handler Mapping으로 요청을 처리할 컨트롤러 탐색
③ Handler Adapter 패턴으로 다양한 컨트롤러 구현 방식 지원
④ ArgumentResolver와 ReturnValueHandler로 전후처리 자동화
⑤ 정적 리소스는 컨트롤러 우선 탐색 후 파일 시스템에서 찾기
실무 활용 팁
① @RestController로 RESTful API 구현
② 인터셉터로 인증/로깅 등 공통 처리
③ ArgumentResolver 커스터마이징으로 반복 코드 제거
④ 정적 리소스 캐싱으로 성능 최적화
Dispatcher Servlet의 동작 원리를 이해하면 Spring MVC의 전체 흐름을 파악할 수 있고, 문제 발생 시 디버깅이 훨씬 쉬워집니다. 8단계 요청 처리 과정을 기억하고, 각 단계에서 어떤 컴포넌트가 어떤 역할을 하는지 이해하는 것이 중요합니다.
끝.
'Development > Web' 카테고리의 다른 글
| [Web] Spring Boot 3.4 + AWS 환경 Tomcat vs JBoss WildFly 성능 비교 분석 (3) | 2025.04.07 |
|---|---|
| [Web] JEUS DB Connection Leak 완벽 해결 방법 (0) | 2024.07.24 |
| [Web] Spring Framework vs Spring Boot 핵심 차이점 완벽 비교 (0) | 2022.09.24 |
| [Web] Spring Framework 3가지 핵심 특징 - IoC, DI, AOP 완벽 이해 (0) | 2022.09.24 |
| [Web] 크로스 도메인 문제 원인부터 해결책까지 총정리 (0) | 2022.09.23 |