본문 바로가기

항해 99/Spring

Spring - Image Resize

학습 목적

토이 프로젝트로 Pixiv 사이트의 기능을 만들어 보고 싶어서 이미지 업로드와 리사이즈를 통한 썸네일화에 대해 배워보려고 한다.

 

이미지 리사이징

하는 이유? 

  1. 페이지 로딩 속도 개선: 큰 이미지 파일은 로딩 시간이 길어질 수 있습니다. 이미지를 리사이즈하면 파일 크기가 줄어들어 웹 페이지의 로딩 속도가 빨라집니다. 빠른 로딩 시간은 사용자 경험을 개선하고, 검색 엔진 최적화(SEO)에도 긍정적인 영향을 미칩니다.
  2. 대역폭 절약: 리사이즈된 이미지는 파일 크기가 작아져 서버와 사용자 양측에서 사용되는 데이터 대역폭을 절약할 수 있습니다. 이는 특히 모바일 데이터 요금이 중요한 상황이나 대역폭이 제한된 환경에서 유용합니다.
  3. 적절한 해상도유지: 디바이스별로 최적의 해상도를 유지하기 위해 이미지를 리사이즈할 수 있습니다. 예를 들어, 모바일 기기에서는 작은 이미지가 더 적합하고, 고해상도 모니터에서는 더 큰 이미지가 필요할 수 있습니다.
  4. 디자인 및 레이아웃 일관성 유지: 웹사이트의 디자인과 레이아웃은 일관성이 있어야 합니다. 이미지를 적절한 크기로 리사이즈하면 페이지의 다른 요소와 조화롭게 배치할 수 있습니다.
  5. 서버 저장 공간 절약: 큰 이미지 파일은 서버 저장 공간을 많이 차지합니다. 이미지를 리사이즈하여 파일 크기를 줄이면 서버 저장 공간을 절약할 수 있습니다.
  6. 성능 최적화: 많은 수의 큰 이미지가 있는 경우, 서버의 성능에 영향을 미칠 수 있습니다. 이미지 파일 크기를 줄이면 서버의 부하를 줄일 수 있습니다

 

SpringBoot를 이용한 이미지 리사이징

Java를 이용한 이미지 리사이징은 다음 라이브러리를 이용하여 가능합니다.

  1. java.awt.Graphics2D
  2. Image.getScaledInstance()
  3. Imgscalr
  4. Thunbnailator
  5. Marvin

각 라이브러리에 대한 간단한 설명

 

java.awt.Graphics2D

  • 설명: Graphics2D는 Java의 기본 그래픽 라이브러리인 AWT(Abstract Window Toolkit)의 클래스입니다. 이미지 리사이징을 수행할 때는 이 클래스를 사용하여 고급 그래픽 조작(예: 회전, 변환, 리사이즈)을 할 수 있습니다.
  • 사용 방법: Graphics2D 객체를 생성하고, drawImage 메서드를 사용하여 이미지를 원하는 크기로 그려 리사이즈합니다. 이 방법은 사용자가 품질을 직접 관리할 수 있는 장점이 있지만, 구현이 다소 복잡할 수 있습니다.

Image.getScaleInstance()

  • 설명: getScaledInstance()는 Java의 Image 클래스의 메서드로, 간단하게 이미지를 리사이즈할 수 있도록 해줍니다.
  • 사용 방법: 이 메서드는 이미지의 너비와 높이를 지정하여 이미지 객체의 크기를 조정한 새로운 이미지를 생성합니다. 추가로, 리사이즈 방법(빠름, 부드러움, 고품질 등)을 지정할 수 있는 플래그를 사용할 수 있습니다. 그러나 이 메서드는 성능과 품질이 다소 제한적일 수 있습니다.

Imgscalr

  • 설명: Imgscalr는 Java에서 이미지 리사이징을 쉽게 할 수 있게 해주는 경량 라이브러리입니다. 이 라이브러리는 고성능과 간단한 API를 제공하여, 여러 가지 리사이징 알고리즘을 지원합니다.
  • 사용 방법: 사용자가 한두 줄의 코드만으로도 이미지 리사이징을 수행할 수 있어 매우 직관적입니다. 예를 들어, Scalr.resize(image, Method.QUALITY, Mode.AUTOMATIC, targetWidth, targetHeight)와 같은 방법으로 사용할 수 있습니다. 이 라이브러리는 빠르고, 대부분의 일반적인 리사이징 작업에 적합 합니다.

