Spring Boot에서 @Async + MultipartFile 조합 시 발생하는 오류 정리 및 해결 방법

2024-01-02


사진: Unsplash 의 Marek Piwnicki


개요

 

Spring Boot에서 비동기 처리를 위해 @Async를 활용할 때, 일반적인 서비스 로직에서는 잘 동작하지만 MultipartFile을 사용하는 경우 예상치 못한 NoSuchFileException 오류가 발생할 수 있습니다.

이번 글에서는 해당 오류의 원인과 해결 방법을 구체적으로 살펴보겠습니다.


문제 상황

 

MultipartFile 파라미터를 @RequestPart로 컨트롤러에서 받고, 그 값을 DTO에 담아 @Async 메서드로 넘긴 뒤, 내부에서 multipartFile.transferTo() 또는 getInputStream() 등을 호출하면 아래와 같은 오류가 발생합니다.

java.nio.file.NoSuchFileException: /private/.../upload_xxxx.tmp

원인 분석

 

MultipartFile의 실제 파일 내용은 **Servlet 컨테이너(Tomcat)**가 multipart 요청을 처리하면서 임시 디렉토리에 저장해 둡니다. 그런데 이 임시 파일은 요청 스레드 생명주기 내에서만 유효합니다.

따라서 아래와 같이 처리하면 문제가 발생합니다:

 

@PostMapping("/upload")
public void upload(@RequestPart MultipartFile file) {
    asyncService.process(file); // ❌ 위험한 코드: @Async 메서드에서 MultipartFile 접근
}

 

 

  • 이때 process() 메서드는 @Async로 정의되어 있고,
  • 요청 스레드는 파일을 받은 후 컨트롤러를 빠져나가면서 임시 파일을 제거
  • 이후 Async 스레드가 file.getBytes()나 file.transferTo() 등을 호출하면 파일이 존재하지 않음 → 예외 발생

 


해결 방법

 

 1. MultipartFile → byte[]로 즉시 변환해서 전달

가장 명확하고 안전한 방법은, MultipartFile을 @Async 메서드로 넘기기 전에 즉시 byte[]로 변환해 넘기는 것입니다.

@PostMapping("/upload")
public void upload(@RequestPart MultipartFile file) {
    try {
        byte[] fileBytes = file.getBytes();
        String originalFilename = file.getOriginalFilename();
        asyncService.processAsync(fileBytes, originalFilename);
    } catch (IOException e) {
        // handle error
    }
}

 

Async 메서드:

@Async
public void processAsync(byte[] fileBytes, String fileName) {
    // 안전하게 사용 가능
    Files.write(Paths.get("/path", fileName), fileBytes);
}

2. DTO 내에서 MultipartFile이 아닌 List<SubImageDTO> 구조로 받기

 

@Data
@Builder
public static class SubImageDTO {
    private String fileName;
    private byte[] bytes;
}

 

컨트롤러에서 변환 후 DTO에 주입:

List<SubImageDTO> subImageDTOList = files.stream().map(file -> {
    try {
        return SubImageDTO.builder()
            .fileName(file.getOriginalFilename())
            .bytes(file.getBytes())
            .build();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}).collect(Collectors.toList());

 


테스트 환경에서만 잘 되고, 실서버에서 실패하는 이유?

 

로컬 개발 환경에선 요청 처리 속도나 비동기 큐 처리가 상대적으로 느려서 파일 삭제 타이밍이 Async 실행보다 느려져서 우연히 잘 동작할 수 있습니다.

그러나 실제 서버에서는:

  • Async 스레드 풀이 별도 동작
  • Tomcat은 컨트롤러 반환 직후 임시 파일 정리
  • 결과적으로 @Async 내에서 파일 접근 시도 → 파일 없음

결론

 

구분설명

❌ 잘못된 방식 MultipartFile을 그대로 @Async로 넘기는 것
✅ 추천 방식 MultipartFile → byte[] 혹은 DTO로 변환해서 넘기기
🚫 피해야 할 패턴 @Async 내에서 file.getInputStream(), file.transferTo() 등 직접 파일 핸들링

팁: MultipartFile을 저장할 때 안전하게 처리하는 유틸

 

public static File byteArrayToFile(String path, byte[] bytes, String fileName) {
    try {
        Path dir = Paths.get(path);
        if (!Files.exists(dir)) {
            Files.createDirectories(dir);
        }
        Path destinationFile = dir.resolve(fileName);
        Files.write(destinationFile, bytes);
        return destinationFile.toFile();
    } catch (IOException e) {
        throw new RuntimeException("파일 저장 실패", e);
    }
}

참고 사항

  • Spring Boot Multipart 설정은 기본적으로 CommonsMultipartResolver 또는 StandardServletMultipartResolver를 사용
  • 기본 임시 저장 경로는 OS 의 /tmp 또는 macOS에서는 /private/var/...와 같은 경로

메인 이미지 출처 : 사진: UnsplashMarek Piwnicki