๐Ÿ“ฆ Spring์—์„œ Multipart/Form-Data๋กœ ํŒŒ์ผ + DTO ์—…๋กœ๋“œ ์‹œ ํ”ํžˆ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ๋ฒ• ์ •๋ฆฌ

๋ฐ•์ค€ํ˜•ยท2025๋…„ 8์›” 17์ผ

์Šคํ”„๋ง ๊ฐœ๋ฐœ

๋ชฉ๋ก ๋ณด๊ธฐ
13/20
post-thumbnail

1. ๋ฌธ์ œ ์ƒํ™ฉ

API ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ํŒŒ์ผ๊ณผ DTO(JSON)๋ฅผ ํ•จ๊ป˜ ๋ฐ›์•„์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.
์ง€๊ธˆ ๋‚ด๊ฐ€ ๊ฐœ๋ฐœํ•˜๊ณ  ์žˆ๋Š” ํ”Œ๋žซํผ์—์„œ๋„, ํ”„๋กœ์ ํŠธ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์ด ์กด์žฌํ•œ๋‹ค. ํ”„๋กœ์ ํŠธ ์„ธ๋ถ€ ์ •๋ณด(JSON)์™€ ์ธ๋„ค์ผ ํŒŒ์ผ์„ ๋™์‹œ์— ์—…๋กœ๋“œํ•ด์•ผ ํ•œ๋‹ค.

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity<SuccessResponse<Void>> uploadProject(
        @Parameter(hidden = true)
        @CurrentUserId
        Long userId,

        @RequestPart(value = "thumbnailFile", required = false)
        MultipartFile thumbnailFile,

        @RequestPart @Validated
        UploadProjectWebRequest webRequest
);

์—ฌ๊ธฐ์„œ thumbnailFile์€ ์„ ํƒ ํŒŒ์ผ์ด๊ณ , webRequest๋Š” JSON DTO๋‹ค.

๋ฌธ์ œ๋Š” Swagger, Postman, ํ”„๋ก ํŠธ์—”๋“œ ์ฝ”๋“œ์—์„œ webRequest ํŒŒํŠธ์˜
Content-Type์ด application/json์ด ์•„๋‹Œ, application/octet-stream์ด๋‚˜
text/plain์œผ๋กœ ๋“ค์–ด์˜ค๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋•Œ Spring์€ Jackson์„
์ด์šฉํ•œ ์—ญ์ง๋ ฌํ™”๋ฅผ ์‹œ๋„ํ•˜์ง€ ๋ชปํ•˜๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋‚˜๋„, ์ฒ˜์Œ์—” ์Šค์›จ๊ฑฐ, ํฌ์ŠคํŠธ๋งจ ๋“ฑ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ๋•Œ ํ˜„์žฌ ์กฐ๊ฑด ๋‚ด์—์„œ ํ•ด๊ฒฐํ•ด๋ณด๋ ค๊ณ  ๊ณ„์† ๋…ธ๋ ฅํ•ด๋ณด์•˜์œผ๋‚˜ ๊ณ„์†ํ•˜์—ฌ ์‹คํŒจํ•˜์˜€๋‹ค.

Content type 'application/octet-stream' not supported


2. ์™œ ์ด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”๊ฐ€?

Spring MVC๋Š” multipart ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ๋‹ค์Œ ํ๋ฆ„์„ ๋”ฐ๋ฅธ๋‹ค.

Client (multipart/form-data)
        โ†’ DispatcherServlet
        โ†’ HandlerAdapter
        โ†’ @RequestPart(webRequest) ๋ฐ”์ธ๋”ฉ
        โ†’ HttpMessageConverter ์„ ํƒ

์ด๋•Œ Converter ์„ ํƒ ๊ธฐ์ค€์€ Content-Type + ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž…์ด๋‹ค.\

  • ์ •์ƒ์ด๋ผ๋ฉด application/json โ†’ MappingJackson2HttpMessageConverter
    ์„ ํƒ โ†’ DTO ๋ณ€ํ™˜ ์„ฑ๊ณต
  • ์ž˜๋ชป ๋“ค์–ด์˜ค๋ฉด application/octet-stream โ†’ Jackson ํ›„๋ณด์—์„œ ์ œ์™ธ โ†’
    ๋ณ€ํ™˜ ์‹คํŒจ

