프레임워크/스프링

DispatcherServlet의 역할: 요청이 들어와서 응답이 나갈 때까지의 여정

eodevelop 2025. 12. 19. 22:17
반응형

 Spring MVC로 웹 애플리케이션을 개발하다 보면 "DispatcherServlet"이라는 이름을 한 번쯤 들어보셨을 겁니다. Controller만 잘 작성하면 알아서 동작하니까 굳이 알 필요가 있나 싶을 수도 있습니다. 하지만 이 녀석이 어떻게 동작하는지 알면, 에러가 발생했을 때 어디서 문제가 생겼는지 파악하기가 훨씬 쉬워집니다. 오늘은 HTTP 요청이 들어와서 응답이 나갈 때까지 어떤 일이 벌어지는지 따라가 보겠습니다.


DispatcherServlet이 뭔가요?

 DispatcherServlet은 이름 그대로 "배달하는(Dispatch) 서블릿"입니다. 쉽게 말하면 모든 요청을 가장 먼저 받아서 적절한 곳으로 배달해주는 중앙 관제탑 같은 역할을 합니다.

비유를 하나 들어보겠습니다.

큰 회사의 안내 데스크를 떠올려 보세요. 방문객이 오면 안내 데스크에서 먼저 접수를 받습니다. 방문객이 "인사팀을 찾아왔어요"라고 하면, 안내 데스크 직원이 인사팀이 몇 층 몇 호인지 찾아서 안내해줍니다. DispatcherServlet이 바로 이 안내 데스크 역할입니다.

 

 

 Spring MVC에서는 모든 HTTP 요청이 먼저 DispatcherServlet을 거칩니다. DispatcherServlet은 요청 URL을 보고 "이 요청은 어떤 Controller가 처리해야 하지?"를 판단해서 적절한 곳으로 보내줍니다.


왜 DispatcherServlet이 필요할까?

"그냥 요청이 바로 Controller로 가면 안 되나요?"라고 생각할 수 있습니다.

만약 DispatcherServlet 없이 각 Controller가 직접 요청을 받는다면 어떻게 될까요?

// 이렇게 각 Controller마다 서블릿을 따로 등록해야 합니다
@WebServlet("/users/*")
public class UserServlet extends HttpServlet { ... }

@WebServlet("/orders/*")
public class OrderServlet extends HttpServlet { ... }

@WebServlet("/products/*")
public class ProductServlet extends HttpServlet { ... }

 

Controller가 10개면 서블릿도 10개, 100개면 100개를 등록해야 합니다. 공통 로직(인코딩 설정, 에러 처리 등)도 각각 구현해야 하고요.

DispatcherServlet은 이 문제를 해결합니다. 하나의 진입점에서 모든 요청을 받고, 공통 처리를 한 번에 적용한 뒤, 적절한 Controller로 분배합니다. 이걸 Front Controller 패턴이라고 부릅니다.

// DispatcherServlet 하나가 모든 요청을 받음
@WebServlet("/")
public class DispatcherServlet extends HttpServlet {
    // 모든 요청을 여기서 받아서 적절한 Controller로 분배
}

요청이 응답이 되기까지: 전체 흐름

 자, 이제 본격적으로 요청이 들어와서 응답이 나갈 때까지 어떤 일이 벌어지는지 단계별로 살펴보겠습니다.

 

 

복잡해 보이지만, 하나씩 따라가면 어렵지 않습니다.


1단계: 요청 수신 (DispatcherServlet)

클라이언트가 GET /users/1 요청을 보냈다고 가정해 봅시다.

GET /users/1 HTTP/1.1
Host: localhost:8080

 

이 요청은 먼저 DispatcherServlet에 도착합니다. Spring Boot를 사용하면 DispatcherServlet이 자동으로 / 경로에 등록되어 있어서, 모든 요청을 받을 수 있습니다.

DispatcherServlet은 요청을 받으면 이렇게 생각합니다:

"GET /users/1 요청이 들어왔군. 이걸 처리할 수 있는 Controller가 누구지?"


2단계: 핸들러 찾기 (HandlerMapping)

DispatcherServlet은 HandlerMapping에게 물어봅니다.

"이 요청을 처리할 수 있는 핸들러(Controller)가 누구야?"

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

 

 HandlerMapping은 등록된 Controller들의 @RequestMapping 정보를 뒤져서, /users/1 요청을 처리할 수 있는 메서드를 찾습니다. 위 코드에서는 UserController.getUser() 메서드가 매칭됩니다.

HandlerMapping은 여러 종류가 있는데, 가장 많이 쓰이는 건 RequestMappingHandlerMapping입니다. 이름에서 알 수 있듯이 @RequestMapping 어노테이션을 기반으로 핸들러를 찾습니다.


3단계: 핸들러 실행 (HandlerAdapter)

핸들러를 찾았으면 이제 실행해야 합니다. 그런데 핸들러(Controller)의 형태가 다양할 수 있습니다.

// 형태 1: @Controller 어노테이션 방식
@Controller
public class UserController { ... }

// 형태 2: Controller 인터페이스 구현 방식 (옛날 방식)
public class OldController implements Controller { ... }

 

 DispatcherServlet이 이 모든 형태를 직접 처리하면 코드가 복잡해집니다. 그래서 HandlerAdapter가 중간에서 실행을 대신해줍니다.

 HandlerAdapter는 핸들러의 형태에 맞는 방식으로 메서드를 호출합니다. 어댑터 패턴을 사용해서 DispatcherServlet은 핸들러가 어떤 형태든 신경 쓰지 않아도 됩니다.

