본문 바로가기

백엔드 개발일지

[Spring/AWS] Pre-Signed Url을 이용하여 S3로 파일 업로드하기 (feat. NCP)

디프만 동아리에서 진행 중인 프로젝트 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 동작 과정

메리가 만든 예시 0_<

 

 클라이언트가 업로드할 파일의 파일 정보(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를 확인하면 고유한 파일명으로 업로드한 이미지 파일이 제대로 업로드됨을 확인할 수 있습니다.