티스토리 뷰
업무 중 파일 업로드를 실제 서버로 파일을 보내는 대신 클라이언트에서 처리하는 방향으로 변경하면서 PreSignedUrl을 사용하게 되어 관련 내용을 정리하려고 합니다. 전 회사에서 해당 방식을 사용한다는 것은 알고 있었지만 어떻게 동작하는지 구체적으로 관심가지지 않아서 몰랐던 내용을 직접 해보면서 정리하였습니다. 😅
1. AWS IAM Role 에 S3 업로드 권한 부여
S3에 파일을 업로드 및 다운로드하기 위해서는 S3에 대한 권한을 가지고 있어야 합니다.
저는 개인적으로 사용하던 기존 User에 S3FullAccess 권한을 부여했습니다. (local AWS CLI를 통해 접근하는 User 또는 EC2에게 주어지는 Role에 권한을 추가해주면 됩니다.)
* 사용자를 새로 생성한 경우 CLI 에서 접속할 수 있는 액세스 키를 생성하고 생성된 키를 .aws/config 파일에 추가해줍니다.
.aws/config 파일에 다음과 같이 작성
[PROFILE_NAME]
aws_access_key_id = 생성된_액세스_키
aws_secret_access_key = 생성된_시크릿_키
2. AWS S3 버킷 생성 및 정책 등록
이후 파일을 업로드할 버킷을 생성합니다. S3 버킷은 기본적으로 퍼블릭 접근을 허용하지 않도록 만들어지지만 지금의 경우 클라이언트에서 접근이 가능해야 하기 때문에 퍼블릭 설정 후 정책을 추가하도록 하겠습니다. (퍼블릭 액세스를 차단하고 접근이 가능한지는 조금 더 살펴봐야할 것 같습니다...ㅠㅠ)
위와 같이 버킷을 생성한 다음 버킷 정책을 설정하였습니다.
{
"Version": "2012-10-17",
"Id": "Policy169930*******",
"Statement": [
{
"Sid": "Stmt169930*******",
"Effect": "Allow",
"Principal": {
"AWS": [
"S3_Full_Access_권한을_가진_사용자_ARN"
]
},
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::BUCKET_NAME/*"
}
]
}
3. Backend 소스 작성
(1) aws credential 설정
위에서 생성한 사용자의 access-key, secret-key 를 가지고 .aws/credential 설정을 합니다. (config 파일에 등록해서 사용해도 됩니다. 1번 하단 더보기 참고)
(2) build.gradel 에 dependency 추가
저는 SpringBoot 3.1.3 + Java 17 을 사용하는 환경이었고 다음과 같이 aws에 대한 dependency를 추가하였습니다.
dependencies {
...
// aws cloud
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
....
}
(2) application.yml 설정
cloud:
aws:
s3:
bucket: BUCKET_NAME
region:
static: ap-northeast-2
(3) AmazonS3Config 작성
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.WebIdentityTokenCredentialsProvider;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Configuration
public class AmazonS3Config {
private static final List<String> SUPPORTED_PROFILES = Arrays.asList("default", "local");
private static final String LOCAL_AWS_ROLE = "HB-DEV";
@Value("${cloud.aws.region.static}")
private String region;
@Value("${spring.profiles.default}")
private String activeProfile;
@Bean
@Primary
public AWSCredentialsProvider awsCredentialsProvider() {
if (SUPPORTED_PROFILES.contains(activeProfile)) {
return new ProfileCredentialsProvider(LOCAL_AWS_ROLE);
}
return new WebIdentityTokenCredentialsProvider();
}
@Bean
public AmazonS3 amazonS3Client() {
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(awsCredentialsProvider())
.build();
}
}
(4) PreSignedUrl 을 생성하는 역할을 하는 서비스 개발
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.UUID;
@Slf4j
@Component
@RequiredArgsConstructor
public class PreSignedUrlUtils {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.region.static}")
private String location;
public String getUploadPreSignedUrl(String prefix, String fileName) {
String onlyOneFileName = onlyOneFileName(fileName);
if (!prefix.equals("")) {
onlyOneFileName = prefix + "/" + onlyOneFileName;
}
return getPreSignedUrl(bucket, onlyOneFileName, HttpMethod.PUT);
}
public String getDownloadPreSignedUrl(String key) {
return getPreSignedUrl(bucket, key, HttpMethod.GET);
}
private String onlyOneFileName(String filename){
return UUID.randomUUID() + "-" + filename;
}
private String getPreSignedUrl(String bucket, String key, HttpMethod httpMethod) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, key)
.withMethod(httpMethod)
.withExpiration(getPreSignedUrlExpiration());
return amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString();
}
private Date getPreSignedUrlExpiration() {
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 2; // 유효 시간 2분
expiration.setTime(expTimeMillis);
log.info(expiration.toString());
return expiration;
}
public String findByName(String onlyOneFileName) {
if (!amazonS3.doesObjectExist(bucket, onlyOneFileName))
return "File does not exist";
log.info("Generating signed URL for file name {}", onlyOneFileName);
return amazonS3.getUrl(bucket, onlyOneFileName).toString();
}
}
(5) Controller 개발
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class FileController {
private final PreSignedUrlUtils presignedUrlUtils;
@PostMapping("/file/upload/presigned")
public ResponseEntity<String> createUploadPreSignedUrl(@RequestBody FileAccessDto dto) {
String path ="contact/" + dto.bucketType(); //원하는 경로 지정
return ResponseEntity.ok(presignedUrlUtils.getUploadPreSignedUrl(path, dto.fileName()));
}
@PostMapping("/file/download/presigned")
public ResponseEntity<String> createDownloadPreSignedUrl(@RequestBody FileAccessDto dto) {
return ResponseEntity.ok((presignedUrlUtils.getDownloadPreSignedUrl(dto.filePath()));
}
}
4. 확인
이제 포스트맨을 통해 preSignedUrl을 얻고 파일을 업로드해보겠습니다.
❗️만약 업로드 시 다음과 같은 에러가 발생한다면...
위와 같은 에러가 발생한다면 해당 버킷의 권한 > 객체 소유권을 편집하여 ACL을 허용해주어야 합니다.
이렇게 PreSignedUrl에 대해 알아보았습니다. 클라이언트에서 PreSignedUrl을 통해 업로드한 뒤 업로드한 정보를 서버로 보내 DB에 저장하는 과정이 추가로 필요합니다. 그래야 나중에 파일에 대한 정보를 조회하고 해당 정보를 바탕으로 다운로드가 가능한 PreSignedUrl을 생성할 수 있기 때문입니다. 이 부분은 해당 글에서 생략하였습니다.
감사합니다.
참고
'AWS' 카테고리의 다른 글
[AWS MSK] SpringBoot와 MSK Serverless 연동 (0) | 2024.01.31 |
---|---|
[AWS APIGateway] APIGateway WebSocket API 사용하기 1 (1) | 2023.12.06 |
[AWS KMS] KMS를 통한 암복호화 (2) | 2022.04.26 |
[Terraform] Terraform 사용하기 (사전 준비) (0) | 2022.01.04 |
[AWS CloudFront] CloudFront에서 API Gateway로 요청 전달 (0) | 2021.12.16 |
- Total
- Today
- Yesterday
- BFS
- 소수
- Dynamic Programming
- java
- 수학
- ionic
- ECR
- DFS
- sort
- Baekjoon
- programmers
- 순열
- 에라토스테네스의 체
- CodePipeline
- CodeDeploy
- cloudfront
- SWIFT
- Algorithm
- EC2
- permutation
- string
- array
- CodeCommit
- AWS
- map
- 프로그래머스
- 조합
- Combination
- search
- spring
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |