스프링 MVC의 모든 요청이 통과하는 단일 진입점(프런트 컨트롤러).
왜 필요한가 — 프런트 컨트롤러 패턴
없다면: 요청마다 서블릿을 따로 만들고, 각 서블릿이
- 인코딩·인증·예외 처리·뷰 렌더링을 전부 중복 구현
있다면: 공통 처리를 한 곳에 모으고, 개별 컨트롤러는 자기 일만 한다
처리 흐름
다이어그램 로딩 중…
각 구성요소
| 구성요소 | 역할 |
|---|---|
| HandlerMapping | URL·메서드 → 어떤 핸들러인지 결정 |
| HandlerAdapter | 핸들러 실행. 파라미터 바인딩·반환값 처리 |
| HandlerExceptionResolver | 예외 → 응답 변환(@ControllerAdvice) |
| ViewResolver | 뷰 이름 → View 객체 |
| ArgumentResolver | 메서드 파라미터를 만들어 주는 확장점 |
| MessageConverter | 본문 ↔ 객체 변환(JSON) |
@RequestBody가 어떻게 객체가 되는가
요청 본문(JSON) → HttpMessageConverter
- MappingJackson2HttpMessageConverter
- Jackson이 역직렬화 → User 객체
반대로 @ResponseBody나 @RestController면
ViewResolver를 타지 않고 컨버터가 바로 응답 본문을 만든다.
- @Controller — → ViewResolver → JSP/Thymeleaf 렌더링
- @RestController — → MessageConverter → JSON 직접 출력
이 분기를 이해하면 "왜 뷰 이름으로 해석돼서 404가 나지?"
(= @ResponseBody를 빠뜨림) 같은 문제를 바로 진단할 수 있다.
ArgumentResolver — 실무에서 가장 유용한 확장점
@GetMapping("/me")
public UserDto me(@LoginUser User user) { … } // 이게 어떻게 채워지나?
public class LoginUserResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter p) {
return p.hasParameterAnnotation(LoginUser.class);
}
public Object resolveArgument(…) {
return userService.find(session.getAttribute("userId"));
}
}
컨트롤러마다 세션에서 사용자를 꺼내는 코드를 반복하지 않아도 된다.
@RequestParam, @PathVariable도 전부 같은 구조로 구현되어 있다.
예외 처리 흐름
컨트롤러에서 예외 발생
↓
DispatcherServlet 이 HandlerExceptionResolver 들에게 차례로 물어본다
↓
ExceptionHandlerExceptionResolver → @ExceptionHandler 를 찾는다
↓
없으면 → ResponseStatusExceptionResolver → DefaultHandlerExceptionResolver
↓
아무도 처리 못 하면 → 서블릿 컨테이너로 → 기본 오류 페이지
@ControllerAdvice + @ExceptionHandler 로 전역 예외 처리를 구성한다.
단, Filter에서 난 예외는 DispatcherServlet 밖이라 여기 못 온다.
등록
스프링 부트에서는 자동 등록된다.
- spring.mvc.servlet.path=/ — # 기본값. 모든 요청을 받는다
/ 매핑이라 정적 리소스 요청도 일단 여기로 온다
(ResourceHttpRequestHandler가 처리).
초기화 시점 — 기본적으로 첫 요청 때 초기화(lazy)된다. 그래서
첫 요청이 유독 느리다. spring.mvc.servlet.load-on-startup=1 로
기동 시 초기화하면 이 지연을 없앨 수 있다 — 웜업이 필요한 서비스에서 유용하다.