본문 바로가기

Spring & Spring Boot

@ResponseBody VS ResponseEntity

한 크루가 그려 준 행성이



Spring을 사용해 RESTful 서버를 개발할 때, 많은 사람들은 일반적으로 다음과 같이 Controller를 개발한다.

  1. @RestController 어노테이션을 통해 Controller 클래스 작성.
  2. ResponseEntity<DTO>를 반환하는 메서드들 작성.


왜 그렇게 할까?

@Controller 가 아닌 @RestController 어노테이션을 붙이는지 생각해 본 적이 있는가?
@RestController 에는 @ResponseBody 가 붙어 있어서? 메서드가 응답하는 값을 JSON으로 쓰려고?
정말 그럴까?
그렇다면 메서드가 반환하는 ResponseEntity 클래스 자체가 JSON으로 쓰여야 하는 것 아닌가? ResponseEntity<DTO>를 반환하는데?
 
오늘은 이 질문들에 대한 답을 알아가보자 한다.
 
 

배경지식

응답으로 JSON을 반환하기 위해 자주 사용하는 방식은 크게 두 가지로 나눌 수 있다.
 

  1. @ResponseBody
    • 핸들러 메서드에서 반환한 객체를 HttpMessageConverter를 통해 직렬화하여 HTTP 응답 본문(Response Body)에 작성한다.
  2. 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;
}
  1. boolean supportsReturnType()
    • 해당 구현체가 특정 반환 타입(MethodParameter)을 처리할 수 있는지를 판별한다.
  2. void handleReturnValue()
    • supportsReturnType()이 true를 반환했을 경우, 해당 구현체는 handleReturnValue()를 통해 실제 반환값을 처리하며, 필요한 경우 ModelAndViewContainer에 view 이름 또는 model 데이터를 설정하거나, 응답 바디에 직접 데이터를 작성한다.

ModelAndViewContainer 는 Handler 가 동작하는 과정에서 생기는 값을 임시로 저장하기 위해 존재하는 클래스이다.
ModelAndViewContainer 값 세팅은 대부분 HandlerMethodReturnValueHandlerhandlerReturnValue() 에서 일어난다.
 
 
 

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);
}

 
두 가지 중요한 로직을 수행한다.

  1. mavContainer.setRequestHandled(true);
    • ModelAndViewContainerrequestHandled 필드를 true 로 만드는 과정이다.
    • 만약 ModelAndViewContainerrequestHandled 필드가 true 라면, HandlerAdapterhandle()null 을 반환하게 된다. (값이 nullModelAndView 를 반환하게 된다)
  2. 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 와 비슷하게 동작한다.

  1. mavContainer.setRequestHandled(true);
    • ModelAndViewContainer 의 requestHandled 필드를 true 로 만드는 과정이다.
    • 만약 ModelAndViewContainer 의 requestHandled 필드가 true 라면, HandlerAdapter 의 handle() 은 null 을 반환하게 된다. (값이 null 인 ModelAndView 를 반환하게 된다)
  2. writeWithMessageConverters(…);
    • response 의 body 에 직접 데이터를 쓰는 과정을 수행한다.
    • 이 과정에서 응답 바디가 작성되므로, ModelAndView 를 반환할 필요가 없어진다.
    • 그러므로, mavContainer.setRequestHandled(true); 까지 수행하는 것이다.

 
 

기억해야 할 점

ResponseEntity(HttpEntity) 를 담당하는 HandlerMethodReturnValueHandler 구현체는 HttpEntityMethodProcessor 이다!
 
 
 
 

4. @ResponseBody 와 ResponseEntity 는 다른 HandlerMethodReturnValueHandler 로 동작한다.

위에서 본 것과 같이, @ResponseBodyResponseEntity 는 다른 HandlerMethodReturnValueHandler 로 동작한다.
 

@ResponeBody 어노테이션이 붙은 핸들러의 반환값을 책임지는 HandlerMethodReturnValueHandler 구현체는 RequestResponseBodyMethodProcessor 이다.
ResponseEntity(HttpEntity) 를 반환하는 핸들러의 반환값을 책임지는 HandlerMethodReturnValueHandler 구현체는 HttpEntityMethodProcessor 이다.

 
 
 
 
 

5. 의문 - @ResponseBody 와 ResponseEntity 를 같이 사용하면 어떤 HandlerMethodReturnValueHandler 가 선택될까?

