AWS S3에 이미지 업로드 및 삭제

늘보·2025년 4월 10일

Spring

목록 보기
21/24

⚙️ 이미지 저장 및 삭제를 위한 기본 설정


1️⃣ 의존성 추가하기

//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 강의를 만들어봤습니다!

유튜브와 인프런 자료를 참고하여 아래 과정을 수행했다.


2️⃣ 버킷 생성하기

1. 버킷 만들기 클릭

2. 버킷 이름 작성

3. 엑세스 차단 풀기

4. 만들어진 버킷의 정책 설정하기

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": "*"
    }
  ]
}

2️⃣ I AM 유저 생성하기

I AM 유저를 생성하여 해당 권한을 설정하고 엑세스 키를 통해 S3에 접근한다.

  1. 사용자 생성 클릭

  1. 사용자 생성을 누른 후 비밀번호 설정하기

  1. 권한 추가

  1. 생성한 사용자로 재로그인

  1. 보안 자격 증명에서 엑세스 키 만들기 클릭

엑세스 키 만든 후 .cvs 저장


🔎 이미지 파일 업로드 추가 및 생성 흐름

📤 이미지 파일 업로드 및 삭제 과정

  1. 사용자가 이미지 업로드 API 요청 ➡️ S3에 이미지 업로드
  1. s3에 저장이 되면 저장된 URL을 리턴해준다.
  1. 해당 값을 받아서 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();
    }
}


파일 업로드 방법 3가지

MultipartFile 업로드 방식을 선택하였다.

➡️ 아래의 Reference 자료를 참고하고 해당 방식을 선택하였지만, 추후 직접 각 방식을 비교하는 시간을 가져야할 것 같다.

아래와 같은 과정을 거쳐 Uploading을 하여 S3에 이미지를 등록한다.

이미지 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에 업로드된 이미지 DB에 저장

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


S3와 DB 모두 삭제

🟢 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에서 이미지 삭제 
    }

📌 Reference

Spring Boot에서 S3에 파일을 업로드하는 세 가지 방법

[AWS 실습 프로젝트] 3. Spring Boot와 S3 연동해서 이미지 업로드하기

Spring Boot로 S3 이미지 업로드 기능 구현하기 (MultipartFile 업로드)

profile
누워만 있지 말고 제발 뭐라도 하자.

0개의 댓글