public class S3Service {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String s3Bucket;
// 파일을 S3에 업로드
public List<String> upload(
List<MultipartFile> multipartFiles,
String dirName
) throws IOException {
List<String> fileNames = new ArrayList<>();
for (MultipartFile multipartFile : multipartFiles) {
// MultipartFile -> File 변환
File uploadFile = convert(multipartFile).orElseThrow(
() -> new S3Exception(ClientErrorCode.S3_CONVERT_FAILURE));
String fileName = uploadFile.getName();
upload(uploadFile, dirName);
fileNames.add(fileName);
}
return fileNames;
}
// Overloading
private String upload(File uploadFile, String dirName) {
String s3FileName = dirName + "/" + uploadFile.getName();
String uploadImageURL = putS3(uploadFile, s3FileName);
boolean deleteSuccessful = uploadFile.delete(); // 임시로 저장된 이미지 삭제
if (!deleteSuccessful) {
throw new S3Exception(ClientErrorCode.S3_TEMP_IMAGE_DELETE_FAILURE);
}
return uploadImageURL;
}
// 실제로 S3로 업로드
private String putS3(File uploadFile, String s3FileName) {
amazonS3Client.putObject(
new PutObjectRequest(s3Bucket, s3FileName, uploadFile)
.withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3Client.getUrl(s3Bucket, s3FileName).toString();
}
public Optional<File> convert(MultipartFile multipartFile) throws IOException {
String originalFileName = multipartFile.getOriginalFilename();
if (originalFileName == null) {
log.error("originalFileName == null");
return Optional.empty();
}
String fileExtension = originalFileName
.substring(originalFileName.lastIndexOf("."));
String fileName = File.separator + originalFileName
.substring(0, originalFileName.lastIndexOf(".")) + "-";
// 유니크한 파일명 -> createTempFile 중복방지: 자체적으로 난수 생성
File convertFile = File.createTempFile(fileName, fileExtension);
if (convertFile.exists()) {
try (FileOutputStream fileOutputStream = new FileOutputStream(convertFile)) {
// fileOutputStream 데이터 -> 바이트 스트림으로 저장
fileOutputStream.write(multipartFile.getBytes());
}
return Optional.of(convertFile);
}
throw new S3Exception(ClientErrorCode.S3_CONVERT_FAILURE);
}
1. 파일 변환: MultipartFile 객체를 File 객체로 변환
2. 임시 파일 생성: 변환 과정에서 임시 파일이 생성 (원본 파일의 이름과 랜덤으로 생성된 난수를 결합한 이름으로 서버의 로컬 디스크에 저장)
3. S3 업로드: 임시 파일을 S3 에 업로드한다.
4. 임시 파일 삭제: S3 업로드가 완료된 후에는 서버의 로컬 디스크에서 임시 파일을 삭제
• 단계별로 어떠한 처리를 하는지 명확하게 판단할 수 있다.
• 각 단계마다 에러 핸들링을 할 수 있다.
• 임시 파일을 저장하면서 삭제가 되지 않을 경우 디스크 공간에 대한 이슈가 발생할 수 있다.
• 디스크에 파일을 쓰고 읽는 작업이기 때문에 작업량이 많아지게 된다면 서버의 전체적인 성능저하가 발생할 수 있다.
@Service
public class S3Service {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String s3Bucket;
public List<String> upload(
List<MultipartFile> images,
String folderName
) throws IOException {
List<String> fileNameList = new ArrayList<>();
for (MultipartFile image : images) {
String fileName = upload(image, folderName);
fileNameList.add(fileName);
}
System.out.println("fileNameList = " + fileNameList);
return fileNameList;
}
public String upload(
MultipartFile multipartFile,
String folderName
) throws IOException {
String fileName = UUID.randomUUID().toString()
.substring(19) + "-" + multipartFile.getOriginalFilename();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());
amazonS3Client.putObject(
new PutObjectRequest(s3Bucket, "images/" + folderName + "/"
+ fileName, multipartFile.getInputStream(), objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead)
);
return fileName;
}
• 단계 간소화: MultipartFile
을 File
로 변환하는 과정, 임시 파일을 생성 및 삭제 등 추가적인 단계없이 바로 S3 에 업로드
• 에러 발생 가능성 감소: 단계가 간소화되면서 파일 변환, 임시 파일 관리(생성, 삭제) 등으로 인해 에러가 발생하지 않는다.
• 디스크 공간 절약: 서버의 디스크 공간을 사용하지 않아 자원 사용 면에서 효율적이고, 디스크에 파일을 쓰고 읽는 작업이 없으므로 시스템의 I/O 부하가 줄어들어 성능이 향상될 수 있다.
코드가 너무 간결해졌다. 가독성도 더 높아지고 이미지가 S3 업로드 될 때 걸리는 시간은 네트워크 상태에 따라 다르지만 MultipartFile을 File 로 변환하는 과정이 없기 때문에 더 빠를 것으로 예상된다.