지난글에서는 CDN을 통해 이미지를 캐싱해 이미지 조회 퍼포먼스를 향상시키는 방법에 대해 간략하게 설명해보았다
다만 이것만으로는 조회 속도가 생각보다는 빨라지지 않음을 확인할 수 있다
왜냐하면 자체 컨텐츠의 크기가 너무 큰 경우 캐싱을 하더라도 만족할만한 수준의 속도를 낼 수 없기 때문이다

그렇기에 이미지 조회 크기를 조정할 수 있다면 전체 요청 용량을 줄일 수 있고, 서비스 전송 속도를 개선할 수 있다
이를 이미지 리사이징이라고 하는데 정해진 방법이 있다기 보다는 사용처에 따라 다양한 방식으로 구현할 수 있다
이과정에서 이미지를 어떻게 전달할지 방법에 따라 성능 개선을 도모할 수 있다
지난글에 이어 이번에도 AWS를 기준으로 예시를 들어보고자한다
다만 이번에는 처리 방식이 굉장히 다양하다
그래서 처리 방식에 대해 먼저 설명을 하고 AWS에서 이를 어떻게 적용하는지 알아보자
이미지를 업로드하는 시점에 용량을 줄이는 방법이다
업로드시점에 작아진 이미지를 적재하고, CDN은 이를 캐싱하도록 연결한다

이방식은 조회를 하던 말던 항상 업로드 시점에 적용되기에 On The Fly Resizing 방식이라고 부른다
AWS에서 이를 적용하는 방법으로는 S3 Object Lambda를 사용하는 방법이 있다
장점으로는 업로드 시점에만 resizing이 동작하기 때문에 FaaS의 동작 횟수가 상당히 적다
하지만 업로드 시점에 redizing이 완료되지 않는다면 원본 이미지를 조회할 수 밖에 없기 때문에 문제가 발생할 수 있다
이미지를 조회하는 시점에 용량을 줄이는 방법이다
정확히 말하자면 이미지를 조회하는 시점보다는 CDN에서 원본 서버에 콘텐츠를 요구할때 즉각적으로 resizing을 하는 방법이다
이 방식을 On-Demand 방식이라고 부르는 분들도 계시고 On The Fly 방식이라고도 부르는거같은데 사실 뭐가 맞는지는 잘 모르겠다

caching이 안되어있는 경우(원본 요청을 해야하는 경우) FaaS등을 사용해 용량을 줄이는 방식이다
물론 CDN에 caching된 이미지가 만료될때마다 함수가 돌아가는 비용이 발생하기는 한다만 위의 업로드 시점에비해 안정적인 장점이 있다
AWS에서 적용하는 방법은 여러가지가 있다
찾아본 바로는 위와같은 방법들이 있는데 사실 더 있을지도 모른다
위 방법들이 비슷해보이지만 맨위의 Lambda@Edge라는 녀석이 조금 특이하다
Lambda@Edge는 다른 Lambda와 다르게 엣지로케이션에서 동작하는 Lambda라서 이미지 최적화와 같이 물리적 위치와도 관련된 작업을 할때 용이하다
차이점을 분석한 글도 있으니 참고해보면 좋을거같다
그럼 필자는 어떤 방법을 선택했느냐?
여러가지 이유가 있지만 Lambda@Edge가 당장의 서비스에 적합하다고 판단했다
이유는 다음과 같다
1. 서비스 특성상 업로드 직후에 즉각적으로 해당 컨텐츠를 조회할 일이 많기때문에 업로드 시점 최적화는 부적합
2. AWS API Gateway를 사용하지 않음
3. CloudFront Function의 경우 CPU와 메모리 지원이 상당히 제한적이지만 이미지 최적화 작업이 이를 얼마나 요구할지 알 수 없음
프리티어 적용이 안되는건 눈물이나지만 Lambda@Edge를 사용해 resizing을 적용해봤다
조회 시점 Resizing 방식에서 썼던 시퀀스 다이어그램과 비슷하게 동작한다
인프라를 구축하는 방법이 그리 어렵지는 않으나 resizing function을 작성하는것이 쉽지 않았다 😂(이 글이 아니었다면 꽤 고생했을거같다)

캐싱된 이미지가 없는 경우 아래의 코드가 Lambda@Edge를 통해 동작하게된다
이때 Lambda@Edge는 US NorthEast 1(버지니아 북부) 리전에서만 동작하니 인지해야한다

Edge Function이 멀면 resizing을 요청할때 먼곳에서 요청을 하기에 너무 느려지지는 않을까 하는 의문이 생겼는데 버지니아 북부에서 구축해도 전세계의 엣지 로케이션에 배포되니 걱정하지 않아도 된다고 한다
사용할 라이브러리가 Node.js의 Sharp이기 때문이다

CDN이 원본을 요청하기 위해 두가지 정책이 필요하다
캐싱 정책: cache hit시 검증되는 정책

원본 요청 정책: cache miss시 원본 요청 과정에서 검증되는 정책

위 두 정책에 쿼리 문자열에 대한 허용을 적용하는 모습을 확인할 수 있다
이는 쿼리 문자열로 Lambda@Edge에서 이미지 resizing을 적용할 매개변수를 받기 때문이다
Lambda@Edge에서 동작할 함수가 필요하다
일단 원본 요청시 동작할 트리거를 추가하자


