[Spring] Swagger + RequestPart๋ฅผ ํ†ตํ•ด ํŒŒ์ผ, Dto ๋™์‹œ ์š”์ฒญ ์‹œ ๋ฐœ์ƒ ์—๋Ÿฌ ํ•ธ๋“ค๋ง

2024. 3. 10. 18:57ใ†Backend/Spring

ํ˜„์žฌ Spring Boot๋ฅผ ํ†ตํ•œ ๋น„๋””์˜ค ์ŠคํŠธ๋ฆฌ๋ฐ ์„œ๋ฒ„๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฐ„๋‹จํ•œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ ํ•ด๋ณด๊ณ  ์žˆ๋Š”๋ฐ, ์ด๋ฅผ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ๋งˆ์ฃผํ•œ ์—๋Ÿฌ์— ๋Œ€ํ•ด ์†Œ๊ฐœํ•˜๊ณ  ์ด๋ฅผ ํ•ด๊ฒฐํ•œ ๋ฐฉ์‹, ๊ทธ๋ฆฌ๊ณ  ์–ด์งธ์„œ ํ•ด๊ฒฐ์ด ๋˜๋Š”์ง€ ๊นŒ์ง€๋„ ๋‹ค๋ค„๋ณด๊ณ ์ž ํ•œ๋‹ค.

 

GitHub - One-armed-boy/spring_stream_video: ๋น„๋””์˜ค ์ŠคํŠธ๋ฆฌ๋ฐ ์„œ๋ฒ„ with Spring boot

๋น„๋””์˜ค ์ŠคํŠธ๋ฆฌ๋ฐ ์„œ๋ฒ„ with Spring boot. Contribute to One-armed-boy/spring_stream_video development by creating an account on GitHub.

github.com

 

๋ฌธ์ œ ์ƒํ™ฉ ๋ฌ˜์‚ฌ

์—”๋“œํฌ์ธํŠธ์— ๋Œ€ํ•ด ์ง์ ‘ ์š”์ฒญ์„ ๋ณด๋‚ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ๋•Œ, ํฌ์ŠคํŠธ๋งจ์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๊ฒ ์ง€๋งŒ ๊ทธ๊ฒƒ๋ณด๋‹จ ์Šค์›จ๊ฑฐ(OpenAPI)๋ฅผ ํ™œ์šฉํ•˜๋Š” ํŽธ์ด ์ดํ›„ ํ˜‘์—… ๋‹จ๊ณ„์—์„œ ๋” ์ข‹๋‹ค๊ณ  ํŒ๋‹จํ•˜๊ณ  ์„œ๋ฒ„ ๋‚ด Swagger EP๋ฅผ ๋šซ์–ด๋‘” ์ƒํƒœ์˜€๋‹ค.

๋„์›Œ๋‘” Swagger ๋ฉ”์ธ ํŽ˜์ด์ง€

๊ทธ ํ›„ ๊ฐ ์—”๋“œํฌ์ธํŠธ์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ.... ์•„๋ž˜ ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

/videos/upload
๋ฐœ์ƒ ์˜ˆ์™ธ ๋ฐ”๋””

"Content-Type 'application/octet-stream' is not supported"๋ผ๊ณ  ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์ฆ‰, application/octet-stream ํ˜•์‹์œผ๋กœ ์š”์ฒญ์„ ๊ฑด๋„ค ๋ฐ›์•˜์œผ๋ฉฐ ์„œ๋ฒ„ ์ธก์—์„œ๋Š” ์ด ํ˜•์‹์„ ์ง€์›ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ์ธ ๊ฒƒ์ด๋‹ค.

 

๋ฌธ์ œ ์›์ธ ๋ถ„์„

์šฐ์„  ์•„๋ž˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋œ ์—”๋“œํฌ์ธํŠธ์˜ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์˜ ์‹ค์ œ ์ฝ”๋“œ์ด๋‹ค.

@PostMapping(path = "/videos/upload", consumes = "multipart/form-data")
public ResponseEntity uploadVideo(@Valid @RequestPart(value = "videoMetadata") UploadVideoRequest request,
	@RequestPart(value = "videoFile") MultipartFile videoFile) {
	var member = securityContextHelper.getMember();
	uploadFacade.uploadVideoSync(UploadVideoCommand.builder()
		.fileName(request.getFileName())
		.extension(request.getExtension())
		.description(request.getDescription())
		.member(member)
		.build(), videoFile);
	return ResponseEntity.ok().build();
}

