안녕하세요, 스코리아입니다.
오늘은 스프링에서 AWS S3를 이용해서 이미지 업로드 하는 방법에 대해서 알아보겠습니다.
왜 AWS S3를 사용할까? (feat. 도입 이유)
현재 제가 진행 중인 프로젝트에 사진 업로드 로직이 존재하였는데, 저장 위치가 로컬이었습니다.
이후 CI/CD를 적용하고, 외부 서버에 배포를 진행하면서, 사진이 쌓이면 쌓일수록 빌드파일 크기가 늘어난다는 것을 확인했습니다. (비효율적)
또한 나중에 서비스가 커져서 로드밸런싱을 진행해야 할 경우가 생기면, 각 서버마다 저장되어 있는 사진이 다르기 때문에 문제가 될 것 같았습니다.
따라서 파일을 저장할 스토리지가 따로 있으면 유용할 것 같았습니다.
Amazon S3는 확장성, 데이터 가용성, 보안과 성능을 제공하는 객체 스토리지 서버스입니다. (파일 저장 스토리지)
Amazon S3의 장점은 다음과 같습니다.
- 99.99999%의 내구성 (파일 보존)
- 99.99% 가용성 (SLA)
- API 다수 지원
- 무제한 용량 (단, 하나의 객체는 5TB까지), 사용한 만큼 지불
- 보안, 성능 우수
- Static/Dynamic Cache 지원
- 암호화 기능 존재
- 버전 관리 가능
- 수명 주기 설정 가능
- 삭제 방지 기능 (MFA)
1. AWS S3 버킷 생성
먼저 버킷을 만들어줍니다. 버킷 -> 버킷 만들기
- 버킷 이름 : 원하는 이름 입력
- 객체 소유권 : ACL 비활성화됨 (권장)
- 모든 퍼블릿 액세스 차단 : 체크를 풀어줍니다.
버킷을 만들고 나서, 해당 버킷의 속성 -> '정적 웹 사이트 호스팅 편집'으로 이동해주세요.
- 정적 웹 사이트 호스팅 : 활성화
- 호스팅 유형 : 정적 웹 사이트 호스팅
- 인덱스 문서 : index.html
설정 후, 변경 사항 저장을 눌러주세요.
설정을 하고 나면, 버킷 웹사이트 엔드포인트 주소가 보입니다.
해당 주소로 버킷에 외부 접속이 가능합니다.
2. AWS IAM 사용자 설정
IAM 사용자에게 S3 접근 권한을 주고 해당 사용자의 액세스 키, 비밀 액세스 키 발급을 위해 'IAM > 액세스 관리 > 사용자 > 사용자 추가'로 이동해 주세요.
- 사용자 이름 : 원하는 사용자 이름 입력
- 권한 옵션 : 직접 정책 연결
- 권한 정책 : 'AmazonS3FullAccess' 검색 후 선택
이후 설정한 내용이 맞는지 검토하고 '사용자 생성' 버튼을 눌러주세요.
IAM 사용자 생성 후, 해당 사용자 페이지 내에서 보안 자격 증명 -> 액세스 키 -> 액세스 키 만들기 버튼 클릭해 주세요.
액세스 키 모범 사례 및 대안
- 사용 사례 : 기타 (이 외 아무거나 선택하셔도 상관없습니다)
액세스 키 생성하기 버튼을 눌러줍니다.
그다음 페이지에서 액세스 키, 비밀 엑세스 키가 표시됩니다. 꼭 복사해서 저장해 두세요.
3. 스프링 설정 추가
1) build.gralde에 dependency 추가
AWS S3 관련 gradle dependency를 추가합니다.
implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4' // AWS S3
implementation 'javax.xml.bind:jaxb-api:2.3.1' // AWS S3
implementation 'com.sun.xml.bind:jaxb-core:2.3.0.1' // AWS S3
implementation 'com.sun.xml.bind:jaxb-impl:2.3.2' // AWS S3
2) application.yml 설정 추가
- access-key, secret-key: 아까 AWS IAM에서 발급받은 액세스 키와 비밀 액세스키를 각각 넣기
- region : ap-northeast-2 (한국 리전)
- bucket : 아까 AWS S3에서 만든 버킷 이름을 입력
application-prod.yml과 같이 따로 설정 yml파일을 만들어서 그곳에서 실제 값을 넣어주시는 것을 추천드립니다.
cloud:
aws:
credentials:
access-key: access-key
secret-key: secret-key
region:
static: ap-northeast-2
stack:
auto: false
aws:
s3:
bucket: bucket
3) AWS S3 Config 생성
AmazonS3ClientBuilder를 이용해서 credentials에 application.yml의 설정한 값들을 불러온 값들을 넣어주었습니다.
@Configuration
public class AwsS3Config {
@Value("${cloud.aws.region.static}")
private String region;
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Bean
public AmazonS3 amazonS3() {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
4) AWS S3 Service 생성
@Value 어노테이션을 통해 application.yml에 설정한 bucket 이름을 가져오고, AmazonS3을 생성자 주입받습니다.
- uploadFile 메소드
- getNewFileName 메소드를 통해 기존 파일의 이름이 비었는지 않은지, 확장자가 올바른지 체크하고 'a.jpg'와 같이 파일이름과 확장자명을 추출해 줍니다.
- MultipartFile의 getInputStream() 메소드를 통해 파일 데이터를 바이트 단위로 읽습니다.
- ObjectMetadata 객체를 생성하고 파일의 type와 size를 지정해 줍니다. (파일 변조 방지)
- amazonS3의 putObject 메소드를 통해 버킷 이름, 파일 full path, fileInputStream, metdata를 넘겨주어, 호출합니다.
- 새로운 파일이름 (newFileName)을 return 합니다.
- deleteFile 메소드
- amazonS3의 deleteObject 메소드를 통해 버킷이름, 파일 full path를 넘겨주어, 호출합니다.
@Service
public class AwsS3Service {
private final AmazonS3 amazonS3;
@Value("${aws.s3.bucket}")
private String bucketName;
@Autowired
public AwsS3Service(AmazonS3 amazonS3) {
this.amazonS3 = amazonS3;
}
public String uploadFile(MultipartFile file, String path, String fileName){
try {
String newFileName = getNewFileName(file, fileName);
InputStream fileInputStream = file.getInputStream();
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
amazonS3.putObject(bucketName, path + newFileName, fileInputStream, metadata);
fileInputStream.close(); // InputStream 을 명시적으로 닫아주는 것이 좋음
return newFileName;
}catch (IOException e){
throw new IllegalArgumentException("S3 파일 업로드에 실패하였습니다.");
}
}
private static String getNewFileName(MultipartFile file, String fileName) {
String originalFileName = file.getOriginalFilename();
if (originalFileName == null || originalFileName.isEmpty()) {
throw new IllegalArgumentException("파일 이름이 비어있습니다.");
}
String fileExtension = originalFileName.contains(".")
? originalFileName.substring(originalFileName.lastIndexOf('.') + 1)
: "";
if (fileExtension.isEmpty()) {
throw new IllegalArgumentException("파일 확장자가 올바르지 않습니다.");
}
return fileName + "." + fileExtension;
}
public void deleteFile(String fullPath) {
amazonS3.deleteObject(bucketName, fullPath);
}
}
5) 서비스 계층의 코드 추가
여러 개의 이미지를 업로드 되게 뜸하고 싶은 경우 아래와 같이 작성해 주세요. MuliparFile을 List형태로 넘겨주면, 반복문을 통해 랜덤으로 파일명을 지정하고 aws s3에 업로드되고, 이미지 full path가 return 됩니다.
path 부분은 원하는 경로로 바꿔주세요.
private List<String> uploadPostImages(List<MultipartFile> requestImages) {
String path = "post/images/";
List<String> images = new ArrayList<>();
for (MultipartFile image : requestImages) {
String fileName = RandomUtil.generateRandomString(32);
String newFileName = awsS3Service.uploadFile(image, path, fileName);
images.add(path + newFileName);
}
return images;
}
아래와 같이 Dto가 존재한다면,
@Getter
@Setter
public class PostAddDtoRequest {
@NotNull(message = "사진을 최소 1장 추가해주세요.")
private List<MultipartFile> images;
}
실제 서비스 로직에서는 이렇게 호출해 주면 됩니다.
public String addPost(PostAddDtoRequest postAddDtoRequest){
Post entity = postAddDtoRequest.toEntity();
// 이미지 S3 업로드
List<String> images = uploadPostImages(postAddDtoRequest.getImages());
entity.setImages(images);
return "사진이 추가되었습니다.";
}
이후 AWS S3의 퍼블릭 주소(정적 웹사이트 엔드포인트)를 통해서 사진에 접근할 수 있습니다.
지금까지 스프링에서 AWS S3를 이용하여 파일(이미지) 업로드하는 방법에 대해서 알아보았습니다.
읽어주셔서, 감사합니다.
'Spring Framework > 스프링' 카테고리의 다른 글
스프링 IP주소 Auditing 적용/구현 (Spring Data JPA) (32) | 2024.08.16 |
---|---|
스프링 MVC 패턴 의미와 구조 [정리] (35) | 2024.08.09 |
(4) Jenkins 스프링 무중단 CI/CD 배포 구현 - Slack 연동 (0) | 2024.07.20 |
(3) Jenkins 스프링 무중단 CI/CD 배포 구현 - Docker Blue&Green, Jenkins 빌드설정 (1) | 2024.07.15 |
(2) Jenkins 스프링 무중단 CI/CD 배포 구현 - Github 연동/Credentials (0) | 2024.07.14 |