Springboot S3 업로드를 구현하는 시리즈입니다.
- [SpringBoot] AWS S3 연동 (1) - 파일 업로드 기본 (AmazonS3ClientBuilder)
- [SpringBoot] AWS S3 연동 (2) - 파일 조작 및 Cloud Front
전체 소스 코드는 여기에서 확인할 수 있습니다.
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
실습에 필요한 부가적인 의존성은 thymeleaf, lombok, JPA 정도이고,
AWS S3를 사용하기 위해 필요한 의존성은 spring-cloud-aws 입니다.
- 참고
- JPA 설정 ( 링크 )
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
이제 설정이 끝났으므로 코드를 구현해보겠습니다.
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를 명시해줍니다.
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/UploadObjSingleOpJava.html
https://preamtree.tistory.com/83