2024-01-02
개요
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/...와 같은 경로
메인 이미지 출처 : 사진: Unsplash의Marek Piwnicki