대용량의 파일을 전송할 때 OOM 이 발생했다.
대용량의 파일의 경우 한번에 보내는 것이 아니고 잘게 쪼개서 전송하여 서버에서 합치는 방식으로 문제를 해결 할 수 있다.
ex) 100MB의 파일을 1MB 단위로 100번 전송한다.
이 때 서버에서는 잘게 쪼개진 파일을 하나로 합치기 위해서 파일의 이름과 총 몇개의 조각으로 나눴는지(totalChunkSize
), 각 조각이 몇 번째 조각인지(chunkNumber
)를 알고 있어야 한다.
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
{filename}.part{chunkNumber}
이런 형태로 임시파일을 저장해 놓는다. ChunkUploadService
@Slf4j
@Service
public class ChunkUploadService {
public boolean chunkUpload(MultipartFile file, int chunkNumber, int totalChunks) throws IOException {
// 파일 업로드 위치
String uploadDir = "video";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
// 임시 저장 파일 이름
String filename = file.getOriginalFilename() + ".part" + chunkNumber;
Path filePath = Paths.get(uploadDir, filename);
// 임시 저장
Files.write(filePath, file.getBytes());
// 마지막 조각이 전송 됐을 경우
if (chunkNumber == totalChunks-1) {
String[] split = file.getOriginalFilename().split("\\.");
String outputFilename = UUID.randomUUID() + "." + split[split.length-1];
Path outputFile = Paths.get(uploadDir, outputFilename);
Files.createFile(outputFile);
// 임시 파일들을 하나로 합침
for (int i = 0; i < totalChunks; i++) {
Path chunkFile = Paths.get(uploadDir, file.getOriginalFilename() + ".part" + i);
Files.write(outputFile, Files.readAllBytes(chunkFile), StandardOpenOption.APPEND);
// 합친 후 삭제
Files.delete(chunkFile);
}
log.info("File uploaded successfully");
return true;
} else {
return false;
}
}
}
chunkNumber
, totalChunks
를 전송 받는다.206
전달하고 마지막 조각까지 전송 된 경우 상태코드 200
전달한다.ChunkUploadController
@Controller
@RequiredArgsConstructor
public class ChunkUploadController {
private final ChunkUploadService chunkUploadService;
@GetMapping("/chunk")
public String chunkUploadPage() {
return "chunk";
}
@ResponseBody
@PostMapping("/chunk/upload")
public ResponseEntity<String> chunkUpload(@RequestParam("chunk") MultipartFile file,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks) throws IOException {
boolean isDone = chunkUploadService.chunkUpload(file, chunkNumber, totalChunks);
return isDone ?
ResponseEntity.ok("File uploaded successfully") :
ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
}
}
chunk.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input id="video-file" type="file" name="file">
<button onclick="sendVideoChunks()">업로드</button>
<div id="result"></div>
</body>
<script>
const sendVideoChunks = () => {
const chunkSize = 1024 * 1024; // 1MB
const file = document.getElementById("video-file").files[0];
const resultElement = document.getElementById("result");
// total size 계산
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
// chunk file 전송
const sendNextChunk = () => {
// chunk size 만큼 데이터 분할
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
// form data 형식으로 전송
const formData = new FormData();
formData.append("chunk", chunk, file.name);
formData.append("chunkNumber", currentChunk);
formData.append("totalChunks", totalChunks);
fetch("/chunk/upload", {
method: "POST",
body: formData
}).then(resp => {
// 전송 결과가 206이면 다음 파일 조각 전송
if (resp.status === 206) {
// 진행률 표시
resultElement.textContent = Math.round(currentChunk / totalChunks * 100) + "%"
currentChunk++;
if (currentChunk < totalChunks) {
sendNextChunk();
}
// 마지막 파일까지 전송 되면
} else if (resp.status === 200) {
resp.text().then(data => resultElement.textContent = data);
}
}).catch(err => {
console.error("Error uploading video chunk");
});
};
sendNextChunk();
}
</script>
</html>
http://localhost:8080/chunk
분할된 파일이 임시 저장 된다.
전송 완료
임시 파일들은 삭제되고 하나로 합쳐진다.
4.39GB의 대용량 파일도 전송이 완료 되었다.
안녕하세요. 궁금한게 생겨서 여쭈어 봅니다!
다름이 아니라, 같은 환경에서 해당 예제를 실행하면,
2023-08-23 15:16:14.678 INFO 18544 --- [ File Watcher] rtingClassPathChangeChangedEventListener : Restarting due to 2464 class path changes (0 additions, 2464 deletions, 0 modifications)
2023-08-23 15:16:14.696 INFO 18544 --- [ Thread-7] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2023-08-23 15:16:14.697 INFO 18544 --- [ Thread-7] o.apache.catalina.core.StandardWrapper : Waiting for [1] instance(s) to
be deallocated for Servlet [dispatcherServlet]
2023-08-23 15:16:14.810 INFO 18544 --- [ Thread-7] o.a.c.c.C.[Tomcat].[localhost].[/] : Destroying Spring FrameworkServlet 'dispatcherServlet'
라는 에러 로그와 함께 spring boot가 제부팅이 되는데, 혹시 해당 원인이 있으셨다면, 어떻게 해결하셧는지 알 수 있을까요?