Spring MVC 내부 동작 구조 (Deep Dive)
2025. 11. 4. 16:15ㆍJava/Spring
❶ 전체 러닝타임 흐름(요청 → 응답) 한눈에 보기
요청이 들어오면 서블릿 컨테이너(Tomcat)가 먼저 받고, DispatcherServlet으로 전달합니다. 그 다음 Spring MVC 내부 컴포넌트들이 다음 순서로 협업합니다.
- Filter Chain (예:
CharacterEncodingFilter,HiddenHttpMethodFilter) - DispatcherServlet 진입
- HandlerMapping으로 핸들러(컨트롤러 메서드) 탐색
- HandlerAdapter가 핸들러 실행 준비 (인자 바인딩/리턴 처리)
- HandlerMethodArgumentResolver로 파라미터 주입
- 컨트롤러 메서드 실행 →
String뷰이름 /ModelAndView/@ResponseBody등 반환 - HandlerMethodReturnValueHandler로 반환값 처리
- ViewResolver로 뷰 선택(예: JSP) 또는 HttpMessageConverter로 JSON 변환
- 렌더링/직렬화 후 DispatcherServlet이 응답 전송
- (예외 시) HandlerExceptionResolver가 예외를 뷰/JSON으로 변환

앞으로 백엔드 개발자로 취업하거나 면접을 보기전엔 이 구조를 머릿속으로 자연스럽게 떠올리는 사람이 되고싶다..
간단한 흐름까지는 생각할수있는데 아직까진 완벽히 면접에서 설명하듯이 할 자신은 없다.
하지만 꾸준히 계속 상기시키며 내부동작구조를 완벽히 이해해야겠다.
❷ DispatcherServlet 라이프사이클 & 초기화 전략
- 서블릿 등록: 스프링 부트는 자동으로
DispatcherServlet을 등록합니다(기본 매핑/). - 초기화 시점: 컨텍스트 로딩 시 내부 전략 빈(전략 패턴)을 조회/사용합니다.
HandlerMapping(요청 → 핸들러 매핑)HandlerAdapter(핸들러 실행 어댑터)HandlerExceptionResolver(예외 → 응답 변환)ViewResolver(뷰 이름 → 뷰 선택)
- 전략 결정: 빈으로 등록된 구현들을 우선순위대로 모아서 체인 형태로 사용합니다.
❸ HandlerMapping: 어떤 메서드를 실행할까?
핸들러 매핑은 URL과 HTTP 메서드 등을 기준으로 컨트롤러 메서드(=핸들러)를 찾아냅니다. 현대 스프링에서는 주로 RequestMappingHandlerMapping이 동작합니다.
- 대상:
@Controller/@RestController+@RequestMapping계열 애노테이션 - 매칭 요소: 경로(
/hello), HTTP 메서드(GET/POST...), 컨슘/프로듀스(consumes/produces), 파라미터/헤더 조건 등 - 결과:
HandlerMethod(컨트롤러 인스턴스 + 실행할 메서드 + 메타정보)
❹ HandlerAdapter: 어떻게 실행할까?
찾아낸 핸들러를 실제로 호출하려면 호출 규약이 필요합니다. 그 역할이 HandlerAdapter입니다. 대표적으로 RequestMappingHandlerAdapter가 동작합니다.
- Argument Resolution: 메서드 파라미터에 값을 주입
@RequestParam쿼리/폼 값@PathVariable경로 변수@RequestBodyJSON → 객체 (MessageConverter)HttpServletRequest/Response,Principal,Locale등 웹객체
- Return Value Handling: 반환값을 응답으로 바꾸는 전략
- 문자열 뷰이름 →
ViewResolver로 JSP 선택 ModelAndView→ 바로 렌더@ResponseBody/ResponseEntity→HttpMessageConverter로 JSON 등
- 문자열 뷰이름 →
❺ ViewResolver vs HttpMessageConverter
- ViewResolver 체인 (JSP 등 템플릿 렌더링)
- 예:
InternalResourceViewResolver+ prefix/suffix →/WEB-INF/jsp/hello.jsp - JSP라면
RequestDispatcher.forward()로 서버사이드 렌더링
- 예:
- HttpMessageConverter (바디 직렬화/역직렬화)
@ResponseBody나ResponseEntity인 경우 동작- 예:
MappingJackson2HttpMessageConverter가 객체 → JSON 변환 - 반대로
@RequestBody는 JSON → 객체 역직렬화
# JSP 뷰 리졸버 예시
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
❻ 예외 처리: HandlerExceptionResolver
컨트롤러 실행/바인딩 중 예외가 발생하면 HandlerExceptionResolver들이 관여하여 오류 페이지 or JSON 오류 응답으로 바꿉니다.
- 기본 제공:
ExceptionHandlerExceptionResolver(컨트롤러의@ExceptionHandler탐색) - 글로벌 처리:
@ControllerAdvice+@ExceptionHandler - 필요시 순서(
@Order) 조정으로 우선순위 제어
❼ 인터셉터(HandlerInterceptor) & 필터(Filter)
- Filter (서블릿 레벨, 서블릿 진입 전후)
- 입·출력 공통 처리: 인코딩, 보안 헤더, 로깅 등
- 예:
CharacterEncodingFilter,HiddenHttpMethodFilter
- HandlerInterceptor (스프링 MVC 레벨)
preHandle(핸들러 실행 전)postHandle(핸들러 실행 후, 뷰 렌더 전)afterCompletion(뷰 렌더 후, 리소스 정리/로깅)- 특정 경로 패턴에만 적용 가능 (
/**,/api/**등)
❽ 정적 리소스 vs 뷰
- 정적 리소스:
src/main/resources/static등에 두면 리소스 핸들러가 직접 서빙 - JSP:
/WEB-INF/jsp아래는 직접 접근 불가 → 반드시 컨트롤러를 통해 뷰 이름으로 접근
❾ 스레드·트랜잭션 관점
- 각 HTTP 요청은 서블릿 컨테이너의 워커 스레드 하나에서 처리
- 동일 요청 내에서
@Transactional은 같은 스레드 전파 정책 하에 동작 - 블로킹 I/O가 길면 스레드 풀 고갈 → 페이징/타임아웃/캐시 고려
➓ JSP 렌더링 vs REST 응답, 언제 무엇을?
- 서버 사이드 HTML(JSP/Thymeleaf 등) 필요 → 문자열 뷰이름 반환,
ViewResolver경유 - JSON API(SPA/모바일 등) 필요 →
@RestControlleror@ResponseBody로 객체 반환 →HttpMessageConverter
// JSP 반환 예시
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello"; // /WEB-INF/jsp/hello.jsp
}
}
// JSON 반환 예시
@RestController
public class ApiController {
@GetMapping("/api/hello")
public Map<String,Object> api() {
return Map.of("msg", "hi"); // JSON으로 직렬화
}
}
✅ 핵심 포인트 요약
- 요청은 항상 DispatcherServlet을 통과한다.
- HandlerMapping이 실행 대상 메서드 결정, HandlerAdapter가 호출 전 과정(인자 바인딩/리턴 처리) 담당.
- 반환이 뷰 이름이면 ViewResolver로 JSP/템플릿 선택, @ResponseBody면 HttpMessageConverter로 JSON.
- 예외는 HandlerExceptionResolver가 뷰/JSON으로 변환.
- 가장 바깥에서 Filter, MVC 안쪽에서 Interceptor가 공통 관심사를 처리.