์–ด๋…ธํ…Œ์ด์…˜์„ ๋ณด๋ฉด ์•Œ ์ˆ˜ ์žˆ์ง€๋งŒ Content-Type์ด 'multipart/form-data'๋กœ ๋ช…์‹œ๋˜์–ด ์žˆ์œผ๋ฉฐ, Swagger์—๋„ ์ด ํƒ€์ž…์€ ์ •์ƒ์ ์œผ๋กœ ๋ฐ˜์˜๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์• ์ดˆ์— ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€์˜ 'appilication/octet-stream'์ด ์–ด๋””์—์„œ ๋“ฑ์žฅํ–ˆ๋Š”์ง€๋ถ€ํ„ฐ ํ™•์ธ์ด ํ•„์š”ํ–ˆ๋‹ค.

application/octet-stream?

์ด๋ฅผ ์•Œ๊ธฐ ์œ„ํ•ด์„œ๋Š” MIME์— ๋Œ€ํ•œ ์‚ฌ์ „ ์ง€์‹์ด ์กฐ๊ธˆ ํ•„์š”ํ•˜๋‹ค. 

MIME์ด๋ž€ Multipurpose Internet Mail Extensions์˜ ์•ฝ์ž๋กœ ์Œ์•…์ด๋‚˜ ์‚ฌ์ง„, ๋น„๋””์˜ค ๋“ฑ์˜ ์ด์ง„ ํŒŒ์ผ์„ ๋„คํŠธ์›Œํฌ๋ฅผ ํ†ตํ•ด ์ „์†กํ•  ๋ชฉ์ ์œผ๋กœ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , ์ด๋ฅผ ์ˆ˜์‹ ํ•˜๋Š” ์ž…์žฅ์—์„œ ๋‹ค์‹œ ํŒŒ์ผํ™”ํ•  ๋•Œ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๊ธฐ์ค€์„ ์ œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค.

์ด ์ค‘ ์ด๋ฒˆ์— ์ง๋ฉดํ•œ ๋ฌธ์ œ์— ๋“ฑ์žฅํ•˜๋Š” ๋‘ ํƒ€์ž…์— ๋Œ€ํ•ด์„œ๋งŒ ์ถ”๊ฐ€์ ์œผ๋กœ ์•Œ์•„๋ณด์ž.

๋จผ์ € multipart/form-data ํƒ€์ž…์ด๋‹ค. ์ด๋Š” ์š”์ฒญ ๋ฐ”๋””์— ๋‹ด๊ธฐ๋Š” ๋‚ด์šฉ์˜ ์ข…๋ฅ˜๊ฐ€ ํ•˜๋‚˜๊ฐ€ ์•„๋‹ˆ๋ผ์„œ ๊ฐ ๊ตฌ์„ฑ์š”์†Œ๋งˆ๋‹ค ๋”ฐ๋กœ MIME ํƒ€์ž…์„ ์ง€์ •ํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์—์„œ ์‚ฌ์šฉํ•œ๋‹ค. 

๋‹ค์Œ์€ application/octet-stream ํƒ€์ž…์ด๋‹ค. ์ด๋Š” ๋ชจ๋“  ์ด์ง„ ํŒŒ์ผ๋“ค์˜ ์ œ๋„ค๋ฆญํ•œ ํƒ€์ž…์„ ์˜๋ฏธํ•˜๋Š”๋ฐ, ๋ง์ด ์ข‹์•„ ์ œ๋„ค๋ฆญ์ด์ง€ ๋”ฐ์ง€๊ณ  ๋ณด๋ฉด ์–ด๋–ค ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜์—ฌ ํ…์ŠคํŠธ๋ฅผ ํ•ด์„ํ•ด์•ผํ• ์ง€ ๋ชจ๋ฅผ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค. (Typescript์˜ any ํƒ€์ž… ๋Š๋‚Œ)

