티스토리 뷰
aws의 S3 서비스
S3는 Simple Storage Service의 약자로 확장성, 데이터 가용성, 보안 및 성능을 제공하는 객체 스토리지 서비스이다.
Spring으로 이미지 업로드 하는 방법
0. 준비 작업
spring으로 이미지 업로드를 구현하기 전에 aws에서의 준비 작업이 필요하다.
0-1. 버킷생성
S3 서비스로 들어가서 주황색 버튼인 버킷 만들기를 눌러 버킷을 하나 생성해준다.
퍼블릭 액세스가 차단되어 있으면 403에러가 발생할 수 있기 때문에 만들 때 아래와 같이 [모든 퍼블릭 액세스 차단] 사항을 해제해 주고 생성한 후에 버킷 정책을 설정하는 방법을 사용한다. 이를 통해 기본적으로는 버킷의 접근을 허용하지만 정책을 통해 권한을 막는다.
버킷 정책을 만들기 위해, 생성한 버킷의 '권한' 탭에 들어간 후 버킷 정책에 아래의 내용을 작성해준다.
{
"Version": "2012-10-17",
"Id": "Policy1577077078140",
"Statement": [
{
"Sid": "Stmt1577076944244",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::{버킷명}/*"
}
]
}
0-2. 사용자 및 키 생성
위의 보안자격 증명에 들어가서 사용자 - 사용자 추가 를 클릭한다.
사용자 이름을 입력하고 액세스 키를 선택한다.
AmazonS3FullAccess를 검색하여 해당 정책을 선택한다.
3단계는 건너뛰고 4단계에서 만든 사용자를 확인한다.
사용자 생성을 완료하면 액세스 키 ID와 비밀 엑세스 키를 확인할 수 있는데, 후에 구현할 때 필요하므로 보관해놓는다. (비밀 액세스 키는 생성 직후가 아니면 볼 수 없으므로 잘 보관해야 한다)
1. 환경 세팅
- IntelliJ IDEA 2021.3.2
- SpringBoot 2.6.4
- Gradle 7.4.1
- spring-cloud-starter-aws 2.2.6
1-1. 의존성 주입
S3를 사용하기 위해 build.gradle에 spring-cloud-starter-aws 의존성을 추가한다.
dependencies {
...
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}
1-2. application-aws.properties 을 통한 aws 설정
cloud.aws.s3.bucket={버킷명}
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false
cloud.aws.credentials.accessKey={access key}
cloud.aws.credentials.secretKey={secret key}
application-aws.properties 파일을 만들어 위의 내용을 적어준다. region은 서울일 경우 ap-northeast-2 를 적어주고, accessKey와 secretKey는 위에서 사용자를 생성할 때 발급받은 키를 이용한다.
* 해당 파일은 설정과 관련된 파일이기 때문에 꼭 .gitignore에 추가해야한다.
1-3. ec2가 아닌 환경에서 사용할 경우 설정 추가
logging.level.com.amazonaws.util.EC2MetadataUtils=error
S3 이미지 업로드를 구현하면서 수많은 에러를 만났는데, 그 중 하나가 ec2환경이 아닌 곳(로컬)에서 spring-cloud-starter-aws 의존성을 주입하여 발생한
com.amazonaws.SdkClientException: Failed to connect to service endpoint: ...
에러였다. 치명적인 에러는 아니라고 하지만 application.properties에 해당 코드를 추가해준 후 에러를 해결하면 좋을 것 같다.
2. 코드 구현
2-1. UploadDto
@Data
public class UploadPostDto {
private double longitude;
private double latitude;
private String location;
private String content;
public Post toPost() {
Post post = new Post();
post.setLongitude(longitude);
post.setLatitude(latitude);
post.setLocation(location);
post.setContent(content);
return post;
}
}
이미지 외의 입력할 정보 객체 UploadDto를 만든다. 게시물에 위치정보와 내용을 함께 담기 위해 위와 같은 코드를 사용했다.
2-2. controller
@PostMapping("/posts/write")
public ResponseMessage uploadPost(@RequestPart(value = "uploadDto") UploadPostDto uploadPostDto, @RequestParam("imageFileList") List<MultipartFile> imageFileList) throws IOException {
postService.postPost(uploadPostDto, imageFileList);
return;
}
미리 만들어둔 UploadDto와 S3에 저장할 이미지 리스트를 받는다. @RequestParam 다음 괄호안에 오는 imageFileList는 서버에 데이터를 전달할 때 변수 명이라고 생각하면 된다. 이미지 여러개 업로드하는 것을 가능하게 하기 위해 List로 받는다. 이후 request에서 받은 uploadDto와 imageFileList를 service로 넘겨준다.
MultipartFile은 spring프레임워크의 인터페이스로 multipart 요청에서 받은 업로드 파일의 표현이다. 파일 내용은 메모리에 저장되거나 일시적으로 디스크에 저장되며, 임시 저장소는 요청 처리가 끝나면 지워진다.
2-3. service
postPost()
@Transactional
public void postPost(UploadPostDto dto, List<MultipartFile> imageFileList) throws IOException {
List<String> urlList = uploadImage(imageFileList);
Post post = dto.toPost();
postRepository.postPost(post);
Long postIdx = post.getPostIdx(); //디비에 저장한 post의 인덱스를 가져옴
if (postIdx == null) {
throw new Error();
}
for (String imgPath:urlList) {
PostImage postImage = new PostImage();
postImage.setPostIdx(postIdx); // 사진테이블에 외래키인 postIdx를 저장하기 위해 set
postImage.setImage(imgPath);
postRepository.postImage(postImage);
Long postImageIdx = postImage.getPostIdx();
if (postImageIdx == null) {
throw new Error();
}
}
return;
}
uploadImage함수에서 S3에 이미지를 올리고, 올린 이미지의 url을 받아온다. 이후의 코드들은 게시물 정보와 이미지 url을 디비에 저장하기 위한 부분이다.
나는 디비에서 게시물 사진 테이블이 게시물 테이블을 참조하고 있기 때문에 게시물 정보(uploadPostDto)를 먼저 디비에 삽입한 후 postIdx를 가져와서 이미지 url을 삽입할 때 같이 넣어주었다. 이부분과 이후 repository는 각자의 로직에 맞추어 구현하면 된다.
만약 디비에 접근할 일이 없다면 해당 메소드는 만들지 않아도 된다.
setS3Client()
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();
}
- @Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
application-aws.properties에서 적었던 정보들을 가져온다. - @PostConstruct
자격증명은 accessKey, secretKey를 의미하는데, 의존성 주입 시점에는 @Value 어노테이션 값이 설정되지 않기때문에 해당 어노테이션 사용
의존성 주입이 이루어진 후 초기화를 수행하는 메서드이며 bean이 한번만 초기화 될 수 있도록 해준다. - new BasicAWSCredentials(this.accessKey, this.secretKey;
accessKey와 secretkey를 이용하여 자격증명 객체를 얻는다. - .withCredentials(new AWSStaticCredentialsProvider(credentials))
자격증명을 통해 S3 Client를 가져온다. - .withRegion(this.region)
region(지역)을 설정한다.
uploadImage()
public List<String> uploadImage(List<MultipartFile> imageFileList) throws IOException {
List<String> urlList = new ArrayList<>();
for (MultipartFile file:imageFileList) {
String fileName = createFileName(file.getOriginalFilename());
s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
.withCannedAcl(CannedAccessControlList.PublicRead));
String s = s3Client.getUrl(bucket, fileName).toString();
urlList.add(s);
}
return urlList;
}
- String fileName = createFileName(file.getOriginalFilename());
S3에 같은 이름의 파일이 들어가면 안되므로 createFileName메소드를 이용해 file이름을 랜덤으로 생성한다. - s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
업로드하기 위해 사용되는 함수이다. bucket은 위의 @Value("${cloud.aws.s3.bucket}")를 통해 설정한 버킷명이다. - String s = s3Client.getUrl(bucket, fileName).toString();
S3에 업로드 후에 해당 url을 디비에 저장하기 위해 get한다.
createFileName()
private String createFileName(String originalFileName) { // 파일 이름 랜덤 생성
return UUID.randomUUID().toString().concat(getFileExtension(originalFileName)); //랜덤으로 생성한 파일명에 원래의 확장자 concat
}
getFileExtension()
private String getFileExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf("."));
}
fileName.lastIndexOf(".")로 "."의 인덱스를 구하고 fileName.substring()를 통해 "."부터 마지막 인덱스까지의 값을 return 한다.
즉 확장자를 구하는 함수이다.
3. 잘 구현됐는지 PostMan으로 확인
Body에서 form-data를 선택한 후 KEY에는 설정했던 이름인 uploadDto, imageFileList를 적는다.
uploadDto는 text를 선택한 후 VALUE에 json형식으로 입력하고, imageFileList는 file로 선택한 후 VALUE 파일을 넣어준다. 이미지를 여러개 넣는다면 똑같이 KEY와 VALUE를 추가해주면 된다.
요청을 완료하면 S3에 이미지가 잘 업로드되는 것을 볼 수 있다.
느낀 점
나의 경우 해당 서비스를 구현할 때 [S3에 한 개의 이미지 업로드] -> [S3에 이미지를 업로드함과 동시에 업로드한 이미지 url, 관련 정보들 디비에 저장하는 로직 구현] -> [여러개의 이미지 업로드, 이미지 이름 랜덤 생성 구현] 순으로 단계를 나누어 구현했다. 나눠가면서 코드를 짜니까 무작정 개발하는 것보다 비교적 수월하게 구현할 수 있었다.
해당 서비스를 구현하면서 수많은 블로그를 봤는데, 나와 다른 디렉토리 구조, 다른 개발환경을 가지고 있었기 때문에 과정 하나하나를 이해하며 개발해야 했다. 덕분에 내 로직에 맞춰 잘 구현할 수 있었고, 프론트 단으로부터 파일을 어떻게 받아오는지, s3 사용법 등을 알 수 있었다.
결국 구현에 성공해서 S3에 이미지가 제대로 올라간 것을 봤을 때 그렇게 기쁠 수가 없었다. 역시 고군분투한 끝에 성공하는 것 만큼 기쁜 일도 없는 것 같다.
'server' 카테고리의 다른 글
[server] 서버구축 하는법 (0) | 2021.10.13 |
---|---|
[server] AWS 서버구축 4 - subdomain적용, redirection 적용 (0) | 2021.07.13 |
[server] AWS 서버구축 3 - domain적용, https(SSL) 적용(let'sencrypt 사용) (0) | 2021.07.12 |
[server] AWS 서버구축 2 - MySQL 외부 접속, phpmyadmin 설치 (0) | 2021.07.12 |
[server] AWS 서버구축 1 - Nginx, PHP, MySQL 설치 + WinSCP설치 (0) | 2021.07.12 |