이미지 리사이징?
- 이미지의 크기를 재조정하는 것으로 서버에서 사용할 때 원본의 이미지를 보여줄 필요가 없는 경우 이미지의 크기를 줄여 서버의 부하를 줄일 수 있다.
기능 추가를 생각하게 된 이유
- 사실 처음엔 이미지에 대한 처리를 고민을 한 적이 아예 없었다. 업로드가 잘 된다고 좋아라~ 하기만 했을뿐 ‘‘이미지’‘라는 포인트에 별다른 생각 자체가 없었다.
- 헌데 AWS에 배포를 시작한 후부터 3가지 문제에 직면하게 되어 이들을 해결하고자 방법을 찾게 됐고, 해결법으로 알게 된 방법이 바로 이미지 리사이징이다. (3가지 이유가 모두 다 얽혀있음)
1. S3에 올라가는 이미지 크기가 들쑥날쑥
- 이미지의 크기가 화면이 가득찰만큼 큰 경우부터 되게 작은 경우까지 크기가 너무 중구난방이다.
- 특히 이 문제는 메뉴판을 모달로 띄울 때 크기가 너무 압도적인 경우가 있어 사소하지만 문제가 됐다.
2. 위의 이유로 인한 이미지 용량 증가
- 결국 크기가 매우 큰 이미지들이 존재하므로 자연스럽게 용량 역시 커진다.
- 이로 인해 AWS S3 스토리지 공간을 효율적이게 사용하지 못 하게 되고, 무엇보다 서버에서 이미지를 불러오거나 업로드를 하는 과정에서 많은 부하가 발생한다.
3. 결국 배포된 서비스가 자주 뻗음
- 배포돼있는 EC2 서버는 프리티어라 RAM 용량이 1GB밖에 되지 않는다. 따라서 이미지를 단시간에 많이 업로드 하거나 출력을 자주 하는 경우 서비스가 정말 자주 뻗는 경험을 하게 되었다.
구현 방법 선정
- 프론트엔드, 백엔드 모두에서 구현이 가능하고 AWS의 Lambda@Edge를 활용하여 구현하는 방법도 있다.
Lambda@Edge란?
AWS의 CloudFront의 기능 중 하나로 애플리케이션의 사용자에게 더 가까운 위치에서 코드를 실행하여 성능을 개선하고 지연 시간을 단축할 수 있게 해줍니다. (AWS Lambda@Edge 소개글)
- 아직 사용해본적 없는 AWS의 CloudFront와 그에 속한 Lambda 기능으로, 많은 분들이 이 기능을 토대로 이미지 프로세싱을 구현하시는 듯 하다.
- 결론부터 말하자면, 난 이 기능을 사용하지 않기로 했기에 현재 이 부분에 대하여 큰 공부를 하지 않았다. 이후에 이 서비스를 도입하게 될 때 다시 한 번 정리해야겠다.
백엔드로 결정
- 언급했다싶이 Lambda@Edge를 사용하지 않고, 프론트엔드 역시 아닌 백엔드단에서 이를 구현하기로 했는데 이유는 아래와 같다.
- javascript가 아닌 java를 이용하여 코드를 구현하고 싶음.
- Lambda@Edge를 사용하기에 추가적으로 사용해야 하는 AWS 서비스에 대한 비용 부담이 크고, 이를 사용하게 되면 On-the-fly 방식으로 리사이징을 진행하는데 S3의 원본 이미지는 그대로 남아있으므로 S3 스토리지 용량 또한 보다 활용적이게 사용하지 못한다.
- 물론 백엔드로 구현 하는 경우에도 java에서 이미지 리사이징을 처리하는 로직이 추가되므로 새로운 부하가 발생한다는 단점은 존재한다.
라이브러리 선택
- Java 이미지 리사이징 참고 게시글
- 막상 찾아보니 java 기본적으로 지원하는 기능과 함께 다양한 라이브러리들이 존재하는 것을 알게 되었는데 라이브러리들이 오래전부터 업데이트가 이루어지지 않고 있었다. (Lambda@Edge같은 Edge Computing 기술들의 보급으로 인한 것 같음)
- 고민 끝에 준수한 품질과 처리 속도를 가진
Imgscarl
라이브러리를 선택하였다.
/*
build.gradle 추가
Imgscalr 이미지 리사이징 라이브러리
https://mvnrepository.com/artifact/org.imgscalr/imgscalr-lib
*/
implementation group: 'org.imgscalr', name: 'imgscalr-lib', version: '4.2'
백엔드로 구현해보자
- 기존 이미지 업로드 코드에 아래의 로직을 작성한다.
/*
이미지 리사이징 메소드
*/
public static MultipartFile getResizedMultipartFile(MultipartFile multipartFile, String originalFileName) throws IOException {
final int TARGET_IMAGE_WIDTH = 420;
final int TARGET_IMAGE_HEIGHT = 200;
final String TARGET_IMAGE_TYPE = "jpg";
BufferedImage bi = ImageIO.read(multipartFile.getInputStream());
bi = resizeImages(bi, TARGET_IMAGE_WIDTH, TARGET_IMAGE_HEIGHT);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(bi, TARGET_IMAGE_TYPE, baos);
baos.flush();
// MultipartFile 인터페이스를 구현한 구현체를 통해 MultipartFile을 생성해줌
return new CustomMultipartFile(baos.toByteArray(), multipartFile.getName(), originalFileName, TARGET_IMAGE_TYPE, false, baos.toByteArray().length);
}
private static BufferedImage resizeImages(BufferedImage originalImage, int width, int height) {
return Scalr.resize(originalImage, width, height);
}
MultipartFile => BufferedImage => MultipartFile로 변환
- 입력받은 MultipartFile을 BufferedImage로 변환한 후, Scalr.resize 메소드를 이용하여 BufferedImage를 리사이징해준다.
- 문제는 이렇게 변환한 BufferdImage를 다시 MultipartFile로 변환할 방법이 구현되어 있지 않다. 따라서 이를 위해선 MultipartFile 인터페이스를 구현할 별도의 구현체를 만들어야 한다.
org.springframework.mock.web.MockMultipartFile
를 사용해도 되는 것 같은데, 이는 너무 Spring에 의존되는 방법이라 구현체를 만들기로 결정하였다.
- StackOverFlow 참고
- 직접 만든 구현체
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/*
MultipartFile을 생성하기 위해
MultipartFile 인터페이스를 상속받은 클래스
*/
public class CustomMultipartFile implements MultipartFile {
private final byte[] bytes;
private final String name;
private final String originalFilename;
private final String contentType;
private final boolean isEmpty;
private final long size;
public CustomMultipartFile(byte[] bytes, String name, String originalFilename, String contentType, boolean isEmpty, long size) {
this.bytes = bytes;
this.name = name;
this.originalFilename = originalFilename;
this.contentType = contentType;
this.isEmpty = isEmpty;
this.size = size;
}
@Override
public String getName() {
return name;
}
@Override
public String getOriginalFilename() {
return originalFilename;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return isEmpty;
}
@Override
public long getSize() {
return size;
}
@Override
public byte[] getBytes() throws IOException {
return bytes;
}
@Override
public InputStream getInputStream() throws IOException {
return null;
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
}
}
반환 받은 MultipartFile을 이후 로직에서 사용
- 아래는 AWS S3에 이미지 업로드를 하는 로직의 일부이다. 위에서 만든 메소드를 활용하여 리사이징된 새로운 MultipartFile을 만들어준 후, 이를 File로 변환하여 이후 로직에 사용하니 정상적으로 작동하는 것을 확인하였다.
/*
이미지 리사이징
*/
// 위는 생략...
MultipartFile resizedMultipartFile = getResizedMultipartFile(multipartFile, originalFileName);
File uploadFile = convert(resizedMultipartFile).orElseThrow(
() -> new IllegalArgumentException("전환 실패"));
imageUrl = getImageUrl(uploadFile, storeFileName);
}
return new UploadFile(originalFileName, storeFileName, imageUrl);
}
적용 결과
- 기존에 업로드한 이미지와 같은 이미지를 리사이징 후 올린 이미지의 크기를 비교해보자.
- 위에서 보는 것처럼 대략 3% 정도로 용량이 줄어든 것을 알 수 있다.
- 물론 이는 모든 이미지에 대해서 해당할 수 있는 수치도 아니며 아직 완벽하게 리사이징 조건을 설정한 것도 아니라서 시행착오를 거친 후, 최적화된 옵션과 이미지 크기를 선정하기로 했다.
결론
- 이미지 리사이징을 구현하는 시간 투자 대비 그 효과는 생각보다 매우 큰 것 같습니다. 현재 메인 페이지에서 노출되는 이미지의 수가 많은데, 이들을 원본 그대로 로드하는 시간이 길어 User Interaction에까지 도달하는 시간이 생각보다 길었으며 이로 인한 UX 역시 좋지 못하였습니다.
- 하지만 이를 도입 후 이미지들을 바꿔주니 확실히 성능적인 체감이 컸기에 만족도가 매우 큽니다.
- 현재 현실과 타협하여 Lambda@Edage를 활용한 On-thy-fly 방식의 리사이징을 채택하지 못하였으나 여유가 될 때 그 부분을 따로 공부하고 다시 한 번 정리하여 포스팅을 하도록 하겠습니다.
댓글남기기