Thumbnailator

  • 설명: Thumbnailator는 Java에서 이미지 리사이징을 쉽게 수행하기 위해 설계된 또 다른 경량 라이브러리입니다. Thumbnailator는 고성능과 간단한 API를 제공하며, 다양한 리사이징 옵션을 지원합니다.
  • 사용 방법: Thumbnailator는 예제와 같이 매우 간단한 사용법을 제공합니다( Thumbnails.of(image).size(width, height).toFile(outputFile) ) 다양한 입력과 출력 형식을 지원하며, 사용자 정의 이미지 필터 및 품질 설정도 가능합니다.

Marvin

  • 설명: Marvin은 이미지 프로세싱을 위한 더 넓은 범위의 기능을 제공하는 Java 프레임워크입니다. 이미지 리사이징뿐만 아니라, 필터링, 변형, 컬러 조정 등 고급 이미지 처리 기능을 지원합니다.
  • 사용 방법: Marvin은 MarvinImage 객체를 사용하여 이미지 조작을 수행합니다. 이미지 리사이징의 경우, MarvinPlugin을 통해 다양한 리사이징 알고리즘을 적용할 수 있습니다. 고급 이미지 처리가 필요한 경우 유용하지만, 상대적으로 다른 라이브러리보다 학습 곡선이 있을 수 있습니다.

 

AWS Lambda Image Resize

AWS Lambda?

AWS Lambda는 아마존 웹 서비스(AWS)에서 제공하는 서버리스 컴퓨팅 서비스입니다. 서버리스 컴퓨팅이란 서버를 직접 관리하지 않고 코드를 실행할 수 있는 컴퓨팅 모델을 말합니다. AWS Lambda를 사용하면 서버를 프로비저닝하거나 관리할 필요 없이 코드를 업로드하고, 특정 이벤트가 발생할 때 해당 코드를 자동으로 실행할 수 있습니다.

 

AWS Lambda의 주요 특징

  1. 서버 관리 불필요: 사용자는 인프라를 관리할 필요 없이 코드 실행에만 집중할 수 있습니다. AWS가 자동으로 인프라를 관리하고, 필요에 따라 확장합니다.
  2. 자동 확장: Lambda 함수는 자동으로 확장되어 동시에 여러 요청을 처리할 수 있습니다. 즉, 트래픽 변화에 따라 필요한 만큼만 리소스를 사용하며, 자동으로 처리 용량을 조절합니다.
  3. 이벤트 기반 실행: Lambda 함수는 다양한 이벤트 소스에 의해 트리거됩니다. 예를 들어, S3 버킷에 파일이 업로드되거나, DynamoDB 테이블에 데이터가 추가될 때, 또는 API Gateway를 통한 HTTP 요청 시 함수가 실행될 수 있습니다.
  4. 요금 모델: AWS Lambda는 사용한 만큼만 비용을 지불하는 방식으로, 코드가 실행된 시간과 사용된 리소스에 따라 과금됩니다. 사용자가 서버를 미리 프로비저닝하지 않기 때문에 비용 효율적입니다.
  5. 다양한 언어 지원: AWS Lambda는 Node.js, Python, Java, C#, Go, Ruby 등 다양한 프로그래밍 언어를 지원합니다. 이를 통해 개발자는 익숙한 언어로 서버리스 애플리케이션을 개발할 수 있습니다.

AWS Lambda는 마이크로서비스 아키텍처, 실시간 파일 처리, 데이터 변환, API 백엔드 구축 등 다양한 용도로 활용될 수 있는 강력한 서버리스 컴퓨팅 서비스입니다.

 

