티스토리 뷰

안녕하세요. 오늘은 SpringBoot 와 Vue를 활용해 문자열 데이터와 멀티파트 파일을 동시에 REST API로 주고받는 방법을 알아보겠습니다. JSON 형식의 문자열 데이터와, 관련 파일을 함께 업로드하는 과정을 단계별로 소스 코드와 함께 설명하겠습니다.

 

목표

  • 프론트엔드: Vue에서 FormData를 사용해 태스크 데이터(JSON)와 파일을 서버로 전송.
  • 백엔드: Spring Boot에서 @RequestPartMultipartHttpServletRequest로 데이터를 받아 처리.
  • 결과: 태스크별로 파일을 분배하고, 데이터베이스에 저장.

1. 프론트엔드 구현(Vue)

Vue에서는 axios를 활용해 multipart/form-data 요청을 생성합니다. 태스크 데이터를 JSON 문자열로, 파일은 태스크별로 구분해 전송합니다.
export async function createTask(data: TaskRegistRequest[]): Promise<boolean> {
  const formData = new FormData();

  // 태스크 데이터를 JSON으로 변환
  const taskData = data.map(task => ({
    clientId: task.clientId,
    taskMajor: task.taskMajor,
    processorId: task.processorId,
    dueDate: task.dueDate,
    // ... 나머지 필드
  }));
  formData.append('tasks', JSON.stringify(taskData));

  // 태스크별 파일 추가
  data.forEach((task, taskIndex) => {
    if (task.files && task.files.length > 0) {
      task.files.forEach(file => {
        formData.append(`files[${taskIndex}]`, file); // 태스크 인덱스로 파일 구분
      });
    }
  });

  // API 호출
  const response = await callMultipart<ApiResponse<TaskInfo[]>>(
    '/api/task',
    'POST',
    formData
  );
  if (response.status !== 200 || !response.result) {
    throw new Error(response.message || 'Task 등록 실패');
  }
  return true;
}
export async function callMultipart<T>(
  url: string,
  method: 'POST' | 'PUT' = 'POST',
  data: FormData
): Promise<T> {
  const response = await apiClient.request<T>({
    url,
    method,
    data,
    headers: { 'Content-Type': 'multipart/form-data' },
  });
  return response.data;
}
  • FormData 에 tasks 키로 JSON 문자열을 추가
  • 파일은 files[0], files[1] 처럼 task 인덱스를 붙여 구분
  • multipart/form-data 헤더로 요청 전송

2. 백엔드 구현(SpringBoot)

Spring Boot에서는 MultipartHttpServletRequest를 사용해 문자열과 파일을 동시에 처리합니다. 태스크별 파일을 매핑해 저장합니다.
 

Controller

@PostMapping("/api/task")
public ResponseEntity<?> addTask(
	@RequestPart("tasks") String tasksJson,
    MultipartHttpServletRequest request
) throws IOException {

	// JSON 문자열 맵핑
    ObjectMapper objectMapper = new ObjectMapper();
    List<TaskRegistRequest> taskRequests 
    	= objectMapper.readValue(tasksJson, new TypeReference<>() {});

    // 파일 매핑
    Map<String, List<MultipartFile>> filesMap = new HashMap<>();
    Map<String, List<MultipartFile>> multiFileMap = request.getMultiFileMap();
    for (Map.Entry<String, List<MultipartFile>> entry : multiFileMap.entrySet()) {
        String key = entry.getKey(); // 예: "files[0]"
        List<MultipartFile> files = entry.getValue();
        String indexStr = key.replaceAll("files\\[(\\d+)\\]", "$1");
        if (!indexStr.equals(key)) {
            int taskIndex = Integer.parseInt(indexStr);
            filesMap.put("files[" + taskIndex + "]", files);
        }
    }

    // 태스크에 파일 연결
    if (!filesMap.isEmpty()) {
        for (int i = 0; i < taskRequests.size(); i++) {
            String key = "files[" + i + "]";
            List<MultipartFile> files = filesMap.get(key);
            if (files != null && !files.isEmpty()) {
                taskRequests.get(i).setFiles(files.toArray(new MultipartFile[0]));
            }
        }
    }

    return ResponseEntity.ok(taskService.addTasks(taskRequests));
}
  • @RequestPart("tasks")로 JSON 문자열 수신
  • MultipartHttpServletRequest.getMultiFileMap()으로 태스크별 파일 리스트 추출

 

Service

@Transactional
public List<TaskInfo> addTask(List<TaskRegistRequest> taskRequests) throws IOException {
    List<TaskInfo> tasks = taskRequests.stream()
        .map(taskRequest -> {
            TaskInfo task = new TaskInfo();
            task.setTaskMajor(taskRequest.getTaskMajor());
            task.setProcessorId(taskRequest.getProcessorId());
            task.setDueDate(taskRequest.getDueDate() != null ? 
                LocalDate.parse(taskRequest.getDueDate()) : null);
            // ... 나머지 필드 매핑
            return task;
        })
        .collect(Collectors.toList());

	// JPA Insert 후 taskId 반환
    List<TaskInfo> savedTasks = taskInfoRepository.saveAll(tasks);

    // 파일 저장(taskId 맵핑)
    for (int i = 0; i < savedTasks.size(); i++) {
        TaskRegistRequest request = taskRequests.get(i);
        if (request.getFiles() != null && request.getFiles().length > 0) {
            fileService.uploads(request.getFiles(), savedTasks.get(i).getTaskId());
        }
    }

    return savedTasks;
}

 

Upload

@Transactional
public FileInfo upload(MultipartFile file, String targetId) throws IOException {
    
    // 파일ID 채번
    String fileId = fileInfoRepository.generateFileId();
    
    // 파일명 & 확장자 추출
    String fileName = file.getOriginalFilename();
    String fileExtension = getFileExtension(fileName);
    
    // 경로지정
    String uploadDir = String.format("%s/%s/", uploadDir, 
        LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")));
        
    // 파일저장
    Files.createDirectories(Paths.get(uploadDir));
    Path filePath = Paths.get(uploadDir + fileId + fileExtension);
    Files.write(filePath, file.getBytes());

    return fileInfo;
}

통합 동작

  • Vue에서 태스크 데이터와 파일을 FormData로 묶어 전송.
  • Spring Boot가 tasksfiles[0], files[1] 등을 수신.
  • 백엔드에서 태스크별 파일을 매핑하고, 데이터베이스에 저장.
  • 파일은 지정된 디렉토리에 저장되고, 태스크에 파일 ID가 연결.

 

 

Spring Boot와 Vue를 사용하면 문자열과 멀티파트 파일을 동시에 처리하는 REST API를 쉽게 구현할 수 있습니다. FormDataMultipartHttpServletRequest를 활용한 이 방식은 확장성도 뛰어나며, 파일 업로드가 필요한 다양한 시나리오에 적용할 수 있습니다. 
 
감사합니다.
최근에 올라온 글
Total
Today
Yesterday