프로젝트를 하며 파일 업로드 기능 구현을 맡게 되었다.
기존에는 프론트 단에서 파일을 건내주면, 서버에서 잠깐 받았다가 S3로 업로드를 하는 식으로 구현했는데.. 이 작업이 서버에 지나친 부하를 줄 것 같다고 생각했다.
따라서 이번에는 Presigned URL을 사용하기로 했다.
Presigned URL이란?
Presigned URL이란 말 그대로 미리 서명한 URL을 의미한다.
모든 S3 객체는 기본적으로 비공개이며, 객체 소유자만 접근할 수 있다. 하지만 S3 객체 소유자가 특정 권한(ex. 업로드, 다운로드 등)을 미리 설정하여 서명된 URL을 클라이언트에게 제공할 수 있다. 이렇게 제공 된 Presigned URL을 통해 클라이언트는 일정 기간동안 서버를 거치지 않고 저장소에 파일을 직접 업로드하거나 다운로드할 수 있다.
해당 글은 실습 위주이므로, 이론에 대한 자세한 내용은 이 글을 확인하자.
구현 과정
1️⃣ AWS S3 버킷 설정
1. S3 버킷을 생성한다.
2. S3를 사용하기 위한 IAM 사용자를 등록한다.
S3FullAccess 권한을 선택하여 사용자를 생성하고, 외부에서 S3를 사용하도록 만들기 위해 액세스 키를 생성해야 한다.
생성된 Access Key와 Secret Key는 추후 프로젝트 설정 파일(`application.yml`)에 등록할 것이다.
3. 버킷 정책 생성
Effect: Allow
Principal: *
AWS Service: Amazon S3
Actions: GetObject, PutObject 선택 (버킷이 수행할 액션)
ARN: arn:aws:s3:::owing-bucket/* // 어떤 버킷에 어떤 리소스에 적용? owing-bucket 버킷에 있는 모든 리소스(객체)
1. 정책 타입 선택
2. 버킷 정책이 적용될 대상 (전체: *)
3. 버킷이 수행할 액션 (GetObject, PutObject 선택)
4. 버킷에 어떤 리소스에 적용할지 (버킷이름/*)
그리고 정책을 생성하면 아래와 같은 코드가 나오는데, 해당 코드를 복사하여 정책으로 등록한다.
{
"Id": "Policy1730363678574",
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1730363676100",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": "arn:aws:s3:::owing-bucket/*",
"Principal": "*"
}
]
}
https://mingeonho1.tistory.com/entry/AWS-S3-%EB%B2%84%ED%82%B7-%EC%83%9D%EC%84%B1
[S3] AWS S3 버킷 생성
S3 프로젝트를 하다가 PDF를 어딘가에 저장해야 하는 일이 생겨서 S3 버킷에 저장해보려고 한다. 그래서 우선 S3를 생성해 보겠다. (AWS계정이 없다면 만들고 와야 한다.) 버킷 생성 Amazon S3 버킷을
mingeonho1.tistory.com
4.CORS 설정
다음과 같이 해당 URL에 접근하였을 때 CORS를 방지한다.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"HEAD",
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
2️⃣ Springoot 코드 작성
1. dependency 추가
implementation 'io.awspring.cloud:spring-cloud-aws-s3:3.0.2'
2. AWS 환경 설정
`application.yml`
cloud:
aws:
region: ${AWS_REGION}
credentials:
access-key: ${IAM_ACCESS_KEY}
secret-key: ${IAM_SECRET_KEY}
s3:
bucket: ${S3_BUCKET}
3. S3 Config 설정
AWS credential(자격 증명) 객체를 생성했다.
`S3Config.java`
@Configuration
public class S3ClientConfig {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String accessSecret;
@Value("${cloud.aws.region}")
private String region;
@Bean
public AwsCredentials basicAWSCredentials() {
return AwsBasicCredentials.create(accessKey, accessSecret);
}
@Bean
public S3Client s3Client (AwsCredentials awsCredentials) {
return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
.build();
}
@Bean
public S3Presigner s3Presigner(AwsCredentials awsCredentials) {
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
.build();
}
}
4. 서비스 코드 작성
서비스 로직에서는 해당 presined url이 어떤 HTTP 메서드를 허락하는지(GET/PUT), 어떤 key로 발급받을 것인지, 그리고 유효기간 등을 설정하여 클라이언트가 S3 Bucket에 안전하게 접근할 수 있도록 구성한다.
나의 경우, 클라이언트가 이미지를 S3 Bucket에 업로드하는 용도로 presigned url을 생성했기 때문에 다음과 같이 설정했다.
- HTTP 메서드: 클라이언트가 이미지를 S3 버킷에 직접 업로드할 수 있도록 PUT 메서드를 허용
- Key 설정: 업로드할 파일의 key는 클라이언트가 제공한 파일 이름으로 설정. (클라이언트가 서버에서 설정한 파일 이름과 동일한 이름으로 요청하지 않으면 업로드가 실패)
- 유효 기간: 유효 기간을 10분으로 설정. (Presigned URL이 생성된 후 10분이 지나면 해당 URL을 통한 업로드가 만료되어 더 이상 사용할 수 없음)
`S3Service.java`
@Service
@RequiredArgsConstructor
@Slf4j
public class S3Service {
private final S3Client s3Client;
private final S3Presigner s3Presigner;
@Value("${cloud.s3.bucket}")
private String bucketName;
public String getPresignUrl(String filename) {
if(filename == null || filename.equals("")) {
return null;
}
// 파일 확장자를 추출하고 contentType 설정
String[] splittedFileName = filename.split("\\.");
String extension = splittedFileName[splittedFileName.length - 1].equalsIgnoreCase("jpg")
? "jpeg" : splittedFileName[splittedFileName.length - 1].toLowerCase();
// 허용된 이미지 확장자 확인
String regExp = "^(jpeg|png|gif|bmp)$";
if (!Pattern.matches(regExp, extension)) {
throw new IllegalArgumentException("올바르지 않은 이미지 확장자입니다.");
}
// contentType 생성
String contentType = "image/" + extension;
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(filename)
.contentType(contentType) // contentType 추가
.build();
PutObjectPresignRequest putObjectPresignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10)) // presignedURL 10분간 접근 허용
.putObjectRequest(putObjectRequest)
.build();
PresignedPutObjectRequest presignedPutObjectRequest = s3Presigner.presignPutObject(putObjectPresignRequest);
String url = presignedPutObjectRequest.url().toString();
s3Presigner.close(); // presigner를 닫고 획득한 모든 리소스를 해제
return url;
}
}
5. 응답 결과
위에서 설명한 것처럼 파일명을 key 값으로 presigned url을 생성하기 때문에, 클라이언트 측에서 업로드한 파일의 이름이 다르면 아래와 같은 에러가 나온다.
개인적인 소감
1. 서버 부하를 줄일 수 있어 좋다.
클라이언트가 파일을 직접 S3에 업로드하기 때문에 서버를 통해 파일을 처리할 필요가 없어졌고, 이를 통해 서버 자원을 더 효율적으로 사용할 수 있었다.
2. 보안 면에서도 유리하다.
presigned url을 통해 필요한 권한만 제한된 시간 동안 부여할 수 있어서, S3 Bucket에 대한 무분별한 접근을 방지할 수 있었다.
3. 요청 흐름을 이해하고 적용하는 데 약간의 시간이 걸렸다.
클라이언트가 직접 AWS S3에 접근하는 구조가 기존의 서버-클라이언트 방식과 다르다보니, 요청 흐름을 이해하는 데 시간이 필요했다. 클라이언트, 서버, S3 간 요청 흐름을 시각화하여 정리하며 이해할 수 있었다.
'✍️ 개발 기록' 카테고리의 다른 글
[👀 Owing] Java Record 도입기 ☕️ (0) | 2024.11.05 |
---|---|
[👀 Owing] 멀티모듈 - 모듈 간 순환참조 이슈 해결 과정 💥 (0) | 2024.11.05 |
[👀 Owing] S3에 파일을 업로드하는 세 가지 방법 (MultipartFile, Stream, PresignedURL) (0) | 2024.10.31 |
[👀 Owing] Framer motion 애니메이션 적용기 (0) | 2024.09.27 |
[트러블슈팅] 504 Gateway Time-out 에러 해결 (1) | 2024.08.05 |