➡️ build.gradle
// thumbnailator
implementation 'net.coobird:thumbnailator:0.4.14'
Thumbnailator 는 자바(JAVA)를 위한 간편하고 강력한 이미지 처리 라이브러리 입니다. 이 라이브러리는 이미지를 쉽게 리사이즈하거나 **변환할 수 있는 기능을 제공하여, 고품질의 썸네일 이미지 생성을 쉽게 만들어 줍니다. Thumbnailator는 단순한 API를 제공하며, 이미지의 크기 조정, 크롭, 회전, 품질 조절 등 다양한 작업을 지원합니다.
기존에 나는 클라이언트로 부터받은 이미지 원본을 그대로 AWS S3 에 저장하였다. 하지만 이는 앞서 설명한 것처럼 이미지의 양이 많아지고 용량이 높아짐에 따라 데이터 로딩 속도가 느려지는 현상으로 이어졌다.
AwsS3Service 메서드
@Service
@RequiredArgsConstructor
@Slf4j
public class AwsS3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;
public List<String> uploadFile(List<MultipartFile> multipartFiles, String dirName){
if(multipartFiles.size() > 5) {
throw new CustomException(ErrorCode.MAX_UPLOAD_PHOTO);
}
List<String> fileUrlList = new ArrayList<>();
multipartFiles.forEach(file -> {
String fileName = createFileName(file.getOriginalFilename());
String filePath = dirName + "/" + fileName; // 폴더 경로 추가
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
log.info("Uploading file: {} | Size: {} | ContentType: {}", fileName, file.getSize(), file.getContentType());
try (InputStream inputStream = file.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucket, filePath, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
// 추가
String fileUrl = amazonS3.getUrl(bucket, filePath).toString();
// S3 URL 생성 및 리스트에 추가
fileUrlList.add(amazonS3.getUrl(bucket, filePath).toString());
log.info("Uploaded file URL: {}", fileUrl);
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다.");
}
});
return fileUrlList;
}
// 먼저 파일 업로드시, 파일명을 난수화하기 위해 UUID 를 활용하여 난수를 돌린다.
public String createFileName(String fileName){
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}
// file 형식이 잘못된 경우를 확인하기 위해 만들어진 로직이며, 파일 타입과 상관없이 업로드할 수 있게 하기위해, "."의 존재 유무만 판단
private String getFileExtension(String fileName){
try{
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e){
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일" + fileName + ") 입니다.");
}
}
public void deleteFile(String fileName){
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
System.out.println(bucket);
}
createPost 메서드
@Transactional
public PostDto.ResponseDto createPost(User user, PostDto.RequestDto requestDto, List<MultipartFile> files) {
User findUser = userService.getUserId(user.getId());
Town town = new Town(
requestDto.getLatitude(),
requestDto.getLongitude(),
requestDto.getDistrict(),
requestDto.getPlaceName(),
requestDto.getPlaceAddr()
);
Post post = Post.builder()
.contents(requestDto.getContents())
.category(requestDto.getCategory())
.createdAt(LocalDateTime.now())
.modifiedAt(LocalDateTime.now())
.town(town)
.user(findUser)
.build();
List<String> imageUrls = awsS3Service.uploadFile(files, "postImages");
imageUrls.forEach(url -> {
PostImage image = PostImage.builder()
.fileName(url)
.imgUrl(url)
.post(post)
.build();
post.getImages().add(image);
});
Post savedPost = postRepository.save(post);
PostSearch postSearch = createPostSearch(savedPost, imageUrls, findUser);
postSearchRepository.save(postSearch);
User userUp = post.getUser();
if (promoteGrade(userUp)){
userUp.updateRank(userUp.getRank());
}
return new PostDto.ResponseDto(post);
}
<코드 동작 설명>
- 파일 제한 검사
사용자가 업로드할 수 있는 파일 수를 최대 5개로 제한합니다. 이를 초과하면 CustomException을 통해 에러를 반환- 파일 처리
업로드된 각 파일에 대해 반복문을 실행합니다.
createFileName 메소드를 통해 각 파일의 저장될 이름을 생성합니다.- 원본 파일 저장
파일의 바이트 배열을 읽어 ByteArrayInputStream을 생성합니다.
AWS S3에 원본 이미지 파일을 저장하고, 접근 권한을 Public Read로 설정합니다.
원본 이미지의 URL을 리스트에 추가합니다.- 이미지 리사이즈 및 저장
첫 번째 파일에 대해서만 리사이즈를 수행합니다 (isFirst 플래그 사용).
Thumbnailator 라이브러리의 Thumbnails.of() 메소드를 사용하여 입력 스트림에서 이미지를 읽고, 60x60 픽셀 크기로 리사이즈한 후, 품질을 75%로 설정합니다.
리사이즈된 이미지를 S3에 저장하고, 그 URL을 별도로 관리합니다.- 응답 생성
함수는 원본 이미지 URL 목록과 리사이즈된 이미지 URL을 포함하는 맵을 반환합니다. 이 맵은 API의 응답으로 사용될 수 있습니다.
✅ 파일 처리 및 원본 이미지 S3 저장
이 섹션에서는 업로드된 각 파일을 반복 처리합니다. 파일 이름은 createFileName 메소드를 통해 생성되며, 각 파일에 대한 메타데이터를 설정합니다.
파일의 바이트 배열을 읽어 ByteArrayInputStream으로 다시 패키징한 후, 이를 AWS S3에 업로드합니다. 파일은 PublicRead 권한으로 업로드되어 외부에서 접근 가능합니다. 업로드된 파일의 URL은 리스트에 추가되어 반환됩니다.
List<String> originalUrls = new ArrayList<>();
String compressedUrl = null;
boolean isFirst = true;
for (MultipartFile file : multipartFiles) {
String fileName = createFileName(file.getOriginalFilename());
String filePath = dirName + "/" + fileName;
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
try (InputStream inputStream = file.getInputStream()) {
byte[] bytes = inputStream.readAllBytes();
InputStream fileInputStream = new ByteArrayInputStream(bytes);
amazonS3.putObject(new PutObjectRequest(bucket, filePath, fileInputStream, metadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
String url = amazonS3.getUrl(bucket, filePath).toString();
originalUrls.add(url);
✅ 이미지 리사이즈 및 압축 이미지 S3 저장
첫 번째 파일에 대해서만 이미지를 리사이즈하고 압축합니다. 이 과정에서 Thumbnailator 라이브러리를 사용하여 이미지 크기를 60x60 픽셀로 조정하고, 품질을 75%로 설정합니다.
리사이즈된 이미지는 별도의 파일로 S3에 저장되며, 압축된 이미지의 URL도 반환됩니다.
if (isFirst) {
InputStream compressedInputStream = new ByteArrayInputStream(bytes);
ByteArrayOutputStream os = new ByteArrayOutputStream();
Thumbnails.of(compressedInputStream).size(60, 60)
.outputQuality(0.75)
.toOutputStream(os);
byte[] compressedImage = os.toByteArray();
ByteArrayInputStream uploadInputStream = new ByteArrayInputStream(compressedImage);
ObjectMetadata compressedMetadata = new ObjectMetadata();
compressedMetadata.setContentLength(compressedImage.length);
compressedMetadata.setContentType(file.getContentType());
String compressedFilePath = "compressed_" + filePath;
amazonS3.putObject(new PutObjectRequest(bucket, compressedFilePath, uploadInputStream, compressedMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
compressedUrl = amazonS3.getUrl(bucket, compressedFilePath).toString();
isFirst = false;
}
✅ 응답 구성
함수는 원본 이미지 URL 목록과 압축 이미지의 URL을 포함하는 맵을 반환합니다. 이 맵은 원본과 리사이즈된 이미지에 대한 접근 정보를 제공하여, 클라이언트 측에서 필요에 따라 적절한 이미지를 선택할 수 있도록 합니다
Map<String, Object> response = new HashMap<>();
response.put("resizedUrl", compressedUrl);
response.put("originalUrls", originalUrls);
return response;
public Map<String, Object> uploadFile(List<MultipartFile> multipartFiles, String dirName) throws IOException {
if(multipartFiles.size() > 5) {
throw new CustomException(ErrorCode.MAX_UPLOAD_PHOTO);
}
List<String> originalUrls = new ArrayList<>();
String compressedUrl = null;
boolean isFirst = true;
for (MultipartFile file : multipartFiles) {
String fileName = createFileName(file.getOriginalFilename());
String filePath = dirName + "/" + fileName;
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
try (InputStream inputStream = file.getInputStream()) {
byte[] bytes = inputStream.readAllBytes(); // InputStream을 byte 배열로 변환
InputStream fileInputStream = new ByteArrayInputStream(bytes); // 원본 파일 InputStream
amazonS3.putObject(new PutObjectRequest(bucket, filePath, fileInputStream, metadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
String url = amazonS3.getUrl(bucket, filePath).toString();
originalUrls.add(url);
if (isFirst) {
InputStream compressedInputStream = new ByteArrayInputStream(bytes); // 리사이즈를 위한 복사된 InputStream
ByteArrayOutputStream os = new ByteArrayOutputStream();
Thumbnails.of(compressedInputStream).size(60, 60)
.outputQuality(0.75)
.toOutputStream(os);
byte[] compressedImage = os.toByteArray();
ByteArrayInputStream uploadInputStream = new ByteArrayInputStream(compressedImage);
ObjectMetadata compressedMetadata = new ObjectMetadata();
compressedMetadata.setContentLength(compressedImage.length);
compressedMetadata.setContentType(file.getContentType());
String compressedFilePath = "compressed_" + filePath;
amazonS3.putObject(new PutObjectRequest(bucket, compressedFilePath, uploadInputStream, compressedMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
compressedUrl = amazonS3.getUrl(bucket, compressedFilePath).toString();
isFirst = false;
}
}
}
Map<String, Object> response = new HashMap<>();
response.put("resizedUrl", compressedUrl);
response.put("originalUrls", originalUrls);
return response;
}
@Transactional
public PostDto.ResponseDto createPost(User user, PostDto.RequestDto requestDto, List<MultipartFile> files) throws IOException {
User findUser = userService.getUserId(user.getId());
Town town = new Town(
requestDto.getLatitude(),
requestDto.getLongitude(),
requestDto.getDistrict(),
requestDto.getPlaceName(),
requestDto.getPlaceAddr()
);
Post post = Post.builder()
.contents(requestDto.getContents())
.category(requestDto.getCategory())
.createdAt(LocalDateTime.now())
.modifiedAt(LocalDateTime.now())
.town(town)
.user(findUser)
.build();
Map<String, Object> uploadResults = awsS3Service.uploadFile(files, "postImages");
String resizedImageUrl = (String) uploadResults.get("resizedUrl");
List<String> originalUrls = (List<String>) uploadResults.get("originalUrls");
// 리사이즈된 이미지
PostImage resizeImage = PostImage.builder()
.fileName(resizedImageUrl)
.imgUrl(resizedImageUrl)
.post(post)
.build();
post.getImages().add(resizeImage);
// 원본 이미지 URL들을 각각 처리
originalUrls.forEach(url -> {
PostImage image = PostImage.builder()
.fileName(url)
.imgUrl(url)
.post(post)
.build();
post.getImages().add(image);
});
Post savedPost = postRepository.save(post);
PostSearch postSearch = createPostSearch(savedPost, originalUrls, findUser);
postSearchRepository.save(postSearch);
User userUp = post.getUser();
if (promoteGrade(userUp)){
userUp.updateRank(userUp.getRank());
}
return new PostDto.ResponseDto(savedPost);
}
🏞️ 원본사진 & 파일 크기
🏞️ 리사이즈된 사진 & 파일 크기
<변경 전>
<변경 후>