
이번 포스팅에서는 실제로 사용자(클라이언트)의 요청이 Spring 어플리케이션에 들어왔을 때,
Spring MVC(Model-View-Controller) 내부에서 어떤 흐름으로 요청이 처리되는지 살펴보려한다.
DispatcherServlet을 시작으로,
HandlerMapping, HandlerAdapter, ArgumentResolver, MessageConverter, ViewResolver까지
요청 처리 과정에 등장하는 주요 구성 요소들이 각각 어떤 역할을 하는지 흐름을 통해 이해하여 보고자 한다.

가장 먼저 클라이언트의 HTTP Request가 들어오면,
Spring MVC의 DispatcherServlet이 해당 요청을 수신한다.
이때 DispatcherServlet은 Spring MVC의 Front Controller로서,
어플리케이션으로 들어온 HTTP 요청을 가장 먼저 받아 적절한 처리 흐름으로 전달한다.
DispatcherServlet은 단지 요청을 분석하고, 적절한 컨트롤러를 찾는 등
HttpRequest가 최종적으로 HttpResponse가 되기까지 실행 흐름을 처리하는 중앙 관문 역할을 한다.
HandlerMapping은 어플리케이션이 시작하는 시점에,
@RequestMapping, @GetMapping, @PostMapping
같은 정보를 스캔하여 매핑 테이블을 만들어둔다.
@GetMapping("/products/{id}")
public ProductResponse getProduct(@PathVariable Long id) {
...
}
DispatcherServlet은 HandlerMapping에게
들어온 HTTP 요청을 처리하는 컨트롤러가 누구인지 확인하는 과정을 거친다.
( URL, HTTP Request Method와 같은 요청 정보를 통해 매핑되는 컨트롤러를 찾는다 )
HandlerMapping은 해당 과정에서 정확하게 일치하는 메서드의 컨트롤러를 찾아
DispatcherServlet에게 알려준다. 이때 요청에 일치하는게 없다면 404를 던진다.
앞선 과정에서 어떤 Handler가 해당 요청을 처리할지 찾았다고 해서,
DispatcherServlet이 바로 Controller 메서드를 호출하는 것은 아니다.
Spring MVC에서는 요청을 처리하는 대상이 하나의 형태로 고정되어 있지 않다.
우리가 흔히 사용하는 @Controller, @RestController 기반의 어노테이션 컨트롤러도 있고
과거 방식의 Controller 인터페이스 구현체나 HttpRequestHandler,
정적 리소스를 처리하는 Handler도 존재할 수 있다.
이처럼 Handler마다 실행 방식이 다르기 때문에,
DispatcherServlet이 모든 Handler의 호출 방식을 직접 알고 실행한다면
코드 복잡도가 증가하고 DispatcherServlet의 책임이 지나치게 커진다.
이를 분리하기 위해 Spring MVC는 HandlerAdapter라는 어댑터 계층을 둔다.
DispatcherServlet은 HandlerMapping이 찾아준 Handler를 직접 실행하지 않고,
해당 Handler를 실행할 수 있는 HandlerAdapter에게 실행 책임을 위임한다.
즉 HandlerAdapter는 다양한 형태의 Handler를 DispatcherServlet이
일관된 방식으로 실행할 수 있도록 중간에서 호출 방식을 맞춰주는 역할을 하는 것이다.
Handler와 Controller의 차이
상단 내용에서 Hanler와 Controller를 섞어 사용하였는데 둘의 차이는 무엇일까?
Controller는 MVC 패턴에서 사용자의 요청을 받아 비즈니스 로직을 호출하고,
그 결과를 응답으로 반환하는 웹 계층 객체를 의미한다.반면 Handler는 Spring MVC 내부에서 사용하는 더 넓은 개념이다.
Handler는 요청을 실제로 처리할 수 있는 모든 대상을 의미한다.일반적인 Spring MVC 애플리케이션에서는 Handler가 대부분 Controller 메서드이기에
“컨트롤러를 찾는다”라고 표현해도 크게 틀리지는 않다.
하지만 정확히 말하면 HandlerMapping은
요청을 처리할 Controller 자체가 아니라 요청을 처리할 Handler를 찾는다.즉 Controller는 Handler의 한 종류이며,
Handler는 Controller 메서드뿐 아니라HttpRequestHandler,
정적 리소스 Handler 등 다양한 요청 처리 대상을 포함하는 더 넓은 표현이다.
Spring MVC에서는 다양한 형태의 컨트롤러가 존재할 수 있기에,
HandlerAdapter가 DispatcherServlet과 Controller 중간에서 실행 방식을 맞춰준다.
DispatcherServlet이 다양한 형태의 컨트롤러를 각각 다른 방식으로 호출해야한다면,
코드 복잡도가 올라가고 DispatcherServlet의 책임이 지나치게 무거워진다.
따라서 어댑터 계층을 두어 실행 가능한 형태의 컨트롤러를 호출하게 책임을 위임하는 것이다.
@GetMapping("/products/{id}")
public ProductResponse getProduct(@PathVariable Long id) {
...
}
컨트롤러가 요청에 맞는 메서드를 실행하려면 파라미터가 필요한 경우가 있다.
위 예시에서 @PathVariable Long id를 통해,
개발자는 URL 경로에 포함된 상품 id 값을 메서드 파라미터로 편하게 받을 수 있다.
하지만 실제로 클라이언트가 보내는 값은 Long타입이 아니며, 자바의 타입도 모른다.
GET /products/1
여기서 1은 HTTP 요청 경로에 포함된 문자열 값일 뿐이다.
그럼에도 컨트롤러 메서드에서는 이 값을 Long id로 받을 수 있다.
이처럼 HTTP 요청에 담긴 문자열, JSON Body, Query Parameter 등의 값을 읽어
컨트롤러 메서드가 요구하는 자바 객체나 값으로 만들어주는 역할을 하는 것이
ArgumentResolver다.
이 과정에서 Long 타입으로 변환해야 하는 자리에 문자가 들어오거나,
JSON Body 파싱에 실패하면 컨트롤러 메서드가 실행되기 전에 400 Bad Request가 발생할 수 있다.
이전까지의 내용은 Spring Framework가 자동으로 처리해주는 영역이었다.
ArgumentResolver를 통해 메서드에 전달될 파라미터까지 준비되면,
HandlerAdapter가 실제 Controller 객체의 메서드를 호출한다.
이 시점부터 우리가 작성한 애플리케이션 코드가 본격적으로 실행된다.
Controller는 요청에 필요한 값을 전달받고,
Service 계층을 호출해 비즈니스 로직을 수행
Repository 계층을 통해 DB와 데이터 통신 등
일련의 처리 과정이 끝나면, Controller는 해당 요청에 대한 처리 결과를 반환한다.
Controller 메서드의 실행이 끝나면 처리 결과가 반환된다.
예를 들어 REST API에서 다음과 같은 UserResponse 객체를 반환한다고 가정해보면
return new UserResponse(id, "username");
Controller가 반환한 UserResponse는 자바 객체이기에,
클라이언트, 예를 들어 React 어플리케이션은 자바 객체를 그대로 이해할 수 없다.
따라서 이 객체를 HTTP 응답으로 전달할 수 있는 형태로 변환해야 한다.
REST API에서는 보통 JSON 문자열로 변환된다.
이런 과정에 관여하는 것이 ReturnValueHandler와 HttpMessageConverter다.
ReturnValueHandler
ReturnValueHandler는 Controller 메서드의 반환값을 어떻게 처리할지 결정한다.
@ResponseBody나 @RestController가 붙어 있다면,
반환값을 View 이름으로 해석하지 않고 HTTP Response Body에 직접 쓰는 방식으로 처리한다.
HttpMessageConverter
HttpMessageConverter는 자바 객체를 JSON 같은 HTTP 메시지 본문 형태로 변환한다.
Spring Boot에서는 보통 Jackson 라이브러리가 사용되어 객체를 JSON으로 직렬화한다.
{
"id": 1,
"name": "username"
}
이 단계는 REST API 방식이 아니라, Thymeleaf나 JSP 같은 전통적인 템플릿 엔진을 사용할 때 거치는 과정이다.
( 전통적인 템플릿 엔진을 쓸 때만 거치는 과정, React가 이러한 View 역할을 맡는다 볼 수 있다 )
REST API에서는 Controller가 반환한 객체를 JSON으로 변환해
HTTP Response Body에 담기 때문에, 보통 ViewResolver를 거치지 않는다.
반면 템플릿 기반 MVC에서는 Controller가 문자열을 반환하는 경우가 많다.
@GetMapping("/products/{id}")
public String productDetail(Model model) {
model.addAttribute("product", productService.findProduct(id));
return "product/detail"; // view 이름
}
DispatcherServlet은 이 View 이름을 ViewResolver에게 전달하고,
ViewResolver는 실제 렌더링할 View 파일을 찾아준다.
View 이름: product/detail
파일: src/main/resources/templates/product/detail.html
즉 ViewResolver는 Controller가 반환한 View 이름을,
실제 View 파일 위치로 변환해주는 역할을 한다.
이렇게 만들어진 HTML 응답이 브라우저로 전달되고, 브라우저는 그 HTML을 화면에 렌더링한다.