Springboot S3 업로드를 구현하는 시리즈입니다.

전체 소스 코드는 여기에서 확인할 수 있습니다.

 

 

 

AWS S3 서비스 소개

AWS S3는 Storage 서비스로서 아래와 같은 특징들이 있습니다.

  • 모든 종류의 데이터를 원하는 형식으로 저장
  • 저장할 수 있는 데이터의 전체 볼륨과 객체 수에는 제한이 없음
  • Amazon S3는 간단한 key 기반의 객체 스토리지이며, 데이터를 저장 및 검색하는데 사용할 수 있는 고유한 객체 키를 할당.
  • Amazon S3는 간편한 표준 기반 REST 웹 서비스 인터페이스를 제공
  • 요금 정책 (링크)
  • 안전하다

AWS S3 FAQ를 통해 많은 정보를 확인할 수 있습니다.

 

 


 

1. 준비 작업

구현하기에 앞서 AWS 계정이 있어야 S3를 사용할 수 있습니다.

모든 과정을 서술하기엔 내용이 길어 링크만 남기겠습니다.

    • AWS 계정의 Access key 와 Secret key
      • key에 대해 잘 모르신다면 여기를 참고해주세요.
      • IAM 계정을 생성하여 S3에 대한 role 부여 후, 진행하길 권장합니다.
    • S3 버킷 생성 ( 참고 ) 
      • 권한을 수정하지 않고 파일을 업로드하면, 퍼블릭 엑세스가 차단된 상태이기 때문에 아래의 403 에러가 발생합니다. ( 참고 )
      • Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied; Request ID:~~~; S3 Extended Request ID: ~~~)
      • 따라서 예제에서는 퍼블릭 엑세스 차단 해제를 하고, 버킷 정책을 설정하는 방법을 사용할 것입니다.
      • 버킷을 생성한 후, 권한을 수정합니다.
          • 즉, "기본적으로는 버킷의 접근을 허용하지만 정책을 통해 권한을 막겠다"입니다.
      • 1) 외부에서 파일을 접근할 수 있도록 퍼블릭 액세스 차단을 비활성화
        •  
      • 2) 버킷 정책 작성 ( 참고 )
        • {
            "Version": "2012-10-17",
            "Id": "Policy1577077078140",
            "Statement": [
              {
                "Sid": "Stmt1577076944244",
                "Effect": "Allow",
                "Principal": "*",
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::{버킷명}/*"
              }
            ]
          }
        • ex) "Resource": "arn:aws:s3:::victolee-s3-test/*"
 

 

 

 

2. 환경 셋팅

  • 개발환경
    • IntelliJ 2019.02
    • SpringBoot 2.2.4
    • Gradle 7.0.1
    • spring-cloud-aws 2.2.1
 
 
build.gradle

실습에 필요한 부가적인 의존성은 thymeleaf, lombok, JPA 정도이고,

AWS S3를 사용하기 위해 필요한 의존성은 spring-cloud-aws 입니다.

  • 참고
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'mysql:mysql-connector-java'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // AWS S3
    compile group: 'org.springframework.cloud', name: 'spring-cloud-aws', version: '2.2.1.RELEASE', ext: 'pom'

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

 

참고로 spring-cloud-aws-autoconfigure를 통해, 아래에서 작성할 application.yml으로 S3 Client를 자동으로 셋팅해주는 기능이 있는데,

AmazonS3Client 가 deprecated가 됨에 따라 AmazonS3을 사용하게 되었습니다.

그래서 해당 의존성은 사용하지 않고, service 단에서 직접 설정해주었습니다.

 

 

 

프로젝트 구조

 

 

 

 

3. AWS 설정

aws 설정은 applcation.yml 파일에 작성합니다.

 

src/main/resources/application.yml

cloud:
  aws:
    credentials:
      accessKey: YOUR_ACCESS_KEY
      secretKey: YOUR_SECRET_KEY
    s3:
      bucket: YOUR_BUCKET_NAME
    region:
      static: YOUR_REGION
    stack:
      auto: false
  • accessKey, secretKey
    • AWS 계정에 부여된 key 값을 입력합니다. ( IAM 계정 사용 권장 )
  • s3.bucket
    • S3 서비스에서 생성한 버킷 이름을 작성합니다.
  • region.static
    • S3를 서비스할 region 명을 작성합니다. ( 참고 )
    • 서울은 ap-northeast-2를 작성하면 됩니다.
  • stack.auto
    • Spring Cloud 실행 시, 서버 구성을 자동화하는 CloudFormation이 자동으로 실행되는데 이를 사용하지 않겠다는 설정입니다.
    • 해당 설정을 안해주면 아래의 에러가 발생합니다.
    • org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cloud.aws.core.env.ResourceIdResolver.BEAN_NAME': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'stackResourceRegistryFactoryBean' defined in class path resource [org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean]: Factory method 'stackResourceRegistryFactoryBean' threw exception; nested exception is java.lang.IllegalArgumentException: No valid instance id defined
 
 

application.yml 파일은 꼭 .gitignore가 되어야 합니다.

애초에 이 파일은 DB 연결정보 및 설정 관련 파일이 있으므로, 꼭 gitignore 해줘야 합니다.

개인 공부할 때 DB 정보가 외부(깃헙 등 외부 저장소)에 노출되어도 사실 아무 문제는 없지만, AWS 키가 노출되면 엄청난 과금을 맛볼수 있습니다...!

꼭 주의해주세요!!

 

.gitignore

### my ignore ###
src/main/resources/application.yml
.gitignore 하고 push를 하기 전에, git status로 application.yml 파일이 commit 목록에 없는지 꼭 확인하세요!
push 하고서도 원격 저장소에 파일이 없는지 확인합니다.

 

 

이제 설정이 끝났으므로 코드를 구현해보겠습니다.

 

 

 

 

4. 서비스 구현 - 퍼블리싱

먼저 퍼블리싱부터 해줍니다

src/main/resources/templates/gallery.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>S3 파일 업로드 테스트</title>
</head>
<body>
    <h1>파일 업로드</h1> <hr>

    <form th:action="@{/gallery}" method="post" enctype="multipart/form-data">
        제목 : <input type="text" name="title"> <br>
        파일 : <input type="file" name="file"> <br>
        <button>등록하기</button>
    </form>

</body>
</html>
  • enctype="multipart/form-data"
    • 파일 업로드이므로, multipart/form-data를 명시해줍니다.
 
 
 
5. 서비스 구현 - Controller

src/main/java/com/victolee/s3exam/controller/GalleryController.java

import com.victolee.s3exam.dto.GalleryDto;
import com.victolee.s3exam.service.GalleryService;
import com.victolee.s3exam.service.S3Service;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Controller
@AllArgsConstructor
public class GalleryController {
    private S3Service s3Service;
    private GalleryService galleryService;

    @GetMapping("/gallery")
    public String dispWrite() {

        return "/gallery";
    }

    @PostMapping("/gallery")
    public String execWrite(GalleryDto galleryDto, MultipartFile file) throws IOException {
        String imgPath = s3Service.upload(file);
        galleryDto.setFilePath(imgPath);



        galleryService.savePost(galleryDto);

        return "redirect:/gallery";
    }
}
  • execWrite(GalleryDto galleryDto, MultipartFile file) throws IOException
    • form으로부터 넘어온 파일 객체를 받기 위해, MultipartFile 타입의 파라미터를 작성해줍니다.
    • S3에 파일 업로드를 할 때 IOException이 발생할 수 있으므로 예외를 던집니다.
  • s3Service.upload(file)
    • s3Service는 AWS S3의 비즈니스 로직을 담당하며, 파일을 조작합니다.
  • galleryService.savePost(galleryDto);
    • galleryService는 DB에 데이터를 조작하기 위한 서비스입니다.

 

 

 

6. 서비스 구현 - S3Service

src/main/java/com/victolee/s3exam/service/S3Service.java

package com.victolee.s3exam.service;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import java.io.IOException;

@Service
@NoArgsConstructor
public class S3Service {
    private AmazonS3 s3Client;

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

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

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

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

    @PostConstruct
    public void setS3Client() {
        AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);

        s3Client = AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(this.region)
                .build();
    }

    public String upload(MultipartFile file) throws IOException {
        String fileName = file.getOriginalFilename();

        s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
                .withCannedAcl(CannedAccessControlList.PublicRead));
        return s3Client.getUrl(bucket, fileName).toString();
    }
}
  • AmazonS3Client가 deprecated됨에 따라, AmazonS3ClientBuilder를 사용했습니다. 
  • @Value("${cloud.aws.credentials.accessKey}")
    • lombok 패키지가 아닌, org.springframework.beans.factory.annotation 패키지임에 유의합니다.
    • 해당 값은 application.yml에서 작성한 cloud.aws.credentials.accessKey 값을 가져옵니다.
  • @PostConstruct​

    • 자격증명이란 accessKey, secretKey를 의미하는데, 의존성 주입 시점에는 @Value 어노테이션의 값이 설정되지 않아서 @PostConstruct를 사용했습니다.
    • new BasicAWSCredentials(this.accessKey, this.secretKey);
      • accessKey와 secretKey를 이용하여 자격증명 객체를 얻습니다.
    • withCredentials(new AWSStaticCredentialsProvider(credentials))
      • 자격증명을 통해 S3 Client를 가져옵니다.
    • .withRegion(this.region)​
      • region을 설정할 수도 있습니다.
      • 예제에서는 application.yml에 있는 값을 설정 했는데, Regions enum을 통해 설정할수도 있습니다.
        • ex) Regions.valueOf("AP_NORTHEAST_2")
    • 의존성 주입이 이루어진 후 초기화를 수행하는 메서드이며, bean이 한 번만 초기화 될수 있도록 해줍니다.
    • 이렇게 해주는 목적은 AmazonS3ClientBuilder를 통해 S3 Client를 가져와야 하는데, 자격증명을 해줘야 S3 Client를 가져올 수 있기 때문입니다.
  • s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)​
    • 업로드를 하기 위해 사용되는 함수입니다. ( AWS SDK 참고 )
    • .withCannedAcl(CannedAccessControlList.PublicRead));
      • 외부에 공개할 이미지이므로, 해당 파일에 public read 권한을 추가합니다.
  • s3Client.getUrl(bucket, fileName).toString()
    • 업로드를 한 후, 해당 URL을 DB에 저장할 수 있도록 컨트롤러로 URL을 반환합니다.
 
 
 
 

7. 서비스 구현 - GalleryService

src/main/java/com/victolee/s3exam/service/GalleryService.java

package com.victolee.s3exam.service;

import com.victolee.s3exam.domain.repository.GalleryRepository;
import com.victolee.s3exam.dto.GalleryDto;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
public class GalleryService {
    private GalleryRepository galleryRepository;

    public void savePost(GalleryDto galleryDto) {
        galleryRepository.save(galleryDto.toEntity());
    }
}
  • DB에 저장하는 로직입니다.

 

 

 

 

8. 서비스 구현 - GalleryRepository

src/main/java/com/victolee/s3exam/domain/repository/GalleryRepository.java

package com.victolee.s3exam.domain.repository;

import com.victolee.s3exam.domain.entity.GalleryEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface GalleryRepository extends JpaRepository<GalleryEntity, Long> {
}

 

 

 

 

9. 서비스 구현 - GalleryEntity

src/main/java/com/victolee/s3exam/domain/entity/GalleryEntity.java

package com.victolee.s3exam.domain.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "gallery")
public class GalleryEntity {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    @Column(length = 50, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT")
    private String filePath;

    @Builder
    public GalleryEntity(Long id, String title, String filePath) {
        this.id = id;
        this.title = title;
        this.filePath = filePath;
    }
}
  • 특별한 것은 없고 filePath 필드로 AWS S3에 저장된 파일 경로를 DB에 저장합니다.

 

 

 

 

10. 서비스 구현 - GalleryDto

src/main/java/com/victolee/s3exam/dto/GalleryDto.java

package com.victolee.s3exam.dto;

import com.victolee.s3exam.domain.entity.GalleryEntity;
import lombok.*;

@Getter
@Setter
@ToString
@NoArgsConstructor
public class GalleryDto {
    private Long id;
    private String title;
    private String filePath;

    public GalleryEntity toEntity(){
        GalleryEntity build = GalleryEntity.builder()
                .id(id)
                .title(title)
                .filePath(filePath)
                .build();
        return build;
    }

    @Builder
    public GalleryDto(Long id, String title, String filePath) {
        this.id = id;
        this.title = title;
        this.filePath = filePath;
    }
}

 

 

 

 

11. 테스트

 

1) 파일 업로드

 

 

2) AWS S3에서 객체 URL 확인

 

브라우저 주소창에 해당 URL을 직접 접근하여 사진 확인할 수도 있습니다.

 

 

3) 테이블에서 file_path 확인하여 객체 URL과 같은지 확인

 

 

 

 

이상으로 S3에 업로드하는 기본 방법에 대해 알아보았습니다.

다음 글에서는 CRUD를 다루면서 좀 더 세부적인 내용들을 다뤄보도록 하겠습니다.

  • 업로드 외에 조회, 수정, 삭제
  • S3에 직접 접근하는 것은 비용을 발생시키므로 CloudFront를 통한 CDN 설정
  • 파일명 중복방지
 
 

[참고 자료]

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/dev/access-control-block-public-access.html

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/dev/access-control-overview.html#about-resource-owner

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/dev/UploadObjSingleOpJava.html

https://preamtree.tistory.com/83

https://private.tistory.com/76

https://stackoverflow.com/questions/52413828/how-to-initialize-aws-sdk-in-spring-boot-application/53509255#53509255