여러 이미지를 로드할 때, 시간을 줄이는 방법에 대해 고민하다가 기업들의 실제 서비스에서 트러블 슈팅한 경험들이 소개되어 있어 참고하였다.
1) https://medium.com/daangn/lambda-edge로-구현하는-on-the-fly-이미지-리사이징-f4e5052d49f3
2) https://www.slideshare.net/awskorea/ondemand-image-resizing
이번 리팩토링의 목표는 CDN과 리사이징을 통해, 레시피를 불러올 때 이미지 로드 시간을 줄이는 것이다.
CDN(Content Delivery Network)은 콘텐츠를 효율적으로 전달하기 위해 여러 노드를 가진 네트워크에 데이터를 저장하여 제공하는 시스템이다.
동작 원리
ISP(Internet Services Provider)의 CDN서버에 콘텐츠를 분산시켜, 유저의 네트워크 경로 상 가장 가까운 곳의 서버로 부터 콘텐츠를 전송받게 한다.
즉, 사용자는 CDN 서버에 캐싱된 데이터를 바로 전달받아 빠르게 콘텐츠를 얻을 수 있다. 만약 요청한 데이터가 CDN 서버에 없다면, 오리진(원본) 서버에서 콘텐츠를 조회하여 CDN에 저장하고 사용자에게 전달한다.
Static Caching: 사용자의 요청이 없어도 Origin Server의 콘텐츠를 운영자가 미리 Cache Service에 복사한다.
Dynamic Caching: 사용자가 콘텐츠를 요청하면, Origin Server에서 콘텐츠를 가져와 저장한다.
Temporal Locality
Temporal Locality(시간적 지역성)는 방금 사용된 데이터는 또 사용될 가능성이 높다는 원칙으로, Cache Hit Ratio가 높은 원인 중 하나이다.
(나머지 하나는 Spatial Locality. 하나의 데이터가 참조되면 곧바로 그 주위의 데이터가 참조될 가능성이 높다.)
집다방에서는 Temporal Locality에 특화된 기능이 다음과 같이 존재한다.
CloudFront
AWS에서는 CloudFront를 통해 CDN을 적용할 수 있다.
프리티어로 제공되는 양이 꽤 많으니 참고해서 사용하면 좋을 듯 하다.
Lambda@Edge는 CloudFront Edge 전후 위치에서 실행되는 Lambda 서비스이다.
서버를 프로비저닝하거나 관리하지 않고 한 리전 US-East-1(버지니아 북부)에서 Node.js 또는 Python 함수를 작성한 후 뷰어에게 가까운 전 세계 AWS 위치에서 해당 함수를 실행할 수 있습니다.
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html
Lambda@Edge를 사용할 때는, us-east-1
리전에서 Lambda 함수를 작성해야 하며, 해당 함수는 CloudFront의 각 지역 단위 edge location에 복사해 배포된다.
유저는 CloudFront 사용 시, 가까운 edge location에 배포된 Lambda@edge 함수를 거쳐 응답받게 된다.
Lambda@Edge trigger Event
우리는 S3(Origin)으로부터 리소스를 CloudFront에 응답할 때 Lambda 함수를 적용할 것이므로 Origin response를 사용한다.
1. 기존 아키텍쳐
기존 방식은 단말에서 S3 URI로 이미지를 바로 호출하고 반환받는 방식이었다.
2. CloudFront (CDN) 적용 후
3. Lambda 통한 Resizing 적용 후
S3에는 원본을 저장하고, 이미지를 불러올 때 리사이징 함수를 호출해 CDN에 저장하도록 하였다.
왜 이미지를 저장할때 리사이징하지 않고 불러올 때 리사이징 하는 것일까?
처음 이미지를 호출하는 유저는 응답 시간이 오래 걸리지 않을까?
이미지는 한 사이즈로만 리사이징 되는 것이 아니다. 집다방 서비스의 예시로는, 썸네일/스텝/미리보기 이미지는 각각 리턴해야 하는 크기가 다르다. 이미지 저장 시, 한번에 필요한 모든 크기를 생성해 저장하는 것은 불필요하다.
또한 리사이징 후 저장된 이미지의 대부분은 거의 쓰이지 않고, S3 공간만 차지하게 된다. 위에서 언급한 Temporal Locality와 관련하여, 주로 호출되는 이미지만 계속 호출된다.
만약 이미지가 등록되어, Lambda가 열심히 리사이징 하는 중에 해당 이미지 삭제 요청이 들어온다면..?
위와 같은 이유로 공간적/비용적 효율을 위해 대부분의 아키텍쳐는 이미지 로드 시 리사이징 하는 방법을 채택하고 있다.
별개로, S3에 저장되는 원본 사이즈를 줄이기 위해 FE에서 이미지를 한번 압축하고 보내주는 것은 참 좋은 것 같다.
Figma에 디자이너가 세팅한 크기에 맞추어 리사이징하며, 원본의 90% 품질로 조정한다.
원본은 2.5MB
의 2252 * 4000 px
이미지이다.
기존 방식 / cdn 적용 / resizing 적용(lambda) 후 각각 테스트를 진행하며,
동시 요청하는 부하테스트 유저 수(Thread 수)는 다음과 같다.
베스트 레시피 하나에 여러 사람이 동시 접근하는 상황이다.
썸네일 한개와, 스텝 별 이미지 5개 총 6개의 이미지를 호출한다.
레시피 상세 페이지
360 * 360 px
328 * 328 px
동시에 다수의 유저가 레시피 목록을 조회하는 상황이다.
10개씩 페이징되므로, 미리보기 썸네일 총 10개의 이미지를 호출한다.
목록 조회(Preview) 페이지
160 * 160 px
(퍼블릭 엑세스가 허용된 S3 버켓이 있다는 가정하에 진행됩니다)
1) 버킷 정책 생성
[CDN을 허용할 버킷 클릭] - [권한] - [버킷 정책] - [편집] - [정책 생성기]
[CDN을 허용할 버킷]-[속성]-[Amazon 리소스 이름(ARN)]
복붙[Add Statement]-[GeneratePolicy]-[Json Document 복사]
복사한 JSON을 [버킷 정책 편집]
에 붙여넣는다.
이 때, 버킷에 있는 모든 객체에 대한 액세스를 허용하려면Resource
에 /*
를 추가한다. 특정 디렉토리만 선택할 수도 있다.
Principal
의 *
는 모든 사용자에 대해 이 정책이 적용됨을 의미한다.
2) 정적 웹 호스팅 활성화
정적 웹 사이트 호스팅은 S3 버킷을 홈페이지처럼 사용할 수 있는 기능이다.
[CDN을 허용할 버킷]-[속성]-[정적 웹 호스팅]-[편집]
1) CloudFront 생성
[CloudFront]-[배포]-[배포 생성]
[표준 로깅]-[켜기]
로 로그를 S3에 저장할 수 있다. 쿠키 로깅도 사용한다.실제 서비스라면 WAF를 활성화 해야겠으나, 테스트 용도이므로 지금은 비활성화를 선택한다.
2) 동작 설정
[생성한 CloudFront]-[동작]-[동작 생성]
나중에 트리거에 적용할 경로 패턴을 설정한다.
3) S3에서 CloudFront로 파일 불러오기
파일을 저장할 때는, 기존에 저장하던 S3에 저장하되, DB에 저장하는 URI는 https://${CloudFront 도메인 이름}/${버킷}/example.jpg
처럼 저장한다.
public String uploadFile(String KeyName, MultipartFile file) throws IOException {
System.out.println(KeyName);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
amazonS3.putObject(new PutObjectRequest(amazonConfig.getBucket(), KeyName,file.getInputStream(), metadata));
//CloudFront 도메인으로 저장하도록 설정
return cloudfrontUri+amazonS3.getUrl(amazonConfig.getBucket(), KeyName).getPath();
}
동일한 이미지를 2번 요청 시 헤더를 확인하면,
X-Cache: Hit from cloudfront
CloudFront가 잘 작동하는 것을 확인할 수 있다.
1. 베스트 레시피 상세 조회
2. 레시피 목록 조회
1) 정책 생성
[IAM]-[액세스 관리]-[정책]-[정책 생성]
총 9개의 권한을 설정한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:CreateServiceLinkedRole",
"s3:GetObject",
"cloudfront:UpdateDistribution",
"lambda:GetFunction",
"lambda:EnableReplication",
"logs:PutLogEvents",
"logs:CreateLogStream"
],
"Resource": [
"*"
]
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"logs:DescribeLogStreams",
"logs:CreateLogGroup"
],
"Resource": "*"
}
]
}
2) 역할 생성
[IAM]-[액세스 관리]-[역할]-[역할 생성]
사용 사례에 Lambda를 선택하고, 방금 생성한 정책을 추가한다.
[방금 생성한 역할]-[신뢰 관계]-[신뢰 정책 편집]
방금 생성한 역할에서, 신뢰할 수 있는 개체를 수정한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
1) Lambda 생성
[버지니아 북부 리전]-[AWS Lambda]-[함수 생성]-[새로 작성]
사용할 언어-버전
과, 방금 생성한 역할을 선택하고 함수를 생성한다.
2) AWS Cloud 9
(브라우저만으로 코드를 작성, 실행 및 디버깅할 수 있는 클라우드 기반 IDE)
[Cloud 9]-[환경 생성]
필요에 따라 인스턴스를 설정한다. 버지니아 북부 리전
에 따로 설정한 vpc는 없으니, 네트워크 설정은 기본값으로 하였다.
[생성된 환경 선택]-[Cloud9 열기]
[좌측 메뉴바의 AWS 로고]-[Add regions to AWS Explorer]-[us-east-1 리전 선택]
[생성한 Lambda 함수 선택]-[Download]
2) npm 초기화 및 설치
cd ResizeImage #생성한 람다함수명으로 디렉토리가 구성되어있음.
npm init -y
npm install sharp
npm install aws-sdk
창 아래의 bash에 명령어를 입력하여 npm 설치를 진행한다.
설치가 완료되면, 리사이징 동작을 할 수 있도록 ResizeImage의 index.js
파일을 수정한다.
Node.js 오랜만에 썼더니 낯설다
'use strict';
const querystring = require('querystring');
const AWS = require('aws-sdk');
const Sharp = require('sharp');
const S3 = new AWS.S3({
region: 'ap-northeast-2'
});
const BUCKET = 'example';
exports.handler = async (event, context, callback) => {
const { request, response } = event.Records[0].cf;
const params = querystring.parse(request.querystring);
const { uri } = request;
const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);
let width, height;
// Set width and height based on querystring.
switch (params.size) {
case 'preview':
width = height = 160;
break;
case 'thumbnail':
width = height = 360;
break;
case 'step':
width = height = 328;
break;
default:
return callback(null, response);
}
// For AWS CloudWatch.
console.log(`parmas: ${JSON.stringify(params)}`); // Cannot convert object to primitive value.
console.log(`name: ${imageName}.${extension}`); // Favicon error, if name is `favicon.ico`.
let s3Object, resizedImage;
try {
s3Object = await S3.getObject({
Bucket: BUCKET,
Key: decodeURI(imageName + '.' + extension)
}).promise();
} catch (error) {
console.log('S3.getObject: ', error);
return callback(error);
}
try {
resizedImage = await Sharp(s3Object.Body)
.resize(width, height)
.toFormat ('jpeg', { quality: 90 })
.toBuffer();
} catch (error) {
console.log('Sharp: ', error);
return callback(error);
}
const resizedImageByteLength = Buffer.byteLength(resizedImage, 'base64');
if (resizedImageByteLength >= 1 * 1024 * 1024) {
return callback(null, response);
}
response.status = 200;
response.body = resizedImage.toString('base64');
response.bodyEncoding = 'base64';
response.headers['content-type'] = [
{
key: 'Content-Type',
value: `image/${extension}`
}
];
return callback(null, response);
};
size
값 preview
/thumbnail
/step
별로 사진 크기를 다르게 리사이징 한다.1MB
가 넘으면 원래의 이미지를 리턴한다. Lambda@Edge의 응답 페이로드는 최대 크기가 1MB로 제한되어 있다.jpeg
로 저장하며, 원본의 90%
품질 이미지를 출력하도록 한다.[Upload Lambda]-[Directory]-[Yes]-[수정한 폴더 선택]-[Open]
작성 완료한 함수를 Lambda에 업로드한다.
3) 트리거 구성 및 배포
[Lambda]-[생성한 함수]-[작업]-[Lambda@Edge 배포]
미리보기/썸네일/스텝별 이미지 사이즈가 각각 다르므로, 조회하는 메소드에서 Response DTO
생성 시 필요한 querystring을 추가한다.
최종 형태는 다음과 같다.
https://${cloudfront 도메인 이름}/${bucket}/${이미지명}?size=
Converter
//Thumbnail Size 360 * 360px 생성
return RecipeResponseDto.build()
.thumbnailUrl(recipe.getThumbnailUrl()+"?size=thumbnail")
.//...
//Step Image Size 328 * 328px 생성
build().image(step.getImageUrl()+"?size=step")
//Preview Size 160 * 160px 생성
build().thumbnailUrl(recipe.getThumbnailUrl()+"?size=preview")
원본 요청시:
Thumbnail 요청시:
Step Image 요청시:
Preview 요청시:
1. 베스트 레시피 상세 조회
2. 레시피 목록 조회
기존 방식 테스트 시 이미지가 FE에서의 압축(WebP 등)을 거치지 않아 좀 더 극명한 차이가 발생했으나 두 상황 모두 상당히 개선된 지연시간 결과를 나타낸다.
가장 처음 원본 이미지를 로드하는 유저는 약 4-5초의 시간이 소요되었지만, 평균적으로는 지연 시간이 8~9배 감소한 것을 확인할 수 있다.
3. Resize 적용하여 100명/1,000명 동시 요청
참고
https://nayoungs.tistory.com/entry/AWS-CDN%EA%B3%BC-Amazon-CloudFront-S3-%EC%97%B0%EB%8F%99