Lambda를 사용해야 하는 경우

  • 파일 처리: 업로드 후 Amazon Simple Storage Service(S3)를 사용하여 Lambda 데이터 처리를 실시간으로 트리거 합니다.
  • 스트림 처리: Lambda 및 Amazon Kinesis를 사용하여 애플리케이션 작업 추적, 거래 주문 처리, 클릭스트림 분석,  데이터 정리, 로그 필터링, 인덱싱, 소셜 미디어 분석, 사물 인터넷(IoT) 디바이스 데이터 텔레메트리 및 계측을 위한 실시간 스트리밍 데이터를 처리합니다.
  • 웹 애플리케이션:  Lambda를 다른 AWS 서비스와 결합하여 여러 데이터 센터에서 고가용성 구성으로 자동으로 스케일 업/ 스케일 다운되고 실행되는 강력한 웹 애플리케이션을 빌드합니다.
  • IoT 백엔드: Lambda를 사용하여 서버리스 백엔드를 구축함으로써 웹, 모바일, IoT 및 서드 파티 API 요청을 처리합니다.
  • 모바일 백엔드: Lambda 및 Amazon API Gateway를 사용하여 백엔드를 구축함으로써 API 요청을 인증하고 처리합니다. AWS Amplify를 사용하여 iOS, Android, 웹 및 React Native 프론트엔드와 손쉽게 통합합니다.

 

주요 기능

  • 환경 변수: 환경 변수를 사용하여 코드를 업데이트하지 않고 함수의 동작을 조정합니다.
  • 버전: 예를 들어 안정적인 프로덕션 버전의 사용자에게 영향을 주지 않고 베타 테스트에 새 함수를 사용할 수 있도록 함수 배포를 관리합니다.
  • 컨테이너 이미지: 기존 컨테이너 도구를 재사용하거나 기계 학습과 같은 상당한 종속 구성 요소에 의존하는 더 큰 워크로드를 배포할 수 있도록 AWS에서 제공하는 기본 이미지 또는 대체 기본 이미지를 사용하여 Lambda 함수에 대한 컨테이너 이미지를 생성합니다.
  • 계층: 라이브러리와 기타 종속 구성 요소를 패키징하여 배포 아카이브의 크기를 줄이고 코드를 더 빠르게 배포할 수 있도록 합니다.
  • Lambda 확장: 모니터링, 관측성, 보안 및 거버넌스를 위한 도구로 Lambda 함수를 보강합니다.
  • 함수 URL: Lambda 함수에 전용 HTTP(S) 엔드포인트를 추가합니다.
  • 응답 스트리밍: Node.js 함수에서 클라이언트 응답 페이로드를 다시 스트리밍하여 첫 번째 바이트까지 시간(TTFB) 성능을 개선하거나 더 큰 페이로드를 반환하도록 Lambda 함수 URL을 구성합니다.
  • 동시성 및 크기 조정 컨트롤: 프로덕션 애플리케이션의 크기 조정 및 응답성에 대해 세밀한 제어를 적용합니다.
  • 코드 서명: 승인도니 개발자만 변경되지 않은 신뢰할 수 있는 코드를 Lambda 함수에 게시하는지 확인합니다.
  • 프라이빗 네트워킹: 데이터베이스, 캐시 인스턴스, 내부 서비스 등의 리소스에 대해 프라이빗 네트워크를 생성합니다.
  • 파일 시스템 액세스: 함수 코드가 높은 동시성으로 안전하고 공유 리소스에 액세스하고 수정할 수 있게 Amazon Elastic File Systme(Amazon EFS)을 로컬 디렉터리에 탑재하도록 함수를 구성합니다.
  • Lambda SnapStart for Java: 일반적으로 함수 코드를 변경하지 않고 추가 비용 없이 Java 런타임의 시작 성능을 최대 10배 향상시킵니다.

 

AWS Lambda 사용 S3 이미지 리사이징 방법

AWS 공식 가이드 문서

 

1. S3 버킷 생성

한 버킷에서 경로를 나눠서 사용하는 것은 재귀 호출이 될 수 있기 때문에 버킷을 나누는 것을 AWS에서 권장하고 있다.

 

버킷 사용 예시

  1. bucket1: Lambda를 생성할 때 소스 코드가 10MB 이상일 경우 S3에 올려서 사용하는데 그때 사용
  2. bucket2: 이미지를 저장할 버킷
  3. bucket3: bucket2에 저장된 이미지를 리사이징 해 저장할 버킷

S3 생성 참고 포스트

 

2. IAM 정책 & 역할 생성

 

정책 생성

 

