어떻게 S3 이미지 업로드 할까?
- Aws S3 방식 이용
- MultipartFile : Spring MVC 제공 인터페이스 활용 방식
- Stream : Server를 byPass 하는 형태로 바로 S3에 업로드
- Pre-Signed URL : client가 직접 S3에 업로드 하는 방식
일반적인 MultipartFile 방식 이용
- 메모리나 임시 디스크에 저장
- 업로드 완료된 이후 제거
- 파일 사이즈 제한하는 설정 하여 무분별한 파일이 업로드 되는것을 방지
- 파일이 서버를 거쳐가서 큰 부하를 주는 단점
- 단점
- 서버의 디스크 용량에 따라 실패할 수 잇다.
- 서버에 부하를 주는 방식
Stream 방식
- client가 Server에 Stream 방식으로 전달
- Server에서 S3에 Stream 방식으로 바로 전달
@PostMapping("/stream")
public String uploadFile (HttpServletRequest request) {
try (InputStream inputStrem = request.getInputStream()) {
String fileName = request.getHeader("file-name");
long contentLength = request.getContentLengthLong();
s3UploadService.uploadFileToS3(fileName, inputStream, contentLength);
return "업로드 완료"
} catch (IOException e) {
return "업로드 실패 : " + e.getMessaget();
}
}
Stream 방식과 MultiPartFile 업로드 방식 비교
구분 | CPU 사용률 | 가비지컬렉션 |
---|
MultipartFile | 50% | 0.8 ops/s |
Stream | 10% | 0.06 ops/s |
- Stream 방식이 서버에 부하를 주지 않는 방식임을 확인
Stream의 장점
- 서버의 부하가 적다.
- 서버의 디스크 용량과 무관하다.
10MB 100개 업로드 테스트
- Stream 방식은 100% 다 업로드 성공을 하지 못했다.
- 원인 : Connection Pool Timeout Exception 발생
- Stream 방식의 Connection 타임아웃
구분 | 잠실캠퍼스 | 선릉캠퍼스 | 우리집 |
---|
MultipartFile | 100 | 100 | 100 |
Stream | 50 | 100 | 49 |
원인 : Server에서 S3로 맺을 수 있는 Connection 최대 수가 50으로 제한
- 50개는 커넥션을 맺을 수 있지만 나머지 50개는 대기하게 됨
- 잠실의 경우 인터넷 속도가 좀 느림
- 업로드 속도 :
27Mbps
- EC2에서의 업로드 속도 :
185Mbps
-> 27Mbps
로 단축되는 효과를 얻음..
- 50개가 빠르게 처리되지 못해서 대기중이던 요청 50개는 대기하다가 타임아웃
- 선릉의 경우
650 Mbps
의 속도로 굉장히 빠름
650
-> 185Mbps
로 먼저 50개 처리
- 대기중이던 요청도 커넥션을 맺어서 처리할 수 있음
반면 MultipartFile 방식
- 어떻게 100개를 안정적으로 하는가?
- client의 속도가 느리더라도 MultipartFile로 변경하는 과정에서 임시 디스크에 저장
- S3에 업로드 속도는 EC2의 속도 그대로 (Client의 업로드 속도가 느려도 영향을 받지 않음)
- Stream 방식은 cilent의 업로드 속도와 무관하게 좀 더 안정적으로 업로드 할 수 있다는 장점이 있음
Stream 방식의 단점
- 클라이언트의 업로드 속도에 따라 S3 업로드에 실패한다.
- 파일을 업로드하는 긴 시간동안 서버의 커낵션을 소모한다.
Pre-Signed URL 방식
- Client가 S3에 바로 파일을 업로드 하는 방식
동작 방식
- Client가 Server에
start
요청
- Server가 S3에
Upload Id
요청
- S3에서
Upload Id
Server로 응답
- Server에서 S3에
Pre-Signed URL
요청
- S3에서 Server로
Pre-Signed URL
응답
- Server에서 Client로
Upload Id
, Pre-Signed URL
응답
- Client는 S3에 파일을 업로드 할 수 있는 권한을 가짐
- 파일을 분할해서 업로드 하는 방식
- Client는 파일을 분할해서 S3에
Pre-Signed URL
로 업로드
- S3에서 각각 파일들의
Etag
들을 Client로 응답
- Client에서
Etag
모두 응답 받음
- Client는 S3에
complete
요청
Server
는 S3에 요청해서 S3는 파일을 하나로 합침
구현
@PostMapping("/pre-signed/start")
public ResponseEntity<AwsMultipartStartResponse> start(
@RequestParam String fileName,
@RequestParam int partCount
) {
String uniqueFileName = UUID.randomUUID() + fileName;
String uploadId = s3UploadService.createMultipartUpload(uniqueFileName);
List<URL> urls = s3UploadService.generatePresignedUrls(uniqueFileName, uploadId, partCount);
return ResponseEntity.ok(new AwsMultipartStartResponse(uploadId, urls, uniqueFileName));
}
@PostMapping("/pre-signed/complete")
public String complete(
@RequestParam String fileName,
@RequestParam String uploadId,
@requestBody List<String> etags
) {
List<CompletedPart> completedParts = s3UploadService.getCompletedParts(eTags);
s3UploadService.completeMultipartUpload(fileName, uploadId, completedParts);
return "업로드 완료";
}
- 파일 자체가 서버를 거치지 않는 방식
start
, end
API 구현 필요
- 서버에 부하를 주지 않는 방식
1GB 5개 업로드 비교
구분 | 시간(m) |
---|
MultipartFile | 21 |
Stream | 21 |
Pre-Signed URL | 7.5 |
Pre-Singed URL
방식이 파일을 쪼개서 보낼 수 있어 훨씬 빠르게 보낼 수 있음
주의할 점
- CORS 설정
- Client가 S3에 이미지 바로 업로드
- S3에서 Client가 요청을 받을 수 있도록 CORS 설정
- Pre-Signed URL 만료시간
- 만료시간이 지나면 S3에 요청을 보낼 수 없음
- 시간을 너무 짧게 잡으면 업로드 실패, 너무 길게 잡으면 보안상 취약
- 적절한 시간 설정 필요
- LifeCycle
- 불안전한 파일 존재할 수 있음
- 라이프 사이클 설정하여 주기적으로 파일 제거해주는 작업 필요
Pre-Signed URL 단점
이미지 최적화와 작업 방식
현재 구현 사항 문제
- PNG, JPEG 등 용량이 큰 이미지 포맷으로 응답
- (해결) 저용량, 고화질 이미지 포맷으로 변경
- JPEG -> webp : -74.91%
- JPEG -> avif : -90.72%
- 사람이 체감하기 어려울 정도
- UI 요구사항보다 큰 사이즈의 원본 이미지 응답
- UI 요구사항에 따라 필요한 이미지 사이즈는 다름
- UI는 변경이 잦음
- (해결) UI 요구사항에 맞게 리사이징
- 768 x 1024 -> 300 x 400 => -90.51%
- 불필요한 네트워크 비용
언제 어디에서 이미지 최적화를 진행할까?
방식 1) 이미지 업로드 및 최적화 작업 서버
- 사용자가 별도 서버 이미지 업로드
- 이미지 최적화 및 저장 S3
- Cloud front 업로드
- 사용자는 Cloud Front 접근
문제
- pre-signed url 업로드 방식을 사용할 수 없음
- 이미지 업로드와 최적화 작업에 대한 서버 구현
- 장애가 발생했을 때 재시도와 롤백 로직 구현
- 별도의 서버를 관리해야 하는 비용 발생
방식 2) 이미지 업로드되는 시점에 최적화
- 사용자 S3 이미지 업로드 (Pre-Signed Url 방식)
- 이벤트 Trigger되어 Lambda가 실행되어 이미지 최적화 하여 S3에 사본 저장 (최적화 완료)
- 이후 사용자가 요청 시 CloudFront를 통해 S3 이미지를 가져온다
장점
- AWS Lambda를 사용한 서버리스
- 스크립트만 작성하면 되서 구현 간편
단점
- 이미지 요청이 들어온 시점에 사본이 생성되지 않을 수 있다.
- 모든 이미지에 대해 사본 생성
- 만약 UI가 변경되어 새로운 사이즈의 이미지가 필요하다면?
방식 3) 이미지 요청 시점에 최적화
- 사용자 S3 이미지 업로드
- 사용자가 이미지 요청 -> CloudFront가 S3 원본 이미지 요청 -> S3가 원본 이미지 반환할 때 Lambda가 가로채서 이미지 최적화 및 캐싱 -> 사용자에게 응답
장점
- 사용자의 요청에 정상 응답
- 사본 이미지를 저장하지 않음
단점
-
이미지를 처음 요청하는 사용자는 긴 시간 대기
-
CDN 캐싱 전 최적화 작업 여러번 반복
-
이미지 변경 요구사항으로 대규모 캐시 미스 발생
-
동시에 많은 리사이징 요청으로 이미지 변환 실패