이전 포스팅(공통 형식의 응답 효율적으로 처리하기) 에서 공통응답형식을 RequesetBodyAdvice를 구현한 객체에서 처리하였다. 핸들러메서드(Controller 메서드)에서는 실제 결과 데이터 서빙에만 집중할 수 있어 전체적으로 가독성과 개발 생산성이 높아졌다.
하지만 얼마가지 않아 한 가지 문제가 발생하였는데, 바로 핸들러 메서드에서 문자열 응답시 공통응답형식의 적용이 불가능하다는 점이었다.
문자열 응답에도 RequesetBodyAdvice에서 공통형식이 적용되었다면 적어도 아래와 같은 응답을 받아야 하지만 예외가 발생하였다.
{
code: 200,
message: "Success",
data: "문자열 응답"
}
이번 포스트에서는 필자가 해당 문제를 해결한 과정을 크게 3 부분으로 나눠 공유해본다.
본 포스트는 다음과 같은 수순으로 진행됩니다.
1. (기존 문제 해결) - 기존 문제의 원인과 솔루션 찾고 구현하기
2. (이슈1 해결) - 솔루션 적용 시 문제1
3. (이슈2 해결) - 솔루션 적용 시 문제2
1. 문자열 응답에도 공통응답형식 적용시키기위한 방법
아래와 같은 수순으로 설명할 예정이며, 바쁘신 분들은 4번 솔루션 구현과정부터 봐도 문제없다.
- 문제 원인 찾기
- 해결 방법을 위한 조사
- 해결 가능한 방법 고민
- 솔루션 구현하기
- 적용하기
1.1 문자열에는 적용이 불가능한 이유
기존에 작성한 `ResposneBodyAdvice`코드를 보면 Controller에서 어떤값을 리턴해도 advice 코드가 실행되도록 만들었으며, 응답값을 `SuccessResponse`로 래핑하였다.
@RestControllerAdvice
class ResponseAdviceHandler: ResponseBodyAdvice<Any> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
return true
}
override fun beforeBodyWrite(body: Any?, returnType: MethodParameter, selectedContentType: MediaType, selectedConverterType: Class<out HttpMessageConverter<*>>, request: ServerHttpRequest, response: ServerHttpResponse
): Any? {
// ...
SuccessResponse(data = body)
}
}
여기서 supports 메서드를 보면 converterType으로 `HttpMessageConverter`가 들어오는 것을 볼 수 있다. 다시말해, ResponseBodyAdvice가 실행되기 전에 HttpMessageConverter가 정해지는 것이다. beforeBodyWrite 메서드에서 응답데이터를 수정하더라도 이미 정해진 컨버터는 바뀌지 않는다.
여기서 중요한 것은 문자열과 객체를 처리하는 컨버터가 다르다는 것이다. 문자열은 `StringHttpMessageConverter`, 문자열 외 객체는 Jackson 기준 `MappingJackson2HttpMessageConverter`가 사용되어진다.
때문에 핸들러 메서드(Controller 메서드)에서 문자열을 리턴해 `StringHttpMessageConverter`로 정해진 상태에서 응답데이터를 객체로 변환시키다보니 컨버터가 이를 처리하지 못해 예외가 발생하는 것이다.
예외만 막고자 한다면 아래 코드처럼 supports 메서드에서 객체 처리용 컨버터 사용시에만 적용되도록 바꾸면 된다. 하지만 핸들러 메서드에서 문자열 리턴시 공통응답형식이 적용되지 않고 문자열 그대로 클라이언트에게 응답될 것이다.
@RestControllerAdvice
class ResponseAdviceHandler: ResponseBodyAdvice<Any> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
return MappingJackson2HttpMessageConverter::class.java.isAssignableFrom(converterType)
}
override fun beforeBodyWrite(body: Any?, returnType: MethodParameter, selectedContentType: MediaType, selectedConverterType: Class<out HttpMessageConverter<*>>, request: ServerHttpRequest, response: ServerHttpResponse
): Any? {
// ...
SuccessResponse(data = body)
}
}
그렇다면 문자열 리턴시에도 공통응답형식을 적용할려면 어떻게 해야할까?
1.2 HandlerAdapter 탐색하며 해결 가능한 구간 찾기
쉽게 생각해보면 MessageConverter가 정해지기 전에 응답을 조작하면 될 것이다. 문제는 그 사이에 응답 코드를 제어할 수 있는 기능이 어떤 것이 있냐는 것이다. 방법을 찾아내기 위해 핸들러 메서드의 실행부터 응답까지 코드를 디버깅해봤다.
1.2.1 핸들러 메서드 실행 전
DispatcherServlet에서 HandlerAdapter로 위임해 요청과 응답 처리 작업을 진행한다. 이 작업들은 `invokeHandlerMethod` 메서드에서 이뤄진다. 먼저, 핸들러 메서드 실행 전에 요청, 응답 처리시 필요한 정보들을 셋팅한다.
눈여겨 볼 부분은 핸들러 어댑터가 가지는 `argumentResolvers`와 `returnValueHandlers`를 실행할 메서드에 그대로 셋팅한다는 것이다. 이 둘은 각각 요청 처리 작업, 응답 작업 처리를 전담하는 역할을 맡는다.
(프레임워크 내부 코드는 java 코드로 되어있다.)
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// ...
// 핸들러메서드를 실행시킬 객체로 래핑
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
// ArgumentResolver 등록(요청 처리 시 사용)
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
// ReturnValueHandler 등록(응답 처리 시 사용)
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
// 실제 요청 및 응답 작업 실행
invocableMethod.invokeAndHandle(webRequest, mavContainer);
// ...
}
1.2.2 핸들러 메서드 실행과 응답
위 코드에서 `invocableMethod.invocableMethod` 메서드 내부에서는 두 가지 핵심 메서드로 요청과 응답값을 처리한다.
- invokeForRequest 메서드
- 행 전 설정에서 셋팅된 `argumentResolvers`들을 통해, 핸들러 메서드를 실행하는데 필요한 정보를 받아오고 핸들러 메서드를 실행시킨다. 개발자들이 작성한 로직들이 수행되고 응답값을 받아온다. 요청처리에 대해 다루는 글은 아니니 자세한 설명은 생략하겠다. 핸들러 메서드의 응답값을 리턴하고 이를 returnValue 변수에 담는다.
- handleReturnValue 메서드
- 설정 시 넘겨준 `returnValueHandlers`를 이용해 응답 데이터를 처리한다. 자세하게 말자하면 응답데이터를 HttpMessage 형식에 맞춰 바꾸고 부가적인 작업들을 처리한다는 의미이다. 가장 처음 문제가 되었던 MessageConverter 선택과 ResponseBodyAdvice가 실행되는 것도 바로 이 부분이다.
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 요청 처리, 응답 받아옴
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// ...
try {
// 응답 데이터 처리
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
// ...
}
1.2.3 실제로 응답을 처리하는 객체
여기가 가장 중요한 부분이다. returnValueHandlers(복수형)는 `HandlerMethodReturnValueHandler`의 인터페이스를 구현한 구현체들의 모음집이다. 해당 인터페이스의 구현체들이 들어가 있으며 응답 처리 전략을 제공한다. 각각이 서로 다른 응답속성(ResponseBody, ModelAndView 등)을 처리하는 전략을 가지고 있다.
핸들러 메서드에서 어떤 응답속성을 리턴하느냐 따라 이러한 전략들 중 하나가 선택된다. 예를 들어, @ResponseBody라면 `RequestResponseBodyMethodProcessor`가, ModelAndView라면 `ModelAndViewMethodReturnValueHandler`가 선택된다.
필자의 서버는 ResponseBody를 리턴하는 REST API 서버이기 때문에 `RequestResponseBodyMethodProcessor`가 선택된다.(@RestController의 메타어노테이션으로 @ResponseBody가 적용되어있다)
RequestResponseBodyMethodProcessor에서는 모든 ResponseBody 응답데이터 처리가 이뤄진다. 해당 구현체 내부에서 MessageConverter를 순회하며 Converter를 선택, ResponseBodyAdvice를 적용하고 이후 Converter로 JSON 직렬화하는 로직을 찾을 수 있었다.
문제 원인에서 말했던 것처럼 ResponseBodyAdvice가 실행되기 전 Converter가 정해지고 이를 이용하여 응답을 쓰는 것을 알 수 있다.
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// ...
// 등록된 MessageConverter를 순회
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
// 해당 Converter로 처리 가능한지 확인
if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) {
// 처리가능하다면 ResponseBodyAdvice 적용 후 결정된 Converter로 write
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);
// ... converter로 write하는 로직
return;
}
}
}
1.3 어떻게 해결할 수 있을까?
응답처리 흐름을 도식화하면 다음과 같다. 응답처리 과정 중 커스텀 객체를 추가할 수 있는 부분은 빨간색으로 칠해놨다.
Converter로 응답이 쓰여지기 전 응답값을 조작해야 한다. 따라서, 다음 두 방법 중 하나를 커스텀하면 될 것이다.
- 커스텀 MessageConverter 등록 (강제로 Jackson 선택하기)
- 커스텀 ReturnValueHandler 등록 (응답 처리 과정 덮어쓰기)
첫번째로 MessageConverter 구현하여 String이 들어와도 Jackson으로 처리되도록 만들 수 있다. 하지만 문맥상 구조가 이상한 느낌이다. Converter 선택과 실행 사이 Advice가 끼어있기에 Advice까지 이해해야만 모든 로직이 이해가 된다. Converter 만 본다면 어이없는 구조일 수도 있다. Converter와 Advice가 의미적으로 강결합 되는 것이다.
두 번째로 ReturnValueHandler를 구현하여 모든 응답처리 로직을 직접 작성하는 방법이다. 하지만 응답 처리 과정은 꽤나 방대하기 때문에 이를 모두 구현하기에는 상당히 버겁기도 한다.
때문에 커스텀 ReturnValueHandler를 구현하되 객체로 교체(String -> 공통응답객체) 하는 로직만 작성하고 나머지 응답처리 과정은 기존 ReturnValueHandler에게 위임시키로 했다. 응답 처리전략 자체를 설정하기에 의미적으로도 자연스럽다고 생각했다.
1.4 커스텀 ReturnValueHandler 구현하기
ReturnValueHandler 구현을 위한 인터페이스를 살펴보자. 응답을 해당 핸들러로 처리할지 체크하는 supports 메서드와 응답처리 부분을 구현하는 handlerReturnValue 메서드로 구성되어있다.
public interface HandlerMethodReturnValueHandler {
// returnValue의 타입을 보고 해당 ReturnValueHandler를 사용할 것인지
boolean supportsReturnType(MethodParameter returnType);
// 실제 응답처리를 전담하는 로직이 들어감
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
support 메서드는 String 타입이며, ResponseBody일 경우에 처리하면 될 것이다. 난감한 부분은 응답처리를 전담하는 로직을 어떻게 작성하냐는 것이다.
기존에 ResponseBody 처리를 담당하던 `RequestResponseBodyMethodProcessor`의 구현체는 어떻게 처리하는지 살펴보자. 아래코드를 보면 눈에 띄는 점은 `writeWithMessageConverters` 호출이다. 위에서 봤듯 응답처리 로직은 전부 해당 메서드에서 진행된다.
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// ...
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
}
위 내용을 모두 구현할 필요는 없다. 필자가 구현할 커스텀 핸들러에서는 넘어온 returnValue와 returnType만 String에서 SuccessResponse로 수정하고 이후는 그대로 기존 RequestResponseBodyMethodProcessors 가 처리하면 될 것이다.
이를 구현한로직은 다음과 같다.(returnType은 수정하지 않아도 괜찮다. 내부적으로 returnValue에서 타입을 추출해 사용한다.)
class StringResponseBodyReturnValueHandler(
// 기존 Handler 주입받기
private val delegate: HandlerMethodReturnValueHandler
): HandlerMethodReturnValueHandler {
// ...suports 메서드
override fun handleReturnValue(returnValue: Any?, returnType: MethodParameter, mavContainer: ModelAndViewContainer, webRequest: NativeWebRequest) {
// String -> SuccessResponse
val realReturnValue = SuccessResponse(message = returnValue as String, data = null)
delegate.handleReturnValue(realReturnValue, returnType, mavContainer, webRequest)
}
}
delegate에는 RequestResponseBodyMethodProcessors를 주입받을 것이다. 타입만 수정하고 응답처리를 전부 위임할 것이다.
그러면 delegate에서 realReturnValue(객체)를 처리할 수 있는 MessageConverter를 고르고 ResponseBodyAdvice도 실행할 것이다.
수정한 realReturnValue는 객체이므로 MappingJackson2HttpMessageConverter 이 선택될 것이고 필자가 작성한 ResponseBodyAdvice 도 실행될 것이다.
1.5 적용하기, 의존성 주입 이슈
WebMvc의 기능을 확장하고자 할때 `WebMvcConfigurer` 인테페이스를 사용한다. 해당 인터페이스에서 returnValueHandler도 등록할 수 있게 메서드를 지원해준다.
returnValueHandler들은 빈으로 등록되는 것이 아니라 빈 주입을 하지 못한다. 따라서 등록시에 기존에 등록되어 있는 `RequestResponseBodyMethodProcessor`를 가져와 주입하기로 하자.
@Configuration
class WebMvcConfig: WebMvcConfigurer {
override fun addReturnValueHandlers(handlers: MutableList<HandlerMethodReturnValueHandler>) {
handlers.firstOrNull { it is RequestResponseBodyMethodProcessor }
?.let { it as RequestResponseBodyMethodProcessor }
?.also {
handlers.add(StringResponseBodyReturnValueHandler(delegate = it))
}
}
}
여기서 새로운 문제가 발생했다.. 아래 사진에서처럼 메서드 인자로 들어오는 handlers가 빈 리스트라는 것이다. 이래서는 의존성을 주입할 수가 없다.
2. 커스텀 ReturnValueHandler 의존성 주입 문제
커스텀 작성한 ReturnValueHandler에서는 기존 RequestResponseBodyMethodProcessor 에게 응답 처리를 위임하므로 반드시 의존성을 주입받아야한다. 응답처리 과정을 전부 구현할 수는 없기에 해당 이슈를 풀어야만 했다.
2.1 WebConfigurer에서 handlers가 존재하지 않는 이유
addReturnValueHandlers 메서드의 handlers에 정보들이 들어가 있지 않는 이유는 간단하다. returnValueHandlers 들은 RequestMappingHandlerAdapter의 초기화 과정에서 생성되는데 그 이전에 WebMvcConfigurer가 적용되기 때문이다.
아래 핸들러 어댑터의 초기화 과정 코드를 보자. `InitializingBean`를 구현하였기에 afterPropertiesSet 메서드가 실행 되는데(빈 생성, 의존성 주입이 완료되고 실행) 정직하게 returnValueHandler들을 생성, 추가하는 현장을 목격할 수 있다.
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
@Override
public void afterPropertiesSet() {
if (this.returnValueHandlers == null) {
// handler들 가져오기
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
// ...
}
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
// ... 등등 add
return handlers;
}
}
2.2 어떻게 의존성을 추가할 것인가?
그렇다면 기존 handler를 활용하기 위해서는 RequestMappingHandlerAdapter 가 생성 및 초기화가 이루어지고 난 후여야 할 것이다. 또한 기존 returnValueHandlers를 가져오고 추가할 수 있어야한다. 정리하면 아래와 같다.
- RequestMappingHandlerAdapter가 생성 및 초기화 된 후에 이를 이용할 수 있는가?
- RequestMappingHandlerAdapter에서 등록된 returnValueHandlers를 가져올 수 있는가?
- RequestMappingHandlerAdapter에 직접 새로운 returnValueHandler를 추가할 수 있는가?
다행히 RequestMappingHandlerAdapter 가 빈으로 등록되기 때문에 첫번째 문제에서는 자유로울 수 있었다. 또한, 내부적으로 returnValueHandlers를 직접 추가, 설정할 수 있는 getter와 setter 메서드가 존재했다. 정말 다행이다.
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
public void setReturnValueHandlers(@Nullable List<HandlerMethodReturnValueHandler> returnValueHandlers) {
// .. 추가하는 로직
}
public List<HandlerMethodReturnValueHandler> getReturnValueHandlers() {
return (this.returnValueHandlers != null ? this.returnValueHandlers.getHandlers() : null);
}
}
2.3 HandlerAdapter 커스텀해서 적용하기, 이슈 발생
구현 가능함을 알았으니 본격적으로 HandlerAdapter를 커스텀해보자. 설정 클래스를 만들고 `RequestMappingHandlerAdapter`를 주입받자. 그러면 자연스럽게 주입될 빈(HandlerAdapter)이 생성된 이후에 설정 클래스가 객체화 될 것이다.
필자가 만든 커스텀 ReturnValueHandler 을 추가하는 로직은 다음의 순서를 따른다.
- HandlerAdapter에서 초기화한 returnValueHandlers 꺼내오기
- 커스텀 객체에 주입할 의존성 객체 뽑기
- 커스텀 객체 생성 및 추가
- HandlerAdapter에 새롭게 구성한 returnValueHandlers 셋팅
마지막으로 필자가 작성한 로직은 해당 빈 생성 후(의존성 주입 완료 후)에 자동으로 적용되어야한다. 따라서, 빈 라이프사이클 콜백을 통해 의존성 주입 이후 적용되도록 만들었다.
@Configuration
class HandlerAdapterCustomConfig (
private val handlerAdapter: RequestMappingHandlerAdapter,
) {
@PostConstruct
fun postConstruct() {
addStringResponseBodyReturnValueHandler()
}
fun addStringResponseBodyReturnValueHandler() {
val newReturnValueHandlers = mutableListOf<HandlerMethodReturnValueHandler>()
.also{
it.addAll(handlerAdapter.returnValueHandlers ?: emptyList())
}
val responseBodyReturnHandler = handlerAdapter.returnValueHandlers
?.firstOrNull { it is RequestResponseBodyMethodProcessor }
?: return
newReturnValueHandlers.add(StringResponseBodyReturnValueHandler(responseBodyReturnHandler))
handlerAdapter.returnValueHandlers = newReturnValueHandlers
}
}
`RequestMappingHandlerAdapter`에서는 클라이언트의 요청이 들어와 핸들러메서드가 실행될때, 가지고있는 returnValueHandlers를 셋팅하는 구조이기 때문에 큰 문제없이 커스텀 객체를 추가할 수 있었다.
그러나 필자가 작성한 ReturnValueHandler가 사용되지 않았다...
3. ReturnValueHandler 적용순서 문제
하지만 빠짐없이 코드를 작성했음에도 커스텀 ReturnValueHandler가 동작하지 않았다.
3.1 사용되지 않는 이유
왜 사용되지 않는가? HandlerAdapter의 초기화 과정에서 보았듯이 List에 returnValueHandlers들을 만든다. 그리고 이들 중 하나가 선택되어 핸들러메서드의 응답을 처리한다.
실행 로직을 찾아봤다. selectHandler 메서드에서 이를 처리하고 있었는데 단순히 List에서 순회하면서 처리가능한 것을 발견하면 곧바로 해당 객체를 적용한다.
보통 Ordered 인터페이스를 구현해 우선순위를 정해주기도 하는데 단순한 List 순회여서 순간 당황스러웠다. 아무튼 기존 `RequestResponseBodyMethodProcessor`로 처리가 가능하기 때문에 커스텀 객체에게는 기회가 가지 않는 것이 문제였다.
3.2 적용 순서 교체하기
해당 문제의 해결법은 단순하다. 커스텀 객체를 returnValueHandlers의 가장 앞에 추가하면 된다.
@Configuration
class HandlerAdapterCustomConfig (
private val handlerAdapter: RequestMappingHandlerAdapter,
) {
// ...
// 커스텀 ReturnValueHandler 의존성 해결, 최우선순위로 등록
private fun addStringResponseBodyReturnValueHandler() {
val newReturnValueHandlers = mutableListOf<HandlerMethodReturnValueHandler>()
val responseBodyReturnHandler = (handlerAdapter.returnValueHandlers
?.firstOrNull { it is RequestResponseBodyMethodProcessor }
?: return)
// 우선 추가
newReturnValueHandlers.add(StringResponseBodyReturnValueHandler(responseBodyReturnHandler))
newReturnValueHandlers.addAll(handlerAdapter.returnValueHandlers ?: emptyList())
handlerAdapter.returnValueHandlers = newReturnValueHandlers
}
}
3.3 기존 ResponseBodyAdvice 수정 & 마무리
마지막으로 기존에 작성했던 ResponseBodyAdvice의 supports 메서드를 수정했다. 커스텀 ReturnValueHandler 작성 당시 returnValue는 객체로 수정해서 기존 ReturnValueHandler로 위임했지만 returnType은 따로 수정하지 않았다.
때문에 returnType은 String으로 유지된다. 이를 이용해서 핸들러메서드에서 String 반환시 ResponseBodyAdvice를 타지 않도록 만들어줬다.(해당 로직을 타면 SuccessResponse가 중첩 적용되기 때문에)
@RestControllerAdvice(basePackages = ["com.adevspoon"])
class SuccessResponseBodyAdvisor: ResponseBodyAdvice<Any> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
return returnType.parameterType != String::class.java &&
MappingJackson2HttpMessageConverter::class.java.isAssignableFrom(converterType)
}
}
다행히 더 이상의 이슈없이 잘 동작했다!(필자는 String 반환 시 SuccessResponse의 data 필드가 아닌 message 필드에 넣는 것으로 ReturnValueHandler를 수정했다)
4. 마무리
긴 과정을 정리해보자
문제 | Controller에서 String 반환시 MessageConvter가 String 처리용으로 정해져 공통응답객체로 변환하지 못함. |
해결방법 | - 응답 데이터 처리를 전담하는 ReturnValueHandler를 직접 구현하여 응답 처리전 String을 공통응답객체로 변환하기 - 기존 ReturnValueHandler를 활용하여 이후 과정은 위임하기 |
이슈 | 1. 기존 ReturnValueHandler를 가져와 커스텀 객체 주입하기 위해서는 HandlerAdapter에 직접 접근해 커스텀해야한다. 2. 등록된 ReturnValueHandler들은 단순 순회하며 선택하기하기 때문에 커스텀 객체를 리스트 앞단에 추가해야한다. |
구현부분을 정리하면 다음과 같다.
- String 응답처리 ReturnValueHandler 구현
- MessageConverter로 응답을 쓰기전에 핸들러 메서드의 응답값을 수정하기 위함
- HandlerAdapter 커스텀 설정 클래스 구현
- 기존 ReturnValueHandler의 꺼내 커스텀 ReturnValueHandler의 의존성으로 사용 및 커스텀 객체를 등록
- 커스텀 객체 우선 적용을 위한 returnValueHandlers의 index 0에 추가
- 기존에 구현한 ResponseBodyAdvice supports 메서드 수정
- (String 타입에서는 사용되지 않도록)
마치며
단순한 문제처럼 보였지만 이를 처리하기 위해 꽤나 많은 과정을 거쳤다. 여러가지로 커스텀하기도 마개조하기도 한 것 같다. HandlerAdapter의 수행과정을 보다 깊이 알아볼 수 있었고 여러모로 배울 수 있었던 과정이었다.
마지막으로 위 과정들을 적용한 프로젝트 PR 링크를 남긴다. 도움이 될 수 있기를..
'Spring' 카테고리의 다른 글
예외 정보를 인터페이스와 infix함수로 확장성, 가독성 높게 관리하기 (0) | 2024.04.18 |
---|---|
API별 인증 해제, 어노테이션으로 효율적으로 처리하기 (0) | 2024.04.09 |
공통 형식의 응답 효율적으로 처리하기 (0) | 2024.04.04 |
Query에 포함된 다수의 Legacy Enum 우아하게 관리하기 (0) | 2024.04.04 |
요청/응답 Body에 포함된 다수의 Legacy Enum 우아하게 관리하기 (0) | 2024.04.04 |