직접 JSON을 입력한다.

  • bucket1에 대한 권한(s3:GetObject), bucket3에 대한 권한(s3:PutObject, s3:PutObjectAcl)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:PutLogEvents",
                "logs:CreateLogGroup",
                "logs:CreateLogStream"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::jikgong-image/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl" // S3 Access Denied 에러 해결
            ],
            "Resource": "arn:aws:s3:::jikgong-resize-bucket/*"
        }
    ]
}
  • S3 Access Denied 에러 발생 시 주석 부분 코드 필요

역할 생성

만든 정책을 사용할 역할을 생성한다.

 

4. Lambda 함수에 사용할 함수 생성

origin S3 버킷에 이미지가 업로드 되었을 때 자동으로 resized 버킷에 리사이징된 이미지가 저장되도록 함수를 생성(Node.js의 경우 AWS CLI 또는 Lambda 콘솔을 사용해 생성 가능)

 

build.gradle 의존성 추가

implementation group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.1'
implementation group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '3.7.0'
implementation group: 'com.amazonaws', name: 'aws-java-sdk', version: '1.11.969'

 

lambda 함수 작성

AWS 제공 예제 코드를 프로젝트에 맞게 수정해서 사용하면 된다.

 

예시 코드

import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3EventNotificationRecord;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
 
public class ResizeHandler implements RequestHandler<S3Event, String> {
 
    private static final float MAX_HEIGHT = 60;
    private final String JPG_TYPE = (String) "jpg";
    private final String JPG_MIME = (String) "image/jpeg";
    private final String JPEG_TYPE = (String) "jpeg";
    private final String JPEG_MIME = (String) "image/jpeg";
    private final String PNG_TYPE = (String) "png";
    private final String PNG_MIME = (String) "image/png";
    private final String GIF_TYPE = (String) "gif";
    private final String GIF_MIME = (String) "image/gif";
 
    public String handleRequest(S3Event s3event, Context context) {
        LambdaLogger logger = context.getLogger();
        try {
            S3EventNotificationRecord record = s3event.getRecords().get(0);
            String srcBucket = record.getS3().getBucket().getName(); // 원본 버킷 이름
            String key = record.getS3().getObject().getUrlDecodedKey(); // 객체의 키 (파일 경로 및 이름)
            String dstBucket = "jikgong-resize-bucket"; // 수정된 저장될 버킷 이름
 
 
            // 파일 확장자 추출
            Matcher matcher = Pattern.compile(".*\\.([^\\.]*)").matcher(key);
            if (!matcher.matches()) {
                logger.log("Unable to infer image type for key " + key);
                return "";
            }
            String imageType = matcher.group(1);
            // 지원하지 않는 이미지 형식인 경우 로그를 남기고 리턴
            if (!(JPG_TYPE.equals(imageType)) && !(JPEG_TYPE.equals(imageType)) && !(PNG_TYPE.equals(imageType)) && !(GIF_TYPE.equals(imageType))) {
                logger.log("Skipping non-image " + key);
                return "";
            }
 
            // S3에서 이미지 가져오기
            AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();
            S3Object s3Object = s3Client.getObject(new GetObjectRequest(srcBucket, key));
            InputStream objectData = s3Object.getObjectContent();
 
            // 이미지 리사이징 처리
            BufferedImage srcImage = ImageIO.read(objectData);
            int srcHeight = srcImage.getHeight();
            int srcWidth = srcImage.getWidth();
            int width = (int) (srcWidth * (MAX_HEIGHT / srcHeight)); // 비율에 맞춰 너비 계산
            int height = (int) MAX_HEIGHT;
 
            // 새 이미지 버퍼 생성 및 그래픽 설정
            BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = resizedImage.createGraphics();
            g.setPaint(Color.white);
            g.fillRect(0, 0, width, height);
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g.drawImage(srcImage, 0, 0, width, height, null);
            g.dispose();
 
            // 바이트 배열로 변환
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ImageIO.write(resizedImage, imageType, os);
            InputStream is = new ByteArrayInputStream(os.toByteArray());
            ObjectMetadata meta = new ObjectMetadata();
            meta.setContentLength(os.size()); // 메타데이터 설정
            meta.setContentType(getMimeType(imageType)); // MIME 타입 설정
 
            // 리사이즈된 이미지를 S3에 저장
            logger.log("Writing to: " + dstBucket + "/" + key);
            try {
                s3Client.putObject(new PutObjectRequest(dstBucket, key, is, meta).withCannedAcl(CannedAccessControlList.PublicRead));
            } catch (AmazonServiceException e) {
                logger.log(e.getErrorMessage());
                System.exit(1);
            }
            logger.log("Successfully resized " + srcBucket + "/" + key + " and uploaded to " + dstBucket + "/" + key);
            return "Ok";
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
 
    // MIME 타입을 반환하는 보조 메소드
    private String getMimeType(String imageType) {
        switch(imageType) {
            case JPG_TYPE:
            case JPEG_TYPE:
                return JPG_MIME;
            case PNG_TYPE:
                return PNG_MIME;
            case GIF_TYPE:
                return GIF_MIME;
            default:
                return "";
        }
    }
}

 

build.gradle에 다음 코드를 추가하고 gradle build 진행

task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
       from configurations.runtimeClasspath
    }
}

 

