제목을 보고 의아함을 느낀 독자가 있을 것이다.
"Json 도 Response Body 에 들어 있는데, 사용자에게 무언가를 보여 주기 위한 정보니까 View 아닌가...?"
"View 를 위해 사용되는 데이터니까 View 아닌가...?"
등등, 많은 의문이 들 수 있다고 생각한다.
관심을 끌기 위해 자극적으로 제목을 지었다.
지금부터 다룰 주제에 적합한 제목은 사실
“Spring MVC 관점 에서 JSON 응답은 View 가 아니다!“ 이다.
지금부터 이를 증명하고자 한다.
이 여정은 한 의문에서 시작되었다
View 의 렌더링은 핸들러가 반환하는 ModelAndView 를 통해 이루어진다.
@ResponseBody 가 붙은 핸들러는 어떤 ModelAndView 값을 반환할까?
1. DispatcherServlet
요청이 들어오면, DispatcherServlet 의 doDispatch() 가 호출된다.
// doDispatch() 내부 코드.
// ha = 핸들러 어댑터
// mv = ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
doDispatch() 내부에서는 핸들러 어댑터를 통해 요청에 해당하는 핸들러를 실행하게 된다.
// 위에서 얻은 ModelAndView(mv) 를 사용해 뷰를 렌더링한다.
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
핸들러가 ModelAndView 를 반환하면, processDispatchResult() 를 통해 뷰를 렌더링한다.
이 과정을 디버깅하다가, @ResponseBody 가 붙은 핸들러가 반환하는 ModelAndView 가 null 임을 알게 되었다.
DispatcherServlet 은 핸들러가 반환한 ModelAndView 가 null 이면 렌더링을 수행하지 않고 건너뛴다.
@ResponseBody 가 붙은 핸들러가 호출되면 값이 null 인 ModelAndView 를 반환하고, 이로 인해 뷰가 렌더링되지 않는다는 사실을 알 수 있다.
그렇다면, @ResponseBody 가 붙은 핸들러는 어떻게 null 을 반환하게 되는 걸까?
2. RequestMappingHandlerAdaptor

@ResponseBody 가 붙은 핸들러의 핸들러 어댑터는 RequestMappingHandlerAdaptor 였다.
RequestMappingHandlerAdaptor 는 HandlerAdaptor 를 implements 하는 추상 클래스인AbstractHandlerMethodAdapter 의 자식 클래스이고, AbstractHandlerMethodAdapter 는 HandlerAdaptor의 handle() 을 Override 하고 있었다.
UML 로 자세하게 보면 다음과 같다.

// AbstractHandlerMethodAdapter 코드 내부
public final ModelAndView handle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
)throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}
@Nullable
protected abstract ModelAndView handleInternal(
HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod
) throws Exception;
AbstractHandlerMethodAdaptor 는 템플릿 메서드 패턴을 사용해 handle() 을 Override 하고 있었다.
handle() 의 핵심 로직은 AbstractHandlerMethodAdaptor 의 구현체 중 하나인 RequestMappingHandlerAdaptor 의 handleInternal() 내부에 존재했다.
AbstractHandlerMethodAdaptor 이므로, HandlerMethod 로 캐스팅해 handleInternal() 에 전달함에 유의하자.
중요한 부분은, invokeHandlerMethod() 로, 이 부분에서 핸들러 메서드를 호출하고 있었다.
RequestMappingHandlerAdaptor 의 invokeHandlerMethod() 를 살펴 보자.
@Nullable
protected ModelAndView invokeHandlerMethod(
HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod
) throws Exception {
...
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
...
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
...
invocableMethod.invokeAndHandle(webRequest, mavContainer);
...
return getModelAndView(mavContainer, modelFactory, webRequest);
}
private @Nullable ModelAndView getModelAndView(
ModelAndViewContainer mavContainer,
ModelFactory modelFactory,
NativeWebRequest webRequest
) throws Exception {
modelFactory.updateModel(webRequest, mavContainer);
if (mavContainer.isRequestHandled()) {
return null;
}
...
}
여기서는 ServletInvocableHandlerMethod 와 ModelAndViewContainer, getModelAndView() 가 중요하다.
getModelAndView() 에서 null 이 리턴된다. ModelAndViewContainer 의 isRequestHandled() 가 true 이기 때문이다.
이 시점에서, RequestMappingHandlerAdaptor의 handle() 의 리턴값이 왜 null 인지 알 수 있다.
일차적으로 RequestMappingHandlerAdaptor 의 getModelAndView() 가 null 을 응답하기 때문이다.
더 깊게 생각해보면, ModelAndViewContainer 라는 클래스의 isRequestHandled() 가 true 가 되기 때문이다.
ServletInvocableHandlerMethod 와 ModelAndViewContainer 를 알아 보며 ModelAndViewContainer의 isRequestHandled()가 무엇이고, 왜 true 인지 살펴 보자.
3. ModelAndViewContainer

