[Spring Boot] Webp로 이미지 파일 최적화하기📉

동재·2024년 8월 1일
4

Delgo

목록 보기
1/3
post-thumbnail

📌개요

안녕하세요, 백엔드 개발자 이동재입니다.😄

이번 프로젝트에서 사진 파일 관리를 담당하게 되면서 몇 가지 문제가 발생했습니다.

이를 해결하기 위해 다양한 방법을 찾아보다가, 해결책으로 Webp를 도입했습니다. 해당 결과가 만족스러워 Webp 적용 과정을 기록해보려고 합니다.

Webp란?

WebP는 구글이 개발한 이미지 포맷으로, JPEG, PNG보다 뛰어난 압축 효율을 제공합니다.

손실 및 무손실 압축을 지원하며, 이미지 품질을 유지하면서 파일 크기를 크게 줄일 수 있습니다.

현대의 웹 브라우저에서 널리 지원되어 웹 페이지 로딩 속도를 개선하는 데 유용합니다.



🛠️원인 분석

사진 파일은 두 가지 종류로 구분할 수 있습니다

  1. 운영진이 수집 및 가공하는 사진 파일 (Ex. 장소 상세 소개 이미지)
  2. 사용자가 등록하는 사진 파일 (Ex. 게시글 이미지)

초기에는 두 종류의 사진을 모두 .png 또는 .jpg 파일로 관리했습니다. 특히 1번 유형의 사진 파일은 디자이너가 직접 이미지를 만들어 제공했기 때문에 크기가 상당히 컸습니다.

이로 인해 첫 번째 유형의 사진 파일에서 문제가 발생했고, 두 번째 유형의 사진 파일이 많아질 경우 예상되는 문제도 있었습니다.

🔥첫 번째 문제

첫 번째 문제는 클라이언트에서 이미지를 로딩하는 데 시간이 많이 걸린다는 점이었습니다.

Image

특히 문제가 되었던 페이지는 위와 같은 장소 상세 페이지였습니다.

해당 페이지는 여러 장의 사진이 하나의 화면을 구성하는 것이 아니라 하나의 통짜 이미지로 되어 있었고, 해당 이미지를 고화질로 보여주려다 보니 로딩이 오래 걸렸습니다.

🔥두 번째 문제

두 번째 문제는 비용이었습니다.

이제 막 사용자에게 배포한 프로그램에서 걱정하기에는 이른 고민이긴 하지만, 나중에 사진 파일의 저장 형식을 변경하려 하면 기존 사진 파일을 적절하게 수정하고 적용하기가 쉽지 않아 보였습니다.

그래서 선제적으로 파일 크기를 줄여서 저장할 수 있는 방법을 찾아보게 되었습니다.



🛠️문제 해결

첫 번째, 두 번째 문제 모두 Webp 적용으로 해결할 수 있었습니다.

구현 스펙

Java: 17
Spring Boot: 2.6.11

Gradle Config

  // Image Processing
    implementation "com.sksamuel.scrimage:scrimage-core:4.0.32"
    implementation "com.sksamuel.scrimage:scrimage-webp:4.0.32"

Service

    public File convertToWebp(String fileName, File originalFile) {
        try {
            return ImmutableImage.loader()// 라이브러리 객체 생성
                    .fromFile(originalFile) // .jpg or .png File 가져옴
                    .output(WebpWriter.DEFAULT, new File(PHOTO_DIR + fileName + ".webp")); // 손실 압축 설정, fileName.webp로 파일 생성
        } catch (Exception e) {
            throw new PhotoException(e.getMessage());
        }
    }

    public File convertToWebpWithLossless(String fileName, File originalFile) {
        try {
            return ImmutableImage.loader()// 라이브러리 객체 생성
                    .fromFile(originalFile) // .jpg or .png File 가져옴
                    .output(WebpWriter.DEFAULT.withLossless(), new File(PHOTO_DIR + fileName + ".webp")); // 무손실 압축 설정, fileName.webp로 파일 생성
        } catch (Exception e) {
            throw new PhotoException(e.getMessage());
        }
    }

