//s3
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0'
implementation platform('software.amazon.awssdk:bom:2.27.21')
implementation 'software.amazon.awssdk:s3'
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.3.0'
AWS S3와 쉽게 연동할 수 있도록 지원하는 라이브러리이다.
버전 3.3.0을 사용하고 있으며, 이는 Spring Boot 3.x 버전과 호환된다.
`implementation platform('software.amazon.awssdk:bom:2.27.21`
`implementation 'software.amazon.awssdk:s3`
AWS SDK for Java v2의 S3 모듈을 추가 후 S3 버킷에서 파일을 업로드하거나 다운로드하는 등의 기능을 제공한다.
📌 비전공자도 이해할 수 있는 AWS 강의를 만들어봤습니다!
유튜브와 인프런 자료를 참고하여 아래 과정을 수행했다.



4-1 버킷 정책 편집 클릭

4-2 버킷 정책 편집 클릭
상단의 정책 생성기 클릭

4-3 Generate Policy 클릭 후 아래와 같이 설정
Principal
Actions
ARN

4-4 Generate Policy 클릭 후 아래와 같이 설정
복사해서 정책 부분에 넣기

🚨 위와 같이 넣었을 때 오류가 뜬다면 아래와 같이 수정해야한다.
{
"Id": "Policy1741118900163",
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1741117250033",
"Action": "s3:GetObject", //작업 종류
"Effect": "Allow",
"Resource": "arn:aws:s3:::aws-foodduck-bucket-250305/*",
"Principal": "*"
}
]
}
I AM 유저를 생성하여 해당 권한을 설정하고 엑세스 키를 통해 S3에 접근한다.





엑세스 키 만든 후 .cvs 저장

- 사용자가 이미지 업로드 API 요청 ➡️ S3에 이미지 업로드
- s3에 저장이 되면 저장된 URL을 리턴해준다.
- 해당 값을 받아서 DB에 저장된 URL을 저장한다.
전체적인 패키지 구조

이미지 업로드를 따로 빼서 관리한다면 이미지 업로드와 책 정보 업데이트가 명확하게 분리되어 유지보수나 확장성 측면에서 좋다.
🚨 이미지 업로드를 분리하지 않을 경우 S3가 아닌 다른 저장소를 사용하게 되었을 때 해당 코드를 일일히 수정해야하는 문제가 있다.
S3Config
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String awsAccessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String awsSecretKey;
@Bean
public S3Client s3Client() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(awsAccessKey, awsSecretKey);
return S3Client.builder()
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.AP_NORTHEAST_2)
.build();
}
}
MultipartFile 업로드 방식을 선택하였다.
➡️ 아래의 Reference 자료를 참고하고 해당 방식을 선택하였지만, 추후 직접 각 방식을 비교하는 시간을 가져야할 것 같다.
아래와 같은 과정을 거쳐 Uploading을 하여 S3에 이미지를 등록한다.
🟢ImageController
public class ImageController {
private final S3ImageService s3ImageService;
@Operation(summary = "이미지 업로드", description = "Multipart 형식으로 입력받은 이미지를 처리하여 S3에 업로드하는 API 입니다.")
@PostMapping("/images")
public Response<ImageResponse> uploadImage(
@RequestPart ("image") MultipartFile imageFile
) {
return Response.of(s3ImageService.uploadImage(imageFile));
}
}
🟢 ImageService
@Override
public ImageResponse uploadImage(MultipartFile image) {
if (image.isEmpty()) {
throw new IllegalArgumentException(NOT_FOUND_IMAGE.getMessage());
}
//고유의 UUID 생성
String imageName = UUID.randomUUID() + "_" + image.getOriginalFilename();
try {
//S3 업로드
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(imageName)
.contentType(image.getContentType())
.contentLength(image.getSize())
.build();
PutObjectResponse response = s3Client.putObject(
putObjectRequest,
RequestBody.fromBytes(image.getBytes())
);
if (response.sdkHttpResponse().isSuccessful()) {
String imageUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, imageName);
String fileName = extractFileName(imageUrl);
String extension = extractExtension(fileName);
return ImageResponse.of(imageUrl, fileName, extension);
} else {
throw new RuntimeException(FAILED_UPLOAD_IMAGE.getMessage());
}
} catch (IOException e) {
throw new RuntimeException(FAILED_UPLOAD_IMAGE.getMessage(), e);
}
}
private String extractFileName(String imageUrl) {
return imageUrl.substring(imageUrl.lastIndexOf("/") + 1);
}
private String extractExtension(String fileName) {
String extension = "";
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex != -1 && dotIndex < fileName.length() - 1) {
extension = fileName.substring(dotIndex + 1).toLowerCase();
}
return extension;
}
🟠 POSTMAN

🔴 결과


S3에 이미지 저장 후 응답된 imageUrl, fileName, extension 값을 입력으로 넣어준다.
🟢 BookImageContoller
@Operation(summary = "책 이미지 업로드", description = "S3에 올라간 책에 대한 이미지를 DB에 업로드하는 API입니다.")
@Secured(ADMIN)
@PostMapping("/{bookId}/image")
public Response<Void> uploadBookImage(
@AuthenticationPrincipal AuthUser authUser,
@PathVariable Long bookId,
@ModelAttribute ImageRequest imageRequest
) {
bookService.uploadBookImage(authUser, bookId, imageRequest);
return Response.empty();
}
🟢 BookService
@Transactional
public void uploadBookImage(AuthUser authUser, Long bookId, ImageRequest imageRequest) {
Book book = findBookByIdOrElseThrow(bookId);
if (!book.getUsers().getId().equals(authUser.getUserId())) {
throw new ForbiddenException(CANNOT_UPLOAD_OTHERS_BOOK_IMAGE.getMessage());
}
BookImage bookImage = BookImage.of(book, imageRequest.imageUrl(), imageRequest.fileName(), imageRequest.extension());
// 등록된 이미지의 개수가 5개를 넘는 경우
if (imageBookRepository.countByBookId(bookImage.getBook().getId()) >= 5) {
throw new BadRequestException(IMAGE_UPLOAD_LIMIT_OVER.getMessage());
}
imageBookRepository.save(bookImage);
}
🟠 POSTMAN

🟢 ImageService
@Override
public void deleteImage(String imageUrl) {
try {
s3Client.deleteObject(
DeleteObjectRequest.builder()
.bucket(bucket)
.key(imageUrl)
.build()
);
} catch (S3Exception e) {
throw new RuntimeException(FAILED_DELETE_IMAGE.getMessage(), e);
}
}
🟢 BookService
@Transactional
public void deleteBookImage(AuthUser authUser, Long imageId) {
//이미지가 존재하지 않는 경우
BookImage bookImage = findBookImage(imageId);
//자신이 등록한 책 이미지가 아닌 경우
if (!authUser.getUserId().equals(bookImage.getBook().getUsers().getId())) {
throw new ForbiddenException(CANNOT_DELETE_OTHERS_IMAGE.getMessage());
}
s3ImageService.deleteImage(bookImage.getFileName()); //S3에서 이미지 삭제
imageBookRepository.delete(bookImage); // DB에서 이미지 삭제
}
Spring Boot에서 S3에 파일을 업로드하는 세 가지 방법
[AWS 실습 프로젝트] 3. Spring Boot와 S3 연동해서 이미지 업로드하기