본문 바로가기

Spring & Spring Boot

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

제목을 보고 의아함을 느낀 독자가 있을 것이다.
"Json 도 Response Body 에 들어 있는데, 사용자에게 무언가를 보여 주기 위한 정보니까 View 아닌가...?"
"View 를 위해 사용되는 데이터니까 View 아닌가...?"
등등, 많은 의문이 들 수 있다고 생각한다.
 
관심을 끌기 위해 자극적으로 제목을 지었다.
지금부터 다룰 주제에 적합한 제목은 사실
“Spring MVC 관점 에서 JSON 응답은 View 가 아니다!“ 이다.

지금부터 이를 증명하고자 한다.
 
 

이 여정은 한 의문에서 시작되었다

 
 

View 의 렌더링은 핸들러가 반환하는 ModelAndView 를 통해 이루어진다.

@ResponseBody 가 붙은 핸들러는 어떤 ModelAndView 값을 반환할까?

 
 
 
 

1. DispatcherServlet

요청이 들어오면, DispatcherServletdoDispatch() 가 호출된다.

// doDispatch() 내부 코드.
// ha = 핸들러 어댑터
// mv = ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

doDispatch() 내부에서는 핸들러 어댑터를 통해 요청에 해당하는 핸들러를 실행하게 된다.
 
 

// 위에서 얻은 ModelAndView(mv) 를 사용해 뷰를 렌더링한다.
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

핸들러가 ModelAndView 를 반환하면, processDispatchResult() 를 통해 뷰를 렌더링한다.
이 과정을 디버깅하다가, @ResponseBody 가 붙은 핸들러가 반환하는 ModelAndViewnull 임을 알게 되었다.
DispatcherServlet 은 핸들러가 반환한 ModelAndViewnull 이면 렌더링을 수행하지 않고 건너뛴다.
@ResponseBody 가 붙은 핸들러가 호출되면 값이 nullModelAndView 를 반환하고, 이로 인해 뷰가 렌더링되지 않는다는 사실을 알 수 있다.
그렇다면, @ResponseBody 가 붙은 핸들러는 어떻게 null 을 반환하게 되는 걸까?
 
 
 
 
 

2. RequestMappingHandlerAdaptor

 
@ResponseBody 가 붙은 핸들러의 핸들러 어댑터는 RequestMappingHandlerAdaptor 였다.
RequestMappingHandlerAdaptorHandlerAdaptorimplements 하는 추상 클래스인AbstractHandlerMethodAdapter 의 자식 클래스이고, AbstractHandlerMethodAdapterHandlerAdaptorhandle()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 의 구현체 중 하나인 RequestMappingHandlerAdaptorhandleInternal() 내부에 존재했다.
AbstractHandlerMethodAdaptor 이므로, HandlerMethod캐스팅해 handleInternal() 에 전달함에 유의하자.
 
 
중요한 부분은, invokeHandlerMethod() 로, 이 부분에서 핸들러 메서드를 호출하고 있었다.
RequestMappingHandlerAdaptorinvokeHandlerMethod() 를 살펴 보자.

@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;
		}
		...
}


여기서는 ServletInvocableHandlerMethodModelAndViewContainer, getModelAndView() 가 중요하다.
 
getModelAndView() 에서 null 이 리턴된다. ModelAndViewContainerisRequestHandled()true 이기 때문이다.
 
이 시점에서, RequestMappingHandlerAdaptorhandle() 의 리턴값이 왜 null 인지 알 수 있다.
일차적으로 RequestMappingHandlerAdaptorgetModelAndView()null 을 응답하기 때문이다.
더 깊게 생각해보면, ModelAndViewContainer 라는 클래스의 isRequestHandled()true 가 되기 때문이다.
 
ServletInvocableHandlerMethodModelAndViewContainer 를 알아 보며 ModelAndViewContainerisRequestHandled()가 무엇이고, 왜 true 인지 살펴 보자.
 
 
 
 

3. ModelAndViewContainer

 
ModelAndViewContainer 는 Spring MVC 내부에서 컨트롤러 실행 결과를 임시로 저장하는 컨테이너 객체이다. DispatcherServletHandlerAdapter, HandlerMethod 사이의 로직 수행 과정에서 모델 데이터, 뷰 이름, 상태 코드 등을 담는 데 사용된다.
 
우리가 눈여겨봐야 할 것은 ModelAndViewContainerrequestHandled 필드이다.
 

public class ModelAndViewContainer {

    ...
    
    private boolean requestHandled = false;
    
    ...
}

 
ModelAndViewContainerrequestHandled 필드는 현재 요청이 컨트롤러 실행 이후에 별도의 뷰 렌더링 없이 직접 처리되었는지를 나타내는 플래그이다.
requestHandledRequestMappingHandlerAdaptorgetModelAndView() 에서 사용된다.
requestHandledtrue 이기 때문에, 핸들러 어댑터의 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.returnValueHandlersHandlerMethodReturnValueHandlerComposite 이다.
HandlerMethodReturnValueHandlerComposite 이 뭘까?
 
HandlerMethodReturnValueHandlerComposite 을 알아보기 전에, HandlerMethodReturnValueHandler 에 대한 이해가 필요하다.
 
 
 
 

5. HandlerMethodReturnValueHandler