해당 Service는 .jpg 또는 .png 파일을 .webp 형식으로 변환합니다. 제공된 라이브러리를 사용하여 간편하게 수행할 수 있습니다.

WebpWriter

public class WebpWriter implements ImageWriter {

   public static final WebpWriter DEFAULT = new WebpWriter();
   public static final WebpWriter MAX_LOSSLESS_COMPRESSION = WebpWriter.DEFAULT.withZ(9);

   private final CWebpHandler handler = new CWebpHandler();

   private final int z;
   private final int q;
   private final int m;
   private final boolean lossless;

   public WebpWriter() {
      z = -1;
      q = -1;
      m = -1;
      lossless = false;
   }

   public WebpWriter(int z, int q, int m, boolean lossless) {
      this.z = z;
      this.q = q;
      this.m = m;
      this.lossless = lossless;
   }

   public WebpWriter withLossless() {
      return new WebpWriter(z, q, m, true);
   }

   public WebpWriter withQ(int q) {
      if (q < 0) throw new IllegalArgumentException("q must be between 0 and 100");
      if (q > 100) throw new IllegalArgumentException("q must be between 0 and 100");
      return new WebpWriter(z, q, m, lossless);
   }

   public WebpWriter withM(int m) {
      if (m < 0) throw new IllegalArgumentException("m must be between 0 and 6");
      if (m > 6) throw new IllegalArgumentException("m must be between 0 and 6");
      return new WebpWriter(z, q, m, lossless);
   }

   public WebpWriter withZ(int z) {
      if (z < 0) throw new IllegalArgumentException("z must be between 0 and 9");
      if (z > 9) throw new IllegalArgumentException("z must be between 0 and 9");
      return new WebpWriter(z, q, m, lossless);
   }

   @Override
   public void write(AwtImage image, ImageMetadata metadata, OutputStream out) throws IOException {
      byte[] bytes = handler.convert(image.bytes(PngWriter.NoCompression), m, q, z, lossless);
      out.write(bytes);
   }
}

또한, WebpWriter 객체 구현체를 확인해보면 손실 여부, 압축률, 이미지 품질 등을 다양한 설정을 통해 조정할 수 있음을 알 수 있습니다.

저는 기본적으로 DEFAULT와 withLossless() 설정만을 비교하여 활용했으나, WebpWriter는 추가적으로 Q, M, Z 값을 이용해 보다 세부적인 설정도 지원합니다.

Q (Quality):

Q는 WebP 이미지의 품질을 설정하는 매개변수로, 0에서 100까지의 값을 가집니다. 값이 높을수록 이미지 품질이 높아지지만 파일 크기가 커지고, 값이 낮을수록 품질이 낮아지지만 파일 크기가 작아집니다.


Ex) WebpWriter qualityWriter = WebpWriter.DEFAULT.withQ(80);

M (Method):

M은 WebP 이미지 압축의 방법을 설정하는 매개변수로, 0에서 6까지의 값을 가집니다. 이 값은 압축 속도와 압축률 간의 균형을 설정합니다.

값이 낮을수록 빠른 속도로 압축되지만 압축률은 낮아지고, 값이 높을수록 압축 속도는 느려지지만 압축률은 높아집니다.


Ex) WebpWriter methodWriter = WebpWriter.DEFAULT.withM(4);

Z (Compression level):

Z는 WebP 이미지의 압축 수준을 설정하는 매개변수로, 0에서 9까지의 값을 가집니다. 값이 높을수록 더 높은 압축률을 제공하며 파일 크기를 줄이지만, 압축 속도는 느려집니다. 이 값은 주로 무손실 압축에서 사용됩니다.


Ex) WebpWriter compressionWriter = WebpWriter.DEFAULT.withZ(9);

예시
WebpWriter customWriter = WebpWriter.DEFAULT.withQ(75).withM(3).withZ(5);


✅테스트 및 결과

