Spring MVC 요청 처리 흐름 완전 정복 – DispatcherServlet부터 View까지


Spring MVC 요청 처리 흐름, 공부하다 보면 “도대체 요청이 어디서 어디로 가는 걸까?” 헷갈린 적 있으신가요? Spring MVC를 처음 접하는 분들도, 면접을 앞두고 다시 정리하고 싶은 분들도 이 글 하나면 충분합니다. DispatcherServlet이 요청을 받아서 View가 화면에 응답을 돌려주기까지, 각 단계를 그림 그리듯 차근차근 설명해 드립니다.


목차

  1. Spring MVC란 무엇인가 – 기초 개념 정리
  2. Spring MVC 요청 처리 흐름의 핵심 원리
  3. 각 컴포넌트가 가져다주는 장점
  4. 자주 하는 실수와 주의점
  5. 실전 단계별 활용법 – 코드로 보는 흐름
  6. 전문가 관점과 추천 학습 도구

1. Spring MVC란 무엇인가 – 기초 개념 정리

Spring MVC는 Java 기반 웹 애플리케이션을 만들 때 사용하는 웹 프레임워크입니다. MVC는 ModelViewController의 약자로, 웹 애플리케이션의 역할을 세 가지로 나눠서 관리하는 설계 방식입니다.

쉽게 비유하면 이렇습니다. 식당에서 손님(사용자)이 주문(HTTP 요청)을 하면, 웨이터(Controller)가 주방(Model)에 전달하고, 완성된 음식을 테이블(View)에 올려주는 구조입니다.

MVC 패턴의 세 가지 역할

  • Model: 데이터와 비즈니스 로직을 담당합니다. 데이터베이스와 통신하고, 필요한 정보를 가공합니다.
  • View: 사용자에게 보여주는 화면(HTML, Thymeleaf, JSP 등)을 담당합니다.
  • Controller: 사용자의 요청을 받아 Model에 작업을 지시하고, 그 결과를 View로 연결하는 중간 관리자 역할입니다.

Front Controller 패턴이란?

Spring MVC는 Front Controller 패턴을 기반으로 합니다. 모든 HTTP 요청이 하나의 진입점, 즉 DispatcherServlet으로 먼저 들어오도록 설계되어 있습니다. 이 방식 덕분에 인증, 로깅, 공통 처리 등을 한 곳에서 관리할 수 있고, 코드의 일관성과 유지보수성이 크게 높아집니다.

기존 서블릿 방식에서는 URL마다 각각 서블릿을 등록해야 해서 코드가 복잡해졌지만, Spring MVC는 DispatcherServlet 하나가 모든 요청을 받은 뒤 적절한 컨트롤러로 위임하는 방식을 채택했습니다. 이 덕분에 개발자는 비즈니스 로직에만 집중할 수 있게 되었습니다.

Spring MVC는 Spring Framework의 spring-webmvc 모듈에 포함되어 있으며, Spring Boot를 사용하면 별도 설정 없이도 자동으로 MVC 환경이 구성됩니다.


2. Spring MVC 요청 처리 흐름의 핵심 원리

이제 Spring MVC 요청 처리 흐름의 핵심 단계를 순서대로 살펴보겠습니다. 아래 흐름을 단계별로 외워두면 면접 질문에도 자신 있게 답할 수 있습니다.

[클라이언트] → ① DispatcherServlet
             → ② HandlerMapping
             → ③ HandlerAdapter
             → ④ Controller(Handler)
             → ⑤ ModelAndView 반환
             → ⑥ ViewResolver
             → ⑦ View 렌더링
             → [클라이언트에 응답]

① DispatcherServlet – 모든 요청의 시작점

클라이언트(브라우저)에서 HTTP 요청이 오면 가장 먼저 DispatcherServlet이 받습니다. DispatcherServlet은 web.xml 또는 Spring Boot의 자동 설정에 의해 모든 URL(/)에 매핑되어 있습니다. 이 서블릿은 요청을 직접 처리하지 않고, 적절한 컴포넌트에게 위임하는 중앙 조율자 역할을 합니다.

② HandlerMapping – 어떤 컨트롤러로 보낼까?

