본문 바로가기
IT 이야기/Spring

[Spring] AWS S3를 연동하기

by Dblog 2022. 1. 3.
728x90

spring boot에 AWS S3를 연동하면서 발생했던 버그들과 그 해결방법에 대한 리뷰를 남깁니다.

토이프로젝트나 실제 클라우드 서버에 웹앱을 배포해서 사용하다보면 이미지나 다른 파일을 관리하는데 어려움을 겪을때가 있습니다.
보통 무료 버전 혹은 적은 비용의 서버를 대여받아 사용하기 때문에 적은 용량에 이미지나 파일을 저장해서 사용하는데 굉장히 제약이 많습니다. 그렇다고 큰 용량의 서버를 사용하기엔 비용이 생각보다 합리적이지 않습니다.

합리적인 파일서버를 찾으면서 구글드라이브 활용방법, 윈도우, 등 여러 파일서버가 있는걸 봤는데 개발할때 정보의 차이라던지 무료버전 지원등의 항목에서 AWS S3를 사용하는게 좋겠다는 생각을 했습니다.

S3 buket으 생성과 권한설정은 인터넷을 보면서 따라했고 아래는 제 설정입니다.

퍼블릭 엑세스 차단은 비활성화 상태로 버킷이 퍼블릭 상태가 됩니다.

버킷 정책, Put과 Get을 허용시켜줬습니다.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::boxtreefiles/*"
        }
    ]
}

 

 

개발 환경

Spring boot 4.0.0 프레임워크 사용중이며 코드의 경우 Tistory jojoldu 이동욱님의 블로그를 참고하였습니다.

https://jojoldu.tistory.com/300

 

SpringBoot & AWS S3 연동하기

안녕하세요? 이번 시간엔 SpringBoot & AWS S3 연동하기 예제를 진행해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을 정리하는 Github와

jojoldu.tistory.com

 

Maven
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-aws</artifactId>
  <version>2.0.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-aws-context</artifactId>
  <version>1.2.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-aws-autoconfigure</artifactId>
  <version>1.2.1.RELEASE</version>
</dependency>

gradle이 아닌 Maven을 사용하고 있어서 maven 으로 dependency를 install받았는데 autoconfigure, context는 따로 없어도 정상적으로 동작할 것 같습니다.

 

S3Upload.java
@Slf4j
@RequiredArgsConstructor
@Component
public class S3Upload {
    private final AmazonS3Client amazonS3Client;

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

    public String upload(MultipartFile file, String dirName) throws IOException {
        File uploadFile = convert(file).orElseThrow(() -> new IllegalArgumentException("file 전달에 실패했습니다."));
        File news= new File(System.getProperty("user.dir") + "/" + UUID.randomUUID() + ".jpg");
        uploadFile.renameTo(news);
        return upload(news, dirName);
    }

    public String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + uploadFile.getName();
        String uploadImageURI = putS3(uploadFile, fileName);
        removeNewFile(uploadFile);
        return uploadImageURI;
    }

    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    public StatusCode deleteFile(String fileName) {
        DeleteObjectRequest deleteObjectRequest = new DeleteObjectRequest(bucket, "static/"+fileName);
        amazonS3Client.deleteObject(deleteObjectRequest);
        return StatusCode.SUCCESS;
    }

    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("파일 삭제 완료");
        }
        else {
            log.info("파일 삭제 실패");
        }
    }

    private Optional<File> convert(MultipartFile file) throws IOException {
        File convertFile = new File(System.getProperty("user.dir") + "/" + file.getOriginalFilename());
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)){
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }
        return Optional.empty();
    }

}

@Value는 Lombok Annotation이 아닌 org.springframework.beans.factory.annotation의 Value입니다.

Flow는 upload(MultipartFile file, String dirName)로 MultipartFile 파일이 들어오면 먼저 File로 Class를 convert하게 됩니다. (Amazone S3가 MultipartFile을 지원하지 않고 File을 지원합니다)

AWS S3가 같은 파일명으로 업로드를 시도할때 중복에 대해 알아서 처리해 주지 않기때문에 UUID를 매번 random값으로 생성해서 파일이름을 설정했습니다. (같은 파일도 랜덤으로 생성된 다른 이름으로 업로드가 가능)

이때 주의할 점은 MultipartFile을 읽어서 로컬저장소에 임시로 저장했다가 저장된 파일의 이름을 바꿔서 S3에 전달하는 방법을 사용하기 때문에 로컬 저장소의 절대경로를 알아야하고 꼭 파일명 끝에 확장자를 명시해 주어야 합니다. 

이후에 로컬 저장소에 파일을 putS3()를 활용해 업로드 하게 되는데 정상적으로 업로드에 성공하면 https://의 URI를 받을 수 있습니다.

추가적으로 제 경우는 중복 요청의 경우 과거에 올렸던 파일을 삭제해야 하기때문에 deleteFile()를 사용해서 삭제하는데 S3에 요청할때는 버킷 이름과 key에 해당하는 파일명을 파라미터로 요청을 보내면 원격 저장소의 파일이 삭제됩니다.

application.properties
cloud.aws.s3.bucket=버킷 이름
cloud.aws.region.static=ap-northeast-2
cloud.aws.credentials.accessKey=ACCESS KEY
cloud.aws.credentials.secretKey=SECRETKEY
cloud.aws.credentials.instanceProfile=true
cloud.aws.stack.auto=false
cloud.aws.s3.bucket.url=https://s3.ap-northeast-2.amazonaws.com/{버킷 이름}
spring.servlet.multipart.maxFileSize=100MB
spring.servlet.multipart.maxRequestSize=100MB

access key, 혹은 secret key의 경우 위에 링크 되어있는 jojoldu 이동욱님의 블로그에 정확하게 설명이 되어있어 참고하시면 좋을 것 같습니다. 

credentials.instanceProfile=true는 secret key, access key를 사용한다는 의미이며 설정하지 않으면 access denied가 발생하게 됩니다.
저는 추가로 파일 업로드에 100MB까지 할 수 있도록 허용해 주었습니다.

 

테스트는 postman을 사용해서 테스트 했고 api만 만들어본 토이프로젝트여서 view는 따로 만들지 않았습니다.

 

728x90

'IT 이야기 > Spring' 카테고리의 다른 글

[MSA] MSA 스터디 1장 Eureka  (0) 2022.11.18
[JPA] @Inheritance 테이블 상속  (0) 2022.03.15

댓글