우선, 정답은 ResponseEntityHandlerMethodReturnValueHandlerHttpEntityMethodProcessor 이다.
 
어떤 과정을 통해 HttpEntityMethodProcessor 가 선택되고, 왜 그런지 알아보자.
 
"넓은 관점에서" HandlerMethodReturnValueHandlerRequestMappingHandlerAdaptor 에서 호출한다.
DispatcherServletRequestMappingHandlerAdaptorhandle() 을 호출하면, RequestMappingHandlerAdaptorServletInvocabeHandlerMethod 를 만든다.
이 때, ServletInvocableHandlerMethodHandlerMethodReturnValueHandlerComposite 를 필드로 가진다.
 
HandlerMethodReturnValueHandlerCompositeHandlerMethodReturnValueHandler 들을 List 로 가지는 전형적인 Composite 패턴 구조를 따른다.
HandlerMethodReturnValueHandlerCompositehandleReturnValue() 를 호출하면, HandlerMethodReturnValueHandlerComposite 은 자신이 가진 HandlerMethodReturnValueHandler 들을 순차적으로 탐색하며, 매개변수로 받은 returnType 을 지원하는 HandlerMethodReturnValueHandler 를 찾아 해당 HandlerMethodReturnValueHandlerhandleReturnValue() 를 호출한다.
 
이 흐름에 대해 더 자세하게 알고 싶다면 아래 글을 참고하자.
https://dev-allday.tistory.com/96

 

Json 은 View 가 아니다 (@ResponseBody 가 붙은 핸들러는 어떻게 응답 바디를 구성할까?)

제목을 보고 의아함을 느낀 독자가 있을 것이다."Json 도 Response Body 에 들어 있는데, 사용자에게 무언가를 보여 주기 위한 정보니까 View 아닌가...?""View 를 위해 사용되는 데이터니까 View 아닌가...?

dev-allday.tistory.com

 
이 흐름에서 얻을 수 있는 정보는, HandlerMethodReturnValueHandlerCompsite 내부에 등록 순서에 따라 어떤 HandlerMethodReturnValueHandler 가 선택될지 결정되고 있었다는 것이다.
 
 
 
디버깅을 통해 ServletInvocableHandlerMethod 가 가진 HandlerMethodReturnValueHandlerComposite 내부를 들여다 보았다.
 
 

 
HttpEntityMethodProcessorRequestResponseBodyMethodProcessor 보다 앞에 등록되어 있다.
그렇기 때문에, @ResponseBodyResponseEntity 를 같이 사용하면 ResponseEntity 방식이 동작하는 것이다.(HttpEntityMethodProcessor 가 선택되기 때문!)
 
그렇다면, HandlerMethodReturnValueHandler 들을 정해진 순서대로 선언하고 동시에 HandlerMethodReturnValueHandlerComposite 에 이를 넘겨주는 역할은 어디서 담당하고 있을까?
 
 
그 역할은 RequestMappingHandlerAdapter 에서 담당하고 있다.
다음은 RequestMappingHandlerAdapter 의 코드 중 일부이다. 해당 코드에서 모든 HandlerMethodReturnValueHandler 들을 등록한다.

 
등록 과정에서 HttpEntityMethodProcessorRequestResponseBodyMethodProcessor 보다 먼저 등록된다.
 
 
 
 
 

결론

ResponseEntity 를 반환하게 되면, @ResponseBody 를 통한 응답 값 핸들링은 일어나지 않는다.
ResponseEntity 를 반환한다면, @RestController 가 아닌 @Controller 를 사용해도 아무 문제가 없다.
그렇지만, 나는 두 가지 관점에서 @RestController 를 계속 사용하고자 마음먹었다.

  1. 명시적
    • 누구든지 내 Controller를 보고 RESTful 한 Controller 라는 것을 한 번에 눈치챌 수 있다.
  2. 실수 방지
    • ResponseEntity 를 반환하는 것은 강제할 수 없다.
    • 만약 @Controller 를 사용했는데 실수로 ResponseEntity 없이 DTO 만 반환한다면 뷰 해석 과정에서 예외가 발생할 것이다.
    • @RestController 를 통해 모든 메서드에 @ResponseBody 를 적용한다면 이런 일을 미연에 방지할 수 있다.