디프만 동아리에서 진행 중인 프로젝트 Bibbi(https://github.com/depromeet/14th-team5-BE)에서는 사용자 프로필이미지/피드 이미지를 업로드할 수 있는 수단이 필요했고 저희 프로젝트에서는 Pre-Signed Url 방식을 채택했습니다.
NCP에서 지원한 크레딧으로 ObjectStorage를 생성하여 Pre-Signed Url을 구현했지만 로직은 AWS에 생성한 S3 버킷을 이용해 구현하는 방식과 같습니다.
1. Pre-Signed Url이란
1) 정의
AWS의 공식 문서에 의하면 Pre-Sigend Url(미리 서명된 URL)은 다른 사람이 AWS 보안 자격 증명이나 권한이 없어도 Amazon S3 버킷에 객체를 업로드하도록 허용할 수 있도록 하는 URL입니다. 즉 S3의 Bucket Policy나 ACL과 같은 권한설정과 관계없이 특정 유효기간에 S3에 PUT, GET이 가능하게 합니다.
2) 장점
S3에 이미지를 업로드 할 때, 클라이언트에서 API를 호출해 Multipart/form-data 형식의 이미지를 전송하고 서버에서 처리하는 방식을 많이 쓰기도 합니다. 하지만 아래에 적힌 Pre-Signed Url의 장점때문에 Pre-Signed Url 또한 많이 쓰이는 방식입니다.
① 보안
Pre-Signed Url을 통해 액세스를 허용하는 기간과 범위를 정확하게 제어하여 유효 기간 동안에만 액세스가 가능하도록 할 수 있습니다. 이를 통해 무단 액세스를 방지하고 보안을 강화할 수 있습니다.
② 성능
이미지 업로드는 JSON을 주고 받는 일반 API 요청에 비하면 훨씬 큰 부하를 발생시키는 작업입니다. 특히 영상 파일과 같이 컨텐츠의 용량이 높은 경우에는 서버에 파일을 전송하고 이를 서버에서 버킷에 저장하는 이중 작업은 비효율적이며 성능적으로도 효과적이지 않습니다.
Pre-Signed Url을 도입하면 서버는 클라이언트가 파일 업로드 권한을 가질 수 있는 url을 발행할 수 있어 클라이언트는 서버 리소스를 거치지 않고 직접 파일 업로드를 수행할 수 있습니다.
2. Pre-Signed Url 동작 과정