1. 손실 압축 테스트

💻 CODE

    @Test
    void convertToWebpTEST() {
        // given
        String testFileName = "F_LOUNGE.png"; // 테스트 파일 명
        File testFile = new File(PHOTO_DIR + testFileName);

        // when
        File convertedFile = photoService.convertToWebp("F_LOUNGE", testFile);

        // then
        double originalFileSizeKB = testFile.length() / 1024.0;
        double convertedFileSizeKB = convertedFile.length() / 1024.0;
      
        double compressionRatio = (convertedFileSizeKB / originalFileSizeKB) * 100; // 압축 비율 계산 (백분율)
        double compressionRate = 100 - compressionRatio; // 압축률 계산

        System.out.printf("Original File Size: %.2f KB%n", originalFileSizeKB);
        System.out.printf("Converted File Size: %.2f KB%n", convertedFileSizeKB);
        System.out.printf("Compression Rate: %.2f%%%n", compressionRate);

        assertTrue(convertedFile.exists(), "Converted file should exist");
    }

📉 RESULT


해당 테스트 코드를 통해 사진 파일의 변환이 완료되었는지 확인하였으며, 기존 파일 크기와 .webp 형식으로 변환된 파일 크기를 비교하여 압축률을 계산했습니다.

테스트 결과, WebpWriter.DEFAULT를 사용하여 라이브러리에서 자동으로 압축을 진행할 경우 최대 90%까지 압축이 가능함을 확인할 수 있었습니다.

2. 무손실 압축 테스트

💻 CODE

    @Test
    void convertToWebpWithLosslessTEST() {
        // given
        String testFileName = "F_LOUNGE.png"; // 테스트 파일 명
        File testFile = new File(PHOTO_DIR + testFileName);

        // when
        File convertedFile = photoService.convertToWebpWithLossless("F_LOUNGE_LOSSLESS", testFile);

        // then
        double originalFileSizeKB = testFile.length() / 1024.0;
        double convertedFileSizeKB = convertedFile.length() / 1024.0;
        // 압축 비율 계산 (백분율)
        double compressionRatio = (convertedFileSizeKB / originalFileSizeKB) * 100;
        // 압축률 계산
        double compressionRate = 100 - compressionRatio;

        System.out.printf("Original File Size: %.2f KB%n", originalFileSizeKB);
        System.out.printf("Converted File Size: %.2f KB%n", convertedFileSizeKB);
        System.out.printf("Compression Rate: %.2f%%%n", compressionRate);

        assertTrue(convertedFile.exists(), "Converted file should exist");
    }

📉 RESULT

테스트 결과, WebpWriter.DEFAULT.withLossless()를 사용하여 무손실 압축을 진행할 경우 최대 40%까지 압축이 가능함을 확인할 수 있었습니다.

손실 압축과 무손실 압축의 차이가 꽤 많이 나는게 보여집니다.

또한, 디렉토리에 사진 파일들이 잘 생성되는 것도 확인할 수 있었습니다.



📌결정

손실 압축과 무손실 압축을 모두 모바일 화면에서 확인할 수 있도록 설정한 후, 기획자와 디자이너와의 회의를 진행했습니다.

저는 손실이 어느 정도 있더라도 파일을 획기적으로 압축할 수 있는 손실 압축 방식을 적용하고 싶었습니다.

다행히 디자이너도 확인 결과 현재의 화질이 충분히 만족스럽다고 판단하여 최종적으로 손실 압축 방식을 채택하기로 결정했습니다.

이 글이 유사한 문제를 겪는 분들께 도움이 되길 바랍니다.
긴 글 읽어주셔서 감사합니다.😄


🔶[번외] Spring Boot 3.0 적용

테스트 결과, Spring Boot 3.3.1에서도 위 코드는 문제 없이 작동하는 것을 확인했습니다. 따라서 Spring Boot 3.3.1에서도 걱정 없이 적용하셔도 될 것 같아요.

profile
Backend Developer

0개의 댓글