[Spring] AWS S3 Presigned URL 활용한 파일 업로드/다운로드 (2) - Spring Boot Presigned URL 설정
해당 프로젝트의 전체소스는 여기 에서 확인하실 수 있습니다.
해당 포스팅에서는 유저 프로필 이미지 업로드/다운로드를 예시로 진행합니다.
Spring Boot Presigned URL 설정
Spring Boot 3.3.3 버전을 기준으로 작성되었습니다.
의존성 버전은 Spring Cloud AWS GitHub Link에서 확인할 수 있습니다.
의존성 추가 및 환경변수 설정
build.gradle
1
2
3
4
5
dependencies {
// Spring Cloud AWS
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.2.1") // BOM
implementation "io.awspring.cloud:spring-cloud-aws-starter-s3"
}
application.yml
S3AutoConfiguration이 자동으로Bean으로 등록되고,
해당 환경 변수들을 기반으로S3Client및S3Presigner가 생성되기 때문에
정확한 경로의 환경 변수 설정이 중요합니다.
1
2
3
4
5
6
7
8
# 공통
spring:
cloud:
aws:
region:
static: ap-northeast-2
s3:
bucket: <YOUR_BUCKET_NAME>
1
2
3
# application-local.yml
storage:
prefix: local
1
2
3
# application-prod.yml
storage:
prefix: prod
Presigned URL 기능 구현
FileDomain
클라이언트 요청 데이터 중 FileDomain 타입을 받는데,
S3 Bucket 경로를 제한하기 위해 ENUM으로 정의했습니다.
로컬 기준 /local/profile/* 경로에서 profile에 해당하는 부분입니다.
1
2
3
4
5
6
7
public enum FileDomain {
PROFILE;
public String getDirectory() {
return this.name().toLowerCase();
}
}
Request DTO
업로드용 Request DTO
1
2
3
4
5
6
7
@Getter
@NoArgsConstructor
public class PresignedPutUrlRequest {
private FileDomain domain; // 파일 도메인 경로
private String filename; // 파일명
}
다운로드용 Request DTO
1
2
3
4
5
6
@Getter
@NoArgsConstructor
public class PresignedGetUrlRequest {
private String fileKey; // 파일 전체 경로
}
Response DTO
업로드용 Response DTO
1
2
3
4
5
6
7
@Getter
@Builder
public class PresignedPutUrlResponse {
private final String fileKey; // 파일 전체 경로
private final String presignedUrl; // Presigned URL
}
다운로드용 Response DTO
1
2
3
4
5
6
7
@Getter
@Builder
public class PresignedGetUrlResponse {
private final String fileKey; // 파일 전체 경로
private final String presignedUrl; // Presigned URL
}
Controller
Presigned URL을 발급해 줄 API 엔드포인트를 생성합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController
@RequestMapping("/files")
@RequiredArgsConstructor
public class FileController {
private final FileService fileService;
@PostMapping("/presigned/upload")
public PresignedPutUrlResponse createPresignedPutUrl(
@AuthenticationPrincipal AuthUser authUser,
@RequestBody PresignedPutUrlRequest request
) {
return fileService.createPresignedPutUrl(
authUser,
request.getDomain(),
request.getFilename()
);
}
@PostMapping("/presigned/download")
public PresignedGetUrlResponse createPresignedGetUrl(
@RequestBody PresignedGetUrlRequest request
) {
return fileService.createPresignedGetUrl(request.getFileKey());
}
}
Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
@Slf4j
@Service
@RequiredArgsConstructor
public class FileService {
private final S3Presigner s3Presigner;
private final S3Client s3Client;
@Value("${spring.cloud.aws.s3.bucket}")
private String bucket;
@Value("${storage.prefix}")
private String prefix;
// === 업로드용 Start ===
public PresignedPutUrlResponse createPresignedPutUrl(
AuthUser authUser,
FileDomain domain,
String fileName
) {
String fileKey = String.format("%s/%s/%d/%s/%s",
prefix, // `local` or `prod`
domain.getDirectory(), // FileDomain
authUser.getId(), // 로그인된 유저의 id
UUID.randomUUID(), // 충돌 방지 및 보안성 강화
fileName // 파일명
);
String presignedUrl = generatePresignedPutUrl(fileKey);
return PresignedPutUrlResponse.builder()
.presignedUrl(presignedUrl)
.fileKey(fileKey)
.build();
}
private String generatePresignedPutUrl(String fileKey) {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(fileKey)
.build();
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10)) // Presigned URL 유효기간
.putObjectRequest(putObjectRequest) // 요청 정보 (Put)
.build();
// AWS 서버에 요청하여 지정 경로(fileKey)에 대한 Presigned URL 생성
return s3Presigner.presignPutObject(presignRequest).url().toString();
}
// === 업로드용 End ===
// === 다운로드용 Start ===
public PresignedGetUrlResponse createPresignedGetUrl(String fileKey) {
String presignedUrl = generatePresignedGetUrl(fileKey);
return PresignedGetUrlResponse.builder()
.presignedUrl(presignedUrl)
.fileKey(fileKey)
.build();
}
// 재사용을 위한 public 메서드
public String getPresignedUrl(String fileKey) {
return generatePresignedGetUrl(fileKey);
}
private String generatePresignedGetUrl(String fileKey) {
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key(fileKey)
.build();
GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
.getObjectRequest(getObjectRequest)
.build();
return s3Presigner.presignGetObject(presignRequest).url().toString();
}
// === 다운로드용 End ===
// === 삭제용 Start ===
public void deleteFile(String fileKey) {
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
.bucket(bucket)
.key(fileKey)
.build();
// 삭제에 성공/실패에 대한 로그를 남기고, 예외를 전파하여 트랜잭션 롤백 처리
try {
s3Client.deleteObject(deleteObjectRequest);
log.info("Successfully deleted file from S3: {}", fileKey);
} catch (SdkException e) {
log.error("Failed to delete file from S3. fileKey: {}", fileKey, e);
throw e;
}
}
// === 삭제용 End ===
}
fileKey 구조
{prefix}/{domain}/{userId}/{uuid}/{filename}
prefix:localorprod(환경별 구분)domain:FileDomainenum 값 (e.g.profile)userId: 로그인된 유저의 고유 iduuid: 고유 식별자 (충돌 방지 및 보안성 강화)filename: 실제 파일명
업로드/다운로드 테스트
Postman을 사용하여 업로드/다운로드 API를 테스트합니다.
업로드 테스트
Presigned URL 발급 요청
S3에 직접 파일 업로드
S3 콘솔에서 업로드된 파일 확인
지정한 시간(10분) 후 Access Denied 응답 확인
다운로드 테스트
Presigned URL 발급 요청
URL을 통해 접속
지정한 시간(10분) 후 Access Denied 응답 확인
다음 포스팅
이번 포스팅에서는 Spring Boot Presigned URL 설정 과정을 정리했습니다.
다음 포스팅에서는 Spring Boot 유저 프로필 이미지 과정을 다룹니다.