가장 많이 쓰이는 건 RequestMappingHandlerAdapter입니다. @RequestMapping이 붙은 메서드를 실행하고, 파라미터 바인딩(@PathVariable, @RequestBody 등)까지 처리해줍니다.


4단계: Controller 실행

드디어 우리가 작성한 Controller가 실행됩니다.

@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
    // 비즈니스 로직 실행
    return userService.findById(id);
}

 

 Controller에서는 Service를 호출하고, Service는 Repository를 통해 데이터베이스에서 데이터를 가져옵니다. 이 과정은 개발자가 직접 작성하는 부분입니다.

 Controller의 반환값에 따라 이후 흐름이 달라집니다.

  • @ResponseBody 또는 @RestController: 반환값이 바로 HTTP 응답 본문이 됨 (JSON 등)
  • String (View 이름): ViewResolver를 통해 실제 View를 찾음
  • ModelAndView: View 이름과 데이터를 함께 반환

5단계: View 결정 (ViewResolver)

Controller가 View 이름을 반환했다면, ViewResolver가 실제 View 파일을 찾습니다.

@Controller
public class UserController {

    @GetMapping("/users/{id}")
    public String getUser(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "user/detail";  // View 이름 반환
    }
}

 

ViewResolver는 "user/detail"이라는 이름을 받아서 실제 파일 경로로 변환합니다.

"user/detail" → "/templates/user/detail.html"

 

 Spring Boot에서 Thymeleaf를 사용하면 ThymeleafViewResolver가 이 역할을 합니다. 기본적으로 클래스패스의 templates/ 경로(개발 시에는 src/main/resources/templates/)에서 .html 파일을 찾습니다.


6단계: View 렌더링

View는 최종 HTML을 생성합니다.

<!-- /templates/user/detail.html -->
<!DOCTYPE html>
<html>
  <body>
    <h1 th:text="${user.name}">사용자 이름</h1>
    <p th:text="${user.email}">이메일</p>
  </body>
</html>

 

 View는 Controller에서 Model에 담아준 데이터(user)를 사용해서 동적으로 HTML을 생성합니다. 이렇게 생성된 HTML이 최종 HTTP 응답이 됩니다.


REST API의 경우는?

요즘은 View를 직접 반환하는 방식보다 REST API가 더 일반적입니다. 프론트엔드는 React, Vue 같은 SPA로 따로 만들고, 백엔드는 JSON 데이터만 제공하는 구조가 많습니다. 이 경우 흐름이 조금 달라집니다.

@RestController
@RequestMapping("/api/users")
public class UserApiController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);  // User 객체 반환
    }
}

 

@RestController(또는 @ResponseBody)를 사용하면 ViewResolver 과정이 생략됩니다. 대신 HttpMessageConverter가 반환값을 JSON으로 변환합니다. 이 변환 과정은 HandlerAdapter가 Controller를 실행하는 과정 안에서 일어납니다.

{
  "id": 1,
  "name": "홍길동",
  "email": "hong@example.com"
}

 

Spring Boot에서는 Jackson 라이브러리가 기본으로 포함되어 있어서, 객체를 JSON으로 자동 변환해줍니다.


전체 흐름 다시 보기

지금까지 살펴본 내용을 코드와 함께 정리해 보겠습니다.

View를 사용하는 경우 (전통적인 MVC)

1. 클라이언트 → GET /users/1
2. DispatcherServlet이 요청 수신
3. HandlerMapping이 UserController.getUser() 찾음
4. HandlerAdapter가 Controller 메서드 실행
5. Controller가 "user/detail" 반환, Model에 user 데이터 담음
6. ViewResolver가 /templates/user/detail.html 찾음
7. View가 HTML 렌더링
8. DispatcherServlet이 응답 전송
9. 클라이언트 ← HTML 응답

REST API의 경우

1. 클라이언트 → GET /api/users/1
2. DispatcherServlet이 요청 수신
3. HandlerMapping이 UserApiController.getUser() 찾음
4. HandlerAdapter가 Controller 메서드 실행
5. Controller가 User 객체 반환
6. HttpMessageConverter가 JSON으로 변환
7. DispatcherServlet이 응답 전송
8. 클라이언트 ← JSON 응답

디버깅할 때 알면 좋은 점

이 흐름을 알면 에러가 발생했을 때 원인을 찾기가 쉬워집니다.

404 Not Found가 뜬다면?

  • HandlerMapping이 맞는 Controller를 못 찾은 것
  • @RequestMapping 경로를 확인해 보세요

500 Internal Server Error가 뜬다면?

  • Controller 실행 중 예외가 발생한 것
  • 스택 트레이스에서 Controller 메서드를 찾아보세요

View를 찾을 수 없다는 에러가 뜬다면?

  • ViewResolver가 View 파일을 못 찾은 것
  • 파일 경로와 이름을 확인해 보세요

JSON 응답이 안 된다면?

  • @ResponseBody@RestController가 빠졌거나
  • HttpMessageConverter가 객체를 변환하지 못한 것
  • Getter가 없으면 Jackson이 변환을 못 합니다

정리

  • DispatcherServlet은 Spring MVC의 핵심으로, 모든 요청의 진입점 역할을 한다
  • Front Controller 패턴을 사용해서 하나의 진입점에서 모든 요청을 처리한다
  • 요청 처리 흐름: DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → ViewResolver → View
  • REST API의 경우 ViewResolver 대신 HttpMessageConverter가 JSON 변환을 담당한다
  • 각 구성 요소가 하나의 역할만 담당하기 때문에, 문제가 발생했을 때 어디서 문제가 생겼는지 파악하기 쉽다
  • 이 흐름을 이해하면 Spring MVC의 동작 방식을 훨씬 잘 이해할 수 있고, 디버깅도 수월해진다
반응형