본문 바로가기
BackEnd/AWS

[AWS] Lambda를 이용한 Image Resizing

by 경험의 가치 2024. 7. 7.

소스 코드는 아래에서 확인하실 수 있습니다!

 

 

GitHub - mclub4/lambda_image_resize: aws lambda로 image resize

aws lambda로 image resize. Contribute to mclub4/lambda_image_resize development by creating an account on GitHub.

github.com

 

1. Image Resizing 작업이 필요한 이유

내가 "이음" 이라는 프로젝트를 진행하고 있는데, 여기서 진행중인 공모전을 보여주는 기능도 있다. 그런데, 이 공모전 리스트를 보여줄 때, 공모전 리스트에서 대표 이미지로 공모전 포스터를 보여준다. 하지만, 문제가 이 대표 이미지가 엄청나게 큰 공모전 포스터 원본 이미지를 사용한다는 것이다. 그래서 보여지는 이미지 크기는 굉장히 작은데, 굉장히 큰 원본 이미지를 S3에서 받아오느라고 응답시간이 꽤 커지고 리소스 낭비가 심해진다.

 

예시로, 목록에서는 50x50짜리 작은 이미지로만 보이면 되는데 400x400짜리 엄청나게 큰 이미지를 불러오면 곤란할 것이다. 실제 사례를 하나 들어보자면

위에 처럼 Github가 있다고 해보자. 그리고 우리가 프로필 이미지를 추가하기 위해서 업로드 했다고 해보자. 왼쪽에 큰 사진은 원본이니깐, 원본을 로드하면 그만인데, 오른쪽 상단에 있는 작은 이미지까지 원본으로 로드해서 크기를 줄이면 시간이 굉장히 오래걸릴 수 있다. (살짝 적절한 예시는 아닌것 같긴 한데... 맥락은 이해가 갈 것이다.) 따라서 사용자의 응답시간을 단축시키기 위해서 Image를 업로드하면, 크기별로 Resizing해서 운영하는 경우가 많다. 실제로 올리브영도 판매 상품 페이지를 보여줄 때 이런식으로 한다고 한다!

 

2. Image Resizing 작업에 사용할 서비스

이미지 리사이징에 관해서 두가지 고려할 요소가 있다.

 

1. 이미지 리사이징 작업은 CPU와 메모리를 많이 사용한다. 따라서, 같이 돌리면 다른 사용자의 요청을 못받는 현상이 발생할 수 있다.

2. 이미지 리사이징 작업은 24시간 365일 모든 사용자가 이용하는 작업은 아니기 때문에 계속 서버를 돌리는 것은 자원 낭비이다.

 

따라서, 일단 1번 요소 때문에 이미지 리사이징 작업 인스턴스는 분리를 해야됨을 알 수 있고, 또 2번 요소 때문에 우리는 AWS Lambda같은 Server Less 서비스 형태가 적합하다는 결론을 도출해낼 수 있다.

 

3. 이미지를 저장할 S3 버킷 생성

 

나같은 경우는 이렇게 2개의 버킷을 만들었다. 위에 있는 버킷은 이미지를 업로드시, 원본 이미지를 저장할 Bucket이다.

아래 있는 버킷은 Resized된 이미지들을 저장한 Bucket이다.

 

그런데 하나 버킷에 저장해도 되지 않냐고 할 수 있는데, 재귀 호출이 되어서 무한정으로 올리고 Resize하고 해서 엄청난 요금이 발생할 수 있어서 AWS에서도 버킷을 나누는 것을 적극 권장하고 있다. 

 

S3 버킷 생성에 관한 부분은 생략하겠다. 아래 해당 블로그에 아주 잘 나와있으므로 참고하길 바란다. 다만 중요한게 나의 코드를 그대로 따라할 경우에는 반드시 리사이즈된 이미지를 저장할 버킷 이름을 "{버킷이름}-resized"로 해야된다!!

 

[AWS] 📚 S3 개념 & 버킷 · 권한 설정 방법

S3 (Simple Storage Service) 개념 AWS S3는 업계 최고의 확장성과 데이터 가용성 및 보안과 성능을 제공하는 온라인 오브젝트(객체) 스토리지 서비스이다. (참고로 S 앞글자가 3개라서 S3 이라고 한다.) 쉽

inpa.tistory.com

 

4. IAM 정책 & 역할 생성

