대용량 파일 업로드를 구현하기 위해서 구글링을 해본 결과 AWS S3에서 대용량 파일 업로드시 Multipart Upload를 지원한다는 문서를 보았다. 하지만 그 공식 문서 외에는 구글링으로 이해할 수 있는 자료를 찾기가 쉽지 않았고, 몇일간 여러가지 시도를 해보다 마침내 성공해서 정리할 겸 글을 남긴다. 참고로 필자의 개발환경은 Java Spring MVC이다.
슬랙, 카카오톡 등에 파일을 업로드하면 progress 상태가 보이면서 순차적으로 파일이 업로드되는 기능을 구현하는것이 목표였다. 업로드 될 파일의 크기는 최대 10GB정도로 예상했다.
AWS 공식문서에 따르면, 멀티파트 업로드를 위해서는 3가지 단계가 필요하다.
- Multipart upload initiation
- Parts upload
- Multipart upload completion
아래와 같이 AWS에 initiate request를 보내면 S3 bucket에 해당 업로드 건이 등록 되고, unique ID를 응답 값으로 준다.
InitiateMultipartUploadResult initiateUpload =
s3Client.initiateMultipartUpload(
new InitiateMultipartUploadRequest(BUCKET_NAME, KEY)); // KEY: file name including directory
log.info("upload id : {}", initiateUpload.getUploadId());
이때 KEY는 AWS S3에 올라갈 파일의 이름이다. ex) dir/object.mp4
그리고 다음 단계에 앞서 parts upload 단계에서 받을 ETag들을 담을 List를 만든다.
private List<PartETag> partETags = new ArrayList<>();
업로드 할 파일을 chunk로 쪼개서 각각 요청을 보내는 단계다.
각 요청마다 chunk part를 보내고 ETag 값을 반환받는다.
이 ETag들을 모아서 complete할 때 보내주어야 업로드에 성공할 수 있다.
아래는 각 part들을 업로드 할 때 사용되는 rule 이다.
- 각 요청에는 initiation에서 받은 unique id를 포함해야 한다.
- 각 파트마다 파트 번호를 명시해야한다. 파트번호는 1부터 10,000까지 가능하고, 연속적일 필요는 없으며 순차적으로 증가하기만 하면 된다. 예를 들어 파트 번호들이 [1, 2, 3, 4, 5] 가 아니라 [1, 3, 5, 7, 9] 여도 문제없이 동작한다.
- 각각의 파트는 최소 5MB 이상이어야 한다. (마지막 파트 제외. 마지막 파트는 5MB 이하 가능)
- 이 때 5MB는 5MiB = 5 * 1024 * 1024 byte 이다.
UploadPartRequest uploadPartRequest = new UploadPartRequest()
.withBucketName(BUCKET_NAME) // 버킷 이름
.withKey(KEY) // s3에 올라갈 파일 경로+이름
.withUploadId(upload_id) // init에서 받은 upload id
.withPartNumber(part_number) // 순차적인 part number (1~10,000)
.withFile(part_file) // chunk file
.withPartSize(part_file.length()); // chunk file size
UploadPartResult uploadPartResult = s3Client.uploadPart(uploadPartRequest);
partETags.add(uploadPartResult.getPartETag()); // init때 만든 배열에 ETag 담기
모든 파트의 업로드가 완료되면 마지막으로 AWS에 완료요청을 보내서 파일 저장을 마무리한다. complete 요청을 보내지 않으면 파일이 생성되지 않는다.
s3Client.completeMultipartUpload(
new CompleteMultipartUploadRequest(
BUCKET_NAME,
KET,
upload_id,
partETags)); // List<PartEtag>
complete 이후에 S3 Bucket 에 파일이 저장되어 있다면 성공이다.
프론트에서 파일을 Chunk로 자르기 위해 사용한 코드
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MiB
let part_index = 1, flag = 0;
const origin_file = input_file_element.files[0];
function partUpload(upload_id) {
const blob = origin_file.slice(flag, Math.min(origin_file.size, flag + CHUNK_SIZE));
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = async () => {
if (flag >= origin_file.size) { // 전송완료
const completed = await completeUpload(upload_id);
return;
}
const success = await sendEncodedByteData(upload_id, reader.result);
if (success) { // 전송 성공
byte_flag += CHUNK_SIZE;
part_index += 1;
partUpload(upload_id); // recursive
} else { // 전송중 에러
// error handle
}
}
}
서버로 보낼때는 Base64로 Encoding 되기 때문에,
컨트롤러에서는 String으로 받은 후에 Base64 Decoding을 통해 byte[] 로 변환했다.
initiate 이후에 에러발생, 업로드 중단 등의 이유로 complete가 되지 않는다면 해당 upload id의 multipart upload는 S3에 계속 남아있게 된다. 그렇게 되면 요금이 부과될 수도 있으므로 꼭 complete 혹은 abort를 하라고 AWS 문서에 나와있다.
s3Client.abortMultipartUpload(
new AbortMultipartUploadRequest(
BUCKET_NAME,
KEY,
upload_id)); // return void
개발 테스트중에 미처 complete, abort 하지 못한 업로드들을 처리해 주기 위해 조회 등록된 업로드들을 조회해야 했다.
MultipartUploadListing multipartUploadListing =
s3Client.listMultipartUploads(new ListMultipartUploadsRequest(BUCKET_NAME);
List<MultipartUpload> multipartUploads = multipartUploadListing.getMultipartUploads();
for (MultipartUpload multipartUpload : multipartUploads) {
log.info("upload id: {}, key: {}", multipartUpload.getUploadId(), multipartUpload.getKey());
// abort(upload id, key) // 필요없는 upload들을 폐기
}
Reference :
AWS S3 Multipart upload doc
https://wave1994.tistory.com/152