Spring

HttpMessageConverter 우선순위(feat. AWS S3)

민철킹 2023. 10. 31. 23:17

최근 회사에서 특정 API에 Spring Interceptor를 통해 HTTP RequestJSON으로 파싱하고 S3에 업로드하는 로직을 수정하는 작업을 진행했다.

 

로직의 흐름은 간단하다.

 

InterceptorafterCompletion 메서드에서 요청이 성공했냐 실패했냐에 따라 다른 S3 경로에 업로드하기만 한다.

 

물론, 우리는 모든 요청은 JSON으로 반환될 것임을 기대하였고 ObjectMapper를 통해 RequestBodyJSON으로 변환하는 로직을 내부에 추가하였다.

 

JSON으로 값이 반환될 수 있도록 

 

@RequestMapping(value = "/something", produces = {MediaType.APPLICATION_JSON_VALUE})

 

produce를 통해 반환될 값을 지정하였다.

 

배포 후, 서버에는 json parse error가 무수히 찍히기 시작했다.(+ content-type이 맞지 않아 aws error도 덤으로)

 

여기서 주목할만한 사항은 요청 성공(200)에는 json으로 반환되어 정상적으로 파싱되어 S3에 업로드되었고, 요청 실패(400)에는 xml로 반환되어 파싱이 되지 않아 기본으로 설정해놓은 DEFAULT_MESSAGE만 S3에 업로드되었다.

 

내부에서 어떤 일이 일어나고 있는 걸까?

 


먼저, 해당 api를 사용하고 있는 외부 제휴사에게 제공된 API 가이드 문서를 확인했다.

 

가이드 문서에는 Content-Type과 Accept 헤더를 모두 application/json으로 보낼 것이라 명시되어 있었다.

 

만약 클라이언트가 우리의 가이드대로 헤더를 모두 application/json으로 요청했다면 애초에 xml이 반환되지 않았을 것이다.

 

사용자는 Accept 헤더 자체를 보내고 있지 않았고

 

즉, 클라이언트는 우리의 예상과 다르게 API를 사용하고 있었다.

 

 

그럼에도

 

서버는 Accept 헤더에 유연하게 대응할 수 있어야한다고 생각한다.

 

물론 내부 비즈니스 로직적으로는 전혀 문제가 되지 않는다. 우리가 인터셉트하는 부가 로직에서 에러가 발생하는 것 뿐이니 사용자에게는 정상적으로 응답이 반환되고 있었다.

 


무엇이 문제였을까?

 

먼저, 정상 흐름에서는 produce를 통해 설정되어 있기 때문에 JSON이 반환되었지만 예외 상황에서는 이 옵션이 적용되지 않은 채 DispatcherServlet에 전달되었고 이를 ExceptionHandler에서 처리하여 인터셉터까지 전달되었을 것이다.

 

즉, 실패상황에서의 Media Type은 서버의 HttpMessageConverter의 우선순위에 따라 결정된다.

 

우리가 사용하는 Spring BootHttpMessageConverter의 우선순위가 궁금해지기 시작했다.

 

내가 알고있던 것은 JSON Converter가 XML Converter보다 우선순위가 높다는 사실이었는데, 위 상황으로 보았을 때는 XML Converter가 우선순위가 높다는 말이되니까.

 

 

아래 글부터는 jackson xml converter 라이브러리가 추가되어있다고 가정된 상황에서 시작한다.

implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'

 


Spring Boot Auto Configuration

 

Spring Boot를 사용하면 대부분의 복잡한 설정을 Spring Boot가 자동으로 올라가는 시점에 도와준다.

 

Spring Boot는 기본 우선순위를 어떻게 설정하고 있을까?

 

HttpMeesageConverters.java

 

HttpMessageConverters 내부를 살펴보면 답을 알 수 있다.

 

getDefaultConverters를 통해 List<HttpMessageConverter>를 가져오고 이를 적절히 가공하여 필드에 담아준다.

 

