AWS Lambda를 이용해 이미지 리사이징 적용 - 이미지 로딩 속도 최적화

오형상·2024년 11월 15일
0

Ficket

목록 보기
13/27

AWS Lambda 도입 배경

행사 등록 화면에서는 하나의 포스터 이미지와 하나의 배너 이미지를 등록하지만, 다양한 화면 요소에 맞춰 적절한 크기의 이미지가 필요합니다. 아래 설계도에서 볼 수 있듯이 각 이미지의 사용 목적에 따라 다양한 사이즈로 제공되어야 합니다.

포스터 이미지
배너 이미지

S3에서 원본 이미지를 그대로 불러오면 불필요한 데이터 전송으로 인해 조회 성능이 저하됩니다. 반대로 서버에서 이미지 리사이징을 처리하면 서버 부하가 증가할 우려가 있습니다. 이러한 문제를 해결하기 위해 AWS Lambda를 활용하여 서버리스 기반 이미지 리사이징 방안을 도입하게 되었습니다.


적용 과정

1. S3 버킷 생성

  • 원본 이미지를 저장할 버킷리사이징된 이미지를 저장할 버킷 총 2개를 생성합니다.

    • 원본 이미지 버킷: ficket-origin
    • 리사이징된 이미지 버킷: ficket-resized

2. IAM 정책 생성

Lambda 함수가 S3 버킷과 통신할 수 있도록 IAM 정책을 생성합니다.

  1. 정책 생성 페이지로 이동

  2. 정책 JSON 입력
    아래 정책에서 sourcebucketsourcebucket-resized 부분을 생성한 S3 버킷 이름으로 수정합니다.

    {
        "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:::sourcebucket/*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "s3:PutObject"
                ],
                "Resource": "arn:aws:s3:::sourcebucket-resized/*"
            }
        ]
    }
  3. 정책 이름 설정 및 생성


3. IAM 역할 생성

Lambda 함수에 위에서 생성한 정책을 부여하기 위해 역할을 생성합니다.

  1. 역할 생성 페이지로 이동

  2. Lambda 역할 선택

  3. 생성한 정책 연결

  4. 역할 생성 완료


4. 배포 패키지 생성