์ฆ‰, JSON์ด octet-stream์œผ๋กœ ๋“ค์–ด์˜ค๋ฉด Jackson์ด ๊ฑด๋“œ๋ฆฌ์ง€ ๋ชปํ•œ๋‹ค๋Š” ๊ฒŒ
ํ•ต์‹ฌ ๋ฌธ์ œ๋‹ค.



3. ํ•ด๊ฒฐ ์ „๋žต: ์ฝ๊ธฐ ์ „์šฉ Jackson ์ปจ๋ฒ„ํ„ฐ ์ถ”๊ฐ€

์ด์ƒ์ ์ธ ํ•ด๊ฒฐ์€ ํ”„๋ก ํŠธ/ํ…Œ์ŠคํŠธ ๋„๊ตฌ๊ฐ€ ๋ฐ˜๋“œ์‹œ application/json์œผ๋กœ
๋ณด๋‚ด๋„๋ก ๊ฐ•์ œํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
ํ•˜์ง€๋งŒ ํ˜„์‹ค์ ์œผ๋กœ๋Š” ์‹ค์ˆ˜๊ฐ€ ๋ฐ˜๋ณต๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์„œ๋ฒ„์—์„œ ๋ฐฉ์–ด์ ์œผ๋กœ
์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๊ฒƒ์ด ๋” ์‹ค๋ฌด์ ์ด๋‹ค.

๐Ÿ‘‰ ์ด์— ๋Œ€ํ•œ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์€ ๊ฐ„๋‹จํ•จ
**Jackson ์ปจ๋ฒ„ํ„ฐ๊ฐ€ application/octet-stream์ด๋”๋ผ๋„ ์ฝ๊ธฐ(read)๋Š” Jackson์œผ๋กœ ์‹œ๋„ํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค.

๊ทธ๋ž˜์„œ webRequest ํŒŒํŠธ๊ฐ€ octet-stream์œผ๋กœ ๋“ค์–ด์™€๋„ ๊ฐ•์ œ๋กœ JSON ํŒŒ์‹ฑ์„ ์‹œ๋„ํ•ด์„œ DTO๋กœ ๋ฐ”์ธ๋”ฉํ•  ์ˆ˜ ์žˆ๋‹ค.
๋‹จ, ์‘๋‹ต(์“ฐ๊ธฐ)์—๋Š” ๊ด€์—ฌํ•˜์ง€ ์•Š๋„๋ก canWrite๋Š” ๋ชจ๋‘ false ์ฒ˜๋ฆฌํ•œ๋‹ค.

๊ตฌํ˜„ ์ฝ”๋“œ

@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {

    public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
    }

    @Override protected boolean canWrite(MediaType mediaType) { return false; }
    @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { return false; }
    @Override public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) { return false; }
}

@Configuration
class WebConfig implements WebMvcConfigurer {
    private final MultipartJackson2HttpMessageConverter octetReader;

    WebConfig(MultipartJackson2HttpMessageConverter octetReader) {
        this.octetReader = octetReader;
    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // ์ฝ๊ธฐ ์ „์šฉ octet-stream Jackson ์ปจ๋ฒ„ํ„ฐ๋ฅผ ์ตœ์ƒ๋‹จ์— ์ถ”๊ฐ€
        converters.add(0, octetReader);
    }
}

์ด์ œ webRequest ํŒŒํŠธ๊ฐ€ ์ข…์ข… application/octet-stream์œผ๋กœ ๋“ค์–ด์™€๋„
Jackson์ด ์ฝ์–ด์„œ DTO ๋ณ€ํ™˜์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.



4. ์š”์ฒญ ์˜ˆ์‹œ

โœ… ์ •์ƒ (application/json)

curl -X POST http://localhost:8080/projects/upload   -H "Content-Type: multipart/form-data"   -F "thumbnailFile=@/path/to/image.png"   -F 'webRequest={
        "title":"ํ”„๋กœ์ ํŠธ๋ช…",
        "topicId":2,
        "analysisPurposeId":2,
        "dataSourceId":3,
        "authorLevelId":3,
        "isContinue":true,
        "parentProjectId":3,
        "content":"์ง€๊ธˆ ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜์— ๋Œ€ํ•ด์„œ ~~.",
        "dataIds":[1,3]
      };type=application/json'