mulitpart/form-data -> application/octet-stream ๋ณ€ํ™˜ ์›์ธ

๊ทธ๋ ‡๋‹ค๋ฉด ์™œ ์„œ๋ฒ„ ์ธก์—์„œ๋Š” ์„ค์ •ํ•ด์ฃผ์ง€๋„ ์•Š์€ ํƒ€์ž…์ธ application/octet-stream์œผ๋กœ ์ธ์‹ํ•˜๊ณ  ์žˆ์„๊นŒ? ์ด๋ฅผ ์œ„ํ•ด ํ•„ํ„ฐ ์ฒด์ธ์—  OncePerFilter๋ฅผ ํ•˜๋‚˜ ์ถ”๊ฐ€ํ•˜๊ณ , ์š”์ฒญ ๋‚ด์˜ ๋ชจ๋“  Content-Type์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐ„๋‹จํ•œ ๋กœ์ง์„ ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

log.info(request.getContentType());
for (var part : request.getParts()) {
	log.info(part.getName());
	log.info(part.getContentType());
}

๊ทธ ํ›„ ๋™์ผํ•œ ๋ฌธ์ œ์˜ EP๋ฅผ ํ˜ธ์ถœํ•ด๋ณด์•˜๋Š”๋ฐ ๊ฒฐ๊ณผ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

์ฝ˜์†” ์ถœ๋ ฅ ๊ฒฐ๊ณผ

๋ชจ๋‘ ์˜๋„๋Œ€๋กœ Content-Type์ด ๋“ค์–ด๊ฐ€์žˆ์ง€๋งŒ ๋‹จ ํ•˜๋‚˜ videoMetadata ์†์„ฑ์˜ ํƒ€์ž…์ด null๋กœ ๋˜์–ด์žˆ์—ˆ๋‹ค...!

null์ด๋ผ์„œ ๋ฌธ์ œ ์†Œ์ง€๊ฐ€ ์žˆ๋Š” ๊ฒƒ์€ ์•Œ๊ฒ ๋Š”๋ฐ ์™œ ํ•˜ํ•„ application/octet-stream ํƒ€์ž…์ธ ๊ฒƒ์ผ๊นŒ?

์ด๋ฅผ ์œ„ํ•ด ๋ฌธ์ œ๊ฐ€ ๋ ๋งŒํ•œ ์Šคํ”„๋ง ๋‚ด๋ถ€ ๊ตฌํ˜„์„ ์ข€ ์ฐพ์•„๋ดค๋Š”๋ฐ, ์•„๋ž˜์˜ ๋กœ์ง์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

// class AbstractMessageConverterMethodArgumentResolver ๋‚ด๋ถ€ ๊ตฌํ˜„
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

	Class<?> contextClass = parameter.getContainingClass();
	Class<T> targetClass = (targetType instanceof Class clazz ? clazz : null);
	if (targetClass == null) {
		ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
		targetClass = (Class<T>) resolvableType.resolve();
	}

	MediaType contentType;
	boolean noContentType = false;
	try {
		contentType = inputMessage.getHeaders().getContentType();
	}
	catch (InvalidMediaTypeException ex) {
		throw new HttpMediaTypeNotSupportedException(
				ex.getMessage(), getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
	}
	if (contentType == null) {
		noContentType = true;
		contentType = MediaType.APPLICATION_OCTET_STREAM;
	}
    ...
}

์œ„ ์ฝ”๋“œ๋Š” Http Request, Response๋ฅผ ์ž๋ฐ” ํด๋ž˜์Šค๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” HttpMessageConverter์˜ ์ถ”์ƒ ๊ตฌํ˜„์ฒด์ธ AbstractMessageConverterMethodArgumentResolver์˜ readWithMessageConverter ๋ฉ”์„œ๋“œ์˜ ์ผ๋ถ€์ด๋‹ค. ํ•ด๋‹น ์ฝ”๋“œ์˜ ์•„๋ž˜์ชฝ์„ ๋ณด๋ฉด ํ™•์ธํ•  ์ˆ˜ ์žˆ๋“ฏ, contentType์˜ ๊ฐ’์ด null์ธ ๊ฒฝ์šฐ์—” ๊ธฐ๋ณธ์ ์œผ๋กœ application/octet-stream ํƒ€์ž…์œผ๋กœ ์ง€์ •ํ•ด๋ฒ„๋ฆฌ๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์„œ๋ฒ„ ์ธก ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€์—๋Š” ํ•ด๋‹น ํƒ€์ž…์ด ๋“ฑ์žฅํ–ˆ๋˜ ๊ฒƒ์ด๋‹ค.