DispatcherServlet은 요청 URL을 보고 “이 요청을 어느 컨트롤러가 처리해야 하지?”를 HandlerMapping에게 물어봅니다. HandlerMapping은 등록된 URL 패턴과 컨트롤러 메서드의 매핑 정보를 갖고 있으며, 가장 많이 사용되는 구현체는 RequestMappingHandlerMapping입니다. 이 구현체는 @RequestMapping@GetMapping@PostMapping 등의 어노테이션을 스캔하여 매핑 테이블을 만들어 둡니다.

HandlerMapping은 요청에 맞는 Handler(컨트롤러 메서드) 객체와, 그 핸들러를 실행하기 위한 인터셉터 체인을 함께 반환합니다.

③ HandlerAdapter – 실제로 컨트롤러를 실행

DispatcherServlet은 HandlerMapping에서 받은 Handler를 직접 실행하지 않습니다. 대신 HandlerAdapter에게 실행을 요청합니다. HandlerAdapter는 다양한 형태의 컨트롤러(어노테이션 기반, 인터페이스 기반 등)를 일관된 방식으로 호출할 수 있도록 어댑터 역할을 합니다. 가장 널리 쓰이는 구현체는 RequestMappingHandlerAdapter입니다.

HandlerAdapter는 컨트롤러 메서드의 파라미터(@RequestParam@PathVariable@RequestBody 등)를 자동으로 바인딩하고, 메서드를 실행한 뒤 결과를 ModelAndView 형태로 반환합니다.

④ Controller – 비즈니스 로직 처리

개발자가 직접 작성하는 Controller 메서드가 실행됩니다. 컨트롤러는 서비스 레이어를 호출하여 필요한 데이터를 가져오고, 뷰에 전달할 데이터를 Model에 담습니다. 반환값으로 뷰 이름(문자열)이나 ModelAndView 객체, 또는 @ResponseBody를 통한 JSON 데이터를 반환할 수 있습니다.

⑤ ViewResolver – 뷰 이름을 실제 파일로 변환

컨트롤러가 반환한 뷰 이름(예: "home")을 실제 뷰 파일 경로로 변환하는 역할을 ViewResolver가 담당합니다. 예를 들어 InternalResourceViewResolver를 사용하면 "home"이라는 이름이 /WEB-INF/views/home.jsp로 변환됩니다. Thymeleaf를 사용하면 templates/home.html로 매핑됩니다.

⑥ View – 최종 응답 생성

ViewResolver가 찾아낸 View 객체가 Model에 담긴 데이터를 이용하여 최종 HTML(또는 JSON) 응답을 생성합니다. 완성된 응답은 클라이언트의 브라우저로 전송됩니다.


3. 각 컴포넌트가 가져다주는 장점

Spring MVC 요청 처리 흐름이 이렇게 여러 단계로 나뉘어져 있는 이유는 무엇일까요? 각 컴포넌트가 명확하게 역할을 분리하기 때문에 다음과 같은 장점이 생깁니다.

유지보수성과 확장성

각 컴포넌트가 독립적으로 존재하기 때문에 특정 부분만 교체하거나 확장할 수 있습니다. 예를 들어 뷰 기술을 JSP에서 Thymeleaf로 교체하고 싶다면 ViewResolver 설정만 바꾸면 됩니다. Controller 코드는 전혀 건드릴 필요가 없습니다.

HandlerInterceptor를 활용하면 인증, 권한 확인, 로깅 등의 공통 기능을 컨트롤러 코드 외부에서 처리할 수 있습니다. 이는 코드 중복을 크게 줄여줍니다.

테스트 용이성

Controller, Service, Repository 레이어가 명확히 분리되어 있어 단위 테스트 작성이 매우 쉽습니다. Spring에서 제공하는 MockMvc를 이용하면 실제 서블릿 컨테이너 없이도 컨트롤러 레이어를 독립적으로 테스트할 수 있습니다.

다양한 응답 형식 지원

Spring MVC는 HTML 뷰뿐만 아니라 JSON, XML, 파일 다운로드 등 다양한 응답 형식을 쉽게 지원합니다. @ResponseBody나 @RestController를 사용하면 ViewResolver 단계를 생략하고 객체를 바로 JSON으로 직렬화하여 응답할 수 있습니다. 이는 REST API 개발에 매우 유리합니다.

또한 ContentNegotiationViewResolver를 활용하면 클라이언트가 요청하는 Accept 헤더에 따라 동일한 엔드포인트에서 HTML과 JSON을 모두 응답할 수 있습니다.

