기록

Spring에서 multipart/form-data로 리스트(List<T>) 데이터를 받는 방법 본문

Web/Spring

Spring에서 multipart/form-data로 리스트(List<T>) 데이터를 받는 방법

zyin 2025. 3. 23. 12:00

시작하면서

Spring에서 multipart/form-data 요청을 처리할 때, 파일과 리스트(List<T>) 데이터를 함께 전송하는 방법이 일반적인 application/json 요청과 다르다.
특히, List<T> 같은 배열 데이터를 JSON으로 보내도 Spring이 자동 변환하지 못하는 경우가 있다.

이 글에서는 Spring이 multipart/form-data 요청에서 List<T>를 자동 변환하도록 처리하는 방법을 정리한다.


📌 문제 상황: multipart/form-data에서 리스트(List<T>)를 받을 수 있을까?

multipart/form-data는 파일과 데이터를 함께 전송할 수 있는 HTTP 요청 방식이다.
그러나, 아래와 같이 리스트(List<T>) 데이터를 JSON으로 보내도 Spring이 자동 변환하지 않는다.

[
    {
        "fileType": "BINARY",
        "description": "파일 설명"
    },
    {
        "fileType": "IMAGE",
        "description": "이미지 설명"
    }
]

Spring이 fileDetails를 List<T>로 변환하지 못하는 이유는 다음과 같다.

🚨 multipart/form-data에서 List<T> 자동 변환이 불가능한 이유

  1. multipart/form-data는 JSON이 아닌 key=value 기반의 폼 필드 방식이다.
    • application/json 요청과 달리 multipart/form-data는 JSON을 자동 매핑할 수 없다.
  2. @RequestBody를 사용할 수 없다.
    • @RequestBody는 application/json 요청에서만 사용할 수 있으며, multipart/form-data에서는 동작하지 않는다.
  3. 결과적으로 fileDetails를 List<T>로 자동 변환할 수 없다.
    • JSON 데이터를 Text 필드로 전송해도 Spring이 이를 List<T>로 변환하지 못한다.

🚀 해결 방법: fileDetails[i].필드명 형식으로 전달

Spring은 multipart/form-data 요청에서 리스트(List<T>) 데이터를 개별적인 Key로 보내면 자동 변환할 수 있다.
즉, fileDetails[i].필드명 형식으로 데이터를 전송하면, List<T>로 변환된다.

✅ Postman 설정

아래와 같이 각 배열 요소를 개별적인 Key로 추가하면 Spring이 자동 변환할 수 있다.

Key Type Value
files File binary1.zip
files File image1.png
fileDetails[0].fileType Text BINARY
fileDetails[0].description Text 파일 설명
fileDetails[1].fileType Text IMAGE
fileDetails[1].description Text 이미지 설명

이렇게 설정하면 Spring이 자동으로 List<T>로 변환한다.


📌 컨트롤러 코드 (@ModelAttribute 활용)

이제, 위 방식대로 데이터를 보내면 Spring 컨트롤러에서 @ModelAttribute를 통해 자동 변환할 수 있다.

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "파일 업로드", description = "파일과 관련 데이터를 함께 업로드합니다.")
public ResponseEntity<FileUploadResponse> uploadFiles(
        @Valid @ModelAttribute FileUploadRequest request,  // Spring이 자동 바인딩
        @RequestParam(value = "files", required = false) List<MultipartFile> files,
) {

    log.info("Received request: {}", request);
    log.info("Received files: {}", files);

    // 파일 리스트가 null이면 빈 리스트로 처리
    List<MultipartFile> fileList = Optional.ofNullable(files).orElse(Collections.emptyList());

    // 파일과 요청 데이터 매칭
    List<FileDetail> fileDetails = request.getFileDetails();
    if (fileDetails.size() != fileList.size()) {
        throw new IllegalArgumentException("파일 개수와 fileDetails 개수가 일치하지 않습니다.");
    }

    List<ProcessedFile> processedFiles = IntStream.range(0, fileDetails.size())
            .mapToObj(i -> ProcessedFile.of(fileDetails.get(i), fileList.get(i)))
            .toList();

    FileUploadCommand command = FileUploadCommand.of(request);
    command.setProcessedFiles(processedFiles);

    return ResponseEntity.ok(fileUploadService.processUpload(command));
}

이제 multipart/form-data에서 List<T>가 자동 변환됨.
파일과 함께 리스트 데이터를 직관적으로 처리할 수 있음.


📌 React에서 multipart/form-data 요청 보내기

React에서도 FormData를 사용하여 fileDetails[i].필드명 형식으로 데이터를 추가하면 된다.

import axios from 'axios';

const uploadFiles = async () => {
    const formData = new FormData();

    // 파일 추가
    formData.append("files", new File(["binary content"], "binary1.zip")); 
    formData.append("files", new File(["image content"], "image1.png")); 

    // fileDetails 리스트를 개별 필드로 추가
    formData.append("fileDetails[0].fileType", "BINARY");
    formData.append("fileDetails[0].description", "파일 설명");
    formData.append("fileDetails[1].fileType", "IMAGE");
    formData.append("fileDetails[1].description", "이미지 설명");

    // 기타 정보 추가
    formData.append("requestId", 1001);
    formData.append("description", "업로드 요청입니다");

    try {
        const response = await axios.post("http://localhost:8080/upload", formData, {
            headers: {
                "Content-Type": "multipart/form-data"
            }
        });

        console.log("업로드 성공:", response.data);
    } catch (error) {
        console.error("업로드 실패:", error);
    }
};

uploadFiles();

React에서도 Spring이 자동 변환할 수 있도록 fileDetails[i].필드명 형식으로 전송
이제 Spring에서 List<T>를 문제없이 받을 수 있음


🔥 최종 정리

해결 방법 장점  단점 추천여부
✅ fileDetails[i].필드명 형식으로 전달 (@ModelAttribute) Spring이 자동 변환, JSON 변환 필요 없음 Postman에서 Key를 여러 개 추가해야 함 가장 추천
⚠️ @RequestParam String 후 JSON 변환 (ObjectMapper) Key 개수를 줄일 수 있음 JSON을 개별적으로 변환해야 함 대체 가능

Spring이 multipart/form-data에서 List<T>를 자동 변환하도록 만들고 싶다면 fileDetails[i].필드명 형식으로 보내야 한다.
React, Postman, MockMvc 테스트 모두 같은 방식으로 데이터를 보내면 Spring에서 자동 변환 가능! 🚀

Comments