원본 S3 버킷에 이미지가 업로드되었을 때 자동으로 리사이징된 이미지를 저장하도록 Lambda 함수를 작성합니다. 여러 언어를 지원하지만, Java 17로 구현했습니다.

  1. 의존성 추가
    build.gradle에 아래 의존성을 추가합니다.

    dependencies {
        implementation 'com.amazonaws:aws-lambda-java-core:1.2.2'
        implementation 'com.amazonaws:aws-lambda-java-events:3.11.1'
        implementation 'com.amazonaws:aws-java-sdk-s3:1.12.429'
    }
  2. Lambda 함수 작성
    아래 함수는 AWS에서 제공하는 예제 코드를 수정한 코드입니다.

    package com.example.resizingimage.ficket;
    
    import com.amazonaws.services.lambda.runtime.LambdaLogger;
    import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3EventNotificationRecord;
    
    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.HashMap;
    import java.util.Map;
    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.PutObjectRequest;
    import com.amazonaws.services.s3.model.S3Object;
    import com.amazonaws.services.s3.AmazonS3ClientBuilder;
    
    public class ResizeHandler implements RequestHandler<S3Event, String> {
    
      private final String JPG_TYPE = "jpg";
      private final String JPG_MIME = "image/jpeg";
      private final String JPEG_TYPE = "jpeg";
      private final String JPEG_MIME = "image/jpeg";
      private final String PNG_TYPE = "png";
      private final String PNG_MIME = "image/png";
    
      private final Map<String, int[]> posterDimensions = new HashMap<>() {{
          put("mobile_poster", new int[]{90, 120});
          put("pc_poster", new int[]{314, 475});
          put("pc_poster_main1", new int[]{244, 321});
          put("pc_poster_main2", new int[]{138, 187});
      }};
    
      private final Map<String, int[]> bannerDimensions = new HashMap<>() {{
          put("pc_banner", new int[]{600, 193});
          put("mobile_banner", new int[]{322, 150});
      }};
    
      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 = "ficketresizebucket";
    
              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))) {
                  logger.log("Skipping non-image " + key);
                  return "";
              }
    
              AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();
              S3Object s3Object = s3Client.getObject(new GetObjectRequest(srcBucket, key));
              InputStream objectData = s3Object.getObjectContent();
              BufferedImage srcImage = ImageIO.read(objectData);
    
              // key 경로에서 포스터와 배너를 구분하여 리사이징
              if (key.contains("poster")) {
                  resizeAndUploadImages(s3Client, srcImage, posterDimensions, dstBucket, key, imageType, logger);
              } else if (key.contains("banner")) {
                  resizeAndUploadImages(s3Client, srcImage, bannerDimensions, dstBucket, key, imageType, logger);
              } else {
                  logger.log("No matching dimensions for key " + key);
              }
    
              return "Ok";
          } catch (IOException e) {
              throw new RuntimeException(e);
          }
      }
    
      private void resizeAndUploadImages(AmazonS3 s3Client, BufferedImage srcImage, Map<String, int[]> dimensions, String dstBucket, String key, String imageType, LambdaLogger logger) {
          for (Map.Entry<String, int[]> entry : dimensions.entrySet()) {
              String sizeKey = entry.getKey();
              int[] dims = entry.getValue();
              int width = dims[0];
              int height = dims[1];
    
              BufferedImage resizedImage = resizeImage(srcImage, width, height);
    
              // S3에 저장
              String resizedKey = sizeKey + "/" + key;
              uploadToS3(s3Client, dstBucket, resizedKey, resizedImage, imageType, logger);
          }
      }
    
      private BufferedImage resizeImage(BufferedImage srcImage, int width, int 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();
          return resizedImage;
      }
    
      private void uploadToS3(AmazonS3 s3Client, String bucket, String key, BufferedImage image, String imageType, LambdaLogger logger) {
          try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
              ImageIO.write(image, imageType, os);
              InputStream is = new ByteArrayInputStream(os.toByteArray());
              ObjectMetadata meta = new ObjectMetadata();
              meta.setContentLength(os.size());
              meta.setContentType(getMimeType(imageType));
    
              logger.log("Writing to: " + bucket + "/" + key);
    
              s3Client.putObject(new PutObjectRequest(bucket, key, is, meta));
          } catch (AmazonServiceException | IOException e) {
              logger.log("Failed to upload resized image to S3: " + e.getMessage());
          }
      }
    
      private String getMimeType(String imageType) {
          switch (imageType.toLowerCase()) {
              case JPG_TYPE:
              case JPEG_TYPE:
                  return JPG_MIME;
              case PNG_TYPE:
                  return PNG_MIME;
              default:
                  return "";
          }
      }
    }
  3. 빌드 및 배포 패키지 생성
    build.gradle에 아래 코드를 추가하여 아래의 명령어를 실행해 ZIP 파일을 생성합니다.

    tasks.register('buildZip', Zip) {
        from compileJava
        from processResources
        into('lib') {
            from configurations.runtimeClasspath
        }
    }

    명령어 실행:

    ./gradlew build
    ./gradlew buildZip

이렇게 생성한 ZIP 파일을 S3(ficket-zip-storage)에 업로드합니다. 파일 사이즈가 커 S3 URL을 통해 AWS Lambda에 적용합니다.


5. Lambda 함수 생성

  1. 함수 생성 및 역할 연결

  2. 코드 업로드
    ficket-zip-storage에 올린 ZIP 파일의 객체 URL을 입력합니다.

  3. 런타임 설정
    패키지.클래스::메소드 형식으로 입력합니다.


6. S3 트리거 생성

  • S3 버킷에 이미지가 업로드될 때 Lambda 함수가 실행되도록 트리거를 설정합니다.


이렇게 설정을 완료하면 원본 이미지는 ficket-origin에 저장되고, ficket-resized에는 리사이징된 이미지가 저장됩니다.


Reference

0개의 댓글