Spring을 사용해 RESTful 서버를 개발할 때, 많은 사람들은 일반적으로 다음과 같이 Controller를 개발한다.
- @RestController 어노테이션을 통해 Controller 클래스 작성.
- ResponseEntity<DTO>를 반환하는 메서드들 작성.
왜 그렇게 할까?
왜 @Controller 가 아닌 @RestController 어노테이션을 붙이는지 생각해 본 적이 있는가?
@RestController 에는 @ResponseBody 가 붙어 있어서? 메서드가 응답하는 값을 JSON으로 쓰려고?
정말 그럴까?
그렇다면 메서드가 반환하는 ResponseEntity 클래스 자체가 JSON으로 쓰여야 하는 것 아닌가? ResponseEntity<DTO>를 반환하는데?
오늘은 이 질문들에 대한 답을 알아가보자 한다.
배경지식
응답으로 JSON을 반환하기 위해 자주 사용하는 방식은 크게 두 가지로 나눌 수 있다.
- @ResponseBody
- 핸들러 메서드에서 반환한 객체를 HttpMessageConverter를 통해 직렬화하여 HTTP 응답 본문(Response Body)에 작성한다.
- ResponseEntity (또는 HttpEntity)
- 본문(body), 헤더, 상태 코드 등을 포함할 수 있는 응답 객체이다.
- 반환된 ResponseEntity의 body 필드에 들어 있는 객체도 HttpMessageConverter를 통해 직렬화되어 HTTP 응답 본문에 작성된다.
반환되는 객체는 반드시 JSON 형식일 필요는 없으며, 클라이언트의 Accept 헤더 값과 서버에 등록된 HttpMessageConverter 구현체에 따라 JSON, XML, 문자열 등 다양한 형식으로 직렬화될 수 있다. 일반적으로는 JSON 형식이 가장 많이 사용된다.
지금부터, 반환되는 객체는 JSON 으로 직렬화된다고 일반화해서 이야기하도록 하겠다.
@ResponseBody 와 ResponseEntity 는 서로 다르게 동작한다!
Controller 클래스에서 메서드로써 구현되는 핸들러가 반환하는 값은 HandlerMethodReturnValueHandler 가 처리한다.
HandlerMethodReturnValueHandler 는 핸들러의 응답 객체를 어떻게 다루고, 어떤 ModelAndView 를 반환할 것인지를 책임지는 인터페이스이다.
@ResponseBody 를 사용했을 때 동작하는 HandlerMethodReturnValueHandler 와 ResponseEntity 를 반환했을 때 동작하는 HandlerMethodReturnValueHandler 를 알아 보며 둘이 어떻게 다른 지 알아보도록 하겠다.
동시에, 둘의 다름으로 인해 발생하는 한 가지 의문에 대해 파헤쳐보도록 하겠다.
1. HandlerMethodReturnValueHandler 란?
HandlerMethodReturnValueHandler 는 핸들러의 응답 객체를 어떻게 다루고 어떤 ModelAndView 를 반환할 것인지를 책임지는 인터페이스이다.
HandlerMethodReturnValueHandler 는 두 가지 메서드를 가진다.
public interface HandlerMethodReturnValueHandler {
boolean supportsReturnType(MethodParameter returnType);
void handleReturnValue(
@Nullable Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest
) throws Exception;
}
- boolean supportsReturnType()
- 해당 구현체가 특정 반환 타입(MethodParameter)을 처리할 수 있는지를 판별한다.
- void handleReturnValue()
- supportsReturnType()이 true를 반환했을 경우, 해당 구현체는 handleReturnValue()를 통해 실제 반환값을 처리하며, 필요한 경우 ModelAndViewContainer에 view 이름 또는 model 데이터를 설정하거나, 응답 바디에 직접 데이터를 작성한다.
ModelAndViewContainer 는 Handler 가 동작하는 과정에서 생기는 값을 임시로 저장하기 위해 존재하는 클래스이다.
ModelAndViewContainer 값 세팅은 대부분 HandlerMethodReturnValueHandler 의 handlerReturnValue() 에서 일어난다.
2. @ResponseBody 를 담당하는 HandlerMethodReturnValueHandler 구현체 - RequestResponseBodyMethodProcessor
RequestResponseBodyMethodProcessor 는 위에서 언급한 HandlerMethodReturnValueHandler 의 두 가지 메서드를 오버라이드함으로써 @ResponseBody 가 붙은 핸들러의 반환값을 다룬다.
boolean supportsReturnType()
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
returnType 의 메서드에 @ResponseBody 어노테이션이 존재하는지 혹은 returnType 의 메서드를 가진 클래스에 @ResponseBody 어노테이션이 존재하는지를 반환한다.
void handleReturnValue()
@Override
public void handleReturnValue(
@Nullable Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest
) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
// ...
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
두 가지 중요한 로직을 수행한다.
- mavContainer.setRequestHandled(true);
- ModelAndViewContainer 의 requestHandled 필드를 true 로 만드는 과정이다.
- 만약 ModelAndViewContainer 의 requestHandled 필드가 true 라면, HandlerAdapter 의 handle() 은 null 을 반환하게 된다. (값이 null 인 ModelAndView 를 반환하게 된다)
- writeWithMessageConverters(…);
- response 의 body 에 직접 데이터를 쓰는 과정을 수행한다.
- 이 과정에서 응답 바디가 작성되므로, ModelAndView 를 반환할 필요가 없어진다.
- 그러므로, mavContainer.setRequestHandled(true); 까지 수행하는 것이다.
기억해야 할 점
@ResponseBody 를 담당하는 HandlerMethodReturnValueHandler 구현체는 RequestResponseBodyMethodProcessor 이다!
3. ResponseEntity(HttpEntity) 를 담당하는 HandlerMethodReturnValueHandler 구현체 - HttpEntityMethodProcessor
HttpEntityMethodProcessor 는 위에서 언급한 HandlerMethodReturnValueHandler 의 두 가지 메서드를 오버라이드함으로써 ResponseEntity(HttpEntity) 를 반환하는 핸들러의 반환값을 다룬다.
boolean supportsReturnType()
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> type = returnType.getParameterType();
return ((HttpEntity.class.isAssignableFrom(type) && !RequestEntity.class.isAssignableFrom(type)) ||
ErrorResponse.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type));
}
핸들러가 응답한 클래스가 HttpEntity 관련 클래스인지를 응답한다.
void handleReturnValue()
@Override
public void handleReturnValue(
@Nullable Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest
) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
// ...
HttpEntity<?> httpEntity;
// ...
httpEntity = (HttpEntity<?>) returnValue;
// ...
writeWithMessageConverters(httpEntity.getBody(), returnType, inputMessage, outputMessage);
}
RequestResponseBodyMethodProcessor 와 비슷하게 동작한다.
- mavContainer.setRequestHandled(true);
- ModelAndViewContainer 의 requestHandled 필드를 true 로 만드는 과정이다.
- 만약 ModelAndViewContainer 의 requestHandled 필드가 true 라면, HandlerAdapter 의 handle() 은 null 을 반환하게 된다. (값이 null 인 ModelAndView 를 반환하게 된다)
- writeWithMessageConverters(…);
- response 의 body 에 직접 데이터를 쓰는 과정을 수행한다.
- 이 과정에서 응답 바디가 작성되므로, ModelAndView 를 반환할 필요가 없어진다.
- 그러므로, mavContainer.setRequestHandled(true); 까지 수행하는 것이다.
기억해야 할 점
ResponseEntity(HttpEntity) 를 담당하는 HandlerMethodReturnValueHandler 구현체는 HttpEntityMethodProcessor 이다!
4. @ResponseBody 와 ResponseEntity 는 다른 HandlerMethodReturnValueHandler 로 동작한다.
위에서 본 것과 같이, @ResponseBody 와 ResponseEntity 는 다른 HandlerMethodReturnValueHandler 로 동작한다.
@ResponeBody 어노테이션이 붙은 핸들러의 반환값을 책임지는 HandlerMethodReturnValueHandler 구현체는 RequestResponseBodyMethodProcessor 이다.
ResponseEntity(HttpEntity) 를 반환하는 핸들러의 반환값을 책임지는 HandlerMethodReturnValueHandler 구현체는 HttpEntityMethodProcessor 이다.
5. 의문 - @ResponseBody 와 ResponseEntity 를 같이 사용하면 어떤 HandlerMethodReturnValueHandler 가 선택될까?
우선, 정답은 ResponseEntity 의 HandlerMethodReturnValueHandler 인 HttpEntityMethodProcessor 이다.
어떤 과정을 통해 HttpEntityMethodProcessor 가 선택되고, 왜 그런지 알아보자.
"넓은 관점에서" HandlerMethodReturnValueHandler 는 RequestMappingHandlerAdaptor 에서 호출한다.
DispatcherServlet 이 RequestMappingHandlerAdaptor 의 handle() 을 호출하면, RequestMappingHandlerAdaptor 는 ServletInvocabeHandlerMethod 를 만든다.
이 때, ServletInvocableHandlerMethod 는 HandlerMethodReturnValueHandlerComposite 를 필드로 가진다.
HandlerMethodReturnValueHandlerComposite 은 HandlerMethodReturnValueHandler 들을 List 로 가지는 전형적인 Composite 패턴 구조를 따른다.
HandlerMethodReturnValueHandlerComposite 의 handleReturnValue() 를 호출하면, HandlerMethodReturnValueHandlerComposite 은 자신이 가진 HandlerMethodReturnValueHandler 들을 순차적으로 탐색하며, 매개변수로 받은 returnType 을 지원하는 HandlerMethodReturnValueHandler 를 찾아 해당 HandlerMethodReturnValueHandler 의 handleReturnValue() 를 호출한다.
이 흐름에 대해 더 자세하게 알고 싶다면 아래 글을 참고하자.
https://dev-allday.tistory.com/96
Json 은 View 가 아니다 (@ResponseBody 가 붙은 핸들러는 어떻게 응답 바디를 구성할까?)
제목을 보고 의아함을 느낀 독자가 있을 것이다."Json 도 Response Body 에 들어 있는데, 사용자에게 무언가를 보여 주기 위한 정보니까 View 아닌가...?""View 를 위해 사용되는 데이터니까 View 아닌가...?
dev-allday.tistory.com
이 흐름에서 얻을 수 있는 정보는, HandlerMethodReturnValueHandlerCompsite 내부에 등록 순서에 따라 어떤 HandlerMethodReturnValueHandler 가 선택될지 결정되고 있었다는 것이다.
디버깅을 통해 ServletInvocableHandlerMethod 가 가진 HandlerMethodReturnValueHandlerComposite 내부를 들여다 보았다.
HttpEntityMethodProcessor 가 RequestResponseBodyMethodProcessor 보다 앞에 등록되어 있다.
그렇기 때문에, @ResponseBody 와 ResponseEntity 를 같이 사용하면 ResponseEntity 방식이 동작하는 것이다.(HttpEntityMethodProcessor 가 선택되기 때문!)
그렇다면, HandlerMethodReturnValueHandler 들을 정해진 순서대로 선언하고 동시에 HandlerMethodReturnValueHandlerComposite 에 이를 넘겨주는 역할은 어디서 담당하고 있을까?
그 역할은 RequestMappingHandlerAdapter 에서 담당하고 있다.
다음은 RequestMappingHandlerAdapter 의 코드 중 일부이다. 해당 코드에서 모든 HandlerMethodReturnValueHandler 들을 등록한다.
등록 과정에서 HttpEntityMethodProcessor 가 RequestResponseBodyMethodProcessor 보다 먼저 등록된다.
결론
ResponseEntity 를 반환하게 되면, @ResponseBody 를 통한 응답 값 핸들링은 일어나지 않는다.
⇒ ResponseEntity 를 반환한다면, @RestController 가 아닌 @Controller 를 사용해도 아무 문제가 없다.
그렇지만, 나는 두 가지 관점에서 @RestController 를 계속 사용하고자 마음먹었다.
- 명시적
- 누구든지 내 Controller를 보고 RESTful 한 Controller 라는 것을 한 번에 눈치챌 수 있다.
- 실수 방지
- ResponseEntity 를 반환하는 것은 강제할 수 없다.
- 만약 @Controller 를 사용했는데 실수로 ResponseEntity 없이 DTO 만 반환한다면 뷰 해석 과정에서 예외가 발생할 것이다.
- @RestController 를 통해 모든 메서드에 @ResponseBody 를 적용한다면 이런 일을 미연에 방지할 수 있다.
'Spring & Spring Boot' 카테고리의 다른 글
Json 은 View 가 아니다 (@ResponseBody 가 붙은 핸들러는 어떻게 응답 바디를 구성할까?) (3) | 2025.05.09 |
---|---|
RestInterceptor 개발기 - build() 메서드 제거와 v1.0 릴리즈 (0) | 2024.12.23 |
RestInterceptor 개발기 - 패턴 매칭 이슈 (1) | 2024.12.02 |
Spring 라이브러리 개발기 - RestInterceptor v0.1 (0) | 2024.11.29 |
Redis 캐싱만 적용하면 끝일까? - Resilience4j 를 통한 Circuit Breaker 적용 (7) | 2024.10.15 |