Spring MVC의 이러한 유연성 덕분에 전통적인 MPA(Multi Page Application)부터 현대적인 REST API 서버까지 폭넓게 활용됩니다.


4. 자주 하는 실수와 주의점

Spring MVC를 처음 배울 때 많은 분들이 공통적으로 실수하는 부분들이 있습니다. 미리 알아두면 불필요한 디버깅 시간을 크게 줄일 수 있습니다.

컴포넌트 스캔 범위 설정 오류

가장 흔한 실수 중 하나는 @ComponentScan 범위를 잘못 설정하는 것입니다. 컨트롤러 클래스에 @Controller 어노테이션을 붙였는데도 404 오류가 난다면, DispatcherServlet의 ApplicationContext가 해당 패키지를 스캔하지 못하고 있을 가능성이 높습니다. Spring Boot에서는 메인 클래스(@SpringBootApplication)가 위치한 패키지와 그 하위 패키지를 자동으로 스캔하므로, 컨트롤러 클래스가 메인 클래스보다 상위 패키지에 있으면 인식되지 않습니다.

ViewResolver 설정 충돌

여러 종류의 ViewResolver를 동시에 등록했을 때 우선순위 설정이 잘못되면 예상치 못한 뷰가 렌더링됩니다. InternalResourceViewResolver는 일반적으로 우선순위를 가장 낮게 설정해야 합니다. Thymeleaf와 JSP를 함께 사용하는 경우에도 마찬가지입니다.

@RequestBody와 @ModelAttribute 혼용

REST API에서는 @RequestBody로 JSON 데이터를 받고, 폼 데이터는 @ModelAttribute로 받습니다. 두 어노테이션을 혼용하면 데이터 바인딩이 실패합니다. HTTP Content-Type 헤더가 application/json이면 @RequestBody를, application/x-www-form-urlencoded이면 @ModelAttribute를 사용하세요.

트랜잭션 경계 오해

Controller 레이어에 @Transactional을 붙이는 경우가 있는데, 트랜잭션 관리는 Service 레이어에서 하는 것이 올바른 패턴입니다. Controller는 요청을 받아 Service에 위임하는 역할에 충실해야 합니다. 트랜잭션 경계가 지나치게 넓어지면 DB 커넥션 점유 시간이 늘어나 성능 문제로 이어질 수 있습니다.

필터(Filter)와 인터셉터(Interceptor) 역할 혼동

필터는 서블릿 컨테이너 레벨에서 동작하여 DispatcherServlet에 도달하기 전에 요청을 처리합니다. 인터셉터는 Spring MVC 레벨에서 동작하여 DispatcherServlet과 Controller 사이에서 작동합니다. 인코딩 처리나 CORS 설정처럼 Spring 컨텍스트와 무관한 처리는 필터에서, 인증·권한 확인처럼 Spring 빈에 접근이 필요한 처리는 인터셉터에서 하는 것이 적합합니다.


5. 실전 단계별 활용법 – 코드로 보는 흐름

이론으로 익힌 Spring MVC 요청 처리 흐름을 실제 코드로 확인해 봅시다. 간단한 게시글 조회 기능을 예시로 사용합니다.

Step 1. 의존성 추가 (Spring Boot 기준)

xml

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Spring Boot Starter Web을 추가하면 DispatcherServlet 설정, HandlerMapping, HandlerAdapter, ViewResolver 등이 자동으로 구성됩니다.

Step 2. Controller 작성

java

@Controller
@RequestMapping("/posts")
public class PostController {

    private final PostService postService;

    public PostController(PostService postService) {
        this.postService = postService;
    }

    // GET /posts/1 요청 처리
    @GetMapping("/{id}")
    public String getPost(@PathVariable Long id, Model model) {
        PostDto post = postService.findById(id);
        model.addAttribute("post", post); // Model에 데이터 담기
        return "post/detail"; // ViewResolver가 templates/post/detail.html을 찾음
    }
}

@GetMapping("/{id}")가 HandlerMapping에 의해 GET /posts/{id} 요청과 연결됩니다. Model에 데이터를 담으면 View에서 해당 데이터를 사용할 수 있습니다.

Step 3. Service 레이어

java

@Service
public class PostService {

    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public PostDto findById(Long id) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("게시글을 찾을 수 없습니다."));
        return PostDto.from(post);
    }
}