당연하게도 리소스의 출처가 동일한곳에서 요청하는것이 아니기 때문에 CORS 설정도 해줘야한다
컨텐츠 저장 위치인 S3로 가서 원본 확보가 가능하도록 CORS 세팅을 해주자

일단 정책 먼저 만들자
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:CreateServiceLinkedRole",
"lambda:GetFunction",
"lambda:EnableReplication",
"cloudfront:UpdateDistribution",
"s3:GetObject",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
"Resource": "*"
}
]
}
객체에 대한 조회 권한과 lambda의 함수 접근 권한, edge location에 함수를 전달하기 위한 복사 권한이 보인다

정책을 연결할 IAM을 만들고

신뢰 관계에 위와 같이 추가해주자
이제 이미지 resizing을 도와줄 Sharp를 사용해보자
찾아보니 resizing말고도 워터마크 작업도 해준다고 하니 혹시라도 필요하게 된다면 적용해봐도 좋을거같다
일단 package.json에 "type": "module"을 추가한 후 아래와같이 index.js를 작성하자
'use strict';
import Sharp from "sharp";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const S3 = new S3Client({
region: 'ap-northeast-2'
});
const getQuerystring = (querystring, key) => {
return new URLSearchParams("?" + querystring).get(key);
};
export const imageResize = async (event, context) => {
const { request } = event.Records[0].cf;
let response = event.Records[0].cf.response || {
status: 200,
statusDescription: "OK",
headers: {}
};
const querystring = request.querystring;
if (!querystring) {
return response;
}
const uri = decodeURIComponent(request.uri);
const width = Number(getQuerystring(querystring, "w")) || null;
const height = Number(getQuerystring(querystring, "h")) || null;
const fit = getQuerystring(querystring, "f");
const quality = Number(getQuerystring(querystring, "q")) || null;
const s3BucketDomainName = request.origin.s3.domainName;
let s3BucketName = s3BucketDomainName.replace(".s3.ap-northeast-2.amazonaws.com", "");
s3BucketName = s3BucketName.replace(".s3.amazonaws.com", "");
const s3Path = uri.substring(1);
let s3Object = null;
try {
s3Object = await S3.send(new GetObjectCommand({
Bucket: s3BucketName,
Key: s3Path
}));
} catch (err) {
console.log("S3 GetObject Fail!! \n" +
"Bucket: " + s3BucketName + ", Path: " + s3Path + "\n" +
"err: " + err);
response.status = 404;
response.statusDescription = "Not Found";
response.body = "Image not found.";
return response;
}
const s3Uint8ArrayData = await s3Object.Body.transformToByteArray();
let resizedImage = null;
try {
resizedImage = await Sharp(s3Uint8ArrayData)
.resize({
width: width,
height: height,
fit: fit
})
.toFormat("webp", {
quality: quality
})
.toBuffer();
console.log("Sharp Resize Success");
} catch (err) {
console.log("Sharp Resize Fail!! \n" +
"Bucket: " + s3BucketName + ", Path: " + s3Path + "\n" +
"err: " + err);
response.status = 500;
response.statusDescription = "Internal Server Error";
response.body = "Image processing failed.";
return response;
}
const resizedImageByteLength = Buffer.byteLength(resizedImage);
if (resizedImageByteLength >= 1048576) {
response.status = 400;
response.statusDescription = "Bad Request";
response.body = "Image exceeds the size limit.";
return response;
}
response.status = 200;
response.body = Buffer.from(resizedImage).toString('base64');
response.bodyEncoding = 'base64';
response.headers['content-type'] = [{ key: 'Content-Type', value: `image/webp` }];
return response;
};
막 복잡해보이지만 사실상 별거 없다
쿼리 파라미터로 받아온 값들에 맞춰 heagith, width, fit(크기에 이미지를 어떻게 맞출지?), quality를 Sharp가 알아서 처리해준다
파일 형식은 webp로 통일했는데, 왜 avif를 안했냐 하면 몰랐기 때문이다...(찾아보니 avif가 조금 더 빠른거 같기도...)
또 잘 보면 resizing한 이미지의 크기가 1048576바이트를 넘기는 경우 에러를 발생시키는데 이는 Lamgda@Edge의 제한사항 때문이다

작성한 프로젝트는 압축해서 Lambda에 올리자

로컬에서 작업하다보니 aws에서 사용하는 모듈을 제대로 못받아왔던것

severless framework를 사용하자

작성된 코드를 무작정 올려봐야 권한이 없기 때문에 객체를 가져오는 과정 자체를 sharp가 호출할 수 없다
Lambda의 실행 권한에 아까 만든 IAM을 추가해주자


크기는 14배 작아지고, 속도도 약 3.5배 빨라진걸 확인할 수 있다
올리브영 테크블로그 웹사이트 최적화 방법 - 이미지 파트
올리브영 테크블로그 AWS Lambda Image Resize 도입기
AWS 개발자 가이드 CloudFront Functions와 Lambda@Edge 간 차이점
온디맨드 리사이징 구현기(feat. CloudFront, Lambda@Edge)