Spring Boot MessageConverter
개요
Spring Boot 기반 개인 프로젝트를 진행하는중 MessageConverter 관련해 문제가 있었다. 이 문제를 해결하며 알게된 내용을 정리해보았다.
문제
@GetMapping("/test2")
public String test2(){
return "test 2";
}
@GetMapping("/test3")
public List<String> test3(){
return List.of("t","e","s","t");
}
기존 코드의 Controller 부분이다. /test2 에 요청하면 String 을 반환하고, /test3 에 요청하면 ["t","e","s","t"] 를 반환한다.
public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return new BaseResponse<>(ResponseStatus.SUCCESS, body);
}
ResponseBodyAdvice 를 구현한 클래스에서 beforeBodyWrite 를 이용해서 최종 응답이 나가기 전에 내가 정의한 BaseResponse 형식으로 통일해서 나간다.
Advice 에서 응답 형식을 맞춰주어 Controller 에서 직접 응답마다 BaseResponse 를 만들어 주는 수고를 줄이고 공통적으로 처리하려했다. 하지만 위 코드를 테스트 해보면 문제가 생긴다.
/test3 인경우는 정상 동작하지만 /test2 에 요청을 보내면 아래와 같은 에러가 발생한다.
java.lang.ClassCastException: class com.cal.calB.dto.response.BaseResponse cannot be cast to class java.lang.String
의미를 보자면 BaseResponse를 String으로 cast 하다가 오류가 발생했다고한다. 왜 이런 에러가 발생했는지 확인해보았다.
원인
결론부터 말하면 String을 반환할때와 그렇지 않을때의 MessageConverter 가 다르기 때문이다.
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
...
}
Spring 의 AbstractMessageConverterMethodProcessor 의 writeWithMessageConverters 일부분이다.
writeWithMessageConverters 는 HandlerAdapter -> invokeAndHandle -> handleReturnValue 에서 호출된다.
저 코드에서 value 는 return 한 값인데 저 밸류가 CharSequence 라면 String 으로 처리해주는것을 볼 수 있다.
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter =
(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
}
}
마찬가지로 writeWithMessageConverters 의 일부분이다. 위에서 정한 valueType, targetType 등에 따라 등록된 converters를 돌면서 canWrite 을 검사한다.
public boolean supports(Class<?> clazz) {
return String.class == clazz;
}
위 코드는 StringHttpMessageConverter 의 supports 함수이다. canWrite가 호출하는 함수인데 여기서 검사하는 Class 가 String 과 일치하는지 비교한다. 우리는 처음 String을 return 했기때문에 여기서 true가 나오고 StringHttpMessageConverter 를 Converter로 선택하게 된다.
canWrite 검사 후 advice를 돌며 beforeBodyWrite 를 해주게 된다. 이때 우리는 결과물로 body 를 내가 정의한 BaseResponse 를 받게된다. 후에 write 하게 되는데, StringConverter 를 선택해놓고 BaseResponse를 반환하니 에러가 발생하는 것이다.
해결
public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof String){
BaseResponse<String> res = new BaseResponse<>(ResponseStatus.SUCCESS, (String) body);
try {
ObjectMapper objectMapper = new ObjectMapper();
String stringRes = objectMapper.writeValueAsString(res);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return stringRes;
} catch (JsonProcessingException err){
throw new RuntimeException("Failed to convert BaseResponse to JSON");
}
}
return new BaseResponse<>(ResponseStatus.SUCCESS, body);
}
Advice 의 beforeBodyWrite에서 String에 대한 처리를 따로 해주었다. 파싱 에러난 부분에 대해서는 추후 추가적인 처리가 필요해 보인다.