
최근 진행중인 사이드 프로젝트에서 신규 기능의 프로덕션 배포가 얼마 남지 않았을 때, 추가 기능 개발 요청이 들어왔다.
기존 배포 예정이었던 신규 기능의 흐름은 아래와 같다.
도전과제, 미션 달성을 통한 포인트 획득 -> 굿즈 상점에서 포인트를 지불하여 해금 -> 디지털 굿즈 다운로드 URL(Cloudflare R2 버킷에 있는 다운로드 URL) Response 리턴
그러나 위 흐름대로 다운로드 URL 응답을 리턴하면 URL을 통해 누구나 다운로드를 할 수 있기 때문에 굳이 포인트를 획득하여 해금하는 이유가 없는 문제점이 있다.
따라서 위 흐름에서 Presigned URL을 추가하여 해결한다.
미리 서명된 URL을 사용하면 버킷 정책을 업데이트하지 않고도 Amazon S3의 객체에 대한 시간 제한적 액세스를 부여할 수 있습니다. 미리 서명된 URL은 브라우저에 입력하거나 프로그램에서 객체를 다운로드하는 데 사용할 수 있습니다. 미리 서명된 URL에 사용되는 자격 증명은 URL을 생성한 AWS 사용자의 자격 증명입니다.
미리 서명된 URL을 사용하여 다른 사람이 Amazon S3 버킷에 특정 객체를 업로드하도록 허용할 수도 있습니다. 이렇게 하면 다른 당사자가 AWS 보안 자격 증명이나 권한을 갖지 않고도 업로드할 수 있습니다. 미리 서명된 URL에 지정된 것과 동일한 키를 가진 객체가 버킷에 이미 있는 경우, Amazon S3는 기존 객체를 업로드된 객체로 대체합니다.
미리 서명된 URL은 만료 날짜 및 시간까지 여러 번 사용할 수 있습니다.
간단히 정리하면, 권한을 보유하고 있는 사람이 객체를 업로드/다운로드하려는 사람에게 미리 서명된 URL을 주는 것이다. 현재 구현하려는 다운로드를 기준으로 프론트엔드와 백엔드 구조를 그려보면 아래 그림과 같다.

제목에는 Cloudflare R2를 이용한다고 해놓고 S3에 대한 설명을 하는 이유는 Cloudflare R2는 S3개념을 따르기 때문이다.
가장 먼저 서버에 의존성 추가 및 Cloudflare R2 관련 설정을 해줘야한다.
# build.gradle
implementation 'software.amazon.awssdk:s3:2.25.4'
implementation 'software.amazon.awssdk:auth:2.25.4'
# application.yaml
cloudflare:
r2:
endpoint: ${R2_END_POINT}
bucket-name: ${R2_BUCKET_NAME}
access-key: ${R2_ACCESS_KEY}
secret-key: ${R2_SECRET_KEY}
endpoint에 Custom Domain을 사용한다면
서명 불일치오류가 발생한다. 따라서 endpoint에는 기본 endpoint를 사용해야한다.
e.g)https://{account}.r2.cloudflarestorage.com
다음으로 S3 Config 클래스를 작성하여 S3Presigner 빈을 등록한다.
import java.net.URI;
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;
@Configuration
public class S3Config {
@Value("${cloudflare.r2.access-key}")
private String accessKey;
@Value("${cloudflare.r2.secret-key}")
private String secretKey;
@Value("${cloudflare.r2.endpoint}")
private String endpoint;
@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.endpointOverride(URI.create(endpoint))
.region(Region.of("auto"))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.build();
}
}
이제 실제 Presigned URL 생성을 요청하는 서비스레이어 코드를 작성한다.
import java.net.URL;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
@Service
@RequiredArgsConstructor
public class PresignedUrlService {
private static final int EXPIRE_SECONDS = 15;
private final S3Presigner s3Presigner;
@Value("${cloudflare.r2.bucket-name}")
private String bucketName;
public URL getPresignedUrl(String key) {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofSeconds(EXPIRE_SECONDS))
.getObjectRequest(objectRequest)
.build();
return s3Presigner.presignGetObject(presignRequest).url();
}
}
Presigned URL에는 만료 시간이 필수로 지정되어야 한다. 범위 : 1초 부터 7일(604,800초)
마지막으로, 위 서비스를 사용할 수 있도록 컨트롤러를 작성해주면 된다.
@RestController
public ExampleController {
private final PresignedUrlService presignedUrlService;
@GetMapping("/{id}/download")
public ResponseEntity<URL> getV1Download(@PathVariable String id) {
URL result = presignedUrlService.getPresignedUrl(id);
return ResponseEntity.ok(result);
}
}
Presigned URL을 생성하여 AWS S3의 파일을 다운로드 하는 글은 많은데, Cloudflare R2의 파일을 다운로드 하는 글은 없기에 한 번 작성해봤다.
구현 방법은 똑같지만 위에 작성한 Custom Domain을 endpoint로 설정하면 안된다는 것을 몰라 꽤 오랜시간 삽질했다.😂