포스트

spring-internals-lab로 다시 읽는 Spring 8 - DispatcherServlet과 MVC 요청 흐름

GitHub 저장소

이번 글의 질문

Spring MVC를 처음 배울 때는 @GetMapping@RequestBody부터 보게 된다. 하지만 이 시점에는 아직 가장 중요한 질문이 비어 있다.

  • 요청은 어떤 순서로 컨트롤러 메서드에 도달하는가
  • HandlerMappingHandlerAdapter는 왜 분리되어 있는가
  • 리터럴 경로와 변수 경로가 겹치면 누가 이기는가
  • 인터셉터는 어디서 실행되고, 예외나 차단 상황에서는 무엇이 생략되는가

이번 글은 dispatcher-servlet-tracemini-webmvc를 바탕으로, MVC를 Front Controller + 책임 분리 파이프라인으로 정리한다.

DispatcherServlet 자체는 의외로 단순하다

핵심 흐름만 줄이면 거의 이렇게 볼 수 있다.

1
2
3
4
5
6
handler = getHandler(request);
adapter = getHandlerAdapter(handler);
mappedHandler.applyPreHandle(...);
result = adapter.handle(...);
mappedHandler.applyPostHandle(...);
processDispatchResult(...);

DispatcherServlet은 모든 걸 직접 처리하기보다, 찾아서 위임한다.

flowchart TD
    A["HTTP Request"] --> B["HandlerMapping으로 핸들러 찾기"]
    B --> C["HandlerAdapter로 실제 호출"]
    C --> D["반환값 처리"]
    D --> E["응답 생성"]

즉 이 서블릿의 본질은 “모든 걸 하는 객체”가 아니라 나머지 협력 객체들에게 일을 떠넘기는 Front Controller다.

HandlerMappingHandlerAdapter를 왜 굳이 나눌까

이 분리는 Spring MVC의 설계 감각을 가장 잘 드러낸다.

타입질문
HandlerMapping이 요청에 맞는 핸들러가 무엇인가
HandlerAdapter그 핸들러를 실제로 어떻게 호출할 것인가

이 둘을 분리하지 않으면, 새로운 핸들러 형태가 생길 때마다 라우팅 로직까지 같이 수정해야 한다.

반대로 분리하면 다음이 가능하다.

  • 애노테이션 기반 핸들러
  • HttpRequestHandler
  • 레거시 Controller

같은 요청 매핑 시스템 안에서 서로 다른 “호출 방식”을 공존시킬 수 있다.

즉 MVC의 유연성은 애노테이션 문법보다 먼저, 찾기와 실행을 나눈 설계에서 나온다.

실제 관찰에서 드러난 세 가지 포인트

dispatcher-servlet-trace 실험으로 특히 명확해진 건 세 가지다.

1. 리터럴 경로가 변수 경로보다 우선한다

/users/me/users/{id}가 같이 있으면 등록 순서가 아니라 리터럴 경로가 우선된다.

이건 아주 중요하다. 만약 등록 순서에 의존했다면 컴포넌트 스캔 순서나 설정 구조가 바뀔 때 라우팅 결과도 흔들릴 수 있다.

즉 Spring은 “더 구체적인 패턴이 이긴다”는 고정 규칙을 택한다.

2. 매핑 못 찾으면 404로 끝난다

매칭되는 핸들러가 없으면 곧바로 404다. 특별한 컨트롤러 예외 처리 이전에, 핸들러 탐색 단계에서 종료된다.

3. 처리하지 못한 예외는 그대로 전파될 수 있다

컨트롤러가 던진 모든 예외가 자동으로 예쁜 HTTP 응답이 되는 건 아니다. 적절한 ExceptionResolver가 처리하지 못하면 예외는 그대로 밖으로 나간다.

즉 MVC의 예외 처리는 “무조건 500 응답 생성”보다, Resolver 체인을 통한 변환 시도에 가깝다.

인터셉터는 양파 구조지만, 예외가 나면 일부 단계는 생략된다

인터셉터의 핵심 메서드는 세 가지다.

  • preHandle
  • postHandle
  • afterCompletion

하지만 이 셋이 항상 다 호출되는 건 아니다.

