Post 등록 - S3 이미지 업로드
구현 기능
- 게시글 작성 및 파일 업로드 동시 처리
- 다중 파일 업로드
- DB에는 파일 관련 정보 및 저장 경로만 저장하고 실제 파일은 S3에 저장
- S3 프리티어 버킷 용량이 한정 되있으므로 최대용량을 설정해준다.
- S3 계정 정보 입력
application.yml
spring:
servlet:
multipart:
maxFileSize: 10MB
maxRequestSize: 20MB
#aws
cloud:
aws:
credentials:
accessKey: YOUR_ACCESS_KEY
secretKey: YOUR_SECRET_KEY
s3:
bucket: YOUR_BUCKET_NAME
region:
static: YOUR_REGION
stack:
auto: false
build.gradle
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}
@Controller
@PostMapping
public ResponseEntity<?> create(
@AuthMember MemberDetails memberDetails,
PostCreateVO postCreate) {
List<String> imagePathList = awsS3Service.StoreFile(postCreate.getFiles());
...
return new ResponseEntity<>(new SingleResponse<>(postId), HttpStatus.CREATED);
}
문제
@RequestBody
는 body로 전달받은 JSON형태의 데이터를 파싱해주지만, Content-Type
이 multipart/form-data
로 전달되어 올 때는 Exception
을 발생시킴
해결
1. @RequestPart
이용
@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
public Long create(
@RequestPart(value="image", required=false) List<MultipartFile> files,
@RequestPart(value = "requestDto") BoardCreateRequestDto requestDto
) throws Exception {
...
}
2. @RequestParam
이용
@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
public Long create(
@RequestParam(value="image", required=false) List<MultipartFile> files,
@RequestParam(value="id") String id,
@RequestParam(value="title") String title,
@RequestParam(value="content") String content
) throws Exception {
...
}
3. VO클래스 생성
- 전달받을 데이터가 적을 경우는
@RequestPart
나 @RequestParam
을 사용해도 상관없으나, 전달받을 데이터가 많을 경우 코드가 지저분하게 보일 수 있어 게시글 -파일 처리용 VO클래스를 하나 선언하여 처리하였다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostCreateVO {
@NotBlank
private String content;
private List<TagDto> tagDtos;
private List<MultipartFile> files;
@Builder
public PostCreateVO(String content, List<TagDto> tagDtos, List<MultipartFile> files) {
this.content = content;
this.tagDtos = tagDtos;
this.files = files;
}
}
S3 파일 업로드
List<MultipartFile>
을 전달받아 파일을 S3 버킷에 저장한 후 저장한 파일 경로를 List<String> imagePathList
에 담아 반환 한다.
업로드 흐름
List<MultipartFile>
의 파일 유무를 체크 - 없을경우 NoImage
예외를 던짐
getOriginalFilename()
메서드로 파일 이름을 추출한후 createStoreFileName()
메서드로 중복되지 않는 파일이름 생성한다.
S3
에 이미지 파일을 업로드한다. - 업로드 실패할 경우 UploadFailed
예외를 던짐
- 추후
S3
에 저장한 이미지를 불러와 빠르게 응답하기 위해 CloudFront 도메인 + 파일명
을 경로로 imagePathList
에 담아 리턴한다.
AWS CloudFront
정적, 동적 컨텐츠를 빠르게 응답하기 위한 캐시 기능을 제공하는 CDN 서비스
@Slf4j
@RequiredArgsConstructor
@Service
public class AwsS3Service {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
@Value("${cloud.aws.cloudFront.distributionDomain}")
private String cloudFront;
public List<String> StoreFile(List<MultipartFile> files) {
isFileExist(files);
List<String> imagePathList = new ArrayList<>();
for (MultipartFile file : files) {
String originalName = file.getOriginalFilename();
String storeName = createStoreFileName(originalName);
long size = file.getSize();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(file.getContentType());
objectMetadata.setContentLength(size);
try (InputStream inputStream = file.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucketName, storeName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw new UploadFailed();
}
String imagePath = cloudFront + "/" + storeName;
imagePathList.add(imagePath);
}
return imagePathList;
}
private void isFileExist(List<MultipartFile> files) {
if (files.isEmpty()) {
throw new NoImage();
}
}
(2)
private String createStoreFileName(String originalName) {
String ext = extractExt(originalName);
String uuid = UUID.randomUUID().toString();
return uuid + "." + ext;
}
private static String extractExt(String originalName) {
int pos = originalName.lastIndexOf(".");
return originalName.substring(pos + 1);
}