명령어를 통해 zip 생성

./gradlew buildZip
  • 생성한 zip 파일을 S3에 업로드(파일 사이즈가 크면 S3를 URL을 통해 AWS Lambda에 적용)

 

5. Lambda 함수 생성

 

함수 이름을 입력하고, 사용할 언어(작성한 함수 언어)를 선택 후 기존 실행 역할 변경에서, 생성한 역할을 선택

 

코드 소스를 업로드(zip 파일을 S3에 업로드 한 경우 URL을 입력)

 

런타임 설정을 편집한다.

  • 사용 언어, 패키지, 클래스, 메서드 명을 입력

 

트리거를 추가한다.

 

  • Lambda에서 작업한 로그들은 모니터링 → Cloud Watch Logs에서 확인할 수 있음

 

6. S3에 이미지를 업로드해 AWS Lambda에서 리사이징을 수행하는지 테스트

 

 

7. 프로젝트에서 리사이징된 이미지 활용

썸네일 이미지에 대해서만 resize를 진행하도록 Prefix 조건을 두어 thumbnail만 처리하도록 수정

 

저장할 때 prefix 설정

// unique 이름 생성
String storeImageName = createStoreImageName(extension);
// 썸네일 이미지라면 Prefix를 등록해 AWS Lambda가 실행되도록 세팅
String prefix = isFirst ? "thumbnail_" : "";
storeImageName = "jobPost/" + prefix + storeImageName;

 

thumbnail url 조회

// 버킷에서 이미지 조회
    public String getImgPath(String fileName) {
        if (amazonS3Client.doesObjectExist(bucket, fileName)) {
            return amazonS3Client.getUrl(bucket, fileName).toString();
        } else {
            throw new CustomException(ErrorCode.S3_NOT_FOUND_FILE_NAME);
        }
    }
 
    // resize버킷에서 이미지 조회
    public String getThumbnailImgPath(String fileName) {
        if (amazonS3Client.doesObjectExist(resize_bucket, fileName)) {
            return amazonS3Client.getUrl(resize_bucket, fileName).toString();
        } else {
            // resize_bucket에 파일이 없을 경우, 메인 bucket에서 검색
            if (amazonS3Client.doesObjectExist(bucket, fileName)) {
                log.warn("resize_bucket 에서 조회를 시도했지만, bucket에만 존재");
                return amazonS3Client.getUrl(bucket, fileName).toString();
            } else {
                // 두 버킷 모두에 파일이 없을 경우 예외 발생
                throw new CustomException(ErrorCode.S3_NOT_FOUND_FILE_NAME);
            }
        }
    }

 

 

Lambda 사용 참조

https://dgjinsu.tistory.com/59
https://velog.io/@kmss6905/Lambda-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95
https://oliveyoung.tech/blog/2023-05-19/aws-lambda-resize/

 

'항해 99 > Spring' 카테고리의 다른 글

프로젝트 코드 리팩토링  (0) 2024.06.04
CI/CD 2  (0) 2024.05.13
CI / CD  (0) 2024.05.11
ORM  (0) 2024.05.03
Web Game 코드 설계 정리  (0) 2024.04.23