22년 8월, 서버 부하 테스트를 마치고 진행했던 업무는,
우리 서비스에 다중 이미지 업로드 기능을 추가할 예정이라, 이에 따라 이미지 썸네일 만드는 작업을 개선 해야했다.
지금까지는 사용자가 한장의 사진만 업로드할 수 있었기 때문에 서버에서 모든 작업을 진행하고 있었지만,
최대 20장까지 한번에 업로드 할 수 있도록 기획하고 있었기 때문에 (나중에는 10장으로 축소되었다.)
지금처럼 API 서버에서 용량이 큰 이미지 파일을 받아 S3에 저장하고, DB에 저장하고, 이미지 썸네일까지 생성한다면
사용자들이 업로드 속도가 느리다고 느낄 만큼 느려질 것이라 예상했다.
실제로 지금 단일 이미지 업로드 속도는 API 기준으로 약 1.n초가 걸렸으며, 10장이라면 10초까지도 걸릴 수 있는 상황이였다. 그래서 이미지 썸네일 만드는 작업을 개선할 필요성이 생겼던 것이였다.
이미지 리사이징 관련 레퍼런스를 찾아보던 중에, 이미지 업로드 시간 자체를 단축시킬수는 없을까? 라는 생각이 들었다.
클라이언트에서 직접 S3로 이미지를 업로드한다면, 서버가 파일을 받아 S3로 업로드 하는 과정을 단축시킬수 있다. 그렇다면 클라이언트에서 직접 S3로 접근했을 때 보안적인 문제가 없을지 찾아보았다.
Pre-Signed URL?
미리 서명된 URL 이란 뜻을 갖고 있는 presignedURL은 이미지를 업로드할 때 백엔드 서버를 거치지 않고
클라이언트에서 바로 S3로 업로드가 가능해진다.
원래는 백엔드가 이미지를 S3로 전달하면서 보안절차 (aws secret key를 활용한 S3 접근) 도 같이 진행한다.
이 과정을 분리시키는 방법이다.
백엔드는 presignedURL을 생성해줘서 보안절차만 작업하고 클라이언트가 S3로 바로 업로드 할 수 있도록 해준다.
실제 파일 업로드는 클라이언트에서 진행되기 때문에 서버의 트래픽이나 대역폭을 신경쓰지 않아도 된다.
presignedURL의 과정은 다음과 같다.

1. 이미지 업로드 요청 시 서버에 presignedURL 을 리턴받을 API 호출
2. 서버에서 AWS S3에 presignedURL 요청
3. AWS에서 presignedURL을 리턴
4. 서버는 클라이언트에게 presignedURL 전달
5. 클라이언트에서 presignedURL으로 S3에 이미지 업로드
6. 이미지 업로드 완료 후 DB에 저장할 데이터 전송
presignedURL을 발급 받는 코드는 다음과 같이 작성하였다.
// controller.ts
@ApiOperation({
summary: 'pre-signed url 발급 요청',
description: `이미지를 업로드할 pre-signed url을 요청한다.`,
})
@UseAuth()
@Post('/presigned-urls')
async getPreSignedUrls(@Res() res, @Body() dto: PreSignedUrlDto) {
const result = await this.fileService.getPreSignedUrl(dto);
return res.status(201).send(result);
}
클라이언트에서 dto에 발급받을 presignedURL 개수와, 이미지를 저장할 버킷 폴더명을 담아 요청을 보낸다.
// service.ts
import * as uuid from 'uuid';
import * as moment from 'moment-timezone';
import { generateSignedUrl } from 'src/utils/s3-utils';
import Bluebird from 'bluebird';
@Injectable()
export class fileService {
constructor() {}
async getPreSignedUrl(dto: PreSignedUrlDto): Promise<string[]> {
return Bluebird.map(new Array(dto.count), () => {
const currentDate = moment().format('YYYYMMDDHHmmss');
const key =`${dto.type}/${currentDate}-${uuidv4()}`;
return generateSignedUrl('putObject', key);
});
}
}
요청한 url 개수만큼 리턴해주어야하는데, 반복문을 병렬로 처리하기 위해 BlueBird 라이브러리를 사용하였다.
// dto.ts
export class PreSignedUrlDto {
@ApiProperty({
description: '발급할 url 개수',
required: true,
default: 1,
})
@IsNotEmpty()
@IsNumber()
count: number;
@ApiProperty({
description: '등록할 S3 폴더',
required: true,
enum: S3FolderName,
default: S3FolderName.Diary,
})
@IsEnum(S3FolderName)
type: S3FolderName;
}
// s3-utils.ts
import AWS from "aws-sdk";
export const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACECSS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
signatureVersion: 'v4',
region: 'ap-northeast-2',
});
export const generateSignedUrl = (operation: string, key: string): string => {
return s3.getSignedUrl(operation, {
Bucket: process.env.AWS_BUCKET,
Key: key,
Expires: 60 * 5,
});
};
이미지를 S3에 저장하는 로직이
클라 -> 서버 -> S3 에서
클라 -> S3 가 되므로, 서버가 이미지 파일을 받아서 업로드 하는 과정이 생략된다.
이렇게 서버에서 파일을 처리하는 작업을 덜어내고, 보안을 위해 presignedURL을 적용했다.
물론 presignedURL을 요청하고 리턴받는 시간이 있지만, 텍스트가 왔다갔다하는 시간이므로 밀리세컨드 단위이다.
아쉬운점은 presignedURL을 적용하고 나서도 얼마나 시간이 줄었는지를 체크했어야하는데, 후에 썸네일 처리 작업까지 하고 두가지가 다 적용된 상태에서 시간을 측정했다.
이제 본론으로 돌아와서, 이미지 업로드 후에 이루어지는 썸네일 생성 방식을 개선해야 한다.
글이 길어져 다음 포스팅에서 람다에 대해 진행하겠다.
'AWS' 카테고리의 다른 글
AWS Lambda를 이용한 이미지 리사이징 (0) | 2023.03.30 |
---|---|
서버 부하 테스트 (feat. Jmeter) (0) | 2023.03.05 |