이전 글에서 S3 bucket과 IAM을 생성하고 SpringBoot project에서 S3 접근에 사용할 accessKey와 secretKey를 얻는 것까지 다뤘다.
2024.01.21 - [DevOps] - [AWS/S3] Spring boot project 이미지 업로드를 위해 S3 버켓 만들기
S3에 이미지 업로드 개념
SpringBoot Project에서 클라이언트에게 MultipartFile로 이미지 파일을 받는다.
이를 S3에 업로드하고, 이 S3에서 어디서나 접근할 수 있는 public url을 반환해 준다.
이 url을 통해 이미지에 어디서나 접근하고 다운로드할 수 있게 된다.
이 url을 DB에 저장하여 필요할 때 이 url로 이미지 데이터를 사용하는 것이다.
Spring Boot - S3 파일 업로드
build.gradle implementation
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
application.properties 작성
맨 위에서 언급한 accessKey와 secretKey를 application.properties에 등록해줘야 한다.
그런데, 이 Key는 절대 외부에 노출되서는 안된다. !!!!!
이 Key를 이용하여 누군가 악용하면 AWS 계정에 비용이 청구될 수 있다.
따라서 application.properties 파일에 아래 코드를 추가해 주고, 따로 application-private.properties 파일을 만들어서. gitignore에 등록하여 github에 올라가지 않도록 해준다.
spring.profiles.include=private
application-private.properties
# S3
cloud.aws.credentials.accessKey={accessKey}
cloud.aws.credentials.secretKey={secretKey}
cloud.aws.s3.bucketName={bucketName}
cloud.aws.region.static={region}
cloud.aws.stack.auto-=false
S3 Config
properties 파일에 등록한 Key와 region 값을 @Value 어노테이션을 이용해서 사용한다.
AmazonS3 객체를 생성하여 @Bean으로 등록해 준다.
이후 AmazonS3 빈을 주입받아서 사용하는 S3 ImageService를 만들어서 사용할 것이다.
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 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.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 amazonS3() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
code - image upload
S3 ImageService
(열기) 전체 코드
전체 코드
package com.blog.som.global.s3;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.util.IOUtils;
import com.blog.som.global.exception.ErrorCode;
import com.blog.som.global.exception.custom.S3Exception;
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.List;
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;
@Slf4j
@RequiredArgsConstructor
@Component
public class S3ImageService {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucketName}")
private String bucketName;
public String upload(MultipartFile image) {
if(image.isEmpty() || Objects.isNull(image.getOriginalFilename())){
throw new S3Exception(ErrorCode.EMPTY_FILE_EXCEPTION);
}
return this.uploadImage(image);
}
private String uploadImage(MultipartFile image) {
this.validateImageFileExtention(image.getOriginalFilename());
try {
return this.uploadImageToS3(image);
} catch (IOException e) {
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_UPLOAD);
}
}
private void validateImageFileExtention(String filename) {
int lastDotIndex = filename.lastIndexOf(".");
if (lastDotIndex == -1) {
throw new S3Exception(ErrorCode.NO_FILE_EXTENTION);
}
String extention = filename.substring(lastDotIndex + 1).toLowerCase();
List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif");
if (!allowedExtentionList.contains(extention)) {
throw new S3Exception(ErrorCode.INVALID_FILE_EXTENTION);
}
}
private String uploadImageToS3(MultipartFile image) throws IOException {
String originalFilename = image.getOriginalFilename(); //원본 파일 명
String extention = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명
String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //변경된 파일 명
InputStream is = image.getInputStream();
byte[] bytes = IOUtils.toByteArray(is);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType("image/" + extention);
metadata.setContentLength(bytes.length);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try{
PutObjectRequest putObjectRequest =
new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata)
.withCannedAcl(CannedAccessControlList.PublicRead);
amazonS3.putObject(putObjectRequest); // put image to S3
}catch (Exception e){
throw new S3Exception(ErrorCode.PUT_OBJECT_EXCEPTION);
}finally {
byteArrayInputStream.close();
is.close();
}
return amazonS3.getUrl(bucketName, s3FileName).toString();
}
public void deleteImageFromS3(String imageAddress){
String key = getKeyFromImageAddress(imageAddress);
try{
amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
}catch (Exception e){
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
private String getKeyFromImageAddress(String imageAddress){
try{
URL url = new URL(imageAddress);
String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
return decodingKey.substring(1); // 맨 앞의 '/' 제거
}catch (MalformedURLException | UnsupportedEncodingException e){
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
}
(여러 번 등장하는 S3 Exception과 ErrorCode는 모두 커스텀 Exception과 직접 만든 enum 클래스이다.)
Image 저장
최 상단에서 public method인 upload()에서 private String uploadImage()를 호출한다.
uploadImage()에서는 private void validateImageFileExtention()을 호출하여 확장자 명이 올바른지 확인하고,
private String uploadImageToS3()를 호출하여 S3에 저장된 imageAddress를 반환받는다.
upload(MultipartFile image)
외부에서 사용할 public 메서드이고, S3에 저장된 이미지 객체의 public url을 반환한다.
public String upload(MultipartFile image) {
//입력받은 이미지 파일이 빈 파일인지 검증
if(image.isEmpty() || Objects.isNull(image.getOriginalFilename())){
throw new S3Exception(ErrorCode.EMPTY_FILE_EXCEPTION);
}
//uploadImage를 호출하여 S3에 저장된 이미지의 public url을 반환한다.
return this.uploadImage(image);
}
uploadImage(MultipartFile image)
1. validateImageFileExtention()을 호출하여 확장자 명이 올바른지 확인한다.
2. uploadImageToS3()를 호출하여 이미지를 S3에 업로드하고, S3에 저장된 이미지의 public url을 받아서 서비스 로직에 반환한다.
private String uploadImage(MultipartFile image) {
this.validateImageFileExtention(image.getOriginalFilename());
try {
return this.uploadImageToS3(image);
} catch (IOException e) {
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_UPLOAD);
}
}
validateImageFileExtention(String filename)
filename을 받아서 파일 확장자가 jpg, jpeg, png, gif 중에 속하는지 검증한다.
private void validateImageFileExtention(String filename) {
int lastDotIndex = filename.lastIndexOf(".");
if (lastDotIndex == -1) {
throw new S3Exception(ErrorCode.NO_FILE_EXTENTION);
}
String extention = filename.substring(lastDotIndex + 1).toLowerCase();
List<String> allowedExtentionList = Arrays.asList("jpg", "jpeg", "png", "gif");
if (!allowedExtentionList.contains(extention)) {
throw new S3Exception(ErrorCode.INVALID_FILE_EXTENTION);
}
}
uploadImageToS3(MultipartFile image)
직접적으로 S3에 업로드하는 메서드이다.
주석으로 설명한다.
private String uploadImageToS3(MultipartFile image) throws IOException {
String originalFilename = image.getOriginalFilename(); //원본 파일 명
String extention = originalFilename.substring(originalFilename.lastIndexOf(".")); //확장자 명
String s3FileName = UUID.randomUUID().toString().substring(0, 10) + originalFilename; //변경된 파일 명
InputStream is = image.getInputStream();
byte[] bytes = IOUtils.toByteArray(is); //image를 byte[]로 변환
ObjectMetadata metadata = new ObjectMetadata(); //metadata 생성
metadata.setContentType("image/" + extention);
metadata.setContentLength(bytes.length);
//S3에 요청할 때 사용할 byteInputStream 생성
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try{
//S3로 putObject 할 때 사용할 요청 객체
//생성자 : bucket 이름, 파일 명, byteInputStream, metadata
PutObjectRequest putObjectRequest =
new PutObjectRequest(bucketName, s3FileName, byteArrayInputStream, metadata)
.withCannedAcl(CannedAccessControlList.PublicRead);
//실제로 S3에 이미지 데이터를 넣는 부분이다.
amazonS3.putObject(putObjectRequest); // put image to S3
}catch (Exception e){
throw new S3Exception(ErrorCode.PUT_OBJECT_EXCEPTION);
}finally {
byteArrayInputStream.close();
is.close();
}
return amazonS3.getUrl(bucketName, s3FileName).toString();
}
이미지 삭제
deleteImageFromS3()
이미지의 public url을 이용하여 S3에서 해당 이미지를 제거하는 메서드이다.
getKeyFromImageAddress()를 호출하여 삭제에 필요한 key를 얻는다.
public void deleteImageFromS3(String imageAddress){
String key = getKeyFromImageAddress(imageAddress);
try{
amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
}catch (Exception e){
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
getKeyFromImageAddress()
private String getKeyFromImageAddress(String imageAddress){
try{
URL url = new URL(imageAddress);
String decodingKey = URLDecoder.decode(url.getPath(), "UTF-8");
return decodingKey.substring(1); // 맨 앞의 '/' 제거
}catch (MalformedURLException | UnsupportedEncodingException e){
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
code - Controller
두 가지 모두 테스트용 Controller 메서드이다.
실제로는 upload 시에는 s3 ImageService를 이용하여 이미지를 저장한 뒤, 반환받은 public url을 DB에 저장하고,
삭제 시에 DB에서 특정 public url을 꺼내서 삭제하는 등의 로직을 처리할 수 있을 것이다.
이미지 업로드
@RequestPart를 이용하여 MultipartFile 데이터를 바인딩한다.
@PostMapping("/s3/upload")
public ResponseEntity<?> s3Upload(@RequestPart(value = "image", required = false) MultipartFile image){
String profileImage = s3ImageService.upload(image);
return ResponseEntity.ok(profileImage);
}
이미지 삭제
쿼리 파라미터로 이미지의 public url을 받아서 삭제한다.
@GetMapping("/s3/delete")
public ResponseEntity<?> s3delete(@RequestParam String addr){
s3ImageService.deleteImageFromS3(addr);
return ResponseEntity.ok(null);
}
'DevOps > Deploy' 카테고리의 다른 글
[Linux / ubuntu] AWS Ubuntu 20.04에 swap 메모리 설정하기, Freetier 메모리 부족 현상 해결 (0) | 2024.01.28 |
---|---|
[AWS EC2 / 배포] SpringBoot Project 배포, EC2 ubuntu, docker (2) | 2024.01.24 |
[AWS/S3] Spring boot project 이미지 업로드를 위해 S3 버켓 만들기 (0) | 2024.01.21 |
[AWS/트러블슈팅] EC2 서버 느려짐 문제 (0) | 2023.12.22 |
[AWS / SpringBoot] 스프링 부트 프로젝트 변경사항 rebuild 후 재배포 (기록용) (0) | 2023.12.15 |