S3 이미지 업로드 개념
1. Spring Boot Project에서 클라이언트에게 MultipartFile로 이미지 파일을 받는다.
2. 이 파일을 S3에 업로드하고 S3에 어디서나 접근 가능한 public url을 반환한다.
3. public url을 통해 이미지에 어디서나 접근/다운이 가능하다.
4. url을 DB에 저장하여 필요할 때 url로 이미지 데이터를 사용할 수 있다.
구현과정
1. S3 버킷 생성
* 퍼블릭 액세스 차단을 해제 해야함
나머지는 기본 설정으로 두고 버킷 생성
버킷 정책 설정
이 작업을 안 해주면 추후 URL이 나왔을 때 들어가보려고 하면 아래와 같은 오류가 난다.
생성한 버킷 - 권한 - 버킷 정책 편집
아래와 같이 작성해준다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<버킷명>/*"
},
{
"Sid": "DenyOtherAccess",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"NotResource": [
"arn:aws:s3:::<버킷명>/*.jpg",
"arn:aws:s3:::<버킷명>/*.png",
"arn:aws:s3:::<버킷명>/*.jpeg",
"arn:aws:s3:::<버킷명>/*.gif"
]
}
]
}
2. IAM 사용자 생성
이름 지정
직접 정책 연결 -> AmazonS3FullAccess 체크
생성한 IAM 사용자에 들어가서 액세스 키 만들기
액세스 키 모범 사례 및 대안 이 나오는데 아무거나 골라도 된다. 나는 CLI 선택
나오는 키를 저장해둔다.
비밀 액세스 키는 다시 볼 수 없으므로 복사해두거나 csv 파일로 저장한다.
3. yml 구성
cloud:
aws:
credentials:
accessKey: {위에서 발급한 key}
secretKey: {위에서 발급한 key}
s3:
bucketName: {버킷 이름}
region:
static: ap-northeast-2
stack:
auto: false
4. S3 config 구성
package com.sj.Petory.config;
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();
}
}
AmazonS3ClientBuilder를 이용해 Credentials과 Region을 등록해주고
AmazonS3 객체를 @Bean으로 등록한다.
5. AmazonS3Service
package com.sj.Petory.domain.member.service;
import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.sj.Petory.exception.S3Exception;
import com.sj.Petory.exception.type.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import static com.sj.Petory.exception.type.ErrorCode.IMAGE_UPLOAD_FAIL;
@RequiredArgsConstructor
@Slf4j
@Service
public class AmazonS3Service {
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.FILE_EMPTY);
}
return this.uploadImage(image);
}
private String uploadImage(MultipartFile image) {
String randomFilename = generateRandomFilename(image);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(image.getSize());
metadata.setContentType(image.getContentType());
log.info("File Upload Started : " + randomFilename);
try {
amazonS3.putObject(bucketName, randomFilename, image.getInputStream(), metadata);
} catch (AmazonS3Exception e) {
log.error("Amazon S3 error while uploading file: " + e.getMessage());
throw new S3Exception(IMAGE_UPLOAD_FAIL);
} catch (SdkClientException e) {
log.error("AWS SDK client error while uploading file: " + e.getMessage());
throw new S3Exception(IMAGE_UPLOAD_FAIL);
} catch (IOException e) {
log.error("IO error while uploading file: " + e.getMessage());
throw new S3Exception(IMAGE_UPLOAD_FAIL);
}
log.info("File upload Success : " + randomFilename);
return amazonS3.getUrl(bucketName, randomFilename).toString();
}
private String generateRandomFilename(MultipartFile image) {
String originName = image.getOriginalFilename();
String fileExtension = validateFileExtension(originName);
return UUID.randomUUID() + "." + fileExtension;
}
private String validateFileExtension(String originName) {
String fileExtension = originName.substring(originName.lastIndexOf(".") + 1);
log.info(fileExtension);
List<String> allowedExtensions = Arrays.asList("jpg", "png", "gif", "jpeg");
if (!allowedExtensions.contains(fileExtension)) {
throw new S3Exception(ErrorCode.FILE_EXTENSION_NOT_ALLOWED);
}
return fileExtension;
}
}
메소드 별로 설명하겠다.
upload(MultipartFile image)
우리가 사용하는 서비스에서 호출될 메소드로 image 파일이 비어있는 지, 파일이름이 null 인지를 검사한다.
정상 파일이면 해당 클래스의 uploadImage() 메소드를 호출한다.
uploadImage(MultipartFile image)
실제 s3에 이미지를 업로드하는 메소드이다.
generateRandomFilename() 메소드를 통해 filename을 랜덤으로 생성한다.
metadate를 설정해주고
amazonS3.putObject() 메소드를 이용해 s3에 업로드한다.
generateRandomFilename(MultipartFile image)
이미지의 원래 파일 명에서 validateFileExtension()을 통해 유효한 확장자인지 (이미지 파일인지)를 검사하고 파일 확장자를 뽑아온다.
반환값으로 랜덤 파일명.파일확장자를 리턴한다.
validateFileExtension(String originName)
파일 이름에서 . 뒷부분 확장자를 추출해서 List에 저장해둔 확장자가 아니면 예외를 터트린다.
유효한 확장자일 경우 확장자 문자열을 리턴한다.
6. 컨트롤러
@PatchMapping("/image")
public ResponseEntity<?> s3ImageUpload(
@AuthenticationPrincipal MemberAdapter memberAdapter
, @RequestPart(value = "image", required = false) MultipartFile image) {
return ResponseEntity.ok(memberService.imageUpload(memberAdapter, image));
}
이미 가입된 회원에 이미지를 변경하는 느낌이므로 PATCH를 사용한다.
MultipartFile 타입으로 이미지를 받는다.
7. 서비스
@Transactional
public String imageUpload(
final MemberAdapter memberAdapter, final MultipartFile image) {
Member member = getMemberByEmail(memberAdapter.getEmail());
String imageUrl = amazonS3Service.upload(image);
member.updateImage(imageUrl);
return imageUrl;
}
5에서 작성한 amazonS3Service클래스에 upload 메소드를 호출한다.
이미 가입된 회원에 이미지 필드를 수정하는 것이므로 회원정보 수정처럼 더티체킹을 이용해야한다.
서비스 메소드에 @Transactional을 붙여 변경을 감지할 수 있도록 해준다.
간단하게 s3에 이미지 업로드가 완료됐다 !
여기에 리턴값으로 내려오는 imageUrl로 접속하면 S3에 업로드한 이미지를 확인 할 수 있다.
간단하게 구현해봤지만
이제 이 이미지 url을 사용자 엔티티에 추가로 넣어야 한다. - O 완료
또한 이미지 삭제, 여러 개의 이미지 업로드 시 저장 로직(커뮤니티 게시글 용)을 구현해야 한다.
Trouble Shooting
버킷 정책을 아래처럼 모든 곳에서 접속할 수 있게 작성하지 않으면 오류가 난다 !
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<버킷명>/*"
},
{
"Sid": "DenyOtherAccess",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"NotResource": [
"arn:aws:s3:::<버킷명>/*.jpg",
"arn:aws:s3:::<버킷명>/*.png",
"arn:aws:s3:::<버킷명>/*.jpeg",
"arn:aws:s3:::<버킷명>/*.gif"
]
}
]
}
참고
버킷 생성, IAM 사용자 생성 참고
https://innovation123.tistory.com/198
버킷 정책 설정 참고 & 전체적인 로직 참고
전체적인 로직 참고
https://innovation123.tistory.com/197
다중 파일 업로드
https://ksh-coding.tistory.com/67
'공부 > AWS' 카테고리의 다른 글
AWS를 위한 기초 용어 (0) | 2024.11.08 |
---|---|
RDS 로컬 MySQL Workbench 연결 오류 해결 - VPC Public 변경 (0) | 2024.08.05 |
윈도우 cmd에서 ec2 접속하기 (0) | 2024.08.05 |
AWS) EC2에 docker설치 (0) | 2024.01.21 |
AWS) EC2 인스턴스 생성 (2) | 2024.01.20 |