이번 프로젝트를 진행하면서, AWS S3 Presigned Url을 스프링 프로젝트에 적용해봤습니다!
이전에 진행했던 프로젝트에서, S3을 사용해 본 경험이라고는
- 3D object 파일 업로드 후 url 통해서 오브젝트 불러오기 (프로젝트 내부에 두기에 크기가 너무 커서 github에도 잘 안 올라가고, 배포 시 vercel blob storage 서비스에 올렸었는데 트래픽 한도가 금세 초과되어 S3를 사용했었음)
- 팝업 정보 검색/기록 서비스에서 포스터 이미지 업로드 시 S3 사용하기 -> 같이 프로젝트 한 다른 친구가 모든 세팅을 다 해주고, 나는 되어 있는 거 가져다 사용만 했었음
이라 사실상 아무것도 모르는 상태였는데, 이번 기회로 직접 세팅 후 적용까지 해 볼 수 있어서 너무 좋은 경험이었습니다 🤩
하지만 아직 AWS 서비스들이나 이 각각의 설정에 대해 스스로 아주 잘 알지는 못하는 느낌이라,
블로그 포스팅 이후 조금 더 시간을 두고 Deep Dive 하면서 각 서비스와 권한 설정 등에 대해 더 자세히 알아보려고 합니다.
- S3 Presigned Url을 도입한 이유
- Spring Boot에서 Presigned Url 적용하기
- IAM 사용자 설정
- S3 Bucket 생성 및 버킷 정책, CORS 설정
- application.yml 설정
- Spring Boot 설정
- Postman에서 테스트
위 순서로 진행하도록 하겠습니다. 🏄
S3 Presigned Url을 도입한 이유
먼저 프로젝트에서 Presigned Url을 도입한 이유는 다음과 같습니다.
용량이 큰 이미지 파일을 주고 받는 상황이 많을 것으로 예상되었기 때문에,
클라이언트에서 서버에 전달해 서버에서 스토리지에 저장하는 방식보다는 서버를 거치지 않고 클라이언트가 직접 S3에 접근하도록 하여
1. 서버의 부하를 줄이고,
2. 네트워크 비용을 절감하기 위해
Presigned URL을 사용했습니다.
또한, 부가적인 이유로는 Presigned URL은 일정 시간 동안만 유효한 접근 권한을 제공하므로, 보안성과 유연성을 모두 확보할 수 있다는 점이 있었습니다.
Presigned Url이 뭔지!에 대한 설명은
[AWS/S3] S3 Presigned Url
Amazon S3 소개Amazon Simple Stroage Servie객체 스토리지 서비스.데이터를 S3에 저장하려면 , 버킷과 객체라는 리소스를 사용해야 함.버킷 : S3에 저장된 객체에 대한 컨테이너객체 : 파일과 해당 파일의
esthrelar.tistory.com
에서 확인해 보실 수 있습니다. 😉
Spring Boot에서 S3 Presigned Url 적용하기
그럼 스프링 부트에서 본격적으로 S3 Presigned Url을 적용해보겠습니다!
1. IAM 사용자 설정
먼저 IAM은, AWS 리소스에 대한 액세스를 관리할 수 있도록 하는 서비스입니다.
S3 Bucket을 생성할 때, 모든 IAM 사용자에게 무제한 접근을 허용하는 방식은 보안상 위험하다고 판단하여,
권한이 제한된 IAM Role을 설정하고 이를 통해 Presigned URL을 사용하는 방식으로 구현했습니다.
사용자 추가 -> 이름 설정 후 S3FullAccess 권한을 주었습니다.
그리고 생성된 사용자 -> 보안자격증명 탭 혹은 위의 액세스 키 만들기를 통해
Access Key, Secret Key를 만들어줍니다.
생성하는 과정에서
저는 아마 EC2 에서 배포한 서비스에 적용하기 위해 이렇게 설정했던 것 같은데, 이 각각의 사례와 각 사례 별 권장되는 대안에 대해서는 추가적인 공부가 필요할 것 같습니다 .. ㅎㅎ 각 사례에 대해 아시는 바가 있으시다면 댓글 남겨주시는 것도 언제나 환영입니다 !! 🙇♀️
아무쪼록 동의 후 액세스 키를 생성하면,
이렇게 생성이 되는데, 이렇게 생성된 Access Key와 Secret Key는 생성 직후에만 확인할 수 있기 때문에 .csv 파일을 다운로드 하여 잘 저장하고 있어야 합니다!
보안상의 이유로 AWS에서 재확인이 불가능하기 때문에 분실 시 새로 키를 생성해야 합니다 😲
2. S3 Bucket 생성 및 버킷 정책, CORS 설정
다음으로, S3 버킷을 생성해줍니다.
S3 -> 버킷 만들기 로 들어가면 이런 화면이 보이는데,
여기서 버킷 이름을 설정한 후 나머지 설정은 기본 설정된 대로 그대로 두고 진행했습니다.
이후 생성된 버킷 -> 권한 탭에서 버킷 정책과 CORS 설정을 진행해주면 됩니다.
여기서 버킷 정책 -> 편집 -> 정책 생성기에 들어가면
이런 화면이 뜨는데,
- Select Type of Policy : S3 Bucket Policy
- Principal : 허락할 IAM의 ARN 입력 (여러 명일 때는 `,` 로 구분하고, 전체 허용 시 `*` 입력)
- Action : 버킷에 허용할 Action 설정 (ex : putObject, GetObject 등)
- Amazon Resource Name (ARN) : 본인 버킷의 ARN (ex : arn:aws:s3:::{본인 버킷 이름}/* )
로 설정하여 권한을 받아서 그 JSON 코드를 붙여 넣어서 S3 버킷 권한 정책을 설정할 수 있습니다.
위 첨부한 링크의 블로그에서 더 자세히 나와 있으니 참고하시면 좋을 것 같습니다 ㅎㅎ
위 방식으로 버킷 정책을 더 디테일하게 설정하는 방식도 있지만,
저는 "읽기는 누구나 가능, 쓰기는 인증된 사용자만 가능" 한 구조를 만들기 위해
위처럼 정책 생성기를 사용해 디테일하게 설정하지는 않고,
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::kobaco1-bucket/*"
}
]
}
로 버킷 정책에 GetObject만 열어두고, 업로드는 IAM Role 기반으로 presigned URL을 통해 제한하는 방식을 선택했습니다.
그래서 위 정책으로는 모든 사용자에게 다운로드(GetObject)만 허용했습니다.
이후 CORS 설정은
버킷 설정에서 조금 더 내려서
란에서
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"DELETE",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [],
"MaxAgeSeconds": 3000
}
]
으로 느슨하게(..) 설정해주었습니다.
이 설정 상, 어떤 요청 헤더든 허용, 주요 HTTP 메소드에 대해 허용, 그리고 어떤 도메인에서 요청하든 다 허용하도록 되어있는데,
클라이언트 요청 도메인을 모르는 상태이기도 했고, 개발/테스트 환경에서 편리하게 하기 위해 이렇게 설정한 후 진행했습니다.
운영 환경에서는 특정 Origin 만 허용하는 방식으로 제한하는 방식이 더 좋습니다.
3. application.yml 설정
다음으로 application.yml 파일을 설정해줍니다.
cloud:
aws:
s3:
bucket: < S3 bucket 이름 >
stack.auto: false
region.static: ap-northeast-2
credentials:
access-key: < 앞서 IAM 설정 시 생성한 Access Key >
secret-key: < 앞서 IAM 설정 시 생성한 Secret Key >
위와 같이 설정해줬습니다.
stack.auto: false 는, Spring Cloud 환경에서 AWS 관련 인프라(리소스)를 자동으로 생성하지 않도록 제어하는 역할을 합니다.
즉, Spring Cloud AWS가 CloudFormation 스택을 자동으로 생성하지 않도록 막는 설정입니다.
GPT의 도움을 빌리면 위와 같다고 하는데,
이 프로젝트의 경우에도 직접 리소스를 설정하고 연결만 하려는 목적이었기 때문에 넣어주었습니다.
4. Spring Boot 설정
이제 이렇게 설정한 것들을 기반으로 스프링 부트로 설정을 진행해 봅시다!
설정 관련해서 찾아볼 때 어떤 어떤 설정들을 해야하는지 감이 안 잡혔어서 더 막막한 마음이 컸던 기억이 있어서 ..
시작 전에 어떤 파일들을 생성 및 수정했는지 먼저 알리는 게 좋을 것 같아 남겨두자면,
- build.gradle
- S3Config
- S3Controller
- S3Service
- dto.response.PresignedUrlResponse
입니다 !
그럼 아래에 각 파일들의 내용을 남겨두겠습니다.
build.gradle
dependencies {
// ...
//AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}
global.s3.config.S3Config
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
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;
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 amazonS3() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(Regions.fromName(region))
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
global.s3.controller.S3Controller
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import kobaco.backend.global.s3.dto.response.PresignedUrlResponse;
import kobaco.backend.global.s3.service.S3Service;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/s3")
@Tag(name = "S3", description = "S3 Presigned Url API")
@RequiredArgsConstructor
public class S3Controller {
private final S3Service s3Service;
// 업로드 Presigned URL 생성
@GetMapping("/presigned/upload")
@Operation(summary = "S3 업로드 Presigned URL 생성",
description = "S3에 파일을 업로드하기 위한 Presigned URL을 생성합니다.")
public PresignedUrlResponse getPresignedUploadUrl(@RequestParam String key) {
return s3Service.generatePresignedUploadUrl(key);
}
// 다운로드 Presigned URL 생성
@GetMapping("/presigned/download")
@Operation(summary = "S3 다운로드 Presigned URL 생성",
description = "S3에 업로드된 파일을 다운로드할 수 있는 Presigned URL을 생성합니다.")
public PresignedUrlResponse getPresignedDownloadUrl(@RequestParam String key) {
return s3Service.generatePresignedDownloadUrl(key);
}
}
global.s3.service.S3Service
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import kobaco.backend.global.s3.dto.response.PresignedUrlResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
// 단일 파일 업로드 Presigned URL 생성
public PresignedUrlResponse generatePresignedUploadUrl(String key) {
String encodedKey = encodeFileName(key);
return PresignedUrlResponse.builder()
.name(encodedKey)
.url(generatePresignedUrl(encodedKey, HttpMethod.PUT))
.build();
}
// 다운로드 Presigned URL 생성
public PresignedUrlResponse generatePresignedDownloadUrl(String key) {
String encodedKey = encodeFileName(key);
return PresignedUrlResponse.builder()
.name(encodedKey)
.url(generatePresignedUrl(encodedKey, HttpMethod.GET))
.build();
}
// 공통 Presigned URL 생성 로직
private String generatePresignedUrl(String key, HttpMethod method) {
Date expiration = new Date();
expiration.setTime(expiration.getTime() + 1000 * 60 * 10); // 10분 후 만료
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucketName, key)
.withMethod(method)
.withExpiration(expiration);
URL presignedUrl = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
return presignedUrl.toString();
}
// 파일 이름을 UTF-8로 인코딩
private String encodeFileName(String fileName) {
System.out.println(URLEncoder.encode(fileName, StandardCharsets.UTF_8));
return URLEncoder.encode(fileName, StandardCharsets.UTF_8);
}
}
위는 OCR 등 생성된 URL을 AI 서비스에 활용할 때,
파일 명으로 한글이 입력될 때를 대비해 파일명을 인코딩하기 위한 로직이 추가되었고,
또 공통으로 PresignedUrl을 생성하는 로직을 분리한 것이지만
조금 더 단순하게 보면
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import kobaco.backend.infra.s3.dto.response.PresignedUrlResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.net.URL;
import java.util.Date;
@Service
public class S3Service {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
public S3Service(AmazonS3 amazonS3) {
this.amazonS3 = amazonS3;
}
// 업로드 Presigned URL 생성
public PresignedUrlResponse generatePresignedUploadUrl(String key) {
Date expiration = new Date();
expiration.setTime(expiration.getTime() + 1000 * 60 * 10); // 10분 후 만료
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucketName, key)
.withMethod(HttpMethod.PUT) // PUT 요청 (업로드)
.withExpiration(expiration);
URL presignedUrl = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
return new PresignedUrlResponse(presignedUrl.toString());
}
// 다운로드 Presigned URL 생성
public PresignedUrlResponse generatePresignedDownloadUrl(String key) {
Date expiration = new Date();
expiration.setTime(expiration.getTime() + 1000 * 60 * 10); // 10분 후 만료
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucketName, key)
.withMethod(HttpMethod.GET) // GET 요청 (다운로드)
.withExpiration(expiration);
URL presignedUrl = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
return new PresignedUrlResponse(presignedUrl.toString());
}
}
이렇게도 충분히 가능합니다!
global.s3.dto.response.PresignedUrlResponse
package kobaco.backend.global.s3.dto.response;
import lombok.Builder;
@Builder
public record PresignedUrlResponse(
String name,
String url
) {}
이렇게 하면 모든 과정이 끝났습니다. 👏👏👏
5. Postman에서 테스트
그럼 잘 되는지, 어떤 방식으로 요청을 보내고 결과를 받을 수 있는지 Postman에서 테스트 해보도록 하겠습니다.
1. 업로드 PresignedUrl 생성
이렇게 key로 파일명을 확장자 포함하여 요청하면,
아래와 같이 파일명과, 파일을 업로드할 수 있는 presigned url이 반환되는 것을 확인할 수 있습니다.
2. PutFileToS3Bucket
그럼 위의 /presigned/upload에서 반환받은 PresignedUrl을 복사해서
PUT Method로, binary로 파일을 첨부하여 요청을 보냅니다.
그럼 이렇게 인스턴스에 파일이 설정한 파일명으로 잘 올라간 것을 확인할 수 있습니다!
3. 다운로드 Presigned Url로 업로드 된 파일 확인하기
/presigned/download 를 통해 업로드 된 파일을 확인할 수 있는 presigned url을 반환받는 것을 테스트해봤습니다.
다운받고자하는 파일 명을 확장자 포함해서 key로 입력하면,
아래처럼 또 파일명과 url이 반환되는 것을 확인할 수 있습니다.
이렇게 반환된 url에 들어가 보면,
이렇게 아까 업로드 한 파일을 확인해볼 수 있습니다!
개인적으로 이번 프로젝트에서 S3 Presigned Url 적용 관련해서 아쉬웠던 점은,
이미지 생성 시 AI에서 /presigned/download된 url을 사용할 때에 권한 문제가 계속 발생해서
개발 시간 상 public url로 객체에 접근할 수 있도록 버킷 설정을 수정했었습니다.
하지만, 버킷 보안 설정을 더 푸는 방식보다는 이것까지 Presigned Url로 접근할 수 있도록 권한을 더 세밀하게 조정하는 것이 더 좋지 않았을까 싶은데, 그러지 못해서 아쉽습니다. 😭
리팩토링 시 이것도 한 번 수정해봐야겠습니다! (까먹을까봐 회고에 남겨둡니다 ..ㅎㅎ)
그럼 이렇게 S3 Presigned Url Spring Boot 적용기를 마치도록 하겠습니다.
S3 Presigned Url을 처음 적용해보는 사람도 쉽게 적용해볼 수 있도록 최대한 자세하게 적으려고 했는데, 이 글이 도움이 되었으면 좋겠습니다. 🙂
피드백과 질문은 언제나 환영이니, 많이 남겨주시면 감사하겠습니다 !!
그럼 Good afternoon, good evening, and good night 입니다 🙋
읽어주셔서 감사합니다 !
'개발' 카테고리의 다른 글
[AWS/S3] S3 Presigned Url (0) | 2025.03.09 |
---|