JustDoEat
[트러블슈팅] Swagger에서 Octet-Stream 관련 문제가 생기는 이유와 해결 과정 본문
개요.
Swagger에서 Multipart/form-data 형식으로 파일과, JSON을 같이 보내면 오류가 나옴.
PostMan으로 똑같이 테스트할 때는 오류가 발생하지 않음.
Unsupported Media Type,
Content-Type 'application/octet-stream' is not supported
왜 발생했는지 프로토콜을 분해해 보자.
Swagger에서 Multipart/form-data 형식으로 API 테스트를 할 때 오류 발생,
PostMan으로 똑같이 테스트할 때는 오류가 발생하지 않음. 먼저 프로토콜을 분해해 봤다.
+ 추가로 알면 좋을 것들.
Multipart 요청은 헤더의 Boundary 값을 지정을 해주고,
Boundary값을 기준으로 본문의 데이터를 Part로 나누겠다는 말이다.
컴퓨터는 사람처럼 데이터를 한 번에 볼 수 있는 게 아니라 데이터 스트림을 쭉 읽다가 Boundary값을 보고,
"어 이거 헤더에서 본 Boundary 값인데, 이게 첫 번째 Part 구나? 확인했어 이제 다음 Part가 있나 볼까?"
"Boundary가 또 있네, 이게 두 번째 Part 구나?"
이런 식으로 데이터를 구분한다.
Request Protocol For PostMan
포스트맨에 들어가서 " cmd + option + c " 버튼을 누르면 볼 수 있다.
Raw로 선택해서 봐야지 다 볼 수 있다.
- 맨 위 헤더에 Content-Type : Multipart/form-data 잘 설정되어 있음.
- Boundary값을 기준선으로 1번과 2번을 보면 각각 Content-Type 이 잘 설정되어 있음
1번 Content-Type : application/json
2번 Content-Type: multipart/form-data
Boundary 값으로 데이터가 나누어져 있는 걸 볼 수 있다.
Request Protocol For Swagger
Swagger 요청은 PostMan 이랑 무엇이 다른지 보자.
- Swagger 또한 맨 위 헤더에 Content-Type : Multipart/form-data 잘 설정되어 있음
- 하지만 Boundary값을 기준선으로 1번과 2번을 보면
1번 Content-Type : 지정 안 됨.
2번 Content-Type: image/png
Postman은 form-data 설정 후 각 파트에 대해 ContentType을 지정해 줄 수 있다.
하지만 구조상 Swagger는 따로 설정할 수가 없다.
+ 추가로 알면 좋을 것들.
@RequestBody
- Content-Type을 확인해 해당하는 HttpMessageConverter를 호출
@RequestPart
- MultipartResolver가 먼저 요청을 파트별로 분리, 헤더값을 기준으로 파트만 먼저 나누는 작업을 함.
- 분리된 각 파트는 다시 각 파트 안에 들어있는 Content-Type에 따라 처리.
- JSON 데이터 → MappingJackson2 HttpMessageConverter
- 파일 데이터 → ByteArrayHttpMessageConverter
"그래서 Swagger에서만 octet-stream으로 인식하는 이유가 뭔데!!"
Spring은 Controller로 HTTP 요청이 오게 된다면 헤더의 Content-Type을 먼저 열어보고 해당 타입을 처리할 수 있는 컨버터를 찾게 된다.
일단 헤더의 Content-Type은 multipart/form-data이다. 이걸 보고 스프링은 1차적으로 해당타입을 처리할 수 있는 컨버터를 찾은 후 데이터를 전달한다. 그 후 multipart/form-data를 처리하는 컨버터는
"아 멀티파트네 일단 boundary기준으로 쪼개볼까 "
"쪼개니까 2개의 파트가 나오네? 각각 열어보자"
"두 번째 타입은 image/png네.. 음 이미지니까, 이거 담당자 나와서 가지고가 ~!"
=> 문제없이 처리.
"첫 번째 타입은.. 뭐야 Content-Type이 없잖아? 이건 뭔 데이터야, Octet-stream으로 분류할 테니까 Octet-stream 담당자 나와서 가지고가"
=> octet-stream을 처리할 담당자(컨버터)가 없으므로, 예외발생
"Spring에는 octet-stream을 담당하는 컨버터(담당자)가 없나요?"
"ByteArrayHttpMessageConverter"가 존재하기는 한다.
하지만 이 친구는 Octet-strean을 처리하지만 요청 형식이 byte []로 와야 한다고 한다.
스프링 입장에서는 Byte []가 아닌 JSON을 가지고 있는 Octet-Stream을 보니까 얼이 빠져서 처리를 못한다.
담당자가 없으니 지원하지 않는 타입이라고 하는 거다..
Octet-stream을 처리하는 담당자를 만들어주자(결론)
아래 클래스를 만들어주면 오류가 해결된다, 이게 결론이지만 왜 이런지 아래 이어서 적어보겠다.
@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}
어떤 원리인지 탐구해 보자.
Octet-stream에 담겨오는 데이터는 JSON형식의 데이터기 때문에 스프링에서 JSON을 처리하는 Jackson에게 임무를 부여하면 된다.
Spring에서 컨트롤러로 들어온 JSON을 객체와 맵핑을 시켜주는 역할로
MappingJackson2 HttpMessageConverter 기본적으로 사용이 되는데.
해당 클래스는 AbstractJackson2 HttpMessageConverter의 구현체이다.
그럼 AbstractJackson2 HttpMessageConverter을 따라서 이동을 해보자.
MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
일단 이 부분을 쫓아가보겠다.
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
this(objectMapper);
this.setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
super.setSupportedMediaTypes(supportedMediaTypes);
}
public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T> {
private List<MediaType> supportedMediaTypes = Collections.emptyList();
protected AbstractHttpMessageConverter(MediaType... supportedMediaTypes) {
this.setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}
쭉 쫓아가다 보면 supportedMediaTypes에 MediaType.APPLICATION_OCTET_STREAM 이 등록이 된다.
MappingJacksonHttpMessageConverter 도?
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
private static final List<MediaType> problemDetailMediaTypes;
@Nullable
private String jsonPrefix;
public MappingJackson2HttpMessageConverter() {
this(Jackson2ObjectMapperBuilder.json().build());
}
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, new MediaType[]{MediaType.APPLICATION_JSON, new MediaType("application", "*+json")});
}
MappingJacksonHttpMessageconverter의 생성자도 쭉 따라서 올라가면
MultipartJackson2 HttpMessageConverterd처럼
쭉 쫓아가다 보면 supportedMediaTypes에 MediaType.MediaType.APPLICATION_JSON 이 등록이 된다.
"여기서 유추가 가능한 건"
여기서 유추가 가능한 건, Jackson이 처리할 수 있는 데이터 타입에 octet-stream을 넣어줌으로, Jackson이
octet-stream 타입의 JSON을 직렬화할 수 있다는 걸 대략 알 수가 있었다.
"Octet-Stream인데 Byte []가 아니잖아 담당자 누구야, 오~ Jackson 네가 이제부터 담당자구나?"
- 기존에는, Byte []가 아닌 Octet-Stream을 직렬화할 수 있는 컨버터가 없어서 오류가 발생함.
- Octet-Stream을 수용할 수 있도록 클래스를 만들어줬음.(MultipartJackson2 HttpMessageConverter)
- MultipartJackson2 HttpMessageConverter는 AbstractJackson2 HttpMessageConverter를 상속받고 있음
- 이제부터는 Byte []가 아닌 Octet-Stream은 MultipartJackson2 HttpMessageConverter로 납치될 거임
이러한 과정으로 Content-Type이 Octet-stream이지만, 요청이 JSON형식이면 직렬화가 가능해짐.