Lambda를 위한 IAM 역할을 부여해줘야 되기 때문에 IAM 정책을 생성해줘야된다.

IAM의 정책에 들어가자. 그리고 오른쪽 상단의 정책 생성을 누른 후, JSON을 누르자

 

그리고 아래와 같은 JSON을 입력해준다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:PutLogEvents",
                "logs:CreateLogGroup",
                "logs:CreateLogStream"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::{원본 버킷 이름}/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl" 
            ],
            "Resource": "arn:aws:s3:::{리사이즈된 버킷 이름}/*"
        }
    ]
}

 

자신의 버킷 이름으로 고쳐서 JSON을 입력하자. 내 버킷 이름이 image-resize라면, arn:aws:s3:::image-resize/* 이런식으로 입력하면 된다. 해당 부분은 S3 버킷의 받아오기 권한, 삭제 권한, 업로드 권한을 추가해주는 것이다. 이게 없으면 403 에러가 난다. 또한, Cloudwatch에 Lambda 로그를 남기기 위한 권한도 추가해줬다.

 

 

이제 Lambda를 위한 IAM 역할을 만든다. 사용 사례를 Lambda로 지정해주고

 

아까 만든 정책을 추가해주면 끝이다.

 

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

이제 이미지가 리사이징 되서 저장되도록하는 함수를 만들어야된다. Lambda는 다양한 언어를 지원하지만, 나는 Java 17로 진행했다.

 

implementation platform('software.amazon.awssdk:bom:2.17.201')
implementation 'software.amazon.awssdk:s3'
implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
implementation 'com.amazonaws:aws-lambda-java-events:3.1.0'
implementation 'com.amazonaws:aws-lambda-java-log4j2:1.2.0'

 

일단 필요한 패캐지 의존성을 추가하자. 위와 같은 의존성을 추가하면 된다. 참고로 내 코드를 보면 Spring 코드도 보이는데 그럴 필요 없다.

 

gradle에서 중요한게, Lambda가 Node.js로 하면 콘솔 코드 편집기로 바로 코드 작성이 가능한데, java17 같은 경우는 zip 파일로 업로드 해야된다. 따라서, 코드를 작성하고 zip 파일로 빌드하는 과정을 거쳐야 된다. 따라서 gradle 의존성에 아래와 같은 것을 추가해야된다.

 

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

tasks.named('build') {
    dependsOn buildZip
}

 

이제 Gradle 빌드시, build 폴더에 zip 파일이 생길 것이다.

 

package com.lambda.demo;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Handler implements RequestHandler<S3Event, String> {

    //이미지 베이스 타입 (Content-Type)
    private final String baseType = "image/";

    //이미지를 줄이거나 늘릴 비율
    private final double SCALE = 0.5;

    //해당 확장자를 가진 이미지만 처리
    private final List<String> allowedTypes = List.of(".jpg", ".jpeg", ".png");

    @Override
    public String handleRequest(S3Event s3Event, Context context) {
        LambdaLogger logger = context.getLogger();

        logger.log(s3Event.getRecords().size() + " Images Uploaded Event Accepted \n");

        S3Client s3Client = null;
        InputStream inputStream = null;

        try{
            //S3 Event로 부터 해당 Bucket 이름, 파일 명을 받아옴
            S3EventNotification.S3EventNotificationRecord record = s3Event.getRecords().get(0);
            String srcBucket = record.getS3().getBucket().getName();
            String srcKey = record.getS3().getObject().getUrlDecodedKey();

            //정규식을 이용하여 확장자 추출
            Matcher matcher = Pattern.compile("(.+/)*(.+)(\\..+)$").matcher(srcKey);
            if (!matcher.matches()) {
                logger.log("Unable to infer image type for key " + srcKey);
                return "";
            }

            String path = matcher.group(1);
            String fileName = matcher.group(2);
            String imgType = matcher.group(3);

            //원하는 이미지 타입이 아니면 무시
            if(!allowedTypes.contains(imgType)){
                logger.log(fileName + " has unsupported image type " + imgType);
                return "";
            }

            s3Client = S3Client.builder().build();
            inputStream = s3Client.getObject(GetObjectRequest.builder()
                    .bucket(srcBucket)
                    .key(srcKey)
                    .build());

            //이미지를 받아와서 메모리에 올림
            BufferedImage srcImage = ImageIO.read(inputStream);

            //받아온 이미지를 가지고 Resized된 이미지 생성
            BufferedImage newImage = resize(srcImage, SCALE);

            //크기를 조정한 이미지를 저장할 버킷
            String dstBucket = srcBucket + "-resized";

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ImageIO.write(newImage, imgType.substring(1), outputStream);

            String contentType = baseType + imgType.substring(1);

            Map<String, String> metadata = new HashMap<String, String>();
            metadata.put("Content-Length", Integer.toString(outputStream.size()));
            metadata.put("Content-Type", contentType);


            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                    .bucket(dstBucket)
                    .key(srcKey)
                    .metadata(metadata)
                    .build();

            //크기 조정한 이미지를 버킷에 저장
            logger.log("Writing to: " + dstBucket + "/" + srcKey);
            s3Client.putObject(putObjectRequest, RequestBody.fromBytes(outputStream.toByteArray()));
            logger.log("Successfully resized " + srcBucket + "/" + srcKey + "and uploaded to " + dstBucket + "/" + srcKey);

            //기존 원본 이미지는 버킷에서 삭제
            logger.log("Deleting from: " + srcBucket + "/" + srcKey);
            s3Client.deleteObject(builder -> builder.bucket(srcBucket).key(srcKey));

            return "Ok";
        } catch (Exception e) {
            logger.log(e.getMessage());
            throw new RuntimeException(e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (Exception e) {
                    logger.log("Error closing inputStream: " + e.getMessage());
                }
            }
            if (s3Client != null) {
                s3Client.close();
            }
        }
    }

    public BufferedImage resize(BufferedImage img, double scale) {
        int originalWidth = img.getWidth();
        int originalHeight = img.getHeight();

        // 비율을 유지하여 새로운 크기를 계산
        int newWidth = (int) (originalWidth * scale);
        int newHeight = (int) (originalHeight * scale);

        BufferedImage resizedImg = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = resizedImg.createGraphics();
        g.setPaint(Color.WHITE);
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(img, 0, 0, newWidth, newHeight, null);
        g.dispose();

        return resizedImg;
    }
}

 

그 다음 Lambda 함수를 작성한다. 주석을 써놨으니 천천히 읽어보고 자신에게 맞게 수정하면 되겠다. 참고로, 나는 원본 이미지가 Bucket에서 삭제되도록 구성했는데, 원본을 남기고 싶다면 해당 부분을 삭제하면 되겠다.

 

그 다음 이제 gradle build를 해보면

 

 

위와 같은 경로에 zip 파일이 생긴다. 이걸 S3에 업로드 해도 되고, 그냥 바로 Lambda에 업로드 해도 된다.

 

6. Lambda 함수 생성

이제 lambda를 만들차례다.

 

 

런타임 환경은 Java 17로 하고, 역할은 아까 만들었던 역할을 부여해주면 된다. 

 

 

Lambda를 생성했다면, 이제 세부 정보에 들어가서 코드 소스에 zip 파일 업로드를 클릭하고 아까 말했던 zip을 업로드하자.

 

 

그 다음 런타임 설정 편집에 들어가서 핸들러에 우리가 만들었던 핸들러 경로를 명시해주자. {패캐지}.{클래스}.{매서드명} 이렇게 입력해주면 된다.

 

 

마지막으로 트리거 추가를 눌러서 Lambda가 발동할 트리거를 추가해주자.

 

 

원본 이미지가 업로드될 S3 버킷을 선택하고, 모든 객체 생성 이벤트로 이벤트 유형을 선택한다. 그리고 여기서도 명시되있듯이, 재귀호출을 조심하라고 써있다.

 

 

Lambda가 작업한 로그는 Lambda의 모니터링에서 Cloudwatch로 보기를 누르면 볼 수 있다.

 

 

이렇게 로그가 남는다.

 

리사이징 전과 후의 용량 비교이다. 이렇게 크게 단축된다. 따라서, 사용자는 이미지를 받아오는데 시간을 줄일 수 있다.

 

추가적으로 CloudFront같은 CDN 서버를 사용하면, 더 빠르게 응답할 수 있을 것이다! Image Resize와 CloudFront를 조합해서 사용해보도록 하자.

'BackEnd > AWS' 카테고리의 다른 글

[AWS] AWS Lambda의 활용  (8) 2024.11.09
[AWS] AWS EC2 프리티어 메모리 부족 현상 해결하기  (0) 2024.05.02