본문 바로가기
Skill Stacks/Java_Spring

AWS S3 스프링에 적용하기 - 2

by JayAlex07 2024. 5. 27.

AWS S3란?

- Amazon Simple Storage Service로 객체 스토리지 서비스이다.

- 업계 최고의 확장성, 데이터 가용성 및 보안과 성능을 제공한다.

- 사용자가 원하는 만큼의 데이터를 저장할 수 있기에, 다양한 타입의 데이터를 저장할 수 있다.

 

AWS S3를 프로젝트에서 사용

  • 데이터를 S3에 저장할 때에, 해당 데이터에 대한 S3 URI를 제공한다.
    • 예시) s3://master-car-application/de8265e3b2984913bbe01cd53733317f_1607146775.jpg
  • 위의 뒷부분은 객체의 key라고 볼 수 있다.
  • 해당 key를 통해 프로젝트에서 S3에 저장된 이미지를 찾을 수 있다.
  • 기본 로직 (DB에 저장)
    • 클라이언트에서 MultipartFile을 통해 파일을 가지고 온다
    • 해당 파일을 AWS S3 버킷에 저장을 한다
    • AWS S3에 저장되고, 제공 되는 URI를 DB에 저장을 한다
    • DB에 저장되면, 유저들을 해당 파일을 볼 수 있게 된다
  • 기본 로직 (DB에서 삭제)
    • DB에서 URI를 꺼낸다
    • 해당 URI에서의 제일 마지막 부분 (location)에서 객체에 대한 Key를 가지고 온다
    • 해당 Key를 통해, AWS S3 버킷 내의 객체를 찾을 수 있고, 해당 객체를 삭제를 한다 (삭제를 하면, 해당 객체는 사용할 수 없게 된다)
    • DB에서 저장한 URI를 삭제한다

 

스프링부트

application.properties

# AWS S3
cloud.aws.s3.bucket={버킷이름}
cloud.aws.s3.credentials.access-key={IAM의 access key}
cloud.aws.s3.credentials.secret-key={IAM에서 access key를 받을 때 받는 키}
cloud.aws.s3.region.static={지역}
cloud.aws.s3.region.auto=false
cloud.aws.s3.stack.auto=false
  • IAM을 생성하면 Access Key를 받을 수 있다.
  • Access Key를 받으면 동시에 Secret Key를 받는데, 해당 정보는 절대로 노출되서는 안 된다.

 

build.gradle

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

 

 

S3Config

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
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.s3.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.s3.credentials.secret-key}")
    private String secretKey;

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

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

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

}

 

 

S3ImageService

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.util.IOUtils;
import com.azero.projecta.global.exception.S3Exception;
import com.azero.projecta.global.exception.errorCode.ErrorCode;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@Component
@RequiredArgsConstructor
@Slf4j
public class S3ImageService {

    private final AmazonS3 amazonS3;

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