ModelAndViewContainer 는 Spring MVC 내부에서 컨트롤러 실행 결과를 임시로 저장하는 컨테이너 객체이다. DispatcherServlet과 HandlerAdapter, HandlerMethod 사이의 로직 수행 과정에서 모델 데이터, 뷰 이름, 상태 코드 등을 담는 데 사용된다.
우리가 눈여겨봐야 할 것은 ModelAndViewContainer 의 requestHandled 필드이다.
public class ModelAndViewContainer {
...
private boolean requestHandled = false;
...
}
ModelAndViewContainer 의 requestHandled 필드는 현재 요청이 컨트롤러 실행 이후에 별도의 뷰 렌더링 없이 직접 처리되었는지를 나타내는 플래그이다.
이 requestHandled 는 RequestMappingHandlerAdaptor 의 getModelAndView() 에서 사용된다.
requestHandled 이 true 이기 때문에, 핸들러 어댑터의 handle() 의 리턴값이 null 이 된다.
requestHandle는 이후에 RequestResponseBodyMethodProcessor 에서 다룰 예정이므로, 기억하고 있자.
4. ServletInvocableHandlerMethod

ServletInvocableHandlerMethod는 Spring MVC 내부에서 사용되는 클래스이며, HTTP 요청을 실제 컨트롤러 메서드에 연결해서 실행하고, 그 결과를 적절히 처리하는 역할을 담당한다.
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
...
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
...
public void invokeAndHandle(
ServletWebRequest webRequest,
ModelAndViewContainer mavContainer,
Object... providedArgs
) throws Exception {
...
this.returnValueHandlers.handleReturnValue(
returnValue,
getReturnValueType(returnValue),
mavContainer,
webRequest
);
...
}
}
ServletInvocableHandlerMethod 가 대표적으로 하는 일은 invokeAndHandle(…) 이다.
invokeAndHandle()은 핸들러를 호출하고, 그 반환값을 적절히 응답으로 변환하는 역할을 한다.
invokeAndHandle() 에서 중요한 부분은, this.returnValueHandlers.handleReturnValue() 의 호출 부분이다.
this.returnValueHandlers 는 HandlerMethodReturnValueHandlerComposite 이다.
HandlerMethodReturnValueHandlerComposite 이 뭘까?
HandlerMethodReturnValueHandlerComposite 을 알아보기 전에, HandlerMethodReturnValueHandler 에 대한 이해가 필요하다.
5. HandlerMethodReturnValueHandler

HandlerMethodReturnValueHandler는 핸들러의 반환값을 HTTP 응답으로 변환하는 역할을 담당하는 전략 인터페이스이다.
즉, 핸들러가 실행되고 나서 반환한 값을 JSON으로 직렬화하거나, 뷰 이름으로 해석하거나, HTTP 상태코드와 함께 Resposne Body를 구성하거나 하는 등의 처리를 이 인터페이스 구현체들이 수행한다.
HandlerMethodReturnValueHandler 는 두 가지의 메서드를 가진다.
1. supportsReturnType(...)
이 핸들러가 해당 반환 타입을 처리할 수 있는지 여부를 반환한다.
2. handleReturnValue(...)
실제로 반환값을 HTTP 응답으로 변환하거나, ModelAndViewContainer에 세팅한다.
주요 HandlerMethodReturnValueHandler 예시
@ResponseBody 가 붙은 핸들러의 응답은 HandlerMethodReturnValueHandler 의 구현체 중 하나인 RequestResponseBodyMethodProcessor 가 담당한다.
RequestResponseBodyMethodProcessor 는 HandlerMethodReturnValueHandlerComposite 를 알아 본 이후에 등장하므로, 기억해두자.
그럼 이제 HandlerMethodReturnValueHandlerComposite 를 알아보자.
6. HandlerMethodReturnValueHandlerComposite

