디스패처 서블릿 구조
Spring 프레임워크 7 DispatcherServlet이 HandlerMapping, HandlerAdapter, 인터셉터, 예외 리졸버, 뷰·메시지 컨버터를 실행하는 순서를 실제 이벤트로 검증합니다.
@GetMapping 메서드가 직접 Servlet 컨테이너에 등록되는 것은 아닙니다.
컨테이너는 DispatcherServlet 하나를 호출하고, DispatcherServlet이 매핑으로 핸들러를 찾고 어댑터로 호출하며 반환값을 뷰 또는 응답 본문으로 처리합니다.
이 흐름을 알면 404·인자 바인딩·컨트롤러 예외·뷰 오류를 같은 “컨트롤러 문제”로 뭉치지 않습니다.
HandlerMapping 탐색
RequestMappingHandlerMapping은 애노테이션 컨트롤러 메서드의 경로, HTTP 메서드, consumes, produces, 헤더·파라미터 조건을 조합해 핸들러를 찾습니다.
결과는 핸들러와 인터셉터를 포함한 실행 체인입니다.
매핑 단계에서 일어나는 대표 결과는 다음과 같습니다.
- 경로와 메서드가 일치하면 핸들러 메서드 선택
- 경로는 있지만 메서드가 다르면 405와
Allow consumes가 맞지 않으면 415produces를 만족하지 못하면 406- 어떤 경로도 없으면 404
- 같은 우선순위 후보가 모호하면 시작 또는 매핑 오류
컨트롤러 메서드 첫 줄에 중단점이 오지 않는다면 매핑과 인자 해결 이전 로그를 봅니다.
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 디버그 로그는 어떤 매핑이 등록되고 선택됐는지 보여 줄 수 있지만 운영에서 과도한 상세 로그와 민감 URI를 주의합니다.
HandlerAdapter 처리
RequestMappingHandlerAdapter는 애노테이션 핸들러 메서드를 실행합니다.
이 안에서 인자 리졸버가 @PathVariable, @RequestParam, @RequestBody, 인증 주체, Model 등을 만들고 반환 값 핸들러가 뷰 이름, ResponseEntity, @ResponseBody를 해석합니다.
직접 만든 프런트 컨트롤러의 파라미터 맵과 PageController 어댑터가 다음과 같이 확장된 셈입니다.
| 직접 구현 | Spring MVC |
|---|---|
| 핸들러 맵 | HandlerMapping |
| PageControllerAdapter | RequestMappingHandlerAdapter |
| 파라미터 문자열 파싱 | HandlerMethodArgumentResolver |
| ModelView | ModelAndView·반환 값 핸들러 |
| JspView 리졸버 | ViewResolver |
| JSON ObjectMapper 호출 | HttpMessageConverter |
어댑터가 핸들러 호출 방식을 아는 덕분에 DispatcherServlet은 애노테이션 메서드의 구체 시그니처를 알 필요가 없습니다.
인터셉터 실행 순서
요청 이벤트를 기록하는 인터셉터와 컨트롤러를 작은 MVC 컨텍스트에서 실행합니다.
package board.mvc;
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
class DispatcherFlowTest {
@RestController
static class FlowController {
private final List<String> events;
FlowController(List<String> events) {
this.events = events;
}
@GetMapping("/api/flow")
FlowResponse flow() {
events.add("controller");
return new FlowResponse("ok");
}
}
static final class RecordingInterceptor
implements HandlerInterceptor {
private final List<String> events;
RecordingInterceptor(List<String> events) {
this.events = events;
}
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
events.add("preHandle");
return true;
}
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {
events.add("postHandle");
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception failure) {
events.add("afterCompletion");
}
}
@Test
void mapping된_handler의_interceptor_순서를_검증한다() {
var events = new ArrayList<String>();
var client = RestTestClient
.bindToController(
new FlowController(events))
.configureServer(builder ->
builder.addInterceptors(
new RecordingInterceptor(events)))
.build();
client.get()
.uri("/api/flow")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.status").isEqualTo("ok");
assertThat(events).containsExactly(
"preHandle",
"controller",
"postHandle",
"afterCompletion");
}
}preHandle()이 false를 반환하면 컨트롤러는 호출되지 않습니다.
여러 인터셉터는 등록 순서로 preHandle()을 실행하고, 성공적으로 진입한 인터셉터가 역순으로 postHandle()·afterCompletion()되는 구조를 이해해야 자원 정리를 올바르게 배치할 수 있습니다.
afterCompletion은 뷰 렌더링까지 끝난 뒤 호출되며 예외 정보가 전달될 수 있습니다.
응답이 이미 커밋된 뒤 오류 본문을 새로 만드는 지점은 아닙니다.
DispatcherFlowTest
> mapping된_handler의_interceptor_순서를_검증한다() PASSED
events = [preHandle, controller, postHandle, afterCompletion]
BUILD SUCCESSFUL반환값 처리 경로
@Controller 메서드가 String을 반환하면 기본적으로 논리 뷰 이름입니다.
@ResponseBody 또는 @RestController의 객체 반환은 메시지 컨버터가 본문으로 씁니다.
ResponseEntity는 상태와 헤더, 본문을 함께 제어합니다.
JSON 컨트롤러에서 ViewResolver를 찾지 못했다는 오류가 난다면 애노테이션과 반환 값 핸들러 선택을 확인합니다.
반대로 HTML 컨트롤러에 @ResponseBody가 붙으면 "post-detail" 문자열 자체가 응답 본문으로 나갑니다.
뷰 경로에서는 컨트롤러의 ModelAndView를 예외 리졸버 처리 뒤 뷰 리졸버가 실제 뷰로 바꾸고 렌더링합니다.
본문 경로에서는 콘텐츠 협상으로 컨버터를 선택하고 응답에 씁니다.
예외 리졸버
핸들러가 예외를 던지면 DispatcherServlet은 등록된 HandlerExceptionResolver를 순서대로 시도합니다.
@ExceptionHandler와 @ControllerAdvice, 상태 애노테이션, 기본 리졸버가 여기에 참여합니다.
package board.web;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(DuplicatePostException.class)
ProblemDetail duplicate(
DuplicatePostException failure) {
var problem = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT,
"A post already exists for that day");
problem.setTitle("Duplicate post");
problem.setProperty(
"code", "POST_DUPLICATE");
return problem;
}
}예외 리졸버는 HTTP 표현 책임을 갖고 애플리케이션 예외는 HTTP를 모릅니다. 스택 트레이스와 데이터베이스 메시지를 클라이언트 상세로 그대로 보내지 않습니다. 트레이스 ID를 속성에 넣더라도 로그의 같은 ID와 연결되는지 검증합니다.
필터·인터셉터·AOP
| 확장 지점 | 알고 있는 범위 | 적합한 관심사 |
|---|---|---|
| Servlet Filter | 모든 Servlet 요청·디스패처 타입 | 인코딩, 보안 체인, 요청 ID |
| HandlerInterceptor | 선택된 MVC 핸들러 | 핸들러별 권한, MVC 경과 |
| ArgumentResolver | 컨트롤러 파라미터 | 현재 회원, 타입이 지정된 쿼리 |
| ControllerAdvice | MVC 예외·바인딩 | ProblemDetail·필드 오류 |
| Service AOP | 애플리케이션 메서드 | 트랜잭션, 메트릭, 정책 관측 |
정적 리소스와 다른 Servlet까지 포함해야 하면 필터, 컨트롤러 애노테이션 메타데이터가 필요하면 인터셉터를 사용합니다. 모든 관심사를 인터셉터에 몰면 매핑 실패 404는 관찰하지 못할 수 있습니다.
연습 문제
두 인터셉터를 등록해 실행 순서를 기록하세요.
첫 번째는 요청 ID, 두 번째는 핸들러 경과를 담당합니다.
컨트롤러 성공과 DuplicatePostException 실패에서 preHandle·postHandle·afterCompletion 순서가 어떻게 달라지는지 검증하고 어드바이스가 409 ProblemDetail을 만드는지 확인합니다.
해설 보기
성공 경로에서는 preHandle이 등록 순서, postHandle과 afterCompletion이 역순입니다. 핸들러가 예외를 던지면 일반적으로 postHandle은 실행되지 않고 예외 리졸버가 응답을 만든 뒤 afterCompletion이 실행됩니다.
var client = RestTestClient
.bindToController(controller)
.configureServer(builder -> builder
.addInterceptors(first, second)
.setControllerAdvice(
new ApiExceptionHandler()))
.build();실패 응답은 409, application/problem+json, code=POST_DUPLICATE를 확인합니다.
이벤트 검증은 프레임워크 부 버전의 내부 구현 세부가 아니라 공개 인터셉터 콜백 계약에 집중합니다.
다음 장에서는 DispatcherServlet 흐름 위에서 @RequestMapping 조건, URI 변수, 쿼리·폼·JSON 바인딩, 메시지 컨버터, CRUD 응답을 실제 게시판 MVC API로 확장합니다.