    /**
     * 이미지 파일을 S3에 저장하기 전 파일 확장자가 제대로 된 확장자인지 확인 후 S3에 저장
     */
    private String saveImage(MultipartFile image) {

        String fileName = image.getOriginalFilename();

        String fileExtension = validateImageFileExtension(fileName);

        try {
            return saveImageToS3(image, fileName, fileExtension);
        } catch (IOException e) {
            throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_SAVE);
        }

    }

    /**
     * 파일 확장자 확인 하기 (확장자가 jpg, jpeg, png, gif 가 아니면 예외 처리)
     */
    private String validateImageFileExtension(String fileName) {

        int lastDot = fileName.lastIndexOf(".");

        if (lastDot == -1) {
            throw new S3Exception(ErrorCode.NO_FILE_EXTENSION);
        }

        String[] allowedExtensions = {"jpg", "jpeg", "png", "gif"};
        String fileExtension = fileName
            .substring(lastDot + 1)
            .toLowerCase();

        if (!Arrays.asList(allowedExtensions).contains(fileExtension)) {
            throw new S3Exception(ErrorCode.INVALID_FILE_EXTENSION);
        }

        return fileExtension;

    }

    /**
     * S3에 이미지 파일 저장하기
     */
    private String saveImageToS3(MultipartFile image,
        String originalFilename,
        String fileExtension) throws IOException {

        // S3에 저장할 파일 이름을 UUID로 생성 (중복 방지)
        String newS3FileName = UUID.randomUUID().toString().substring(0, 10)
            + "_" + originalFilename;

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

        // Amazon S3에 저장되는 파일 또는 객체와 관련된 정보 (파일의 유형, 크기, 버전 등)
        // 사용자가 정의한 추가적인 정보를 포함할 수 있음. 파일의 작성자, 설명, 태그
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType("image/" + fileExtension);
        metadata.setContentLength(bytes.length);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        try {
            // PutObjectRequest : Amazon S3에 객체를 저장하는 요청
            PutObjectRequest putObjectRequest =
                new PutObjectRequest(bucket, newS3FileName, byteArrayInputStream, metadata);

            amazonS3.putObject(putObjectRequest);

        } catch (Exception e) {
            throw new S3Exception(ErrorCode.PUT_OBJECT_ERROR);
        }

        return amazonS3.getUrl(bucket, newS3FileName).toString();
    }

    /**
     * 이미지 파일의 주소로부터 key 추출
     */
    private String getKeyFromImageAddress(String imageAddress) {

        try {
            URL url = new URL(imageAddress);
            String key = URLDecoder.decode(url.getPath(), "UTF-8");

            return key.substring(1);

        } catch (MalformedURLException | UnsupportedEncodingException e) {
            throw new S3Exception(ErrorCode.INVALID_FILE_EXTENSION);
        }
    }

    /**
     * 이미지 파일을 S3에 저장 먼저 이미지 파일이 비어있는지 확인 (비어있으면 예외 처리)
     */
    public String uploadImage(MultipartFile image) {

        if (image.isEmpty() || Objects.isNull(image.getOriginalFilename())) {
            throw new S3Exception(ErrorCode.EMPTY_FILE);
        }

        return saveImage(image);
    }

    /**
     * 이미지의 Key를 가지고 와서 S3내 객체를 삭제한다
     */
    public void deleteImage(String urlAddress) {

        String key = getKeyFromImageAddress(urlAddress);

        try {
            amazonS3.deleteObject(bucket, key);
        } catch (Exception e) {
            throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
        }

    }

}

 

 

S3ImageService (Detail하게 설명)

- 모든 S3Exception은 커스텀으로 만든 예외들이다

    /**
     * 이미지 파일을 S3에 저장 먼저 이미지 파일이 비어있는지 확인 (비어있으면 예외 처리)
     */
    public String uploadImage(MultipartFile image) {

        if (image.isEmpty() || Objects.isNull(image.getOriginalFilename())) {
            throw new S3Exception(ErrorCode.EMPTY_FILE);
        }

        return saveImage(image);
    }

 

uploadImage

  • MultipartFile로 받아온 이미지에 대해 해당 이미지 파일이 존재하는지 먼저 확인을 한다

 

    /**
     * 이미지 파일을 S3에 저장하기 전 파일 확장자가 제대로 된 확장자인지 확인 후 S3에 저장
     */
    private String saveImage(MultipartFile image) {

        String fileName = image.getOriginalFilename();

        String fileExtension = validateImageFileExtension(fileName);

        try {
            return saveImageToS3(image, fileName, fileExtension);
        } catch (IOException e) {
            throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_SAVE);
        }
    }

 

saveImage

  • MultipartFile을 통해 가져온 파일에서, 파일 이름을 가지고 온다 (fileName)
  • 해당 이미지 파일이 제대로된 파일인지 확인을 한다 (validateImageFileExtension 메서드를 통해)
    • 이미지 파일이 jpg, jpeg, png, gif가 아니면 파일을 업로드 시키지 못 하게 한다
  • 에러가 발생하지 않으면 saveImageToS3 메서드를 통해 S3에 이미지를 저장한다

 

/**
     * 파일 확장자 확인 하기 (확장자가 jpg, jpeg, png, gif 가 아니면 예외 처리)
     */
    private String validateImageFileExtension(String fileName) {

        int lastDot = fileName.lastIndexOf(".");

        if (lastDot == -1) {
            throw new S3Exception(ErrorCode.NO_FILE_EXTENSION);
        }

        String[] allowedExtensions = {"jpg", "jpeg", "png", "gif"};
        String fileExtension = fileName
            .substring(lastDot + 1)
            .toLowerCase();

        if (!Arrays.asList(allowedExtensions).contains(fileExtension)) {
            throw new S3Exception(ErrorCode.INVALID_FILE_EXTENSION);
        }

        return fileExtension;
    }

 