로직을 보면 WebMvcConfigurationSupportgetMessageConverters를 통해 기본 컨버터 목록을 가져오는데 내부를 살펴보면 WebMvcConfigurationSupport 클래스는 아래와 같은 우선순위로 컨버터를 생성해 담아준다.

 

 

 

이 코드만 보면 "어? XML이 우선순위가 더 높네!"라고 말할 수도 있지만 사실은 그렇지 않다.

 

이렇게 반환된 컨버터들을 HttpMessageConverters가 재가공한다.

 

HttpMessageConverters.getDefaultConverters 로직을 살펴보면 reorderXmlConvertersToEnd 라는 메서드가 호출되고 있는 것이 보이는데, 메서드명 그대로 XML 컨버터의 우선순위를 최하위로 낮추는 역할을 한다.

 

HttpMessageConverters.reorderXmlConvertersToEnd

 

이렇게 우리가 별도의 설정없이 Spring Boot Auto Configuration을 사용한다면 JSON이 XML보다 우선순위가 높다.

 

 


WebMvcConfigurationSupport

 

그럼 문제가 발생한 코드에서는 어떠한 이유로 XML이 우선순위가 높아지게 되었을까?

 

 

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

    @Bean
    public Something something() {
    	return doSomething();
    }
}

 

사내 코드의 WebMvcConfig 클래스는 위와 같은 형식으로 작성되어 있었는데, 여기서 주목할 것은 상속받고 있는 WebMvcConfigurationSupport이다.

 

우리의 WebMvcConfig는 구체 클래스인 WebMvcConfigurationSupport를 상속하고 있기 때문에 Spring Boot가 자동으로 설정해주는 MessageConverter가 아닌 WebMvcConfigurationSupportMessageConverter의 우선순위를 따르게 된 것이다.

 

(WebMvcConfigurationSupport는 앞서 말했듯이 XML이 우선순위가 높고 이를 HttpMessageConverters 로직에 의해 재조정된다.)

 

이를 직접 확인해보면,

 

XML Converter가 JSON Converter보다 우선순위가 높다.

 

 


해결책

 

어떻게하면 우선순위를 조정할 수 있을까?

 

WebMvcConfigurationSupport가 제공하는 extendMessageConverters를 오버라이드해 HttpMessageConverters.reorderXmlConvertersToEnd가 하는 것과 유사한 로직을 작성해서 해결할 수 있다.

 

 

 

직접 오버라이딩을 통해 우선순위를 조정한 뒤 디버깅을 통해 우선순위가 조정되었고 Accept 헤더가 없는 경우 JSON이 반환되는 것을 확인할 수 있었다.

 

 


WebMvcConfigurer

 

우리는 일반적으로 WebMvcConfig를 커스텀하게 작성하기위해 WebMvcConfigurer 인터페이스를 많이 활용한다.

 

말그대로 WebMvcConfigurer는 인터페이스이다.

 

우리가 별도로 구현한 것만 반영이 되고 그 외는 Spring Boot의 기본 설정을 따라가지만 WebMvcConfigurationSupport는 구체 클래스이므로 상속하는 순간 기본 설정이 덮어씌여진다.

 

WebMvcConfigurer 인터페이스를 사용하면 위와 같이 xml이 최하위의 우선순위를 가지게 되는 것을 확인할 수 있다.

 


결론적으로, Spring Boot의 기본 설정을 유지하면서도 좀 더 유연하게 확장이 가능한 WebMvcConfigurer 인터페이스를 구현해 커스텀하게 WebMvcConfig를 작성하는 것이 좋은 방식이라고 느껴진다.

 

또한, 이번 계기로 클라이언트는 우리가 기대한대로 사용하지 않으며 그에 대한 대비를 하는 것이 개발자의 숙명이라는 느낌을 받았다.

반응형

'Spring' 카테고리의 다른 글

yml에서 List Object 사용법  (0) 2022.04.29
@ModelAttribute와 @RequestBody 그리고 Setter  (4) 2021.11.24
Spring과 Redis를 연동하여 Session Clustering  (0) 2021.11.19
패키지 구조 설계  (0) 2021.07.17
Thymeleaf와 Spring  (0) 2021.07.04