โš ๏ธ ์‹ค์ˆ˜ (application/octet-stream) โ†’ ์ปค์Šคํ…€ ์ปจ๋ฒ„ํ„ฐ๊ฐ€ ์ฒ˜๋ฆฌ

curl -X POST http://localhost:8080/projects/upload   -H "Content-Type: multipart/form-data"   -F "thumbnailFile=@/path/to/image.png"   -F 'webRequest={
        "title":"ํ”„๋กœ์ ํŠธ๋ช…",
        "topicId":2,
        "analysisPurposeId":2,
        "dataSourceId":3,
        "authorLevelId":3,
        "isContinue":true,
        "parentProjectId":3,
        "content":"์ง€๊ธˆ ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜์— ๋Œ€ํ•ด์„œ ~~.",
        "dataIds":[1,3]
      }'


5. DTO ๊ฒ€์ฆ ์„ค๊ณ„

์—ฌ๊ธฐ WebRequest์— ์“ฐ์ธ UploadProjectWebRequest๋Š” ๋‹จ์ˆœํ•œ JSON ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์•„๋‹ˆ๋ผ, ์ž…๋ ฅ ๊ทœ์น™์„
๋ช…ํ™•ํžˆ ์žก์•„์ฃผ๋Š” ๊ณ„์•ฝ
์ด๋‹ค.

@Schema(description = "ํ”„๋กœ์ ํŠธ ์—…๋กœ๋“œ ์›น ์š”์ฒญ DTO")
public record UploadProjectWebRequest(
        @NotBlank(message = "์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”")
        String title,

        @NotNull @Min(1)
        Long topicId,

        @NotNull @Min(1)
        Long analysisPurposeId,

        @NotNull @Min(1)
        Long dataSourceId,

        @NotNull @Min(1)
        Long authorLevelId,

        @NotNull
        Boolean isContinue,

        @Min(1)
        Long parentProjectId,

        @NotBlank(message = "๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”")
        String content,

        @NotNull
        List<Long> dataIds
) {}


6. ํ๋ฆ„ ๋‹ค์ด์–ด๊ทธ๋žจ



7. ๊ฒฐ๋ก 

  • ๋ฌธ์ œ ์›์ธ: multipart ์š”์ฒญ์—์„œ DTO ํŒŒํŠธ์˜ Content-Type์ด
    application/json์ด ์•„๋‹ˆ๋ฉด Jackson ์ปจ๋ฒ„ํ„ฐ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค.\
  • ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: ์ฝ๊ธฐ ์ „์šฉ MultipartJackson2HttpMessageConverter๋ฅผ
    ๋“ฑ๋กํ•ด application/octet-stream๋„ JSON์œผ๋กœ ํŒŒ์‹ฑ๋˜๋„๋ก ํ•œ๋‹ค.\
  • ์‹ค๋ฌด ํฌ์ธํŠธ: DTO ๊ฒ€์ฆ, ์—๋Ÿฌ ์‘๋‹ต ํ‘œ์ค€ํ™”, ํŒŒ์ผ-๋ฉ”ํƒ€ ์ผ๊ด€์„ฑ,
    ๋ณด์•ˆ/๋ชจ๋‹ˆํ„ฐ๋ง๊นŒ์ง€ ํ•จ๊ป˜ ์ฑ™๊ฒจ์•ผ ํ•œ๋‹ค.

๐Ÿ‘‰ ์ด๋ ‡๊ฒŒ ์„ค์ •ํ•ด๋‘๋ฉด Swagger/Postman/ํ”„๋ก ํŠธ ์‹ค์ˆ˜๊นŒ์ง€ ๋ฐฉ์–ดํ•  ์ˆ˜ ์žˆ๊ณ ,
API ์‚ฌ์šฉ์„ฑ์ด ํฌ๊ฒŒ ๊ฐœ์„ ๋œ๋‹ค. ๐Ÿš€

profile
๋งค์ผ ๋งค์ผ ์„ฑ์žฅํ•˜๊ธฐ

0๊ฐœ์˜ ๋Œ“๊ธ€