Controller는 Service에 작업을 위임하고, Service는 Repository를 통해 데이터를 조회합니다. 각 레이어의 역할이 명확히 분리됩니다.

Step 4. Thymeleaf View 작성

html

<!-- templates/post/detail.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="${post.title}">제목</title>
</head>
<body>
    <h1 th:text="${post.title}">게시글 제목</h1>
    <p th:text="${post.content}">게시글 내용</p>
</body>
</html>

ViewResolver가 "post/detail" 이라는 뷰 이름을 templates/post/detail.html로 변환하고, Thymeleaf가 Model 데이터를 이용해 최종 HTML을 생성합니다.

Step 5. REST API로 변환하기

뷰가 아닌 JSON 응답이 필요하다면 @RestController를 사용합니다.

java

@RestController
@RequestMapping("/api/posts")
public class PostApiController {

    private final PostService postService;

    public PostApiController(PostService postService) {
        this.postService = postService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<PostDto> getPost(@PathVariable Long id) {
        PostDto post = postService.findById(id);
        return ResponseEntity.ok(post);
    }
}

@RestController = @Controller + @ResponseBody이므로 ViewResolver 단계를 건너뛰고, Jackson 라이브러리가 PostDto 객체를 JSON으로 자동 직렬화하여 응답합니다.


6. 전문가 관점과 추천 학습 도구

공식 문서와 레퍼런스

Spring 공식 문서(docs.spring.io)는 Spring MVC의 각 컴포넌트를 매우 상세하게 설명하고 있습니다. 특히 “Web on Servlet Stack” 챕터는 DispatcherServlet의 내부 동작부터 고급 설정까지 체계적으로 정리되어 있습니다. 영문이지만 공식 문서를 직접 읽는 습관을 들이면 버전 업그레이드 시에도 빠르게 대응할 수 있습니다.

추천 학습 순서

처음 배우는 분이라면 다음 순서를 추천합니다.

  1. 서블릿(Servlet) 기초: DispatcherServlet이 결국 서블릿이므로, 서블릿의 생명주기와 HttpServletRequest/Response를 먼저 이해하면 이후 개념이 훨씬 자연스럽게 연결됩니다.
  2. Spring Core(IoC, DI): Spring MVC는 Spring의 IoC 컨테이너 위에서 동작하므로 의존성 주입 개념을 먼저 익혀야 합니다.
  3. Spring MVC 설정 없이 직접 구현해보기: Spring Boot 자동 설정에 의존하기 전에 web.xml과 servlet-context.xml을 직접 작성해보면 각 컴포넌트의 역할이 명확해집니다.
  4. Spring Boot MVC: 자동 설정 원리(@EnableAutoConfiguration)를 이해하며 Spring Boot MVC를 익힙니다.

추천 도구 및 리소스

도구/리소스용도
Spring Initializr (start.spring.io)프로젝트 빠른 생성
IntelliJ IDEASpring MVC 구조 탐색, 디버깅
Postman / HTTPieHTTP 요청 테스트
MockMvc (Spring Test)Controller 단위 테스트
김영한 스프링 MVC 강의 (인프런)한국어 심화 학습

면접 대비 핵심 포인트

Spring MVC 요청 처리 흐름은 백엔드 개발자 면접에서 매우 자주 나오는 주제입니다. 단순히 순서를 외우는 것보다 각 컴포넌트가 왜 필요한지어떤 문제를 해결하기 위해 존재하는지를 설명할 수 있어야 합니다. DispatcherServlet이 왜 Front Controller 패턴을 사용하는지, HandlerAdapter가 왜 별도로 존재하는지(OCP 원칙)를 연결해서 답할 수 있다면 깊이 있는 이해를 보여줄 수 있습니다.


결론

Spring MVC 요청 처리 흐름은 DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → ViewResolver → View의 6단계로 이루어집니다. 각 컴포넌트는 단일 책임 원칙에 따라 역할이 명확하게 분리되어 있어 유지보수와 확장이 용이합니다. Spring MVC 요청 처리 흐름을 완전히 이해하면 디버깅 능력은 물론 Spring 생태계 전반에 대한 이해가 한층 깊어집니다. 오늘 배운 내용을 바탕으로 직접 간단한 컨트롤러를 만들고 요청을 보내보세요. 직접 손으로 써봐야 진짜 내 것이 됩니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다