이미지를 업로드할 때, 일반적으로 서버를 통해 이미지를 받아서 S3에 전달하는 방식을 사용한다.
하지만 이런 방식은 서버에 부하를 주고, 불필요한 네트워크 비용을 발생시킨다.
Presigned URL을 활용하면 클라이언트가 직접 S3에 업로드할 수 있어 서버 부하를 줄이고 속도를 개선할 수 있다.
클라이언트가 직접 S3에 파일을 업로드 하려면 AWS 리소스인 S3에 접근하기 위해서는 엑세스 키처럼 권한이 필요한데, 이 키를 프론트엔드가 직접 사용하는 것은 적절하지 않다. 프론트엔드는 사용자가 접근 가능한 영역이므로, 프론트엔드에 키를 줄 경우 유출 문제가 생길 수 있기 때문이다.
이를 해결하기 위해 사용하는 것이 Presigned url 이다. presigned url은 서버에서 가지고 있는 권한을 사용해 클라이언트에게 임시적인 권한을 발급해주는 것을 의미한다.
s3 버킷으로 접근할 수 있는 endPoint 라고 볼 수 있다.
결국 서버에서 presigned url 을 생성해주고 반환해주면 클라이언트는 s3에 직접 이미지를 업로드 할 수 있다.
일단 스프링부트 프로젝트에 sdk 의존성을 추가해준다.
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.767'
implementation 'software.amazon.awssdk:s3:2.27.3'
implementation 'software.amazon.awssdk:s3control:2.27.3'
implementation 'software.amazon.awssdk:s3outposts:2.27.3'
그런 다음 생성해놓은 s3 버킷에 대한 정보를 설정 파일에 담아주어야 한다. 위에서 만든 버킷 이름, 버킷이 위치한 리전, 그리고 엑세스 키와 시크릿 키 정보를 넣어준다. 이때 엑세스 키와 시크릿 키는 무슨 일이 있어도 github에 올라가서는 안되므로 로컬 상에 환경변수로 등록해서 사용하도록 하자!
cloud:
aws:
s3:
bucket: ${AWS_BUCKET:default}
path:
projects: projects
region:
static: ${AWS_REGION:default}
stack:
auto: false
credentials:
accessKey: ${AWS_ACCESS_KEY_ID:default}
secretKey: ${AWS_SECRET_ACCESS_KEY:default}
S3Presigner는 S3에 접근하기 위한 Presigned URL을 생성하는 데 사용된다.
package com.partnerd.config;
import com.amazonaws.auth.*;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
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.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
/**
*
* S3Config를 통해 AmazonS3라는 클라이언트를 빈에 등록
* 이 클라이언트가 있어야 AWS 리소스와 연결하고 정보를 가져오는 등의 일을 할 수 있다.
*
**/
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public S3Presigner s3Presigner() {
StaticCredentialsProvider credentials = StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)
);
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(credentials)
.build();
}
}
s3 Presigned url 생성을 위해 권한 정책 세팅은
https://developer-been.tistory.com/36 해당 블로그를 참고했다!
@PostMapping("/preSignedUrl")
@Operation(summary = "PUT 메서드용 S3 preSignedUrl 요청 ", description = "PUT 메서드용 S3 preSignedUrl를 요청하는 API입니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200",description = "OK, 성공"),
})
public ApiResponse<S3ResponseDTO.S3ResponseDTOforPut> getPreSignedUrlForPut(@RequestBody S3RequestDTO.S3RequestDTOForPut s3RequestDTO) {
return ApiResponse.onSuccess(s3Service.getPresignedUrlForPut(s3RequestDTO));
}
Put 메서드 용 presigned url 요청을 위한 API 를 구현했다.
이 API 를 통해 클라이언트는 저장하고 싶은 이미지에 대한 presigned url 을 전달 받을 수 있다.
public S3ResponseDTO.S3ResponseDTOforPut getPresignedUrlForPut(S3RequestDTO.S3RequestDTOForPut s3RequestDTO) {
String folderName = s3RequestDTO.getFolderName();
String fileName = s3RequestDTO.getFilename();
String type = "";
if ( s3RequestDTO.getType() == 0 ) {
type = String.valueOf(ImageType.BANNER);
} else if (s3RequestDTO.getType() == 1){
type = String.valueOf(ImageType.MAIN);
} else {
type = String.valueOf(ImageType.EVENT);
}
String uniqueFileName = UUID.randomUUID() + "-" + fileName;
String keyName = folderName + "/" + type + "/" + uniqueFileName;
String contentType = s3RequestDTO.getContentType();
System.out.println(contentType);
PutObjectRequest objectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(keyName)
.contentType(contentType)
.metadata(Map.of("Cache-Control", "max-age=864000")) // 캐싱 기간 10일
.build();
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofHours(2)) // 유효시간 2시간
.putObjectRequest(objectRequest)
.build();
PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
String presignedUrl = presignedRequest.url().toString();
s3Presigner.close();
return S3ResponseDTO.S3ResponseDTOforPut.builder()
.preSignedUrl(presignedUrl)
.keyName(keyName)
.build();
}
url 을 생성할 때 keyName 설정해준 뒤 그에 해당하는 presigned url을 발급밥는다. 여기서 keyName은 s3 버킷에 저장되는 경로 라고 생각하면 된다.
진행하는 프로젝트에서는 서비스마다 이미지 등록할 때, banner 이미지/main 이미지/ event 이미지를 등록할 수 있어야 했기때문에 서비스명/이미지 유형(banner,main,event)/파일이름 형식으로 키네임을 설정해서 구분하였다.
저장할 이미지에 캐싱 설정, 컨텐츠타입, 키네임을 설정해준 뒤 해당 이미지를 업로드할 수 있는 presigned url 을 생성해 프론트에 url 과 keyname 을 전달해준다.
프론트는 전달받은 url로 PUT 메서드를 통해 s3에 이미지를 업로드하고, 전달받은 keyname은 서비스 최종 저장 API 에 요청값으로 설정하여 API에서 해당 서비스의 image keyname 으로 저장한다. 이렇게 되면, 서비스의 이미지를 불러올 때 keyname 을 통해 조회를 위한 url을 요청하여 해당 url을 통해 이미지를 조회할 수 있다.
나는 s3에 대한 cloud Front 배포 도메인을 설정하였다.
CloudFront를 사용하여 S3에 대한 직접적인 접근을 차단하고,
CloudFront는 캐싱을 적용하여 성능을 최적화하기 위해서이다.
CloudFront 배포 설정도 아래 블로그를 참고했다!
https://developer-been.tistory.com/38
추후에 CloudFront 에서 임시 도메인말고 진행하고 있는 프로젝트에 도메인으로 변경할 예정이다.
참고:
https://cn-c.tistory.com/119
https://developer-been.tistory.com/38