์–ด๋””๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ• ๊นŒ?

๊ฒฐ๊ตญ Content-Type์ด Null์ผ ๋•Œ ์ €์ ˆ๋กœ application/octet-stream์œผ๋กœ ํ• ๋‹น๋˜๋Š” ๊ฒƒ์€ ์ดํ•ด๋ฅผ ํ–ˆ๋Š”๋ฐ, ์ด ๋ฌธ์ œ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์–ด๋””๋ฅผ ์ˆ˜์ •ํ•ด์•ผํ• ๊นŒ? ์ด๋ฅผ ์œ„ํ•ด ์„œ๋ฒ„ ์ธก์˜ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ์ฝœ์Šคํƒ์„ ์ถ”์ ํ•˜์˜€๊ณ  ์ด๋ฒˆ์—๋„ ์œ„์—์„œ ์ƒ๊ธฐํ•œ AbstractMessageConverterMethodArgumentResolver.readWithMessageConverter ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฌธ์ œ๊ฐ€ ๋˜๊ณ  ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์•„๋ž˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ๋กœ์ง์ด๋‹ค. (๋„ˆ๋ฌด ๊ธฐ๋‹ˆ๊น ๊ฑด๋„ˆ ๋›ฐ์‹œ๋ฉด ์š”์•ฝ ์žˆ์Œ)

// AbstractMessageConverterMethodArgumentResolver.readWithMessageConverter ๋ฉ”์„œ๋“œ ๋‚ด ์ผ๋ถ€ ๋กœ์ง
EmptyBodyCheckingHttpInputMessage message = null;
try {
	message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
	for (HttpMessageConverter<?> converter : this.messageConverters) {
		Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
		GenericHttpMessageConverter<?> genericConverter =
				(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
		if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
				(targetClass != null && converter.canRead(targetClass, contentType))) {
			if (message.hasBody()) {
				HttpInputMessage msgToUse =
						getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
				body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
						((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
				body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
			}
			else {
				body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
			}
			break;
		}
	}
	if (body == NO_VALUE && noContentType && !message.hasBody()) {
		body = getAdvice().handleEmptyBody(
				null, message, parameter, targetType, NoContentTypeHttpMessageConverter.class);
	}
}
catch (IOException ex) {
	throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
finally {
	if (message != null && message.hasBody()) {
		closeStreamIfNecessary(message.getBody());
	}
}

if (body == NO_VALUE) {
	if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || (noContentType && !message.hasBody())) {
		return null;
	}
	throw new HttpMediaTypeNotSupportedException(contentType,
			getSupportedMediaTypes(targetClass != null ? targetClass : Object.class), httpMethod);
}

์ฝ”๋“œ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์œผ๋‹ˆ ๊ฐ„๋žตํžˆ ์š”์•ฝํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

  1. this.messageConverters์—์„œ ํŠน์ • Content-Type์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ฐพ์•„์„œ ์ฒ˜๋ฆฌ๋ฅผ ์œ„์ž„ํ•œ๋‹ค.
  2. ๋งŒ์•ฝ body์˜ ๋‚ด์šฉ์ด ์กด์žฌํ•˜๋‚˜ ์ ์ ˆํ•œ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ, "Content-Type ~ is not supported" ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.

๋”ฐ๋ผ์„œ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” application/octet-stream ํƒ€์ž…์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ๋ฅผ Spring ์ปจํ…์ŠคํŠธ์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•จ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

 

๋ฌธ์ œ ํ•ด๊ฒฐ

์œ„์—์„œ ๋ถ„์„ํ•œ ๋ฌธ์ œ์˜ ๋ฐฐ๊ฒฝ์„ ํ†ตํ•ด, ๊ฒฐ๊ตญ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” application/octet-stream์„ ๋‹ค๋ค„์ฃผ๋Š” HttpMessageConverter๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ์•˜๋‹ค. ์ด๋ฅผ ์œ„ํ•ด, ๋‹ค์Œ ๋‘ ํด๋ž˜์Šค๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.

@Configuration
public class WebConfig implements WebMvcConfigurer {
	private OctetStreamReadMsgConverter octetStreamReadMsgConverter;

	@Autowired
	public WebConfig(OctetStreamReadMsgConverter octetStreamReadMsgConverter) {
		this.octetStreamReadMsgConverter = octetStreamReadMsgConverter;
	}

	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
		converters.add(octetStreamReadMsgConverter);
	}
}
@Component
public class OctetStreamReadMsgConverter extends AbstractJackson2HttpMessageConverter {
	@Autowired
	public OctetStreamReadMsgConverter(ObjectMapper objectMapper) {
		super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
	}

	// ๊ธฐ์กด application/octet-stream ํƒ€์ž…์„ ์“ฐ๊ธฐ๋กœ ๋‹ค๋ฃจ๋Š” ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ๊ฐ€ ์ด๋ฏธ ์กด์žฌ (ByteArrayHttpMessageConverter)
	// ๋”ฐ๋ผ์„œ ํ•ด๋‹น ์ปจ๋ฒ„ํ„ฐ๋Š” ์“ฐ๊ธฐ ์ž‘์—…์—๋Š” ์ด์šฉํ•˜๋ฉด ์•ˆ๋จ
	@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;
	}
}