① 클라이언트가 업로드할 파일의 파일 정보(ex. 파일명)을 서버에 전달합니다.
② 서버는 내부적으로 파일 저장 경로를 생성하여 S3에 경로에 대한 서명 요청을 합니다.
③ S3가 서명한 경로를 클라이언트에게 전달하고 클라이언트는 해당 경로를 통해 S3에 파일 업로드를 수행합니다.
3. Pre-Signed Url 구현
AWS의 S3 버킷(혹은 NCP의 ObjectStorage)이 존재한다는 전제 하에 글을 작성했습니다.
1) AWS S3 설정 파일
① 의존성 추가
implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4'
Amazon Web Services를 쉽게 사용할 수 있게 돕는 라이브러리를 추가합니다.
- org.springframework.cloud:spring-cloud-starter-aws는 2021년도 이후로 업데이트가 되지 않으니 위의 의존성을 추가하는 걸 권장합니다.
② AmazonS3Client 빈 생성
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(properties.accessKey(),
properties.secretKey());
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(properties.endPoint(),
properties.region()))
.build();
}
해당 메서드에서는 Amazon S3와 상호작용하기 위한 AWS SDK의 AmazonS3Client 객체를 생성합니다. 이 객체를 사용하여 S3 버킷과 상호작용할 수 있습니다.
- BasicAWSCredentials: AWS의 자격 증명을 나타내는 클래스로, 액세스 키와 시크릿 키를 사용하여 객체를 생성합니다
- AmazonS3ClientBuilder: AmazonS3Client 객체를 생성하는 빌더 클래스입니다
- .withCredentials(): AWS 자격 증명을 빌더에 설정합니다
- .withEndpointConfiguration(): S3 클라이언트에 대한 엔드포인트 설정을 지정합니다
- .build(): 설정이 완료된 빌더를 사용하여 AmazonS3Client 객체를 생성하고 반환합니다
2) S3PreSignedUrlProvider
@Slf4j
@RequiredArgsConstructor
@Component
public class S3PreSignedUrlProvider implements PreSignedUrlGenerator {
@Value("${cloud.ncp.storage.bucket}")
private String bucket; // AWS S3 버킷명과 동일
private final AmazonS3Client amazonS3Client;
private final IdentityGenerator identityGenerator; // 프로젝트 내에서 만든 ULID 생성기
@Override
public PreSignedUrlResponse getPreSignedUrl(String imageName) {
GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest("feed",
imageName);
return new PreSignedUrlResponse(generatePreSignedUrl(generatePresignedUrlRequest));
}
private String generatePreSignedUrl(GeneratePresignedUrlRequest generatePresignedUrlRequest) {
String preSignedUrl;
try {
preSignedUrl = amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest).toString();
} catch (AmazonServiceException e) {
log.error("Pre-signed Url 생성 실패했습니다. {}", e.getMessage());
throw new IllegalStateException("Pre-signed Url 생성 실패했습니다.");
}
return preSignedUrl;
}
private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String directory, String imageName) {
String savedImageName = generateUniqueImageName(imageName);
String savedImagePath = "images" + "/" + directory + "/" + savedImageName;
return getPreSignedUrlRequest(bucket, savedImagePath);
}
private GeneratePresignedUrlRequest getPreSignedUrlRequest(String bucket, String imageName) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, imageName)
.withMethod(HttpMethod.PUT)
.withExpiration(new Date(System.currentTimeMillis() + 180000));
generatePresignedUrlRequest.addRequestParameter(
Headers.S3_CANNED_ACL,
CannedAccessControlList.PublicRead.toString());
return generatePresignedUrlRequest;
}
/**
* image path 형식 : "images" + "/" + "기능 dir" + "/" + ULID + 확장자;
*/
private String generateUniqueImageName(String imageName) {
String ext = imageName.substring(imageName.lastIndexOf("."));
return identityGenerator.generateIdentity() + ext;
}
@Async
@Override
public void deleteImageByPath(String imagePath) {
try {
amazonS3Client.deleteObject(bucket, imagePath);
} catch (AmazonServiceException e) {
log.error("이미지 삭제 실패했습니다. {}", e.getMessage());
throw new IllegalStateException("이미지 삭제 실패했습니다.");
}
}
}
위의 코드는 Pre-Sigend Url을 관리하는 파일입니다. 위에서부터 차례대로 기능을 설명했습니다.
① getPresignedUrl(String imageName)
getGeneratePreSignedUrlRequest 메서드를 통해 디렉터리 이름(옵션)과 이미지 명으로 Pre-Signed Url Request를 만듭니다. 이후 해당 Request를 가지고 미리 서명된 Url을 만들어 응답하는 기능을 합니다.
② generatePreSignedUrl(GeneratePresignedUrlRequest generatePresignedUrlRequest)
GeneratePresignedUrlRequest를 가지고 미리 서명된 Url을 생성합니다.
③ getPresignedUrlRequest(String bucket, String imageName)
getGeneratePreSignedUrlRequest 메서드를 통해 디렉터리 이름(옵션)과 이미지 명으로 Pre-Signed Url Request 요청 객체를 만듭니다.
이 요청은 PUT 메서드를 사용하여 S3에 객체를 업로드하도록 구성되어 있으며, URL의 유효기간은 180,000 밀리초(3분)로 설정됩니다.
추가적으로 S3 객체에 대한 액세스 권한을 설정하기 위해 Headers.S3_CANNED_ACL 헤더와 CannedAccessControlList.PublicRead.toString() 값을 추가하고 있습니다. 이를 통해 업로드된 객체에 대한 공개 읽기 권한이 설정됩니다.
④ generateUniqueImageName(String imageName)
S3 객체에 이미지를 저장할 때, 원본 그대로의 이미지명을 저장하기 보다 중복되지 않은 고유한 값으로 구성된 이미지명으로 저장해야 합니다. 해당 메서드에서는 ULID를 통해 imagePath를 생성합니다.
⑤ deleteImageByPath(String imagePath)
해당 메서드는 주어진 imagePath에 존재하는 파일을 삭제합니다.
3) 동작
① Pre-Signed Url 요청

imageName과 함께 Pre-Signed Url API를 요청하면 응답으로 미리 서명된 Url이 제공됩니다.
② Pre-Signed Url을 통해 이미지 업로드

`① Pre-Signed Url 요청` 단계에서 얻은 미리 서명된 Url에 이미지 파일과 함께 PUT 요청을 보냅니다.
③ 이미지 업로드 확인

S3 버킷 혹은 Object Storage를 확인하면 고유한 파일명으로 업로드한 이미지 파일이 제대로 업로드됨을 확인할 수 있습니다.
'백엔드 개발일지' 카테고리의 다른 글
[NCP] NCP Cloud DB for Redis 구축하기 (0) | 2024.01.19 |
---|---|
[Spring] 코드 정적 분석을 위한 SonarCloud 도입기 (1) | 2024.01.01 |
[Spring] Redis를 통한 Refresh Token 도입기 (2) (1) | 2023.12.18 |
[Spring] Redis를 통한 Refresh Token 도입기 (1) (1) | 2023.12.18 |
[Spring] Code Coverage 측정을 위한 JaCoCo 적용하기 (0) | 2023.12.04 |