Pre-signed URL을 통한 다이렉트 업로드

IMWEB tech
imweb tech
Published in
8 min readMar 22, 2024

안녕하세요, Expert Squad의 백엔드 엔지니어 서상우입니다.

오늘은 Expert 서비스에서 업로드를 구현하기 위해서 도입 중인 기술을 공유하고자 합니다.

여러분들은 보통 파일 업로드 처리를 어떻게 하시나요? 대부분의 경우에는 다음과 같은 방법을 택할 것이라고 생각합니다.

클라이언트에서 서버로 파일을 전송하고, 서버는 S3에 파일을 업로드합니다. S3에 파일 업로드가 끝나면 서버는 업로드 결과를 알 수 있고, 클라이언트에 업로드가 완료되었음을 알립니다.

많은 경우에는 이런 방식이 좋긴 합니다. 단순하고, 직관적입니다. 클라이언트의 경우 (Connection time이 좀 길긴 하겠지만) 한 번의 Round trip으로 성공/실패 여부까지 확인할 수 있습니다.

그러나 몇몇 시나리오에서는 이게 조금 문제가 될 수 있습니다. 이번에 Expert Squad에서 개발하려는 기능이 그렇습니다.

기능 요구사항을 설명하기 위해 저희 스쿼드에 대해 소개하자면, Expert Squad에서는 직접 웹사이트를 만들기 어려운 분들과 전문 디자이너분들을 연결해 주는 신규 서비스를 준비하고 있습니다.

사이트 디자인부터 로고, 상세 페이지 이미지 제작까지 아임웹으로 최고의 퀄리티를 만들어내시는 분들을 브랜드와 매칭시켜 드리기 위해 노력하고 있습니다.

전문가분들은 개성 있고 퀄리티 있는 결과물들을 포트폴리오 형태로 게시하고 영업활동을 하실 수 있는데요, 아무리 뛰어나신 분들이라도 고화질의 이미지가 아니라면 본인의 역량을 제대로 어필하기 어려우실 것입니다.

따라서 포트폴리오에는 최대 10MB에 달하는 이미지를 10개까지 업로드 할 수 있도록 구현하고 있습니다. 이런 상황에서 발생할 수 있는 문제는 어떤 것이 있을까요?

사용성 문제

10MB 파일 10개를 100Mbps(≒12MB/s) 속도로 업로드 한다고 하면 약 8.3초가 걸리고 스트림 업로드가 아니라 서버에 한 번 적재한 다음 다시 S3로 업로드한다고 하면 업로드에만 10초 이상 소요되는 어마어마한 작업이 됩니다.

만약 게시글 등록 버튼을 눌렀는데 등록에만 10초 이상 소요된다면 사용성에 엄청난 저하를 가져올 것이 분명합니다.

안정성

비연결성과 무상태성은 HTTP 프로토콜의 특징이지만, 웹서버 자체도 무상태 수준이 높을수록 스케일링에 유리합니다. 100MB짜리 파일을 8.3초 동안 업로드 받아서 서버에 저장하고, S3에 다시 몇초가량 업로드를 수행한다면 이 시간 동안 웹서버는 클라이언트와의 커넥션을 유지하고 서버가 셧다운되지 않도록 유지해야 합니다. 같은 클러스터에 컨테이너가 수십 개 실행 중이더라도 이 순간 해당 유저의 요청 상태를 관리하고 있는 서버는 단 하나이기 때문입니다. 이런 상황은 MSA의 장점인 신속한 스케일링에 방해가 될 수 있습니다.

인프라 비용

아임웹은 서비스 안정성과 유연한 리소스 분배를 위해서 마이크로 서비스 아키텍쳐를 적용시키는 중입니다. 애플리케이션 특성에 따라 다르겠지만 MSA + node.js 구성에서는 Java나 PHP 서버에 비해서 적은 RAM과 vCPU를 할당하고 스케일 아웃 방식으로 트래픽에 대응하는데요. 이런 환경에서는 특정한 상황의 Peak memory 증가가 서버 클러스터 전체의 최저 스펙을 올려야 하는 상황을 강제할 수 있습니다.

클러스터 환경에서는 비용이 2GB x2가 아닙니다

해결책 1. 실시간 업로드

이미지 파일은 작성 완료 시점에 업로드되지 않습니다. 모든 파일은 작성 당시부터 업로드되고 에디터 화면에서 즉시 보여집니다. End-user Squad에서 구축해 주신 On-the-fly 리사이징 환경 덕분에 실제 게시될 사이즈의 이미지를 손쉽게 CDN으로 제공할 수 있게 되었는데요. 혹시 리사이징 과정에서 색감이 틀어지지는 않는지, 화질이 보장되는지 게시 전에 미리 확인할 수 있게 됩니다.

