JAVA

Spring Boot MessageConverter

devchive 2024. 6. 24. 01:29

개요

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에 대한 처리를 따로 해주었다. 파싱 에러난 부분에 대해서는 추후 추가적인 처리가 필요해 보인다.