오늘은 프로젝트를 진행하며 겪었던 파일 업로드 관련 에러를 말해보려고 한다.
프론트단에서 아래와 같은 여러개의 파일을 업로드를 구현했을때,
백엔드에서 동일한 파일 저장시 file not found에러가 발생하는 문제가 생겼다.
(왜 저장이 안되는거지?)
<input type="file" name="uploadFile">파일1</input>
<input type="file" name="uploadFile">파일2</input>
<input type="file" name="uploadFile">파일3</input>
<input type="file" name="uploadFile">파일4</input>
const fileInput = document.querySelector('input[name="uploadFile"]');
const uploadButton = document.querySelector('#uploadButton');
uploadButton.addEventListener('click', function() {
const files = fileInput.files;
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
const xhr = new XMLHttpRequest();
xhr.open('POST', 'api/file/upload');
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Files uploaded successfully');
} else {
console.log('Error uploading files');
}
};
xhr.send(formData);
});
위 코드는 js에서 uploadFile이라는 name을 가진 태그들을 선택해서 파일을 업로드하는 부분이고
아래는 스프링 부트에서 파일을 업로드하는 간단한 로직이다.
설명을 위한 코드이기 때문에 최대한 간단히 구성했다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
public class FileUploadController {
@Value("${upload.dir}")
private String uploadDir;
@PostMapping("/api/file/upload")
public ResponseEntity<String> handleFileUpload(@RequestPart("files") MultipartFile[] files) {
StringBuilder fileNames = new StringBuilder();
for (MultipartFile file : files) {
Path fileNameAndPath = Paths.get(uploadDir, file.getOriginalFilename());
try {
Files.write(fileNameAndPath, file.getBytes());
fileNames.append(file.getOriginalFilename()).append(", ");
} catch (IOException e) {
e.printStackTrace();
}
}
return ResponseEntity.status(HttpStatus.OK)
.body("Uploaded the files successfully: " + fileNames.toString());
}
}
이렇게 하면 간단하게 스프링 부트에서 파일 업로드를 하는 기능이 완성된다.위의 코드는 정말 단순히 동일한 파일 업로드시의 문제를 보여주기 위해 생성한 예시 코드이고 현업에서는 추가로 db와 연결하여 해당 파일의 원본 파일명을 저장하고, 실제 저장하는 파일명(ex.20230426_abcd.txt)도 같이 저장하게 된다.
하지만, 동일한 파일 업로드시 하나의 문제점이 발생하게 된다.
스프링 부트에서는 파일을 업로드하면
[/tmp/tomcat.4296537502689403143.5000/work/Tomcat/localhost/ROOT]
이렇게 생긴 임시 폴더에 파일을 잠깐 저장하게 되는데 .tmp라는 형태로 임시 저장하여 multipartFile 객체에서
파일의 정보를 볼 수 있게 된다.
파일을 저장하게 되면 .tmp 형식의 파일이 삭제되는데
1.사용자는 a라는 파일을 3개 업로드
2.하지만 .tmp 파일은 1개
3.첫번째 a라는 파일을 저장하면 성공
4.두번째부터 a라는 파일을 저장에 실패(파일을 찾을 수 없음)
이렇게 로직이 진행되었기 때문에 문제가 발생하였다.
스프링부트 버전 2.7.4에서는 내부적으로 같은 파일을 저장시 하나의 파일만을 .tmp로 저장하였기 때문에
이 문제를 해결하려 고민했었고, 4가지의 방법이 떠올랐다.
🎈 그렇다면 어떻게 해야 이 문제를 해결 할 수 있을까?
- 처음에는 근본적인 문제인 .tmp 파일을 못찾아서 문제가 생겼으니까, .tmp가 여러개 생성되도록, 혹은 파일 업로드 로직이 끝날때까지 .tmp 파일이 삭제되지 않도록 하는 방법을 생각했었다. 하지만 스프링부트 내장톰캣이기 때문에 설정하는 방법도 몰랐었고, 시스템적으로 그렇게 막아둔 이유가 있을거라고 생각했다. 간단히 생각해봐도 똑같은 파일이 여러개 올라왔으면 하나만 저장해도 문제는 없으니까!
- 그럼 tomcat 버전을 수정해볼까? 라는 생각도 했었다. 하지만 파이썬을 개발하면서 버전 호환의 중요성을 알고 있기 때문에 tomcat 버전을 수정하는건 최후의 보루로 미뤄뒀다. Spring initailizer를 통해서 만든 프로젝트고, Spring, Spring Boot, Tomcat을 최적의 버전으로 세팅되어 있으니 건들면 지금까지 개발한 코드 중 하나가 동작하지 않을(side effect가 터질) 가능성이 높기 때문에....
- 그럼 똑같은 파일 A를 복사해서 파일명만 A1, A2, A3, A4로 바꿔서 업로드하면 어떻게 될지 테스트도 해봤는데 이 경우에는 파일이 정상적으로 업로드 되었다. 그래서 개발자 친화적으로(?) 똑같은 파일이 있으면 파일명을 변경하고 다시 업로드하라는 유효성검사를 통해 메시지를 출력하는 로직을 넣었고... 당연하게도 사용자의 불만은 폭주했다.
- 가장 최선의 방법은 사용자는 그냥 동일한 파일을 업로드하고, 다운로드 받을때도 동일한 파일을 받을 수 있어야 했다. 즉 사용자 편의성이 가장 중요하다! 그렇기 때문에 내부적인 로직 자체를 DB 데이터 저장시에는 여러개를 저장하였고(DB 설계 구조상) 파일은 실제로 하나만 저장하였다. HashMap을 사용해서 map<String, String>을 통해서 원본파일명,파일저장경로를 key, value로 저장하고 만약 원본 파일명이 동일하다면 파일을 다시 저장하지 않도록하였다.(이것도 파일명과 확장자가 완전히 동일한 경우지만 파일 내용이 다르면 문제가 발생할걸 고려해서 파일사이즈가 다른지 체크하는 유효성 검사를 진행)
💡 결론, 방법 => 결과
- .tmp 파일 여러개 생성하는 방법 => 방법을 못찾음, 비효율적이라 생각
- tomcat 버전 수정 => 사이드이펙트의 걱정으로 최후의 방법으로 미뤄둠
- 업로드시 파일명을 다르게하여 업로드하라고 사용자에게 알림 => UX 저하
- 내부 로직을 통해 파일을 한번만 저장하게 함(다운로드시 하나의 파일의 주소를 여러곳에서 사용) => 성공
'Error > 트러블슈팅' 카테고리의 다른 글
[JAVA] ENUM에 setter를 쓰면 생기는 일 (feat. 싱글턴) (1) | 2024.12.10 |
---|---|
[Trouble Shooting - CORS] Spring Boot CORS 와일드카드(*) 설정방법 (0) | 2023.07.22 |
[Trouble Shooting - Transaction과 DB Session] DB 특정 테이블 (row) 업데이트 안되는 문제 (0) | 2023.07.17 |
[Trouble Shooting - RequestBody와 생성자] 스프링부트 LocalDateTime JsonFormat 안됨 (2) | 2023.05.19 |