avatar
Steady

[Java] 이미지 용량을 줄여보자

Java
3 months ago
·
6 min read

until-1588

어디에 쓰이나?


서버 개발을 하다보면 트래픽을 생각하지 않을 수가 없게 된다. 이때 단순 텍스트로 이루어진 데이터들은 크게 문제가 되지 않지만 이미지나 동영상이 끼게 되면 아무래도 복잡해진다. 트래픽은 다 서버 비용을 늘리는 주된 범인이니 해결 방법을 찾아야했다.

그래서 우선적으로 이미지의 용량을 줄이는 방법을 생각하게 되었다.

이미지 용량 줄이는 법


이미지의 용량을 줄이는 방법은 크게 두 가지가 있는데

1. 해상도 줄이기

높이와 너비 픽셀 수를 조정하여 이미지의 사이즈 자체를 줄여버리는 방법으로 확장자와 상관없이 사용 가능하고 일정 크기까지는 그렇게 불편하게 보이지는 않는다.

2. 압축률 조정하기

이미지 파일들은 확장자 별로 2가지 방식의 압축 방식을 가지고 있다.

손실 압축 (ex. JPG)

손실 압축을 사용하는 확장자의 경우에는 압축률을 줄일수록 다양한 색상이나 디테일을 포기하는 대신 용량이 확연히 줄어들게 된다. 하지만 압축률에 따른 이미지가 정말 이상해진다.

비손실 압축 (ex. PNG)

비손실 압축을 사용하는 확장자의 경우에는 압축률에 따른 이미지 손상이 크게 눈에 띄진 않지만, 용량의 변화가 크지 않다.

어떻게 할까?


내가 개발하는 기능의 경우에는 다양한 확장자의 이미지 파일을 다뤄야 하고 용량을 최대한으로 줄여 트래픽을 줄여야 하는 상황이라 해상도를 줄이는 방법을 선택하기로 했다.

(사실 JPG 압축률을 한번 줄여보긴 했는데 압축률을 많이 줄여서 그런지 내 기준으로 못 쓸만한 이미지가 되었다.)

개발 과정


개발 하다 보니 1:1 비율로 이미지를 자르는 기능도 필요해 추가하게 되었다.

public class ImageUtil {

    // POST WEB 통신을 통해 전달받는 데이터 포맷이 MultipartFile 이었다.
    public MultipartFile resizeAndCrop(MultipartFile file, float quality) throws IOException {

        // 원본 이미지 읽기
        BufferedImage originalImage = ImageIO.read(file.getInputStream());

        // 해상도 낮추기(0.0 ~ 1.0 까지의 실수를 받아 얼마나 줄일지를 결정했다.)
        BufferedImage resizeImage = resizeImage(originalImage, quality);

        // 이미지를 1:1 비율로 변환
        BufferedImage croppedImage = cropImage(resizeImage);

        // 이미지를 스트림에 저장
        // ImageIO.write 부분을 변경해 파일을 직접 저장할 수도 있다.
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ImageIO.write(croppedImage, "jpg", byteArrayOutputStream);  // format: "jpg", "png" 등

        // ByteArrayInputStream을 사용하여 MultipartFile로 변환
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        MultipartFile resultFile = new MockMultipartFile(
                file.getName(),                // 원본 파일 이름
                file.getOriginalFilename(),    // 원본 파일 이름 그대로 사용
                "image/jpeg",                  // MIME 타입 설정
                byteArrayInputStream           // 입력 스트림으로 변환
        );

        // 결과 파일을 반환
        return resultFile;
    }

    // 해상도를 낮추는 메서드
    private BufferedImage resizeImage(BufferedImage originalImage, float quality) {

        // 너비와 높이를 quality 값에 맞는 비율로 조정해 임시 이미지 파일 생성
        int targetWidth = (int)(originalImage.getWidth() * quality);
        int targetHeight = (int)(originalImage.getHeight() * quality);
        BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, originalImage.getType());

        // 사이즈에 맞춰 이미지를 실제로 그려낸다.
        Graphics2D g2d = resizedImage.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
        g2d.dispose();

        return resizedImage;
    }

    // 중앙을 기준으로 1:1 비율로 크롭하는 메서드
    private BufferedImage cropImage(BufferedImage image) {
        int width = image.getWidth();
        int height = image.getHeight();

        // 가로와 세로 중 짧은 쪽으로 1:1 비율 만들기
        int newSize = Math.min(width, height);

        // 중심 좌표 계산
        int x = (width - newSize) / 2;
        int y = (height - newSize) / 2;

        // 중앙을 기준으로 크롭
        return image.getSubimage(x, y, newSize, newSize);
    }
}

돌아보며


단순한 기능이라 생각했지만 나름 시간을 많이 잡아먹었다.

결과적으로 사용은 하진 않았지만, 압축률을 조정해 보며 직접 테스트해 본 건 좋은 경험이었던 것 같다. 이 기억이 남아있다면 다음에는 시간을 절약할 수 있길 바라본다.

추가로 곧 동영상도 다루게 될 것 같은데 시간이 된다면 동영상을 다루는 것도 작성해 보도록 하겠다.







여기서는 꾸준해보자