๊ฐ ํŒŒ์ผ์— ๋Œ€ํ•œ ์„ค๋ช…์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

WebConfig

Spring์ด ๊ธฐ๋ณธ์ ์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” HttpMessageConverter ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€๋กœ ์ƒ์„ฑํ•ด ์ค„ ์ปค์Šคํ…€ ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๊ธฐ ์œ„ํ•œ ์„ค์ • ํŒŒ์ผ. ์ด ์„ค์ •์„ ํ†ตํ•ด, ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•  ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ๊ฐ€ Http ๋ฉ”์‹œ์ง€๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •์—์„œ ์ฐธ์—ฌํ•  ์ˆ˜ ์žˆ์–ด์ง.

OctetStreamReadMsgConverter

application/octet-stream ํƒ€์ž…์˜ Http ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•œ ์ฝ๊ธฐ ์ž‘์—…(Http ๋ฉ”์‹œ์ง€ -> Java class)์„ ์ˆ˜ํ–‰ํ•  ์ปค์Šคํ…€ ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ. ์ด ๋•Œ AbstractJackson2HttpMessageConverter๋ฅผ ์ƒ์†ํ•ด์ค€ ์ด์œ ๋Š” ์ด๋ฅผ ํ†ตํ•˜๋ฉด Jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฌธ์ž์—ด ํ˜•ํƒœ์˜ json์„ ํด๋ž˜์Šค๋กœ ๋ณ€ํ™˜ํ•ด์ค„ ์ˆ˜ ์žˆ๋Š”๋ฐ, ํ˜„์žฌ ์„œ๋ฒ„ ์ธก์—์„œ ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ๊ฒฝ์šฐ๋Š” Multipart/form-data ์ดํ•˜์˜ application/json ํƒ€์ž…์ด ๋ˆ„๋ฝ๋˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ.

 

๊ฒฐ๊ณผ

๋ฌธ์ œ์˜ EP๋ฅผ ๋‹ค์‹œ ํ˜ธ์ถœํ•ด๋ณด๋ฉด ์•„๋ž˜์ฒ˜๋Ÿผ 200์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ž˜๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

 

์ฐธ์กฐ

 

MIME types (IANA media types) - HTTP | MDN

A media type (also known as a Multipurpose Internet Mail Extensions or MIME type) indicates the nature and format of a document, file, or assortment of bytes. MIME types are defined and standardized in IETF's RFC 6838.

developer.mozilla.org

 

@RequestPart with mixed multipart request, Spring MVC 3.2

I'm developing a RESTful service based on Spring 3.2. I'm facing a problem with a controller handling mixed multipart HTTP request, with a Second part with XMLor JSON formatted data and a second part

stackoverflow.com