HandlerMethodReturnValueHandler는 핸들러의 반환값을 HTTP 응답으로 변환하는 역할을 담당하는 전략 인터페이스이다.
즉, 핸들러가 실행되고 나서 반환한 값을 JSON으로 직렬화하거나, 뷰 이름으로 해석하거나, HTTP 상태코드와 함께 Resposne Body를 구성하거나 하는 등의 처리를 이 인터페이스 구현체들이 수행한다.
 
HandlerMethodReturnValueHandler 는 두 가지의 메서드를 가진다.

1. supportsReturnType(...)
이 핸들러가 해당 반환 타입을 처리할 수 있는지 여부를 반환한다.

2. handleReturnValue(...)
실제로 반환값을 HTTP 응답으로 변환하거나, ModelAndViewContainer에 세팅한다.

 
 
주요 HandlerMethodReturnValueHandler 예시
@ResponseBody 가 붙은 핸들러의 응답은 HandlerMethodReturnValueHandler 의 구현체 중 하나인 RequestResponseBodyMethodProcessor 가 담당한다.
RequestResponseBodyMethodProcessorHandlerMethodReturnValueHandlerComposite 를 알아 본 이후에 등장하므로, 기억해두자.
그럼 이제 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;
    }
}

 
HandlerMethodReturnValueHandlerCompositeHandlerMethodReturnValueHandler 의 여러 구현체들을 List 로 가지고 있다. 동시에, HandlerMethodReturnValueHandler 를 구현한다.
핸들러의 응답 값, 타입을 통해 적절한 HandlerMethodReturnValueHandler 를 찾고, 해당 HandlerMethodReturnValueHandlerhandleReturnValue() 를 호출해 핸들러의 반환값을 HTTP 응답으로 변환해준다.
이 과정에서, @ResponseBody 어노테이션이 붙은 핸들러의 응답 값에 대한 HandlerMethodReturnValueHandlerRequestResponseBodyMethodProcessor 이 선택된다.
이제 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() 에서는 ModelAndViewContainerrequestHandledtrue 로 만들어 주고, writeWithMessageConverters 내부에서 outputMessageoutputBufferbb(HeapByteBuffer) 에 응답 데이터를 write() 한다.
이 과정에서, ServletServerHttpResponse 객체의 Response Body 에 값이 쓰여지고, ModelAndViewContainerrequestHandledtrue 가 된다.
그로 인해, 해당 핸들러가 반환하는 ModelAndViewnull 이 됨과 동시에, ViewResolverrender() 호출 없이 Response Body 에 값이 쓰이는 것이다.
 
 
 
 

최종 흐름 정리

  1. DispatcherServletdoDispatch() 호출
  2. doDispatch() 내부에서 RequestMappingHandlerAdaptorhandle() 호출
  3. RequestMappingHandlerAdaptorhandle() 의 주요 로직을 handleInternal() 로 구현하고 있음.
  4. RequestMappingHandlerAdaptorhandleInternal()RequestMappingHandlerAdaptorinvocableMethod 필드의 invokeAndHandle() 을 호출
    1. ServletInvocableHandlerMethodHandlerMethodReturnValueHandlerComposite 를 사용.
    2. HandlerMethodReturnValueHandlerComposite 은 적절한 HandlerMethodReturnValueHandler 를 찾아 응답을 처리.
    3. HandlerMethodReturnValueHandler 의 구현체 중 하나인 RequestResponseBodyMethodProcessor 가 선택됨.
    4. RequestResponseBodyMethodProcessorModelAndViewContainerrequestHandledtrue 로 만들어 주고, writeWithMessageConverters() 내부에서 outputMessageoutputBufferbb(HeapByteBuffer) 에 응답 데이터를 write().
  5. RequestMappingHandlerAdaptorinvocableMethod 필드의 타입은 ServletInvocableHandlerMethod 임.
  6. RequestMappingHandlerAdaptorhandleInternal()RequestMappingHandlerAdaptorgetModelAndView() 호출
    1. ModelAndViewContainerisRequestHandled()true 이므로, null 리턴.
  7. 최종적으로, doDispatch() 내부에서 호출한 RequestMappingHandlerAdaptorhandle() 에서는 null 이 리턴됨.

 
 
 

세 줄 요약

  1. 핸들러 어댑터가 반환하는 ModelAndViewnull 임.
  2. 왜냐하면, handle() 과정에서 Response Body 에 “직접” 값을 쓰기 때문임.
  3. 직접 값을 쓰고, ModelAndViewnull 을 리턴함으로써, View 를 렌더링하지 않음.

 
 

결과적으로...

한 크루가 흐름 정리를 하며 힘들어하던 나에게 그려 준 그림이다 (바보같은 그림이라 피식했다)

 
@ResponseBody 가 붙은 핸들러는 response 에 직접 값을 쓰고, ModelAndViewnull 로 리턴함.
그러므로, View 를 반환한다고 보기 어려움.
 
따라서, JSON 반환 방식은 전통적인 MVC 에서 벗어났다.

JSON 반환 방식은 Spring MVC 로 동작하긴 하지만 ModelAndView 를 반환하지 않고(null 을 반환하고), 이에 따라 ViewResolver 를 사용하지 않기 때문에 View 로 보기 힘들다는 결론을 내렸다.