위에서 알아 본 HandlerMethodReturnValueHandler 는 핸들러의 반환값을 HTTP 응답으로 변환하는 역할을 담당하는 전략 인터페이스이므로, 여러 구현체들이 존재한다.
public class HandlerMethodReturnValueHandlerComposite
implements HandlerMethodReturnValueHandler {
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList();
...
public void handleReturnValue(
@Nullable Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest
) throws Exception {
HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
} else {
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
@Nullable
private HandlerMethodReturnValueHandler selectHandler(
@Nullable Object value,
MethodParameter returnType
) {
boolean isAsyncValue = this.isAsyncReturnValue(value, returnType);
for(HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if ((!isAsyncValue || handler instanceof AsyncHandlerMethodReturnValueHandler) && handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
}
HandlerMethodReturnValueHandlerComposite 는 HandlerMethodReturnValueHandler 의 여러 구현체들을 List 로 가지고 있다. 동시에, HandlerMethodReturnValueHandler 를 구현한다.
핸들러의 응답 값, 타입을 통해 적절한 HandlerMethodReturnValueHandler 를 찾고, 해당 HandlerMethodReturnValueHandler 의 handleReturnValue() 를 호출해 핸들러의 반환값을 HTTP 응답으로 변환해준다.
이 과정에서, @ResponseBody 어노테이션이 붙은 핸들러의 응답 값에 대한 HandlerMethodReturnValueHandler 로 RequestResponseBodyMethodProcessor 이 선택된다.
이제 RequestResponseBodyMethodProcessor 에 대해 알아보자.
7. RequestResponseBodyMethodProcessor

RequestResponseBodyMethodProcessor 는 @ResponseBody 가 붙은 핸들러의 응답을 처리하는 클래스이다.
다음은 RequestResponseBodyMethodProcessor 의 코드 내부이다.
위에서 HandlerMethodReturnValueHandler 인터페이스는 supportsReturnType() 과 handleReturnValue() 메서드를 선언하고 있다고 했다.
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
}
@Override
public void handleReturnValue(
@Nullable Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest
) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
...
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
supportsReturnType() 에서는 핸들러에 @ResponseBody 어노테이션이 붙어 있는지 여부를 반환한다.
handleReturnValue() 에서는 ModelAndViewContainer 의 requestHandled 를 true 로 만들어 주고, writeWithMessageConverters 내부에서 outputMessage 의 outputBuffer 의 bb(HeapByteBuffer) 에 응답 데이터를 write() 한다.
이 과정에서, ServletServerHttpResponse 객체의 Response Body 에 값이 쓰여지고, ModelAndViewContainer 의 requestHandled 가 true 가 된다.
그로 인해, 해당 핸들러가 반환하는 ModelAndView는 null 이 됨과 동시에, ViewResolver 의 render() 호출 없이 Response Body 에 값이 쓰이는 것이다.
최종 흐름 정리

- DispatcherServlet 의 doDispatch() 호출
- doDispatch() 내부에서 RequestMappingHandlerAdaptor 의 handle() 호출
- RequestMappingHandlerAdaptor 는 handle() 의 주요 로직을 handleInternal() 로 구현하고 있음.
- RequestMappingHandlerAdaptor 의 handleInternal() 은 RequestMappingHandlerAdaptor 의 invocableMethod 필드의 invokeAndHandle() 을 호출
- ServletInvocableHandlerMethod 는 HandlerMethodReturnValueHandlerComposite 를 사용.
- HandlerMethodReturnValueHandlerComposite 은 적절한 HandlerMethodReturnValueHandler 를 찾아 응답을 처리.
- HandlerMethodReturnValueHandler 의 구현체 중 하나인 RequestResponseBodyMethodProcessor 가 선택됨.
- RequestResponseBodyMethodProcessor 는 ModelAndViewContainer 의 requestHandled 를 true 로 만들어 주고, writeWithMessageConverters() 내부에서 outputMessage 의 outputBuffer 의 bb(HeapByteBuffer) 에 응답 데이터를 write().
- RequestMappingHandlerAdaptor 의 invocableMethod 필드의 타입은 ServletInvocableHandlerMethod 임.
- RequestMappingHandlerAdaptor 의 handleInternal() 은 RequestMappingHandlerAdaptor 의 getModelAndView() 호출
- ModelAndViewContainer 의 isRequestHandled() 가 true 이므로, null 리턴.
- 최종적으로, doDispatch() 내부에서 호출한 RequestMappingHandlerAdaptor 의 handle() 에서는 null 이 리턴됨.
세 줄 요약
- 핸들러 어댑터가 반환하는 ModelAndView 는 null 임.
- 왜냐하면, handle() 과정에서 Response Body 에 “직접” 값을 쓰기 때문임.
- 직접 값을 쓰고, ModelAndView 는 null 을 리턴함으로써, View 를 렌더링하지 않음.
결과적으로...

@ResponseBody 가 붙은 핸들러는 response 에 직접 값을 쓰고, ModelAndView 를 null 로 리턴함.
그러므로, View 를 반환한다고 보기 어려움.
따라서, JSON 반환 방식은 전통적인 MVC 에서 벗어났다.
JSON 반환 방식은 Spring MVC 로 동작하긴 하지만 ModelAndView 를 반환하지 않고(null 을 반환하고), 이에 따라 ViewResolver 를 사용하지 않기 때문에 View 로 보기 힘들다는 결론을 내렸다.
'Spring & Spring Boot' 카테고리의 다른 글
내 프로젝트에 Docker가 정말 필요했을까? (3) | 2025.07.14 |
---|---|
@ResponseBody VS ResponseEntity (9) | 2025.05.20 |
RestInterceptor 개발기 - build() 메서드 제거와 v1.0 릴리즈 (0) | 2024.12.23 |
RestInterceptor 개발기 - 패턴 매칭 이슈 (1) | 2024.12.02 |
Spring 라이브러리 개발기 - RestInterceptor v0.1 (0) | 2024.11.29 |