S3 이미지 파일 압축(Compressed)

Mugeon Kim·2023년 8월 15일
1

글을 작성하는 이유


  • 프로젝트를 하면서 이미지에 대한 고민을 하게 되었다. 쇼핑몰, 이커머스등 많은 이미지 자료를 사용하는 서비스에서 이미지를 어떻게 처리할지에 대하여 궁금증을 가지게 되었다.

  • 기존에 파일을 3가지 방식으로 저장을 하면서 사용을 했다. (1) path에 사진을 저장 (2) Byte로 변환하여 DB에 이미지 저장 (3) S3에 이미지 업로드 위 3가지 방식에서 3번 방식이 제일 효율적이기 때문에 선호한다.

  • 이미지를 업로드 하면서 S3의 버킷 크기, 요금을 생각하면 단순히 이미지를 Upload를 하기 보다는 더욱 최적화 하여 크기를 줄이고 비용을 줄이는 방식에 대해 고민하게 되었다.

AWS S3


1. S3란

Amazon S3 (Simple Storage Service)은 Amazon Web Services (AWS)의 클라우드 컴퓨팅 플랫폼에서 제공하는 스토리지 서비스입니다. Amazon S3는 사용자 및 기업이 데이터를 저장하고 검색하며 관리하는 데 사용되는 확장 가능하고 내구성이 뛰어난 객체 스토리지 서비스입니다.

Amazon S3는 다음과 같은 특징을 가지고 있습니다:

객체 스토리지: S3는 데이터를 "객체" 또는 "파일" 단위로 저장하는데 사용됩니다. 파일과 메타데이터가 함께 저장되며, 데이터를 고유한 키로 식별합니다.

무한한 확장성: S3는 필요에 따라 수천 개의 버킷(저장 공간)을 생성하고 그 안에 무수히 많은 객체를 저장할 수 있는 무한한 확장성을 제공합니다.

데이터 내구성: Amazon S3는 여러 가용 영역과 리전에 걸쳐 데이터를 복제하여 내구성을 보장합니다. 데이터의 높은 내구성을 유지하면서도 일관성과 가용성도 제공됩니다.

데이터 보안: S3는 데이터를 보호하기 위해 다양한 보안 기능을 제공합니다. 데이터 암호화, 액세스 제어, 버킷 정책 및 IAM 역할 등을 활용하여 데이터 보안을 강화할 수 있습니다.

비용 효율적: Amazon S3는 사용한 스토리지의 양에 따라 요금을 부과하므로 스토리지 비용을 관리하기 용이합니다.

Spring에서 S3를 연결을 하기 위해서는 IAM과 s3를 생성을 해야됩니다.

  • aws s3 버킷 퍼블릭 엑세스는 버킷 정책 편집에서 진행해야 한다!

  • 다음 페이지(Amazon S3 > 버킷 > {본인 버킷 저장소} > 버킷 정책 편집)로

