AWS S3 생성, Spring Boot와 연동

작성일

스프링 부트에서 AWS S3를 사용하기 위한 과정을 작성하였다.

과정은 다음과 같다.

  1. S3 버킷을 생성하고 AccessKey와 SecretKey를 발급받는다.
  2. 발급받은 AccessKey와 SecretKey를 통해 Spring Boot 에서 S3에 접근 가능하도록 한다.

S3 생성

스크린샷 2024-03-26 오후 4 49 51

스크린샷 2024-03-26 오후 4 50 50

스크린샷 2024-03-26 오후 4 50 56

IAM 생성

스크린샷 2024-03-26 오후 4 52 10

스크린샷 2024-03-26 오후 4 52 30

IAM AccessKey, SecretKey

IAM → 사용자 → 보안 자격 증명 → 액세스 키 만들기

스크린샷 2024-03-26 오후 5 06 22

스크린샷 2024-03-26 오후 5 07 42

스크린샷 2024-03-26 오후 5 07 56

스크린샷 2024-03-26 오후 5 08 28

발급받은 accessKey와 secretKey는 잘 저장해둔다.

이제 스프링 부트에서 S3로 접근하는 코드를 작성해보자.

Spring Boot, S3 이미지 업로드

gradle.build

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

application.yml

cloud:
  aws:
    credentials:
      accessKey: 발급받은 accessKey 값
      secretKey: 발급받은 secretKey 값
    s3:
      bucketName: s3 버킷 이름
    region:
      static: aws 지역 이름
    stack:
      auto: false

S3 Config

@Configuration
public class S3Config {

    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;
    @Value("${cloud.aws.credentials.secretKey}")
    private String secretKey;
    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3 amazonS3() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return AmazonS3ClientBuilder
                .standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region)
                .build();
    }
}

S3 Service

@Service
@RequiredArgsConstructor
@Slf4j
public class S3ImageService {

    private final AmazonS3 amazonS3;

    @Value("${cloud.aws.s3.bucketName}")
    private String bucketName;

    public String upload(MultipartFile image) {
        if (image.isEmpty() || Objects.isNull(image.getOriginalFilename())) {
            throw new PinterestException("S3 에러, 파일이 없습니다.");
        }
        return this.uploadImage(image);
    }

    private String uploadImage(MultipartFile image) {
        this.validateImageFileExtension(image.getOriginalFilename());
        try {
            return this.uploadImageToS3(image);
        } catch (IOException e) {
            throw new PinterestException("S3 에러, 이미지 업로드 시 에러가 발생하였습니다.");
        }
    }

    private void validateImageFileExtension(String filename) {
        int lastDotIndex = filename.lastIndexOf(".");
        if (lastDotIndex == -1) {
            throw new PinterestException("S3 에러, 파일 형식이 잘못되었습니다.");
        }

        String extension = filename.substring(lastDotIndex + 1).toLowerCase();
        List<String> allowedExtensionList = Arrays.asList("jpg", "jpeg", "png", "gif");

        if (!allowedExtensionList.contains(extension)) {
            throw new PinterestException("S3 에러, 파일 형식은 jpg, jpeg, png, gif 이어야 합니다.");
        }
    }

    private String uploadImageToS3(MultipartFile image) throws IOException {
        String originalFilename = image.getOriginalFilename(); //원본 파일 명
        String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명

        String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //변경된 파일 명

        InputStream is = image.getInputStream();
        byte[] bytes = IOUtils.toByteArray(is);

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType("image/" + extension);
        metadata.setContentLength(bytes.length);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        try {
            PutObjectRequest putObjectRequest =
                    new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata)
                            .withCannedAcl(CannedAccessControlList.PublicRead);
            amazonS3.putObject(putObjectRequest); // put image to S3
        } catch (Exception e) {
            throw new PinterestException("S3 파일 에러가 발생하였습니다.");
        } finally {
            byteArrayInputStream.close();
            is.close();
        }

        return amazonS3.getUrl(bucketName, s3FileName).toString();
    }

    public void deleteImage(String imageAddress) {
        String key = getKeyFromImageAddress(imageAddress);
        try {
            amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
        } catch (Exception e) {
            throw new PinterestException("S3 에러, 이미지 삭제 시 오류가 발생하였습니다.");
        }
    }

    private String getKeyFromImageAddress(String imageAddress) {
        try {
            URL url = new URL(imageAddress);
            String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
            return decodingKey.substring(1); // 맨 앞의 '/' 제거
        } catch (MalformedURLException | UnsupportedEncodingException e) {
            throw new PinterestException("S3 파일 에러가 발생하였습니다.");
        }
    }
}

S3 Controller

@Controller
@RequiredArgsConstructor
public class FileController {

    private final S3ImageService s3ImageService;

    @PostMapping("/s3/upload")
    public ResponseEntity<?> s3Upload(@RequestPart(value = "image", required = false) MultipartFile image){
        String profileImage = s3ImageService.upload(image);
        return ResponseEntity.ok(profileImage);
    }

    @GetMapping("/s3/delete")
    public ResponseEntity<?> s3delete(@RequestParam String addr){
        s3ImageService.deleteImageFromS3(addr);
        return ResponseEntity.ok(null);
    }
}