이미지 리사이징을 도입한 이유는 S3 프리티어 저장 공간이 90% 이상 차면서 이미지 관리의 필요성을 느꼈습니다.
서버에 저장된 이미지의 용량을 최적화하면 저장 공간을 효율적으로 활용할 수 있습니다. 효율적인 용량 관리는 비용 절감에 도움이 될 수 있습니다. 그리고 사용자들은 다양한 디바이스에서 중고거래 사이트를 이용합니다. 이미지를 리사이징하여 다양한 디바이스에서도 적절한 크기로 보여줄 수 있습니다.
단순하게 용량을 제한 하는것 만으로도 무지막지한 용량의 이미지를 차단할 수 있는 장점이 있었다.
java.awt.Graphics2D, Imgscalr, Marvin 등 이미지 리사이징 라이브러리 사용 시 ,이미지IO와 변환과정에서 3mb이상의 이미지는 리사이징하는데 상당히 오랜 시간이 소요된다.
아래와 같이 multipart 사이즈에 제약 조건을 application.yml파일에 설정하였다.
spring:
servlet:
multipart:
maxFileSize: 3MB # 파일 하나의 최대 크기
maxRequestSize: 3MB # 한 번에 최대 업로드 가능 용량
백엔드 vs Lambda
3mb이상의 이미지 IO 발생 시, 제약없이 빠르게 업로드 후 리사이징 할 수 있는 방법이 있었다. CloudFront와 AWS LAMBDA를 사용하여 온디멘드 방식의 리사이징을 하는 것이다. Serverless인 람다에서 이미지를 리사이징하는 방법은 저장된 큰 이미지의 용량을 줄이고 좀 더 빠른 업로드를 하는 이점은 있었으나, 하나의 이미지가 원본 S3공간과 리사이징된 S3공간 두 곳으로 저장되어 관리포인트가 더 늘어났고 그 결과 용량절감의 효과가 떨어졌다.그렇기 때문에 Marvin 라이브러리 사용 백엔드 쪽에서 처리하는걸 선택하였다.
marvin 라이브러리를 사용하기 위해선 build.gradle에 아래의 내용을 추가해야한다.
implementation 'com.github.downgoon:marvin:1.5.5'
implementation 'com.github.downgoon:MarvinPlugins:1.5.5'
이 코드는 Spring Framework에서 이미지 업로드 및 리사이징을 처리하는 메서드와 리사이징을 담당하는 별도의 메서드 이다.
public String imageUpload(@RequestParam("upload") MultipartFile file) throws IOException {
try {
String fileName = file.getOriginalFilename();
String ext = fileName.substring(fileName.lastIndexOf("."));
String uuidFileName = UUID.randomUUID() + ext;
// 리사이징
MultipartFile resizedImage = resizer(file);
try (InputStream inputStream = resizedImage.getInputStream()) {
ObjectMetadata metadata = new ObjectMetadata();
byte[] bytes = IOUtils.toByteArray(inputStream);
metadata.setContentType(resizedImage.getContentType());
metadata.setContentLength(bytes.length);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// S3에 업로드
s3Config.amazonS3Client().putObject(
new PutObjectRequest(bucket, uuidFileName, byteArrayInputStream,
metadata).withCannedAcl(
CannedAccessControlList.PublicRead));
}
String s3Url = s3Config.amazonS3Client().getUrl(bucket, uuidFileName).toString();
return s3Url;
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드 및 리사이징에 실패했습니다.", e);
}
}
private MultipartFile resizer(MultipartFile originalImage) {
try {
BufferedImage image = ImageIO.read(originalImage.getInputStream());
int originWidth = image.getWidth();
int originHeight = image.getHeight();
int targetWidth = 500; // 원하는 리사이즈 폭
if (originWidth <= targetWidth) {
return originalImage;
}
MarvinImage marvinImage = new MarvinImage(image);
Scale scale = new Scale();
scale.load();
scale.setAttribute("newWidth", targetWidth);
scale.setAttribute("newHeight", targetWidth * originHeight / originWidth);
scale.process(marvinImage.clone(), marvinImage, null, null, false);
BufferedImage resizedImage = marvinImage.getBufferedImageNoAlpha();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// JPEG 화질 설정
Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByFormatName("jpeg");
if (imageWriters.hasNext()) {
ImageWriter imageWriter = imageWriters.next();
ImageWriteParam writeParam = imageWriter.getDefaultWriteParam();
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
writeParam.setCompressionQuality(0.999f); // 예시로 0.9로 설정, 필요에 따라 조절
imageWriter.setOutput(ImageIO.createImageOutputStream(baos));
imageWriter.write(null, new IIOImage(resizedImage, null, null), writeParam);
imageWriter.dispose();
}
baos.flush();
return new CustomMultipartFile(originalImage.getName(), "jpg", originalImage.getContentType(), baos.toByteArray());
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 리사이징에 실패했습니다.", e);
}
}
- imageUpload 메서드: 클라이언트로부터 받은 MultipartFile을 처리하여 S3에 업로드하고 해당 이미지의 URL을 반환합니다.
- file.getOriginalFilename(): 업로드된 파일의 원래 파일 이름을 가져옵니다.
- UUID.randomUUID() + ext: 파일 이름에 UUID를 추가하여 중복을 방지하고 고유성을 유지합니다.
- resizer(file): 리사이징을 수행하는 메서드를 호출하여 이미지를 원하는 크기로 조절합니다.
- 이미지를 S3에 업로드하고 해당 이미지의 URL을 생성하여 반환합니다.
- resizer 메서드: 주어진 MultipartFile을 리사이징하여 새로운 MultipartFile을 반환합니다.
- ImageIO.read(originalImage.getInputStream()): MultipartFile에서 이미지를 읽어 BufferedImage로 변환합니다.
- 리사이징을 위해 Marvin 프레임워크를 사용합니다. 이미지의 가로 폭이 목표 폭보다 작으면 리사이징을 수행하지 않고 원본 이미지를 반환합니다.
- 이미지를 조절하고 JPEG로 변환하여 새로운 MultipartFile을 생성합니다.
메모리에 있는 이미지 데이터를 MultipartFile 인터페이스를 필요로 하는 코드와 통합할 때 유용합니다.
package com.example.booktalk.domain.imageFile.service;
import com.mysema.commons.lang.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class CustomMultipartFile implements MultipartFile {
private final String name;
private String originalFilename;
private String contentType;
private final byte[] content;
boolean isEmpty;
public CustomMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
Assert.hasLength(name, "Name must not be null");
this.name = name;
this.originalFilename = (originalFilename != null ? originalFilename : "");
this.contentType = contentType;
this.content = (content != null ? content : new byte[0]);
this.isEmpty = false;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getOriginalFilename() {
return this.originalFilename;
}
@Override
public String getContentType() {
return this.contentType;
}
@Override
public boolean isEmpty() {
return (this.content.length == 0);
}
@Override
public long getSize() {
return this.content.length;
}
@Override
public byte[] getBytes() throws IOException {
return this.content;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(this.content);
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(this.content, dest);
}
}
인터페이스 구현: MultipartFile 인터페이스의 메서드들을 구현합니다.
- getName(): 파일의 이름을 반환합니다.
- getOriginalFilename(): 원래 파일의 이름을 반환합니다.
- getContentType(): 파일의 컨텐츠 타입을 반환합니다.
- isEmpty(): 파일이 비어있는지 여부를 반환합니다.
- getSize(): 파일의 크기를 반환합니다.
- getBytes(): 파일의 내용을 바이트 배열로 반환합니다.
- getInputStream(): 파일의 내용을 읽어오는 InputStream을 반환합니다.
- transferTo(File dest): 파일의 내용을 지정된 파일로 복사합니다.
리사이징 전 리사이징 후
위에가 리사이징 전 아래가 리사이징 후 용량이 확실히 변한걸 확인할수 있다.
'프로젝트 > booktalk(책 중고 거래 서비스)' 카테고리의 다른 글
QueryDSL product 조회 코드 구현(페이징 처리,카테고리처리, 조회 결과 정렬) (0) | 2024.01.30 |
---|---|
product (1) | 2024.01.25 |
[SpringBoot] AWS S3로 이미지 업로드하기 (1) | 2024.01.23 |
1/15 (0) | 2024.01.23 |
1/11 (0) | 2024.01.23 |