들어가서 정책 생성기 버튼을 누른다.

  • Add Statment > Generate Policy 를 누르면 JSON을 반환을 한다. 이걸 아까 버킷 정책 편집 페이지에 작성을 한다.
{
  "Id": "XXXXXXX218549",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "XXXXXX7157978",
      "Action": [
        "s3:DeleteObject",
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::XXXXXXX/*",
      "Principal": "*"
    }
  ]
}

스프링에서 S3 연결하기

  • gradle에 의존성을 추가를 한다.
    implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.3.1'
  • 이후 .ymlconfig를 작성을 한다.
    • yml
cloud:
  aws:
    credentials:
      accessKey: ${access}
      secretKey: ${secret}
    s3:
      bucket: ${bucketName}
    region:
      static: ap-northeast-2
    stack:
      auto: false
  • config
@Configuration
public class S3Config {

    @Value("${cloud.aws.credentials.accessKey}")
    private String iamAccessKey;

    @Value("${cloud.aws.credentials.secretKey}")
    private String iamSecretKey;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    @Primary
    public AmazonS3 amazonS3Client() {
        BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(iamAccessKey, iamSecretKey);
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
                .build();
    }
}

2. 기본 파일 업로드


이미지 업로드 시나리오

  • 프로젝트에서 이미지가 필요한 부분은 회원의 사진을 업로드가 필요했습니다. 이 부분은 회원과 File 테이블을 OneToOne 관계를 가지게 됩니다.
 @Override
    @Transactional
    public List<String> uploadFiles(MultipartFile[] multipartFileList, LoginUserDto loginUserDto) throws Exception {

        Member member = memberRepository.findById(loginUserDto.getMemberId())
                .orElseThrow(() -> new NotFoundMemberId(loginUserDto.getMemberId()));

        List<String> imagePathList = new ArrayList<>();

        for (MultipartFile multipartFile : multipartFileList) {
            String imagePath = uploadFile(multipartFile, member);
            imagePathList.add(imagePath);
        }

        return imagePathList;
    }
     @Operation(summary = "S3 파일 업로드", description = "AWS S3 버켓에 IAM 파일 업로드")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "파일 업로드 성공"),
            @ApiResponse(responseCode = "500", description = "HttpStatus.INTERNAL_SERVER_ERROR")
    })
    @GetMapping("/upload")
    public ResponseEntity<Object> upload(
            @Parameter(name = "multipartFileList", description = "Multi part file")
            @RequestParam("files") MultipartFile[] multipartFileList,
            @Parameter(name = "loginUserDto", description = "로그인 했던 회원의 회원 정보")
            @IfLogin LoginUserDto loginUserDto
    ) {
        try {
            List<String> imagePathList = fileService.uploadFiles(multipartFileList, loginUserDto);
            return new ResponseEntity<>(imagePathList, HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>("Failed to upload files.", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
  • 해당 코드를 통하여 이미지를 업로드가 가능합니다. PostMan에 이미지를 업로드를 하기 위해서는 `form-data에 key에 files value에 파일을 선택하면 가능합니다.

  • 이 과정을 시퀀스 다이어그램으로 확인하면 다음과 같은 동작을 진행을 합니다.

  1. 클라이언트가 업로드 요청을 컨트롤러에 전송합니다.
  2. 컨트롤러는 받은 요청을 기반으로 서비스에 업로드 요청을 전달합니다.
  3. 서비스는 해당 회원 정보를 데이터베이스에서 찾아옵니다.
  4. 서비스는 Amazon S3에 파일을 업로드하고 업로드된 이미지 경로를 생성합니다.
  5. 서비스는 생성된 이미지 경로를 컨트롤러에 반환합니다.
  6. 컨트롤러는 클라이언트에게 이미지 경로를 반환합니다.
  7. 클라이언트는 받은 이미지 경로들을 처리합니다.

3. 압축 파일 업로드

Compression (압축)

  • 압축이란 데이터를 더 작은 크기로 줄이는 과정을 의미합니다. 이를 통해 저장 공간을 절약하거나 데이터 전송 시간을 줄이는 등의 이점을 얻을 수 있습니다.

  • 압축은 크게 2가지 유형으로 나눌 수 있습니다.

무손실 압축 (Lossless Compression)

  • 데이터를 압축하여 크기를 줄이지만, 압축 후에도 원본 데이터를 정확하게 복원할 수 있는 방식입니다. 파일 포맷이나 데이터 구조가 변하지 않습니다. 주로 텍스트 데이터나 프로그램 코드 등에 사용을 합니다.

손실 압축 (Lossy Compression)

  • 데이터를 압축할 때 약간의 품질 손실을 감수하고 그에 따라 더 큰 압축률은 얻는 방식이다. 이미지나 음성과 같이 약간의 손실이 미치지 않을 경우에 사용 합니다. 이미지에서는 일부 세부 정보나 색상을 제거, 변환시켜 크기를 줄이는 방식이 손실 압축의 있습니다.

무손실 압축 VS 손실 압축

  • 이번 프로젝트에서는 무손실 압축을 사용을 하였습니다. 원본 데이터의 크기를 줄여 s3의 크기를 줄여 과금을 줄이고 원본 데이터를 볼 수 있는게 무손실 압축이 적합하다고 생각을 했습니다.

파일 압축

S3 Upload 소스 코드



    public static byte[] compressImage(byte[] data) {
        Deflater deflater = new Deflater();
        deflater.setLevel(Deflater.BEST_COMPRESSION);
        deflater.setInput(data);
        deflater.finish();

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
        byte[] tmp = new byte[4 * 1024];
        while (!deflater.finished()) {
            int size = deflater.deflate(tmp);
            outputStream.write(tmp, 0, size);
        }
        try {
            outputStream.close();
        } catch (Exception ignored) {
        }
        return outputStream.toByteArray();
    }


    public static byte[] decompressImage(byte[] data) {
        Inflater inflater = new Inflater();
        inflater.setInput(data);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length);
        byte[] tmp = new byte[4 * 1024];
        try {
            while (!inflater.finished()) {
                int count = inflater.inflate(tmp);
                outputStream.write(tmp, 0, count);
            }
            outputStream.close();
        } catch (Exception ignored) {
        }
        return outputStream.toByteArray();
    }

    public String uploadCompressedImage(MultipartFile file) {
        if (file == null || file.isEmpty())
            return "";

        byte[] originalImageBytes;
        try {
            originalImageBytes = file.getBytes();
        } catch (IOException e) {
            log.error("Error reading multipartFile", e);
            return "";
        }

        byte[] compressedImageBytes = ImageUtils.compressImage(originalImageBytes);

        String originalFilename = file.getOriginalFilename();
        String fileName = UUID.randomUUID() + ".compressed";

        log.info("uploadCompressedImage fileName: {}", fileName);
        s3Client.putObject(new PutObjectRequest(bucketName, fileName, new ByteArrayInputStream(compressedImageBytes), null));

        return fileName;
    }

Deflater, Inflater

Deflater

This class provides support for general purpose compression using the popular ZLIB compression library. The ZLIB compression library was initially developed as part of the PNG graphics standard and is not protected by patents. It is fully described in the specifications at the java.util.zip package description.
The following code fragment demonstrates a trivial compression and decompression of a string using Deflater and Inflater.
  • 이 클래스는, 일반적인 ZLIB 압축 라이브러리를 사용해 범용의 압축 알고리즘을 지원합니다. ZLIB 압축 라이브러리는, 당초 PNG 그래픽 표준의 일부로서 개발된 것으로, 특허로는 보호되고 있지 않습니다.

  • 처음에 Deflater를 객체를 생성자를 통해 호출하여 압축 레벨을 설정을 합니다.

  • Deflater 객체를 생성한 이후 압축 레벨을 설정할 수 있다. 압축 레벨은 압축률과 처리 속도 사이를 조절합니다.

Deflater.DEFAULT_COMPRESSION 기본 압축 레벨 (보통 -1)
Deflater.NO_COMPRESSION 압축하지 않음 (0)
Deflater.BEST_SPEED 가장 빠른 압축 (1)
Deflater.BEST_COMPRESSION 가장 효과적인 압축 (9)

  • Deflater를 살펴보면 기본적으로 Deflater.DEFAULT_COMPRESSION로 설정이 되어져 있습니다.

  • 이번 프로젝트에서는 가장 효과적인 압축을 하기 위해서 Deflater.BEST_COMPRESSION 레벨로 압축을 하였습니다.

  • 이후 입력 데이터 설정 및 압축을 합니다.
deflater.setInput(inputData); //메서드를 사용하여 압축할 데이터를 설정
deflater.finish(); // 메서드로 압축 과정을 시작합니다.

ByteArrayOutputStream

ByteArrayOutputStream는 메모리, 즉 바이트배열에 데이터를 입출력 하는데 사용되는 스트림이다. 주로 다른 곳에 입출력 하기 이전에 데이터를 임시로 바이트 배열에 담아서 변환 등의 작업을 하는데 사용한다.

  • 이것을 사용한 이유는 Utill에 S3 Client에게 파일을 전달하기 이전에 암축하고 이것을 S3에 업로드 하기 때문에 ByteArrayOutputStream를 사용한다.

스트림

  • 자바에서 입출력을 수행하려면 즉. 어느 한쪽에서 다른 쪽으로 데이터를 전달하려면 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요하다. 이것을 스트림이라 정의하며 스트림은 연속적인 연속적인 데이터의 흐름과 비슷하며 단방향통신만 가능하기 대문에 하나의 스트림으로 입력과 출력을 동시에 할 수 없다.

압축

    while (!deflater.finished()) {
        int size = deflater.deflate(tmp);
        outputStream.write(tmp, 0, size);
    }
  • while문을 돌면서 객체가 압축을 끝내지 않았을 동안 반복을 시키며 Deflater 객체의 deflate() 메서드를 호출하여 데이터를 압축하고, 압축된 크기를 size에 저장합니다.

  • 이후outputStream.write(tmp, 0, size); : 압축된 데이터를 출력 스트림에 기록합니다.

deflate

  • deflater.deflate() 메서드는 Deflater 클래스의 인스턴스를 사용하여 데이터를 압축하는 역할을 수행합니다. 이 메서드는 압축된 데이터를 생성하고, 생성된 데이터의 크기를 반환합니다.

https://www.geeksforgeeks.org/deflater-deflate-function-in-java-with-examples/

4. 결론


  • AWS에 이미지를 업로드를 했을 때 다음과 같이 69.9kb -> 58.5kb로 사이즈가 줄었습니다.
  • 이후 차이점을 살펴보면 유형이 jpg에서 compressed로 변경이 되었습니다.
  • 업로드 한 사진의 상세 정보는 jpg 타입의 69.6kb 입니다.
    업로드중..

겪었던 이슈

  • 처음에는 jpg를 이용하여 압축을 하였을 때 높은 압축률과 사이즈가 작아지는 효과를 얻을 수 있었습니다.

  • 하지만 png, gif등 다른 확장자를 압축하고 Size에 따라서 압축률이 차이가 발생을 하였습니다.

  • 다양한 이미지 파일 형식들은 내부적으로 서로 다른 방식으로 데이터를 표현합니다. jpeg, png, gif등 각각 다른 압축 및 인코딩 기술을 사용을 하여 데이터를 저장을 합니다. 이러한 차이로 압축이 다르게 이루어져 서로 다른 압축률을 발생을 했습니다.

https://dydtjr1128.github.io/image/2019/07/01/Image-compression.html

jpg

jpg는 무손실 압축 방식을 사용하여 이미지를 압축하면 사진과 같은 컬러 이미지에 효과적이며 압축률이 높다

PNG

PNG는 무손실 압축 방식을 사용하여 이미지를 저장합니다. 이 형식은 투명도를 지원하며, 그래픽이나 아이콘과 같이 선명한 경계와 대체로 작은 영역을 다룰 때 더 효과적일 수 있습니다. 하지만 JPEG보다 용량이 큰 경우가 많습니다.

GIF

GIF는 무손실 또는 제한적인 손실 압축 방식을 사용합니다. 주로 애니메이션과 단순한 그래픽에 사용되며, 한정된 색상 팔레트를 사용하므로 색상 정보의 수가 줄어들어 용량이 비교적 작습니다.

출처


https://www.geeksforgeeks.org/deflater-deflate-function-in-java-with-examples/

https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html

https://dydtjr1128.github.io/image/2019/07/01/Image-compression.html

profile
빠르게 실패하고 자세하게 학습하기

0개의 댓글