AWS S3란?
- Amazon Simple Storage Service로 객체 스토리지 서비스이다.
- 업계 최고의 확장성, 데이터 가용성 및 보안과 성능을 제공한다.
- 사용자가 원하는 만큼의 데이터를 저장할 수 있기에, 다양한 타입의 데이터를 저장할 수 있다.
AWS S3를 프로젝트에서 사용
- 데이터를 S3에 저장할 때에, 해당 데이터에 대한 S3 URI를 제공한다.
- 예시) s3://master-car-application/de8265e3b2984913bbe01cd53733317f_1607146775.jpg
- 위의 뒷부분은 객체의 key라고 볼 수 있다.
- 해당 key를 통해 프로젝트에서 S3에 저장된 이미지를 찾을 수 있다.
- 기본 로직 (DB에 저장)
- 클라이언트에서 MultipartFile을 통해 파일을 가지고 온다
- 해당 파일을 AWS S3 버킷에 저장을 한다
- AWS S3에 저장되고, 제공 되는 URI를 DB에 저장을 한다
- DB에 저장되면, 유저들을 해당 파일을 볼 수 있게 된다
- 기본 로직 (DB에서 삭제)
- DB에서 URI를 꺼낸다
- 해당 URI에서의 제일 마지막 부분 (location)에서 객체에 대한 Key를 가지고 온다
- 해당 Key를 통해, AWS S3 버킷 내의 객체를 찾을 수 있고, 해당 객체를 삭제를 한다 (삭제를 하면, 해당 객체는 사용할 수 없게 된다)
- DB에서 저장한 URI를 삭제한다
스프링부트
application.properties
# AWS S3
cloud.aws.s3.bucket={버킷이름}
cloud.aws.s3.credentials.access-key={IAM의 access key}
cloud.aws.s3.credentials.secret-key={IAM에서 access key를 받을 때 받는 키}
cloud.aws.s3.region.static={지역}
cloud.aws.s3.region.auto=false
cloud.aws.s3.stack.auto=false
- IAM을 생성하면 Access Key를 받을 수 있다.
- Access Key를 받으면 동시에 Secret Key를 받는데, 해당 정보는 절대로 노출되서는 안 된다.
build.gradle
// aws s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
S3Config
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
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.s3.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.s3.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.s3.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}
}
S3ImageService
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.util.IOUtils;
import com.azero.projecta.global.exception.S3Exception;
import com.azero.projecta.global.exception.errorCode.ErrorCode;
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.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;
@Component
@RequiredArgsConstructor
@Slf4j
public class S3ImageService {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
/**
* 이미지 파일을 S3에 저장하기 전 파일 확장자가 제대로 된 확장자인지 확인 후 S3에 저장
*/
private String saveImage(MultipartFile image) {
String fileName = image.getOriginalFilename();
String fileExtension = validateImageFileExtension(fileName);
try {
return saveImageToS3(image, fileName, fileExtension);
} catch (IOException e) {
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_SAVE);
}
}
/**
* 파일 확장자 확인 하기 (확장자가 jpg, jpeg, png, gif 가 아니면 예외 처리)
*/
private String validateImageFileExtension(String fileName) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot == -1) {
throw new S3Exception(ErrorCode.NO_FILE_EXTENSION);
}
String[] allowedExtensions = {"jpg", "jpeg", "png", "gif"};
String fileExtension = fileName
.substring(lastDot + 1)
.toLowerCase();
if (!Arrays.asList(allowedExtensions).contains(fileExtension)) {
throw new S3Exception(ErrorCode.INVALID_FILE_EXTENSION);
}
return fileExtension;
}
/**
* S3에 이미지 파일 저장하기
*/
private String saveImageToS3(MultipartFile image,
String originalFilename,
String fileExtension) throws IOException {
// S3에 저장할 파일 이름을 UUID로 생성 (중복 방지)
String newS3FileName = UUID.randomUUID().toString().substring(0, 10)
+ "_" + originalFilename;
InputStream inputStream = image.getInputStream();
byte[] bytes = IOUtils.toByteArray(inputStream);
// Amazon S3에 저장되는 파일 또는 객체와 관련된 정보 (파일의 유형, 크기, 버전 등)
// 사용자가 정의한 추가적인 정보를 포함할 수 있음. 파일의 작성자, 설명, 태그
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType("image/" + fileExtension);
metadata.setContentLength(bytes.length);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try {
// PutObjectRequest : Amazon S3에 객체를 저장하는 요청
PutObjectRequest putObjectRequest =
new PutObjectRequest(bucket, newS3FileName, byteArrayInputStream, metadata);
amazonS3.putObject(putObjectRequest);
} catch (Exception e) {
throw new S3Exception(ErrorCode.PUT_OBJECT_ERROR);
}
return amazonS3.getUrl(bucket, newS3FileName).toString();
}
/**
* 이미지 파일의 주소로부터 key 추출
*/
private String getKeyFromImageAddress(String imageAddress) {
try {
URL url = new URL(imageAddress);
String key = URLDecoder.decode(url.getPath(), "UTF-8");
return key.substring(1);
} catch (MalformedURLException | UnsupportedEncodingException e) {
throw new S3Exception(ErrorCode.INVALID_FILE_EXTENSION);
}
}
/**
* 이미지 파일을 S3에 저장 먼저 이미지 파일이 비어있는지 확인 (비어있으면 예외 처리)
*/
public String uploadImage(MultipartFile image) {
if (image.isEmpty() || Objects.isNull(image.getOriginalFilename())) {
throw new S3Exception(ErrorCode.EMPTY_FILE);
}
return saveImage(image);
}
/**
* 이미지의 Key를 가지고 와서 S3내 객체를 삭제한다
*/
public void deleteImage(String urlAddress) {
String key = getKeyFromImageAddress(urlAddress);
try {
amazonS3.deleteObject(bucket, key);
} catch (Exception e) {
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
}
S3ImageService (Detail하게 설명)
- 모든 S3Exception은 커스텀으로 만든 예외들이다
/**
* 이미지 파일을 S3에 저장 먼저 이미지 파일이 비어있는지 확인 (비어있으면 예외 처리)
*/
public String uploadImage(MultipartFile image) {
if (image.isEmpty() || Objects.isNull(image.getOriginalFilename())) {
throw new S3Exception(ErrorCode.EMPTY_FILE);
}
return saveImage(image);
}
uploadImage
- MultipartFile로 받아온 이미지에 대해 해당 이미지 파일이 존재하는지 먼저 확인을 한다
/**
* 이미지 파일을 S3에 저장하기 전 파일 확장자가 제대로 된 확장자인지 확인 후 S3에 저장
*/
private String saveImage(MultipartFile image) {
String fileName = image.getOriginalFilename();
String fileExtension = validateImageFileExtension(fileName);
try {
return saveImageToS3(image, fileName, fileExtension);
} catch (IOException e) {
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_SAVE);
}
}
saveImage
- MultipartFile을 통해 가져온 파일에서, 파일 이름을 가지고 온다 (fileName)
- 해당 이미지 파일이 제대로된 파일인지 확인을 한다 (validateImageFileExtension 메서드를 통해)
- 이미지 파일이 jpg, jpeg, png, gif가 아니면 파일을 업로드 시키지 못 하게 한다
- 에러가 발생하지 않으면 saveImageToS3 메서드를 통해 S3에 이미지를 저장한다
/**
* 파일 확장자 확인 하기 (확장자가 jpg, jpeg, png, gif 가 아니면 예외 처리)
*/
private String validateImageFileExtension(String fileName) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot == -1) {
throw new S3Exception(ErrorCode.NO_FILE_EXTENSION);
}
String[] allowedExtensions = {"jpg", "jpeg", "png", "gif"};
String fileExtension = fileName
.substring(lastDot + 1)
.toLowerCase();
if (!Arrays.asList(allowedExtensions).contains(fileExtension)) {
throw new S3Exception(ErrorCode.INVALID_FILE_EXTENSION);
}
return fileExtension;
}
validateImageFileExtension
-
- 파일 이름을 가지고 올때, 확장자를 확인하기 위해서는 '.'의 위치를 확인하면 된다
- jjanggu.jpg - '.' 의 위치는 7 이다 (여기서 jpg가 확장자다)
- '.' 이 없거나, 맨 뒤에 있을 경우 확장자가 없다는 것임으로 예외처리를 한다
- '.' 의 위치를 통해 substring을 사용하여 확장자를 가지고 오고, jpg, jpeg, png, gif 중 하나인지 확인한다
- 없으면 예외처리를 한다
- 파일 이름을 가지고 올때, 확장자를 확인하기 위해서는 '.'의 위치를 확인하면 된다
/**
* S3에 이미지 파일 저장하기
*/
private String saveImageToS3(MultipartFile image,
String originalFilename,
String fileExtension) throws IOException {
// S3에 저장할 파일 이름을 UUID로 생성 (중복 방지)
String newS3FileName = UUID.randomUUID().toString().substring(0, 10)
+ "_" + originalFilename;
InputStream inputStream = image.getInputStream();
byte[] bytes = IOUtils.toByteArray(inputStream);
// Amazon S3에 저장되는 파일 또는 객체와 관련된 정보 (파일의 유형, 크기, 버전 등)
// 사용자가 정의한 추가적인 정보를 포함할 수 있음. 파일의 작성자, 설명, 태그
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType("image/" + fileExtension);
metadata.setContentLength(bytes.length);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try {
// PutObjectRequest : Amazon S3에 객체를 저장하는 요청
PutObjectRequest putObjectRequest =
new PutObjectRequest(bucket, newS3FileName, byteArrayInputStream, metadata);
amazonS3.putObject(putObjectRequest);
} catch (Exception e) {
throw new S3Exception(ErrorCode.PUT_OBJECT_ERROR);
}
return amazonS3.getUrl(bucket, newS3FileName).toString();
}
saveImageToS3
-
- AWS S3 버킷에 객체를 저장할 때에, 이름이 겹치지 않도록 UUID를 사용해서 새로운 이름을 만들어준다
- 예) 원래 jjanggu.jpg -> dk4ds3dc20_jjanggu.jpg
- InputStream과 byte[]를 통해서 파일을 바이트 배열로 변환한다
- ObjectMetadata : Amazon S3ㅔ 저장되는 파일 또는 객체와 관련된 정보를 저장을 하는 클래스이다
- 파일의 유형, 크기, 버전 등
- 사용자가 정의한 추가적인 정보를 포함할 수 있다. (파일의 유형, 크기, 버전 등)
- PutObjectRequest : Amazon S3에 객체를 저장하는 요청이다
- amazonS3.getUrl() : 해당 메서드를 통해, S3 저장 후, 설정된 URL을 리턴한다
- AWS S3 버킷에 객체를 저장할 때에, 이름이 겹치지 않도록 UUID를 사용해서 새로운 이름을 만들어준다
/**
* 이미지의 Key를 가지고 와서 S3내 객체를 삭제한다
*/
public void deleteImage(String urlAddress) {
String key = getKeyFromImageAddress(urlAddress);
try {
amazonS3.deleteObject(bucket, key);
} catch (Exception e) {
throw new S3Exception(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
deleteImage
- getKeyFromImageAddress() 메서드를 통해서 받아온 이미지 URL에서 S3에 저장된 Key를 가져온다
- amazonS3.deleteObject() 를 통해서, 버켓 안에 있는 객체를 삭제한다
/**
* 이미지 파일의 주소로부터 key 추출
*/
private String getKeyFromImageAddress(String imageAddress) {
try {
URL url = new URL(imageAddress);
String key = URLDecoder.decode(url.getPath(), "UTF-8");
return key.substring(1);
} catch (MalformedURLException | UnsupportedEncodingException e) {
throw new S3Exception(ErrorCode.INVALID_FILE_EXTENSION);
}
}
getKeyFromImageAddress
- 이미지 URL에서 맨 뒷 부분을 가져온다 (url.getPath())
- 뒷 부분이 AWS S3 버킷 객체의 key다
'Skill Stacks > Java_Spring' 카테고리의 다른 글
AWS S3 스프링에 적용하기 - 1 (0) | 2024.05.27 |
---|---|
스프링 회원기능 (1) | 2023.11.17 |
[Java] 오늘 배운 것 20231025 (0) | 2023.10.25 |
[Java] Nginx (0) | 2023.09.14 |
[Java] Kafka (0) | 2023.09.13 |