Spring boot를 사용한 백엔드 프로젝트에 API를 추가하는 작업 중이었다. 모듈간 통신으로 Request Body에 객체를 담아 전달했고 DTO 구조는 다음과 같았다.
import lombok.*;
import org.apache.commons.collections4.keyvalue.MultiKey;
import java.time.LocalDateTime;
import java.util.*;
@Builder
@Getter @Setter
public class ParamDataDto {
private Map<MultiKey<Integer>, Integer> dataMap;
...
}
개발을 마무리하고 테스트를 하는데 다음과 같은 에러 가 떨어졌다.
[WARN ] [27,14:47:51.027] [https-jsse-nio-8080-exec-10 AbstractHandlerExceptionResolver.java:208] logException - Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported]
org.springframework.web.reactive.function.client.WebClientResponseException$UnsupportedMediaType: 415 Unsupported Media Type from POST https://{Server ip}:8080/.../send-data
at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:212)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
...
로그에서 확인한 415 에러는 client 에서 보내주는 데이터와 server에서 받는 데이터의 contentType이 다를 때 발생할 수 있다고 한다. 이 문제를 해결하기 위해 아래와 같은 작업을 해줬다.
1) Header에 contentType을 application/json; charset=utf-8으로 설정
2) Controller에 contentType 명시(consumes)
@PostMapping(value = "{uri}/data", consumes = MediaType.APPLICATION_JSON_VALUE)
...
그럼에도 해결되지 않았다^^
Dto를 넘기는게 문제인 것 같아 어떻게든 해결해보고자 String으로 Request Body 데이터를 넘겨봤다. Dto를 Gson으로 직렬화해서 넘기고 Controller에서 Gson으로 역직렬화 해줬다.
...!
일단 415 에러는 해결되면서 api가 호출됐다. 원인은 알 수 없지만 일단 이렇게 처리해볼까 했는데 역직렬화한 데이터를 보니 위의 Dto에서 dataMap 데이터가 제대로 넘어오지 않았다.
지(식)인의 도움을 받아.. 원인을 찾아냈다. 결론부터 말하자면 원인은 MultiKey
였다.
@RequestBody
의 동작 원리부터 정리해보자면
그렇다면 Jackson 라이브러리는 어떻게 동작하는걸까?
ObjectMapper
를 사용하여 역직렬화를 수행하는데 기본 생성자
로 Dto를 생성한 뒤 Reflection을 통해 필드 정보를 확인, 데이터를 할당하는 방식으로 객체를 생성한다.그런데 MultiKey는? 기본 생성자가 없다..!
해결 방법은 간단하다. 기본 생성자가 있는 타입의 객체를 사용하면 된다.
만약 위의 예시처럼 MultiKey를 사용하고 싶다면 우선 multiKey.getKeys()를 통해 키 전체를 순회하며
1) String으로 합쳐 하나의 문자열로 만들거나
2) ArrayList에 idx 순서대로 담아주면 된다. (참고로 List는 또한 interface이기 때문에 생성자가 없어서 안된다.)
+++ 별도의 Deserializer를 만들어서 사용하는 방법도 있는데, 작업이 복잡하기 때문에 빠르게 해결하기 위해 위의 방법을 사용했다.