validateImageFileExtension

    • 파일 이름을 가지고 올때, 확장자를 확인하기 위해서는 '.'의 위치를 확인하면 된다
      • jjanggu.jpg - '.' 의 위치는 7 이다 (여기서 jpg가 확장자다)
    • '.' 이 없거나, 맨 뒤에 있을 경우 확장자가 없다는 것임으로 예외처리를 한다
    • '.' 의 위치를 통해 substring을 사용하여 확장자를 가지고 오고, jpg, jpeg, png, gif 중 하나인지 확인한다
    • 없으면 예외처리를 한다

 

/**
     * S3에 이미지 파일 저장하기
     */
    private String saveImageToS3(MultipartFile image,
        String originalFilename,
        String fileExtension) throws IOException {

        // S3에 저장할 파일 이름을 UUID로 생성 (중복 방지)
        String newS3FileName = UUID.randomUUID().toString().substring(0, 10)
            + "_" + originalFilename;

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

        // Amazon S3에 저장되는 파일 또는 객체와 관련된 정보 (파일의 유형, 크기, 버전 등)
        // 사용자가 정의한 추가적인 정보를 포함할 수 있음. 파일의 작성자, 설명, 태그
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType("image/" + fileExtension);
        metadata.setContentLength(bytes.length);

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        try {
            // PutObjectRequest : Amazon S3에 객체를 저장하는 요청
            PutObjectRequest putObjectRequest =
                new PutObjectRequest(bucket, newS3FileName, byteArrayInputStream, metadata);

            amazonS3.putObject(putObjectRequest);

        } catch (Exception e) {
            throw new S3Exception(ErrorCode.PUT_OBJECT_ERROR);
        }

        return amazonS3.getUrl(bucket, newS3FileName).toString();
    }

 

saveImageToS3

    • AWS S3 버킷에 객체를 저장할 때에, 이름이 겹치지 않도록 UUID를 사용해서 새로운 이름을 만들어준다
      • 예) 원래 jjanggu.jpg -> dk4ds3dc20_jjanggu.jpg
    • InputStream과 byte[]를 통해서 파일을 바이트 배열로 변환한다
    • ObjectMetadata : Amazon S3ㅔ 저장되는 파일 또는 객체와 관련된 정보를 저장을 하는 클래스이다
      • 파일의 유형, 크기, 버전 등
      • 사용자가 정의한 추가적인 정보를 포함할 수 있다. (파일의 유형, 크기, 버전 등)
    • PutObjectRequest : Amazon S3에 객체를 저장하는 요청이다
    • amazonS3.getUrl() : 해당 메서드를 통해, S3 저장 후, 설정된 URL을 리턴한다

 

    /**
     * 이미지의 Key를 가지고 와서 S3내 객체를 삭제한다
     */
    public void deleteImage(String urlAddress) {

        String key = getKeyFromImageAddress(urlAddress);

        try {
            amazonS3.deleteObject(bucket, key);
        } catch (Exception e) {
            throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
        }
    }

 

deleteImage

  • getKeyFromImageAddress() 메서드를 통해서 받아온 이미지 URL에서 S3에 저장된 Key를 가져온다
  • amazonS3.deleteObject() 를 통해서, 버켓 안에 있는 객체를 삭제한다

 

    /**
     * 이미지 파일의 주소로부터 key 추출
     */
    private String getKeyFromImageAddress(String imageAddress) {

        try {
            URL url = new URL(imageAddress);
            String key = URLDecoder.decode(url.getPath(), "UTF-8");

            return key.substring(1);

        } catch (MalformedURLException | UnsupportedEncodingException e) {
            throw new S3Exception(ErrorCode.INVALID_FILE_EXTENSION);
        }
    }

 

getKeyFromImageAddress

  • 이미지 URL에서 맨 뒷 부분을 가져온다 (url.getPath())
  • 뒷 부분이 AWS S3 버킷 객체의 key다

'Skill Stacks > Java_Spring' 카테고리의 다른 글

AWS S3 스프링에 적용하기 - 1  (0) 2024.05.27
스프링 회원기능  (1) 2023.11.17
[Java] 오늘 배운 것 20231025  (0) 2023.10.25
[Java] Nginx  (0) 2023.09.14
[Java] Kafka  (0) 2023.09.13