해결책 2. Pre-signed URL

S3에 저장된 파일을 지정된 유저에게만 제공하는 기능을 구현해 본 적이 있으신가요?

유료 컨텐츠를 제공하기 위해서 구매자만 이용 가능한 다운로드 링크를 만들어보셨다면 S3 (또는 Cloudfront)의 Presigned URL을 사용해 보신 경험이 있으실 겁니다.

Presigned URL은 S3 객체에 접근할 수 있는 URL의 한 종류입니다. 발급 권한을 서버에서 가지고 있고, 원하는 대로 규칙을 설정할 수 있기 때문에 의도하지 않은 유저가 무단으로 파일에 접근하는 것을 막을 수 있습니다.

가장 많이 사용되고, 간단하게 적용되는 방법은 짧은 시간 동안만 유효한 URL을 생성해서 전달하는 것입니다.

그런데 이 Presigned URL은 업로드를 할 때에도 사용할 수 있습니다.

기본적인 원리는 똑같습니다. 원래 Pre-signed URL이란 S3에 위치한 리소스에 대한 URL에, 믿을 수 있는 토큰과 접근 권한을 쿼리스트링 및 헤더 등을 이용해서 유효한 URL이라면 다른 Credential 없이 접근이 가능하도록 만든 기술입니다 이번엔 그게 다운로드가 아니라 업로드일 뿐이죠.

node.js용 예제 코드를 작성해 보면 다음과 같습니다.

import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import {
S3Client,
PutObjectCommand,
type PutObjectCommandInput,
} from '@aws-sdk/client-s3';

async getSignedUrl(
input: Pick<PutObjectCommandInput, 'Key' | 'ContentType' | 'ContentLength'>,
expiresIn: number,
) {
const client = new S3Client({
region: 'ap-northeast-2,
credentials: {
accessKeyId: '',
secretAccessKey: '',
},
});

const command = new PutObjectCommand({
Bucket: '{업로드할 버킷명}',
Key: '{업로드할 객체 키}',
ContentType: 'image/png',
ContentLength: 1024,
});

return getSignedUrl(this.client, command, {
expiresIn,
signableHeaders: new Set(['content-type', 'content-length']),
});
}

생성되는 Pre-signed URL의 포맷

https://s3.ap-northeast-2.amazonaws.com/{버킷 이름}/{오브젝트 키}?
...
X-Amz-Date=20240301T073659Z&
X-Amz-Expires=600&
X-Amz-Signature=...&
X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost&
x-id=PutObject

여기서 X-Amz-SignedHeaders 부분을 보면 업로드할 파일 크기나 파일 종류까지도 제한할 수 있다는 것을 알 수 있습니다.

의의와 한계

  • 서버가 업로드를 관리하지 않게 되면서 서버의 무상태성(Stateless)를 유지하기 용이해집니다. 상술했다시피 업로드는 파일 용량이 커질수록 속도도 오래 걸리고, 실패 가능성이 높아집니다.
  • 이어서, 이것은 수평적 확장이 용이하다는 것을 말합니다. 여러 서버에서 각기 다른 데이터를 SSD 혹은 RAM에 보관하는 것은 서버가 내려갈 때 데이터 유실을 발생시킬 수 있습니다. 서버가 직접 업로드를 관리하지 않으면 서버를 늘리거나 줄이는 것이 쉬워집니다.

한계점은 다음과 같습니다.

  • signableHeaders옵션을 통해 헤더에 대한 일부 검증을 수행할 수는 있으나 업로드될 파일에 대한 온전한 제어권을 갖지는 못한다는 것입니다. 아시다시피 Content-Type(Mime type)은 물론 파일의 magic number까지 클라이언트가 마음만 먹으면 조작하지 못할 것은 없습니다. 악의적인 파일에 대한 면밀한 스크리닝은 결국 업로드 사후에 이루어져야 합니다.
  • 클라이언트 입장에서는 한 번의 Request로 끝날 업로드 작업이 세 번으로 늘어나게 됩니다. 서버 측에서도 업로드 전후로 DB에 기록을 하거나 경우에 따라 온전히 끝나지 않은 업로드 기록 등을 정리하는 작업이 필요해집니다.

장단점을 잘 고려해보고 서비스에 도입하면 유의미하게 사용성을 개선할 수 있는 Pre-signed URL 한번 검토해 보시면 어떨까요?

Reference.
미리 서명된 URL로 작업 — Amazon Simple Storage Service

--

--