AWS S3 버킷을 생성하고 스프링에서 이미지 저장하는 로직을 설계하는 전체적인 흐름을 기록하겠다.
버킷을 생성하는 건 어렵지 않다.
버킷 이름은 글로벌하게? 중복되지 않는다는 것만 기억하고, 대문자 금지, _ 금지 라는 점만 유의해서 이름을 짓고 생성하면 된다.

그리고 생성한 버킷에 들어가 Permissions - Bucket policy를 설정해줘야하는데, 이때 바로 위에 있는 Block public access (bucket settings)은 off 해주었다.
Bucket policy - Edit - Policy generator 에 들어가면

이 화면에서 Actions 는 GetObject를 선택, ARN은 바로 전 화면에서 확인할 수 있는 내 Bucket ARN을 복사해서 넣어주면 Policy를 만들어준다.
그걸 복사해서 Policy에 넣어주면

이런 형태가 되고 Resource 뒤쪽에 /* 만 붙여서 저장해준다!
그럼 버킷 생성은 끝!
Springboot에 S3를 연동하기위해서는 Access key와 Secret key가 필요한데 위 블로그에 너무 잘 설명되어 있어 그대로 실행하였다.
Access key와 Secret key 극극 민감정보라 오픈되지 않도록 꼭 유의!!
build.gradle, application.yml, S3Config 파일을 업데이트하면 된다.
// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
cloud:
aws:
s3:
bucket: 버킷이름
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}
region:
static: ${S3_REGION}
stack:
auto: false
액세스키, 시크릿키, 리전은 민감한 정보이기 때문에 반드시 환경변수에 넣어준다. (위에서 IAM 통해 발급받은 키값들)
@Configuration
public class S3Config {
@Value("${S3_ACCESS_KEY}")
private String accessKey;
@Value("${S3_SECRET_KEY}")
private String secretKey;
@Value("${S3_REGION}")
private String region;
@Bean
public AmazonS3Client s3Client() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3Client.builder()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
여기까지 해줬으면 초기세팅은 끝.
이제 S3FileService을 만들어준 후 이미지 파일을 넣어줘야하는 곳에 삽입해주면 된다.
@Service
@RequiredArgsConstructor
public class S3FileService {
// S3
private final AmazonS3Client s3Client;
public String uploadFile(MultipartFile image, String bucket) {
try {
// 이미지 파일 유효성 검증
validateFile(image);
// 이미지 이름 변경
String originalFileName = image.getOriginalFilename();
String fileName = changeFileName(originalFileName);
// S3에 파일을 보낼 때 파일의 종류와 크기를 알려주기
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(image.getContentType());
metadata.setContentLength(image.getSize());
metadata.setContentDisposition("inline");
// S3에 파일 업로드
s3Client.putObject(bucket, fileName, image.getInputStream(), metadata);
return s3Client.getUrl(bucket, fileName).toString();
} catch (IOException e) {
throw new ApiException(ErrorStatus._FILE_UPLOAD_FAILED);
}
}
이미지 파일을 유효성 검사를 먼저 하고,
똑같은 사진을 올리면 이름이 겹치는 걸 방지해서 이름에 날짜를 붙여 변경해준다.
다음으로 metadata에 파일 정보를 담아 업로드! 하고 스트링형태로 이미지의 url을 반환하게 된다.
여기서 필요한 validateFile, getOriginalFilename 메서드를 추가로 만들어준다.
이미지 파일 형식은 jpeg, png, jpg 형식만 저장 가능하며, 최대 크기는 5MB이다.
// 이미지 파일 크기 제한
private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
private void validateFile(MultipartFile file) {
// 이미지 파일이 비어있을 경우
if (file.isEmpty()) {
throw new ApiException(ErrorStatus._EMPTY_FILE);
}
//파일 크기가 5MB 초과하였을 경우
if (file.getSize() > MAX_FILE_SIZE) {
throw new ApiException(ErrorStatus._FILE_SIZE_EXCEEDED);
}
// 파일 형식 제한
if (!isSupportedContentType(file.getContentType())) {
throw new ApiException(ErrorStatus._UNSUPPORTED_FILE_FORMAT);
}
}
// 파일 형식 제한 메서드
private boolean isSupportedContentType(String contentType) {
return contentType.equals("image/jpeg") || contentType.equals("image/png") || contentType.equals("image/jpg");
}
이미지 파일 이름 변경하는 메서드
이미지 등록 날짜를 붙여서 반환
private String changeFileName(String originalFileName) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
return LocalDateTime.now().format(formatter) + "_" + originalFileName;
}
등록된 사진 기존 URL 원본 파일이름으로 디코딩하는 메서드
String 타입의 원본 파일 이름 반환
이건 해당 이미지를 삭제할 때 원본 이름을 가져오기 위한 메서드이다.
public String extractFileNameFromUrl(String url) {
// URL 마지막 슬래시의 위치를 찾아서 인코딩된 파일 이름 가져오기
String encodedFileName = url.substring(url.lastIndexOf("/") + 1);
// 인코딩된 파일 이름을 디코딩 해서 진짜 원본 파일 이름 가져오기
return URLDecoder.decode(encodedFileName, StandardCharsets.UTF_8);
}
나는 공연 생성 시 이미지 파일을 같이 업로드하는 것과 공연 수정시 기존 이미지를 삭제하고 다시 새로운 이미지로 업로드하는 로직을 작성했다.
@PostMapping
public ApiResponse<CreateEventResponse> createEvent(
@AuthenticationPrincipal AuthUser authUser,
@ModelAttribute CreateEventRequest eventRequest) {
CreateEventResponse response = eventService.createEvent(authUser, eventRequest, eventRequest.getImage());
return ApiResponse.ok(response);
}
Trouble 1: 공연 생성할때 415 Unsupported Media Type 에러 발생
→ MultipartFile 타입을 받을 때는 @RequestBody 가 아닌 @ModelAttribute , 혹은 @RequestPart 를 사용해야 한다.
Trouble 2 : 공연 생성할때 이미지 빼고 모든 값들이 Null 로 나옴
→ @ModelAttribute 사용하려면 객체에 데이터를 바인딩할 수 있는 생성자혹은 Setter메서드가 하나는 필요하다. CreateEventRequest에 처음엔 Setter를 달아줬다가 세터가 아닌 방법이 뭐 없나 찾아보니 생성자를 달아줘도 가능하다는 글을 보았다! 그래서 @AllArgsConstructor 넣어주니 해결!
private final S3FileService s3FileService;
// S3
private final AmazonS3Client s3Client;
@Value("nugulticket")
private String bucket;
public CreateEventResponse createEvent(AuthUser authUser, CreateEventRequest eventRequest, MultipartFile image) {
int price = 140000;
int vipSeatCount = 20;
int rSeatCount = 20;
int aSeatCount = 20;
User user = userService.getUser(authUser.getId());
if (!user.getUserRole().equals(UserRole.SELLER)) {
throw new ApiException(ErrorStatus.SELLER_ROLE_REQUIRED);
}
// 업로드한 파일의 S3 URL 주소
String imageUrl = s3FileService.uploadFile(image, bucket);
Event event = new Event(user,eventRequest, imageUrl);
Event savedEvent = eventRepository.save(event);
eventTimeService.createEventTimes(event,
eventRequest.getStartDate(),
eventRequest.getEndDate(),
LocalTime.now(),
price,
vipSeatCount,
rSeatCount,
aSeatCount);
return new CreateEventResponse(savedEvent);
}
Controller 쪽은 생성과 비슷하다
@Transactional
public UpdateEventResponse updateEvent(AuthUser authUser, Long eventId, UpdateEventRequest eventRequest) {
User user = userService.getUser(authUser.getId());
if (!user.getUserRole().equals(UserRole.SELLER)) {
throw new ApiException(ErrorStatus.SELLER_ROLE_REQUIRED);
}
Event event = eventRepository.findById(eventId)
.orElseThrow(() -> new ApiException(ErrorStatus.EVENT_NOT_FOUND));
if (!event.getUser().equals(user)) {
throw new ApiException(ErrorStatus._PERMISSION_DENIED);
}
String imageUrl = "";
// 업데이트리퀘스트에 업데이트할 이미지가 있다면
if (eventRequest.getImage() != null) {
// 기존 등록된 URL 가지고 이미지 원본 이름 가져오기
String eventImageName = s3FileService.extractFileNameFromUrl(event.getImageUrl());
// 가져온 이미지 원본 이름으로 S3 이미지 삭제
s3Client.deleteObject(bucket, eventImageName);
// 업로드한 파일의 S3 URL 주소
imageUrl = s3FileService.uploadFile(eventRequest.getImage(), bucket);
}
event.updateEvent(eventRequest, imageUrl);
return new UpdateEventResponse(event);
}
이렇게 설계하고

포스트맨으로 작성할 때 바디값을 form-data형식으로 값을 입력하고 image 필드만 file 형식으로 이미지파일을 넣어주면 된다.

데이터베이스에 image_url이 잘 들어갔고, S3에도 잘 들어가는 걸 확인할 수 있다.

https://hanstory33.tistory.com/218
해당 블로그를 적극 참고하였습니다.