Spring MVC 내부 동작 구조 (Deep Dive)

2025. 11. 4. 16:15Java/Spring

❶ 전체 러닝타임 흐름(요청 → 응답) 한눈에 보기

요청이 들어오면 서블릿 컨테이너(Tomcat)가 먼저 받고, DispatcherServlet으로 전달합니다. 그 다음 Spring MVC 내부 컴포넌트들이 다음 순서로 협업합니다.

  1. Filter Chain (예: CharacterEncodingFilter, HiddenHttpMethodFilter)
  2. DispatcherServlet 진입
  3. HandlerMapping으로 핸들러(컨트롤러 메서드) 탐색
  4. HandlerAdapter핸들러 실행 준비 (인자 바인딩/리턴 처리)
  5. HandlerMethodArgumentResolver로 파라미터 주입
  6. 컨트롤러 메서드 실행 → String 뷰이름 / ModelAndView / @ResponseBody 등 반환
  7. HandlerMethodReturnValueHandler로 반환값 처리
  8. ViewResolver로 뷰 선택(예: JSP) 또는 HttpMessageConverter로 JSON 변환
  9. 렌더링/직렬화 후 DispatcherServlet이 응답 전송
  10. (예외 시) 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 경로 변수
    • @RequestBody JSON → 객체 (MessageConverter)
    • HttpServletRequest/Response, Principal, Locale 등 웹객체
  • Return Value Handling: 반환값을 응답으로 바꾸는 전략
    • 문자열 뷰이름 → ViewResolver로 JSP 선택
    • ModelAndView → 바로 렌더
    • @ResponseBody / ResponseEntityHttpMessageConverter로 JSON 등

❺ ViewResolver vs HttpMessageConverter

  • ViewResolver 체인 (JSP 등 템플릿 렌더링)
    • 예: InternalResourceViewResolver + prefix/suffix → /WEB-INF/jsp/hello.jsp
    • JSP라면 RequestDispatcher.forward()로 서버사이드 렌더링
  • HttpMessageConverter (바디 직렬화/역직렬화)
    • @ResponseBodyResponseEntity인 경우 동작
    • 예: 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/모바일 등) 필요 → @RestController or @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/템플릿 선택, @ResponseBodyHttpMessageConverter로 JSON.
  • 예외는 HandlerExceptionResolver가 뷰/JSON으로 변환.
  • 가장 바깥에서 Filter, MVC 안쪽에서 Interceptor가 공통 관심사를 처리.