우리는 개발을 하다 보면 클라우드를 사용할 것 이다.
그 중에 이미지 사진 및 동영상을 S3에 올리고 , 배포 하는 방법을 공유 한다.
S3는 각자 알아서 찾아 보길 바란다.
1. 이미지를 왜 S3에서 배포를 하는 것인가?
이미지를 가져와서 다른 사용자에게 보이게 하기 위해선 , public Ip가 필요한데 ,
대부분의 개인 노트북 또는 데스크탑에서 개발 시 , 로컬 아이피 이기 때문에
다른 사용자가 노트북이나 데스크탑에 들어가서 이미지를 확인할 수 없다.
그렇기 때문에 아마존에서 잘 관리된 S3에서 이미지를 올려
이 이미지의 link를 html로 보여주면 모든 사용자에게 이미지를 제공 할 수 있다.
우리는 이러한 동영상 + 이미지를 사용하여 영상을 올려보도록 한다.
2.S3를 이용하여 이미지를 배포 하는방법 (백엔드 부분)
implementation 'software.amazon.awssdk:s3:2.20.27'
implementation 'software.amazon.awssdk:auth:2.20.27'
implementation 'software.amazon.awssdk:regions:2.20.27'
build gradle에 이러한 의존성을 추가한다.
implementation 'software.amazon.awssdk:s3:2.20.27'
1. 이 라이브러리는 AWS SDK for java 2에서 제공하는 S3 클라이언트 라이브러리이다.
2. S3 버킷 생성 , 객체 업로드 및 다운로드 , 버킷 및 객체 삭제 등의 S3의 관련작업을 수행 할 수 있다.
(즉 서버 내에서 S3 버킷 안에 있는 객체들을 삭제 등을 진행 할 수 있다)
implementation 'software.amazon.awssdk:auth:2.20.27'
1. 이 라이브러리는 AWS SDK for java 2에서 제공하는 인증 및 자격 관 증명 라이브러리 이다.
2. AWS에서 자격 관리를 하고 , 요청의 필요한 서명을 생성하는 기능을 제공한다.
( 이 라이브러리가 없으면 AWS 인증을 할 수 없음 필수 라이브러리)
implementation 'software.amazon.awssdk:regions:2.20.27'
1. 이 라이브러리는 AWS SDK for java 2에서 제공하는 리전 관련 라이브러리이다.
2. AWS에 접근하는 리전을 설정하고 , 관리하는데 사용 된다.
1. Application properties에 환경 변수 만들기
spring.servlet.multipart.max-file-size=10GB
spring.servlet.multipart.max-request-size=10GB
aws.accessKeyId=yourAccessKeyId
aws.secretKey=yoursecretKey
aws.region=ap-northeast-2
aws.s3.bucket=yourBucket
프로퍼티스 부분에 이러한 형식을 만들자.
s3을 사용할려면 반드시 엑세스키와 시크릿 키 , 리전 , 버킷 이 4가지 정보가 필요하기 때문이다.
2. S3 Config 만들기
@Configuration
public class S3Config {
@Value("${aws.accessKeyId}")
private String accessKeyId;
@Value("${aws.secretKey}")
private String secretKey;
@Value("${aws.region}")
private String region;
@Value("${aws.s3.bucket}")
private String bucketName;
@Bean
public S3Client s3Client() {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretKey);
return S3Client.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(StaticCredentialsProvider.create(awsCreds))
.build();
}
1. S3 config를 만들어 프로퍼티스에 환경변수로 등록한 시크릿 키 , 버킷 , 레진 , 엑세스 키 이 4개를
value로 가져온다.
2. S3Client에 우리가 등록한 값들을 설정해야 한다.
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretKey);
각 메서드는 다음과 같다.
1. AwsBasicCredentials 객체를 생성한다. 이 객체는 aws 자격 증명을 포함하며 ,
acessKeyId와 secretKey는 aws에 인증에 필요한 키 이다.
credentialsProvider(StaticCredentialsProvider.create(awsCreds))
자격 증명 공급자를 설정한다. StaticCredentialsProvider는 고정된 자격 증명을 제공하는 공급자이다.
여기서는 앞어 생성한 AwsBasicCredentials를 사용한다.
.build();
모든 설정을 적용하여 S3client 객체를 생성하고 반환한다.
이로써 , S3Client는 설정된 리전과 자격 증명을 사용하여 S3 서비스와 상호작용할 준비가 되었다.
S3Serivce 만들기
private final S3Client s3Client;
private final String bucketName;
public S3Service(S3Config s3Config) {
this.s3Client = s3Config.s3Client();
this.bucketName = s3Config.getBucketName();
}
이러한 의존성을 추가할 준비를 마친다.
@Bean으로 등록했기 때문에 초기화시에 자동으로 s3Client의 의존성의 자동 주입된다.
/* 이미지 업로드 메서드 */
public String upload(MultipartFile file) throws IOException {
String key = String.valueOf(Paths.get(System.currentTimeMillis() + "-" + file.getOriginalFilename()));
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(file.getBytes()));
URL fileUri = s3Client.utilities().getUrl(builder -> builder.bucket(bucketName).key(key));
return fileUri.toString();
}
String key = String.valueOf(Paths.get(System.currentTimeMillis() + "-" + file.getOriginalFilename()));
1. 이 key는 버킷내에 파일을 식별하는 고유한 이름이다.
2. Systemcurrent를 사용하여 현재 시간을 밀리초 단위로 얻고 , 여기에 원래 파일이름 + getOriginalFilename을 사용해
고유한 파일 이름을 얻는다 즉 현재 시간 + 고유한 파일이름을 설정한다.
이렇게 안하면 이름이 중복된 파일을 올릴 시 , 중복된 파일이 버킷내에 들어가기 때문에 반드시 설정해줘야 한다.
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
1. putObjectRequest는 객체에 s3에 업로드 하기위한 요청을 정의한다.
1. 버킷명
2. 버킷의 들어갈 고유한 키(파일의 고유한 이름)이 필요하다.
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(file.getBytes()));
1. s3client.putObject를 이용하여 파일을 s3에 업로드한다.
2. 첫번째 인수로 putOject를 전달하여 업로드할 버킷과 키를 지정한다.
3. 두번째 인수로 requestBody.fromByte를 사용하여 파일의 바이트 데이터를 전달한다.
4. file.getBytes()는 MultipartFile 객체로부터 파일 데이터를 바이트 배열로 변환한다.
URL fileUri = s3Client.utilities().getUrl(builder -> builder.bucket(bucketName).key(key));
1. S3client.untiltites.getUrl()을 사용하여 업로된 파일의 Url을 생성한다.
2. build.bucket(bucketName).key(key) : Url을 생성할 버킷 이름과 파일 키를 지정한다.
3. 이 후 업로드된 파일 Url을 가져와야함으로 fileUrl은 그 업로드된 파일의 url을 반환한다.
return fileUri.toString();
그 후 return 값으로 파일의 url을 String 타입으로 리턴한다.
public class S3ServiceDemo {
S3Client s3Client;
String bucketName;
S3ServiceDemo(S3Config s3Config) {
this.s3Client = s3Config.s3Client();
this.bucketName = s3Config.getBucketName();
}
/* 이미지 업로드 메서드 */
public String upload(MultipartFile file) throws IOException {
String key = String.valueOf(Paths.get(System.currentTimeMillis() + "-" + file.getOriginalFilename()));
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(file.getBytes()));
URL fileUri = s3Client.utilities().getUrl(builder -> builder.bucket(bucketName).key(key));
return fileUri.toString();
}
}
클래스의 로직은 이렇게 되어있다.
이로써 upload메서드를 이용하여 이미지 타입의 Url을 받고 이 Url을 DB에 저장하여 프론트에게 뿌리면
S3로 이미지 영상을 배포 하는 것은 끝이다.
------------------동영상 파트 부분 ----------------------------------------------------------------------------------------
3. S3를 이용하여 동영상 배포
이 동영상은 이미지 업로드보단 훨씬 어려운 편에 속한다.
왜냐하면 동영상은 이미지 처럼 바로 올리는 것이 아닌
각 영상을 파트 단위로 쪼개고 , 업로드 해야하는 이유이기 때문이다.
4. 동영상을 업로드 하는 방식
동영상을 S3로 업로드하여 유저에게 배포하는 방법은 크게 두 가지로 나뉜다.
1. 클라이언트(프론트)에서 동영상을 직접 서버에 전달시켜 서버가 동영상을 S3로 직접 업로드 하는 방식
2. 클라이언트가 S3로 업로드를 직접하는 방법
이 두 가지 방법이 존재하는데 우리는 2번째 방법을 사용하겠다.
그 이유는 첫 번째 방법을 사용한다면 서버에게 큰 부담을 주기 때문에 가급적 사용하지 않는 편이 좋다.
또한 두번째 방식은 넷플릭스 , 라프텔 , 무빙 등 동영상 스트리밍 사이트들이 주로 사용하는 방법이다.
두 번째 방식은 이렇게 진행될 예정이다.
1. 클라이언트가 백엔드로 부터 서명된 Url을 받아옴.
클라이언트는 각 파트를 업로드 하기 위해 백엔드에게 서명된 Url을 요청해야한다.
백엔드는 S3에 요청하여 각 파트에 대한 사전 서명된 Url을 생성하고 , 이를 클라이언트에게 반환한다.
2. 클라이언트는 서명된 Url을 가지고 각 파트를 쪼개서 서명된 Url과 함께 S3에 보낸다.
클라이언트는 받은 사전 서명된 Url을 사용하여 각 파트를 S3에 업로드를 진행한다.
각 파트를 업로드할 때 , 사전 서명된 URL을 사용하여 S3에 직접 요청을 보낸다.
3. 이 파트 작업이 S3에 다 올라가면 백엔드에게 완료 요청을 보낸다.
모든 파트가 성공적으로 업로드 되면 , 클라이언트는 모아놓은 List객체 파트 부분들의 정보를
백엔드로 전송한다.
List 객체는 각 파트의 Etag과 파트 번호를 포함하여 , 백엔드로 업로드 완료 요청을 보낸다.
4. 백엔드는 완료 요청을 받으면 S3에게 완료 되었음과 동시에 조립 방법을 S3에게 보낸다.
백엔드는 클라이언트로 받은 정보(각 파트에 해당하는 Etag과 ParNumber)를 보내
S3에게 어떻게 조립을 해야하는 견적서를 보낸다.
5. S3는 백엔드에게 완료 요청을 받으면 , 요청서에 적힌대로 동영상 파트를 조립한다.
이 후 동영상은 완전한 형식이 만들어지며 동영상 Url을 반환한다.
4. 버킷 설정 및 Cors 설정 ( Aws s3)에서 설정
Aws s3 설정에서 버킷 정책을 이렇게 설정한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::videoserver-static-files/*" " < --- 생성한 버킷이름(내 버킷 이름임)
},
{
"Sid": "PublicUploadPutObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::videoserver-static-files/*" < --- 생성한 버킷이름(내 버킷 이름임)
}
]
}
CORS(Cross-origin 리소스 공유)에 다음과 같이 설정
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"POST",
"PUT",
"DELETE",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"ETag"
],
"MaxAgeSeconds": 30000
}
]
5. S3를 이용하여 동영상 업로드 구현 방법
1. S3Config 설정
public class S3Config {
@Value("${aws.accessKeyId}")
private String accessKeyId;
@Value("${aws.secretKey}")
private String secretKey;
@Value("${aws.region}")
private String region;
@Value("${aws.s3.bucket}")
private String bucketName;
@Bean
public S3Client s3Client() {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretKey);
return S3Client.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(StaticCredentialsProvider.create(awsCreds))
.build();
}
// Getter for bucket name
public String getBucketName() {
return bucketName;
}
//동영상을 업로드 하기위한 Config
@Bean
public S3Presigner s3Presigner() {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretKey);
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsCreds))
.build();
}
}
세부 설명
- AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretKey);
- AwsBasicCredentials 객체를 생성 이 객체는 AWS 자격 증명을 포함한다.
- accessKeyId와 secretKey는 AWS 계정의 인증에 필요한 자격 증명 정보이다.
이는 보통 애플리케이션의 설정 파일이나 환경 변수에서 가져온다. - AwsBasicCredentials.create(accessKeyId, secretKey)는 AwsBasicCredentials 객체를 생성하여 반환합니다.
- S3Presigner.builder()
- S3Presigner 객체를 구성하기 위한 빌더 객체를 생성한다.
빌더 패턴을 사용하여 여러 설정을 적용한 후 S3Presigner 객체를 생성할 수 있다.
- S3Presigner 객체를 구성하기 위한 빌더 객체를 생성한다.
- region(Region.of(region))
- region 메서드는 S3Presigner 객체가 사용할 AWS 리전을 설정
- Region.of(region)는 지정된 리전을 나타내는 Region 객체를 생성한다.
예를 들어, Region.of("ap-northeast-2")는 서울 리전을 의미함.
- credentialsProvider(StaticCredentialsProvider.create(awsCreds))
- credentialsProvider 메서드는 AWS 자격 증명을 제공하는 공급자를 설정함.
- StaticCredentialsProvider.create(awsCreds)는 고정된 자격 증명을 제공하는 StaticCredentialsProvider 객체를 생성 여기서는 앞에서 생성한 AwsBasicCredentials 객체를 사용한다.
- build()
- build 메서드는 설정된 모든 옵션을 사용하여 S3Presigner 객체를 생성하고 반환한다.
이로써 S3Presigner는 설정된 리전과 자격 증명을 사용하여 S3 서비스와 상호작용할 준비가 되었음을 의미
- build 메서드는 설정된 모든 옵션을 사용하여 S3Presigner 객체를 생성하고 반환한다.
전체적인 흐름
즉 이 메서드는 다음과 같이 동작한다.
- AwsBasicCredentials 객체를 생성하여 AWS 자격 증명을 설정합니다.
- S3Presigner.builder()를 호출하여 S3Presigner의 빌더 객체를 생성합니다.
- region 메서드를 호출하여 S3Presigner의 리전을 설정합니다.
- credentialsProvider 메서드를 호출하여 자격 증명 공급자를 설정합니다.
- build 메서드를 호출하여 S3Presigner 객체를 생성하고 반환합니다.
6. 클라이언트와 백엔드간의 통신할 api 컨트롤러 3개 생성
@GetMapping("/api/s3/create-multipart-upload")
public ResponseEntity<Map<String, String>> createMultipartUpload(@RequestParam String fileName) {
String uploadId = s3Service.createMultipartUpload(fileName);
Map<String, String> response = new HashMap<>();
response.put("uploadId", uploadId);
return ResponseEntity.ok(response);
}
// 서명된 Url을 주는 api 파트 마다 한번씩 요청 됨
@GetMapping("/api/s3/generate-presigned-url")
public ResponseEntity<String> generatePresignedUrl(@RequestParam String fileName,
@RequestParam int partNumber,
@RequestParam String uploadId) {
String url = s3Service.generatePresignedUrl(fileName, partNumber, uploadId);
return ResponseEntity.ok(url);
}
//
@PostMapping("/api/s3/complete-multipart-upload")
public ResponseEntity<Void> completeMultipartUpload(@RequestBody CompleteMultipartUploadRequestCustom request) {
s3Service.completeMultipartUpload(request);
return ResponseEntity.ok().build();
}
@GetMapping("/api/s3/create-multipart-upload")
public ResponseEntity<Map<String, String>> createMultipartUpload(@RequestParam String fileName) {
String uploadId = s3Service.createMultipartUpload(fileName);
Map<String, String> response = new HashMap<>();
response.put("uploadId", uploadId);
return ResponseEntity.ok(response);
}
이 컨트롤러 메서드는 클라이언트가 멀티파트
업로드를 시작할 수 있도록 uploadId를 생성하여 반환하는 역할을 한다.
멀티파트 업로드는 큰 파일을 여러 파트로 나누어 업로드할 때 사용되며,
uploadId는 이 업로드 세션을 식별하는 데 사용된다.
상세 설명
- HTTP GET 요청:
- 이 메서드는 /api/s3/create-multipart-upload 엔드포인트로 들어오는 HTTP GET 요청을 처리한다.
- 요청 파라미터:
- @RequestParam String fileName: 요청 파라미터로 파일 이름을 받는다. 이 파라미터는 업로드할 파일의 이름이다.
- 멀티파트 업로드 시작:
- s3Service.createMultipartUpload(fileName): fileName을 인수로 s3Service의 createMultipartUpload 메서드를 호출하여 멀티파트 업로드를 시작하고, uploadId를 생성한다.
- 이 uploadId는 멀티파트 업로드 세션을 식별하는 고유한 ID로, 모든 파트 업로드 요청에서 사용된다.
- s3Service.createMultipartUpload(fileName): fileName을 인수로 s3Service의 createMultipartUpload 메서드를 호출하여 멀티파트 업로드를 시작하고, uploadId를 생성한다.
- 응답 생성:
- Map<String, String> response = new HashMap<>();: 응답 데이터를 담을 Map 객체를 생성한다.
- response.put("uploadId", uploadId);: 생성된 uploadId를 response 맵에 추가한다.
- return ResponseEntity.ok(response);: uploadId를 포함한 응답을 HTTP 200 OK 상태로 클라이언트에게 반환한다.
전체적인 흐름
- 클라이언트가 /api/s3/create-multipart-upload 엔드포인트로 파일 이름을 포함한 GET 요청을 보낸다.
- 서버는 fileName 파라미터를 받아 s3Service.createMultipartUpload(fileName)를 호출하여 멀티파트 업로드를 시작하고 uploadId를 생성한다.
- 서버는 생성된 uploadId를 응답 맵에 담아 클라이언트에게 반환한다.
- 클라이언트는 반환된 uploadId를 사용하여 각 파트를 업로드할 때 이 ID를 포함시킨다.
@GetMapping("/api/s3/generate-presigned-url")
public ResponseEntity<String> generatePresignedUrl(@RequestParam String fileName,
@RequestParam int partNumber,
@RequestParam String uploadId) {
String url = s3Service.generatePresignedUrl(fileName, partNumber, uploadId);
return ResponseEntity.ok(url);
}
이 메서드는 클라이언트가 S3에 파일의 특정 파트를 업로드할 수 있도록 사전 서명된 URL을 생성해주는 역할을 한다.
- 클라이언트가 /api/s3/generate-presigned-url 엔드포인트로 GET 요청을 보내면 이 메서드가 호출된다.
- fileName, partNumber, uploadId라는 쿼리 파라미터를 받는다.
- fileName: 업로드할 파일의 이름이다.
- partNumber: 업로드할 파일의 파트 번호이다.
- uploadId: 멀티파트 업로드 과정에서 uploadId는 하나의 업로드 세션을 식별하는 고유한 값이다
- 이 정보들을 s3Service.generatePresignedUrl(fileName, partNumber, uploadId) 메서드에 전달하여 사전 서명된 URL을 생성한다.
- 생성된 URL을 응답으로 반환한다.
@PostMapping("/api/s3/complete-multipart-upload")
public ResponseEntity<Void> completeMultipartUpload(@RequestBody CompleteMultipartUploadRequestCustom request) {
s3Service.completeMultipartUpload(request);
return ResponseEntity.ok().build();
}
이 메서드는 멀티파트 업로드를 완료하는 역할을 한다.
멀티파트 업로드는 큰 파일을 여러 파트로 나누어 업로드한 후,
이 파트들을 합쳐 하나의 파일로 만드는 과정이다.
이 메서드는 모든 파트 업로드가 완료된 후, 업로드를 마무리하는 요청을 처리한다.
즉 S3에게 어떻게 조립을 해야할지 , 조립방법을 S3에게 보내는 역활을 한다.
상세 설명
- HTTP POST 요청:
- 이 메서드는 /api/s3/complete-multipart-upload 엔드포인트로 들어오는 HTTP POST 요청을 처리한다.
- 요청 본문:
- @RequestBody CompleteMultipartUploadRequestCustom request: 요청 본문으로 CompleteMultipartUploadRequestCustom 객체를 받는다. 이 객체는 멀티파트 업로드를 완료하는 데 필요한 정보를 포함한다.
- 멀티파트 업로드 완료 요청:
- s3Service.completeMultipartUpload(request): s3Service의 completeMultipartUpload 메서드를 호출하여 멀티파트 업로드를 완료한다.
- 이 메서드는 요청 본문에 포함된 정보를 사용하여 S3에 업로드 완료 요청을 보낸다.
- s3Service.completeMultipartUpload(request): s3Service의 completeMultipartUpload 메서드를 호출하여 멀티파트 업로드를 완료한다.
- 응답 생성:
- return ResponseEntity.ok().build();: HTTP 200 OK 상태를 클라이언트에게 반환한다. 응답 본문은 없다.
CompleteMultipartUploadRequestCustom 클래스
이 클래스는 멀티파트 업로드를 완료하는 데 필요한 정보를 포함하는 커스텀 요청 객체다. 일반적으로 포함되는 정보는 다음과 같다:
- uploadId: 멀티파트 업로드 세션을 식별하는 ID
- bucketName: 업로드할 버킷의 이름
- objectKey: 업로드할 객체의 키 (파일 이름)
- partNumber : 각 동영상 객체를 파트 단위로 나눴을 때 각 동영상의 파트 순서( 순서대로 조립해야 하기 때문)
- ETAG(매우 중요)
전체 흐름에서의 ETag 역할
- 파트 업로드: 클라이언트는 파일의 각 파트를 S3에 업로드한다. 이때 S3는 각 파트에 대한 ETag를 반환한다.
- ETag 저장: 클라이언트는 각 파트의 ETag를 저장한다.
- 업로드 완료 요청: 모든 파트를 업로드한 후, 클라이언트는 저장한 ETag 목록과 함께 업로드 완료 요청을 보낸다.
- S3 업로드 완료 확인: S3는 각 파트의 ETag를 확인하여 모든 파트가 올바르게 업로드되었는지 확인하고, 최종적으로 파일을 병합한다.
전체적인 흐름
- 클라이언트가 모든 파트를 S3에 업로드한 후, 업로드를 완료하기 위해 /api/s3/complete-multipart-upload 엔드포인트로 POST 요청을 보낸다.
- 요청 본문에는 uploadId, bucketName, objectKey, partETags와 같은 정보가 포함된다.
- 서버는 이 정보를 받아 s3Service.completeMultipartUpload(request) 메서드를 호출하여 S3에 업로드 완료 요청을 보낸다.
- S3는 각 파트를 병합하여 하나의 파일로 만든다.
- 서버는 HTTP 200 OK 응답을 클라이언트에 반환하여 업로드 완료를 확인한다.
@PostMapping("/api/s3/complete-multipart-upload")
에 필요한 커스텀 클래스(직접 만들어야 한다)
@Data
public class CompleteMultipartUploadRequestCustom {
private String fileName;
private String uploadId;
private List<CompleteMultipartUploadRequestCustomDto> completedParts;
}
1. fileName과 uploadId가 들어간다.
2.List형태로 프론트가 각 동영상 파트를 저장하는 객체 형태의 List임.
List형태에는 객체의 번호와 , 성공 완료 여부인 ETAG가 들어가게 된다.
@Data
public class CompleteMultipartUploadRequestCustomDto {
@JsonProperty("ETag")
private String ETag;
@JsonProperty("PartNumber")
private int partNumber;
}
public String createMultipartUpload(String objectKey) {
CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder()
.bucket(bucketName)
.key(objectKey)
.build();
CreateMultipartUploadResponse response = s3Client.createMultipartUpload(createMultipartUploadRequest);
return response.uploadId();
}
uploadid 생성
이 메서드는 AWS S3에서 멀티파트 업로드를 시작하는 역할을 한다.
멀티파트 업로드는 큰 파일을 여러 개의 작은 파트로 나누어 업로드할 수 있도록 하는 기능이다.
이 메서드는 멀티파트 업로드를 시작하기 위해 필요한 요청을 생성하고, S3로부터 업로드 ID를 받아서 반환한다.
CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder()
.bucket(bucketName)
.key(objectKey)
.build();
파라미터
- objectKey: 업로드할 객체(파일)의 키(이름). S3 버킷 내에서 파일을 식별하는 이름이다.
동작 과정
CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder()
.bucket(bucketName)
.key(objectKey)
.build();
- CreateMultipartUploadRequest 생성:
- CreateMultipartUploadRequest.builder(): 빌더 패턴을 사용하여 CreateMultipartUploadRequest 객체를 생성한다.
- .bucket(bucketName): 업로드할 S3 버킷의 이름을 설정한다.
- .key(objectKey): 업로드할 객체의 키(이름)를 설정한다.
- .build(): 설정된 요청 객체를 빌드한다.
CreateMultipartUploadResponse response = s3Client.createMultipartUpload(createMultipartUploadRequest);
return response.uploadId();
- s3Client.createMultipartUpload(createMultipartUploadRequest):
S3 클라이언트를 사용하여 멀티파트 업로드 요청을 보낸다. - 이 요청에 의해 S3는 멀티파트 업로드 세션을 시작하고, 업로드 ID를 생성하여 반환한다
프론트는 이 uploadId를 통하여 파일을 업로드 하게 된다
전체적인 흐름
- 클라이언트가 업로드할 파일의 키를 인수로 메서드를 호출한다.
- 메서드는 S3에 멀티파트 업로드 요청을 생성하고 전송한다.
- S3는 멀티파트 업로드 세션을 시작하고, 업로드 ID를 반환한다.
- 메서드는 이 업로드 ID를 클라이언트에게 반환하여 이후의 파트 업로드 요청에서 사용하도록 한다.
요청된 Url 생성하는 서비스 로직
public String generatePresignedUrl(String objectKey, int partNumber, String uploadId) {
UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
.bucket(bucketName)
.key(objectKey)
.uploadId(uploadId)
.partNumber(partNumber)
.build();
UploadPartPresignRequest presignRequest = UploadPartPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(60))
.uploadPartRequest(uploadPartRequest)
.build();
URL url = s3Presigner.presignUploadPart(presignRequest).url();
return url.toString();
}
이 메서드는 AWS S3에서 멀티파트 업로드를 수행하기 위한 사전 서명된 URL을 생성하는 역할을 한다.
사전 서명된 URL을 사용하면 클라이언트가 AWS 자격 증명이 없어도
해당 URL을 통해 S3에 파일의 특정 파트를 업로드할 수 있다. 이 메서드는 특정 파트를 업로드하기 위한 URL을 생성하고, 이를 문자열로 반환한다.
파라미터
- objectKey: 업로드할 객체(파일)의 키(이름). S3 버킷 내에서 파일을 식별하는 이름이다.
- partNumber: 업로드할 파트의 번호. 멀티파트 업로드에서 각 파트는 고유한 번호를 가진다.(즉 조립할 순서를 정의함)
- uploadId: 멀티파트 업로드 세션을 식별하는 업로드 ID. createMultipartUpload 메서드를 통해 생성된 값이다.
동작 과정
-
- UploadPartRequest.builder(): 빌더 패턴을 사용하여 UploadPartRequest 객체를 생성한다.
- .bucket(bucketName): 업로드할 S3 버킷의 이름을 설정한다.
- .key(objectKey): 업로드할 객체의 키(이름)를 설정한다.
- .uploadId(uploadId): 멀티파트 업로드 세션을 식별하는 업로드 ID를 설정한다.
- .partNumber(partNumber): 업로드할 파트의 번호를 설정한다.
- .build(): 설정된 요청 객체를 빌드한다.UploadPartRequest 생성
UploadPartPresignRequest presignRequest = UploadPartPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(60))
.uploadPartRequest(uploadPartRequest)
.build();
- UploadPartPresignRequest.builder():
- UploadPartPresignRequest 객체를 빌더 패턴을 사용하여 생성하기 위한 빌더 객체를 반환한다. 빌더 패턴은 객체 생성 시 필요한 설정들을 체인 메서드 형태로 제공하여 가독성을 높이고, 객체 생성 과정을 단순화한다.
- .signatureDuration(Duration.ofMinutes(60)):
- 사전 서명된 URL의 유효 기간을 설정한다. Duration.ofMinutes(60)은 이 URL이 생성된 시점부터 60분 동안 유효함을 의미한다.
- 이 기간 내에 클라이언트는 이 URL을 사용하여 특정 파트를 업로드할 수 있다. 기간이 지나면 URL은 만료된다.
- .uploadPartRequest(uploadPartRequest):
- 업로드할 파트에 대한 요청 객체를 설정한다. uploadPartRequest는 업로드할 파트의 세부 정보를 포함하는 UploadPartRequest 객체다.
- UploadPartRequest 객체에는 다음과 같은 정보들이 포함되어 있다:
- bucketName: 업로드할 S3 버킷의 이름.
- objectKey: 업로드할 파일의 키(이름).
- uploadId: 멀티파트 업로드 세션을 식별하는 업로드 ID.
- partNumber: 업로드할 파트의 번호.
- bucketName: 업로드할 S3 버킷의 이름.
- 이 설정을 통해 S3는 어느 버킷에, 어느 파일의, 몇 번째 파트를 업로드할 것인지 알게 된다.
- 업로드할 파트에 대한 요청 객체를 설정한다. uploadPartRequest는 업로드할 파트의 세부 정보를 포함하는 UploadPartRequest 객체다.
- .build():
- 빌더 패턴을 사용하여 설정된 옵션들을 기반으로 UploadPartPresignRequest 객체를 생성하고 반환한다.
- UploadPartPresignRequest 객체는 사전 서명된 URL을 생성하기 위해 필요한 모든 정보를 포함하고 있다.
전체적인 흐름
- 사전 서명된 URL 유효 기간 설정:
- .signatureDuration(Duration.ofMinutes(60))를 통해 URL이 60분 동안 유효하도록 설정한다.
- 업로드할 파트 요청 객체 설정:
- .uploadPartRequest(uploadPartRequest)를 통해 업로드할 파트에 대한 세부 정보를 포함한 UploadPartRequest 객체를 설정한다.
- 객체 생성:
- .build()를 통해 설정된 모든 옵션을 기반으로 UploadPartPresignRequest 객체를 생성한다.
public void completeMultipartUpload(CompleteMultipartUploadRequestCustom customRequest) {
List<CompletedPart> completedParts = customRequest.getCompletedParts().stream()
.map(dto -> {
String eTag = dto.getETag();
log.info("Original ETag: {}", eTag); // ETag 값 로그 출력
eTag = eTag.replace("\"", ""); // 따옴표 제거
log.info("Sanitized ETag: {}", eTag); // 제거 후 ETag 값 로그 출력
if (dto.getPartNumber() < 1) {
throw new IllegalArgumentException("PartNumber must be >= 1");
}
return CompletedPart.builder()
.eTag(eTag)
.partNumber(dto.getPartNumber())
.build();
})
.collect(Collectors.toList());
CompleteMultipartUploadRequest completeMultipartUploadRequest = CompleteMultipartUploadRequest.builder()
.bucket(bucketName)
.key(customRequest.getFileName())
.uploadId(customRequest.getUploadId())
.multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build())
.build();
// 디버깅용 로그 추가
log.info("CompleteMultipartUploadRequest: {}", completeMultipartUploadRequest);
try {
s3Client.completeMultipartUpload(completeMultipartUploadRequest);
} catch (S3Exception e) {
log.error("S3Exception: {}", e.awsErrorDetails().errorMessage());
log.error("Request ID: {}", e.requestId());
log.error("HTTP Status Code: {}", e.statusCode());
log.error("AWS Error Code: {}", e.awsErrorDetails().errorCode());
log.error("Error Type: {}", e.awsErrorDetails().errorMessage());
log.error("Service Name: {}", e.awsErrorDetails().serviceName());
throw e;
}
}
이 메서드는 AWS S3에서 멀티파트 업로드를 완료하는 역할을 한다.
멀티파트 업로드는 큰 파일을 여러 개의 작은 파트로 나누어 업로드하는 방식이며,
이 메서드는 모든 파트를 업로드한 후 최종적으로 업로드를 완료하는 요청을 S3에 보낸다.
List<CompletedPart> completedParts = customRequest.getCompletedParts().stream()
.map(dto -> {
String eTag = dto.getETag();
log.info("Original ETag: {}", eTag); // ETag 값 로그 출력
eTag = eTag.replace("\"", ""); // 따옴표 제거
log.info("Sanitized ETag: {}", eTag); // 제거 후 ETag 값 로그 출력
if (dto.getPartNumber() < 1) {
throw new IllegalArgumentException("PartNumber must be >= 1");
}
return CompletedPart.builder()
.eTag(eTag)
.partNumber(dto.getPartNumber())
.build();
})
.collect(Collectors.toList());
- customRequest.getCompletedParts()는 사용자 요청에서 업로드된 파트들의 리스트를 가져온다.
- 각 파트는 ETag와 PartNumber를 포함하는 DTO 객체로 표현된다.
- 각 DTO 객체를 CompletedPart 객체로 변환한다.
- ETag 값에서 따옴표를 제거한다.
- PartNumber가 1보다 작은 경우 예외를 던진다.
- CompletedPart 객체를 생성한다.
CompleteMultipartUploadRequest completeMultipartUploadRequest = CompleteMultipartUploadRequest.builder()
.bucket(bucketName)
.key(customRequest.getFileName())
.uploadId(customRequest.getUploadId())
.multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build())
.build();
CompleteMultipartUploadRequest 생성:
- CompleteMultipartUploadRequest 객체를 빌더 패턴을 사용하여 생성한다.
- bucket(bucketName): 업로드할 S3 버킷의 이름을 설정한다.
- key(customRequest.getFileName()): 업로드할 파일의 키(이름)를 설정한다.
- uploadId(customRequest.getUploadId()): 멀티파트 업로드 세션을 식별하는 업로드 ID를 설정한다.
- multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build()): 업로드 완료 요청에 필요한 파트 정보들을 설정한다.
try {
s3Client.completeMultipartUpload(completeMultipartUploadRequest);
} catch (S3Exception e) {
log.error("S3Exception: {}", e.awsErrorDetails().errorMessage());
log.error("Request ID: {}", e.requestId());
log.error("HTTP Status Code: {}", e.statusCode());
log.error("AWS Error Code: {}", e.awsErrorDetails().errorCode());
log.error("Error Type: {}", e.awsErrorDetails().errorMessage());
log.error("Service Name: {}", e.awsErrorDetails().serviceName());
throw e;
}
멀티파트 업로드 완료 요청:
- s3Client.completeMultipartUpload(completeMultipartUploadRequest):
S3 클라이언트를 사용하여 멀티파트 업로드 완료 요청을 보낸다.
completeMultipartUploadRequest
이때 이 객체는 S3에게 어떻게 조립을 해야하는 양식서를 저장한 객체이다.
S3는 이 객체의 정보를 토대로 동영상을 조립한다.
총 로직 부분
Config
package com.example.springsecurity04.Config.S3;
import com.example.springsecurity04.Table.Test.FileRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
@Configuration
public class S3Config {
@Value("${aws.accessKeyId}")
private String accessKeyId;
@Value("${aws.secretKey}")
private String secretKey;
@Value("${aws.region}")
private String region;
@Value("${aws.s3.bucket}")
private String bucketName;
@Bean
public S3Client s3Client() {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretKey);
return S3Client.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(StaticCredentialsProvider.create(awsCreds))
.build();
}
// Getter for bucket name
public String getBucketName() {
return bucketName;
}
//동영상을 업로드 하기위한 Config
@Bean
public S3Presigner s3Presigner() {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(accessKeyId, secretKey);
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsCreds))
.build();
}
}
총 S3 로직 이미지 + 동영상 서비스 로직
package com.example.springsecurity04.Service.S3Serivce;
import com.example.springsecurity04.Config.S3.S3Config;
import com.example.springsecurity04.Request.CompleteMultipartUploadRequestCustom;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.UploadPartPresignRequest;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class S3ServiceDemo {
private final S3Client s3Client;
private final String bucketName;
private final S3Presigner s3Presigner;
S3ServiceDemo(S3Config s3Config,S3Presigner s3Presigner) {
this.s3Client = s3Config.s3Client();
this.bucketName = s3Config.getBucketName();
this.s3Presigner = s3Presigner;
}
/* 이미지 업로드 메서드 */
public String upload(MultipartFile file) throws IOException {
String key = String.valueOf(Paths.get(System.currentTimeMillis() + "-" + file.getOriginalFilename()));
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(file.getBytes()));
URL fileUri = s3Client.utilities().getUrl(builder -> builder.bucket(bucketName).key(key));
return fileUri.toString();
}
// ObjectKey : 파일의 이름
public String createMultipartUpload(String objectKey) {
CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder()
.bucket(bucketName)
.key(objectKey)
.build();
CreateMultipartUploadResponse response = s3Client.createMultipartUpload(createMultipartUploadRequest);
return response.uploadId();
}
/* 이 부분의 코드는 실제로 S3에 파일을 업로드하는 것은 아니고,
앞서 클라이언트가 부분적으로 업로드한 각 파트의 정보를 S3에 전달하여 이 파트들을 결합해 최종 파일로 완성하라는 요청을 보내는 역할을 합니다.
즉, S3에게 "이 파트들을 결합하여 하나의 파일로 만들어라"라는 명령을 내리는 것입니다. */
public void completeMultipartUpload(CompleteMultipartUploadRequestCustom customRequest) {
List<CompletedPart> completedParts = customRequest.getCompletedParts().stream()
.map(dto -> {
String eTag = dto.getETag();
log.info("Original ETag: {}", eTag); // ETag 값 로그 출력
eTag = eTag.replace("\"", ""); // 따옴표 제거
log.info("Sanitized ETag: {}", eTag); // 제거 후 ETag 값 로그 출력
if (dto.getPartNumber() < 1) {
throw new IllegalArgumentException("PartNumber must be >= 1");
}
return CompletedPart.builder()
.eTag(eTag)
.partNumber(dto.getPartNumber())
.build();
})
.collect(Collectors.toList());
CompleteMultipartUploadRequest completeMultipartUploadRequest = CompleteMultipartUploadRequest.builder()
.bucket(bucketName)
.key(customRequest.getFileName())
.uploadId(customRequest.getUploadId())
.multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build())
.build();
// 디버깅용 로그 추가
log.info("CompleteMultipartUploadRequest: {}", completeMultipartUploadRequest);
try {
s3Client.completeMultipartUpload(completeMultipartUploadRequest);
} catch (S3Exception e) {
log.error("S3Exception: {}", e.awsErrorDetails().errorMessage());
log.error("Request ID: {}", e.requestId());
log.error("HTTP Status Code: {}", e.statusCode());
log.error("AWS Error Code: {}", e.awsErrorDetails().errorCode());
log.error("Error Type: {}", e.awsErrorDetails().errorMessage());
log.error("Service Name: {}", e.awsErrorDetails().serviceName());
throw e;
}
}
public String generatePresignedUrl(String objectKey, int partNumber, String uploadId) {
UploadPartRequest uploadPartRequest = UploadPartRequest.builder()
.bucket(bucketName)
.key(objectKey)
.uploadId(uploadId)
.partNumber(partNumber)
.build();
UploadPartPresignRequest presignRequest = UploadPartPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(600))
.uploadPartRequest(uploadPartRequest)
.build();
URL url = s3Presigner.presignUploadPart(presignRequest).url();
return url.toString();
}
}
컨트롤러 로직(동영상 + 이미지)
@GetMapping("/api/s3/create-multipart-upload")
public ResponseEntity<Map<String, String>> createMultipartUpload(@RequestParam String fileName) {
String uploadId = s3Service.createMultipartUpload(fileName);
Map<String, String> response = new HashMap<>();
response.put("uploadId", uploadId);
return ResponseEntity.ok(response);
}
// 서명된 Url을 주는 api 파트 마다 한번씩 요청 됨
@GetMapping("/api/s3/generate-presigned-url")
public ResponseEntity<String> generatePresignedUrl(@RequestParam String fileName,
@RequestParam int partNumber,
@RequestParam String uploadId) {
String url = s3Service.generatePresignedUrl(fileName, partNumber, uploadId);
return ResponseEntity.ok(url);
}
//
@PostMapping("/api/s3/complete-multipart-upload")
public ResponseEntity<Void> completeMultipartUpload(@RequestBody CompleteMultipartUploadRequestCustom request) {
s3Service.completeMultipartUpload(request);
return ResponseEntity.ok().build();
}
(프론트 Vue js 3 + vite 동영상 업로드 로직)
const submitForm = async () => {
if (!videoFile.value) {
alert('파일을 선택하십시오.');
return;
}
try {
const token = localStorage.getItem("jwt");
const filename = videoFile.value.name; // 인코딩 없이 원본 파일 이름 사용
console.log("Starting multipart upload for:", filename);
// Create multipart upload
const createResponse = await axios.get(`${backServer.backUrl}/api/s3/create-multipart-upload`, {
params: { fileName: encodeURIComponent(filename) }, // 여기는 인코딩된 파일 이름 사용
headers: { 'Authorization': `Bearer ${token}` }
});
console.log("Multipart upload created:", createResponse.data);
const { uploadId } = createResponse.data;
const partSize = 5 * 1024 * 1024; // 5MB
const parts = [];
for (let start = 0, partNumber = 1; start < videoFile.value.size; start += partSize, partNumber++) {
const end = Math.min(start + partSize, videoFile.value.size);
const partBlob = videoFile.value.slice(start, end);
// Get presigned URL for the part
const presignPartResponse = await axios.get(`${backServer.backUrl}/api/s3/generate-presigned-url`, {
params: { fileName: encodeURIComponent(filename), partNumber: partNumber, uploadId: uploadId }, // 여기도 인코딩된 파일 이름 사용
headers: { 'Authorization': `Bearer ${token}` }
});
console.log(`Presigned URL for part ${partNumber}:`, presignPartResponse.data);
const partPresignedUrl = presignPartResponse.data;
const uploadResponse = await fetch(partPresignedUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
},
body: partBlob
});
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text();
console.error(`Error uploading part ${partNumber}:`, errorText);
throw new Error(`Part upload failed: ${errorText}`);
}
console.log(`Part ${partNumber} uploaded successfully.`);
// Get and check ETag value
let eTag = uploadResponse.headers.get('ETag');
console.log(`ETag for part ${partNumber}:`, eTag); // ETag 값 로그 출력
if (eTag) {
eTag = eTag.replace(/"/g, ''); // Remove any double quotes from the ETag
parts.push({ ETag: eTag, PartNumber: partNumber });
} else {
console.error(`ETag is null for part ${partNumber}`);
alert(`ETag is null for part ${partNumber}`);
throw new Error(`ETag is null for part ${partNumber}`);
}
// Update progress
progress.value = Math.round((start + partSize) / videoFile.value.size * 100);
}
console.log("All parts uploaded, completing multipart upload.");
console.log('Completed parts:', parts); // completedParts 로그 출력
// Complete multipart upload
await axios.post(`${backServer.backUrl}/api/s3/complete-multipart-upload`, {
fileName: encodeURIComponent(filename), // 여기도 인코딩된 파일 이름 사용
uploadId: uploadId,
completedParts: parts
}, {
headers: { 'Authorization': `Bearer ${token}` }
});
// Generate the full video URL
const bucketUrl = "https://videoserver-static-files.s3.ap-northeast-2.amazonaws.com/";
const encodedFilename = encodeURIComponent(filename); // 인코딩된 파일 이름 사용
const videoUrl = `${bucketUrl}${encodedFilename}`; // 인코딩된 파일 이름 사용
// Upload metadata and files
const videoDto = {
title: title.value,
episodeNumber: episodeNumber.value,
ImageUrl: '', // Set this after uploading the image
videoUrl: videoUrl,
description: description.value,
genre: selectedType.value
};
const formData = new FormData();
formData.append('videoDto', new Blob([JSON.stringify(videoDto)], { type: "application/json" }));
formData.append('Image', imageFile.value);
if (subtitleFile.value) {
formData.append('subtitle', subtitleFile.value);
}
await axios.post(`${backServer.backUrl}/api/file/video/save-metadata`, formData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'multipart/form-data'
}
});
alert('Upload successful!');
} catch (error) {
console.error('Error uploading the file', error);
alert('Error uploading the file');
}
};
결과 이런식으로 동영상이 잘 올라오는 걸 확인 할 수 있다.