상황호출 결과
정상 처리preHandle -> postHandle -> afterCompletion
preHandle=false컨트롤러 미호출, 이미 통과한 인터셉터만 afterCompletion
컨트롤러 예외postHandle 생략, afterCompletion은 실행 가능

이 구조는 트랜잭션의 try/finally와 비슷하다. postHandle은 정상 결과를 다듬는 훅이고, afterCompletion은 뒷정리 훅에 가깝다.

flowchart TD
    A["preHandle 1"] --> B["preHandle 2"]
    B --> C{"false 반환?"}
    C -- yes --> D["이미 통과한 인터셉터 afterCompletion"]
    C -- no --> E["컨트롤러 호출"]
    E --> F{"예외 발생?"}
    F -- no --> G["postHandle 역순"]
    G --> H["afterCompletion 역순"]
    F -- yes --> H

JSON 응답도 자동이 아니라 협력 객체 덕분이다

실험에서 실제로 겪은 함정 중 하나가 이것이다. Jackson이 없으면 @RestController가 있다고 해서 JSON 응답이 저절로 만들어지지 않는다.

즉 아래는 다 별도 협력 객체의 역할이다.

  • @RequestBody 역직렬화
  • 객체를 JSON으로 직렬화
  • ResponseEntity 처리

이건 MVC를 이해할 때 중요한 감각이다. @RestController는 기능 자체가 아니라, 여러 메시지 변환기와 반환값 처리기를 묶은 경험에 가깝다.

mini-webmvc는 구조의 핵심만 남긴다

mini-webmvcMiniDispatcherServlet은 아주 작은 반복문으로 시작한다.

1
2
3
4
for (HandlerMapping mapping : handlerMappings) {
    HandlerMethod handler = mapping.getHandler(request);
    if (handler != null) return handler;
}

이 구현이 보여주는 사실은 단순하다.

  • Front Controller 자체는 복잡하지 않다.
  • 복잡성은 handler mapping, adapter, argument resolution, return value handling 쪽에 쌓인다.

DispatcherServlet은 거대한 중앙 로직이라기보다, 작은 규칙들을 모아 흐름을 조율하는 coordinator에 가깝다.

정리

이번 글의 핵심은 네 가지다.

  1. DispatcherServlet의 본질은 Front Controller와 위임이다.
  2. HandlerMappingHandlerAdapter 분리가 MVC 확장의 핵심이다.
  3. 인터셉터는 정상/예외/차단 경로에서 서로 다르게 호출된다.
  4. @RestController 경험은 여러 메시지 변환기와 처리기의 합성 결과다.

결국 Spring MVC는 “컨트롤러 메서드 호출 프레임워크”가 아니라, 라우팅, 호출, 변환, 예외 처리, 인터셉션을 분리한 파이프라인이다.

다음 글에서는 마지막으로 Spring Boot 쪽을 본다. SpringApplication, 자동 설정, 조건부 설정이 어떻게 기존 컨테이너 위에 얇게 얹히는지 정리한다.

  1. 1 spring-internals-lab로 다시 읽는 Spring 1 - 이 프로젝트는 무엇을 검증하려는가
  2. 2 spring-internals-lab로 다시 읽는 Spring 2 - BeanDefinition과 등록 단계
  3. 3 spring-internals-lab로 다시 읽는 Spring 3 - refresh()와 Bean 생성 파이프라인
  4. 4 spring-internals-lab로 다시 읽는 Spring 4 - 의존성 주입과 후보 선택
  5. 5 spring-internals-lab로 다시 읽는 Spring 5 - Bean lifecycle과 후처리기
  6. 6 spring-internals-lab로 다시 읽는 Spring 6 - 프록시와 AOP
  7. 7 spring-internals-lab로 다시 읽는 Spring 7 - @Transactional의 실체
  8. 8 spring-internals-lab로 다시 읽는 Spring 8 - DispatcherServlet과 MVC 요청 흐름
  9. 9 spring-internals-lab로 다시 읽는 Spring 9 - Spring Boot는 무엇을 자동으로 조립하는가
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

댓글

아직 댓글이 없습니다