ABOUT ME

-

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

     

     

    'JAVA' 카테고리의 다른 글

    Spring Boot AOP를 이용해 로그인 검증하기  (0) 2024.07.29
    Java mutable 과 immutable  (1) 2023.10.08
    Java와 일급 함수  (0) 2023.09.18
    Java Generic Compile 동작  (0) 2023.09.11
    JVM Memory Area  (0) 2023.09.08
Designed by Tistory.