[댕댕워크] AWS S3로 확장성 있는 이미지 서버 구축하기

acho·2024년 7월 3일
0
post-thumbnail

배경

댕댕워크 서비스에는 '산책 중 사진 찍기' 기능이 있어 이미지 서버 구축이 필요합니다.
이미지 저장용 서버를 따로 두고, 해당 서버에 접근하는 URL만 저장하는 형식으로 구현하려고 합니다.

이슈

  1. 이미지를 어디에 저장할 것인가
  2. 이미지를 저장소에 어떻게 보관할 것인가
  3. 특정 산책 일지와 이미지의 연관성을 어떻게 나타낼 것인가

세 가지가 주요 포인트입니다.

이슈 1. 이미지를 어디에 저장할 것인가

  1. 로컬에 저장
    • 지금은 하나이지만 추후 서비스가 커짐에 따라 서버가 여러 대 올라갈 수도 있는데, 한 서버의 로컬에 데이터가 저장되어있으면 공유가 어려워집니다.
  2. 외부 DB - Amazon S3, Azure File Storage, Google Cloud Storage 등
    - 1번의 문제를 해결할 수 있습니다.
    - 이 중 aws가 업계 표준이라고 해도 될만큼 많이 쓰입니다.
    - aws의 S3는 1기가당 .023 달러밖에 안해 부담이 적습니다.
    - 또 용량을 정해놓고 쓰는 게 아니라 저장하는 만큼 쓸 수 있어 무한히 확장 가능합니다.
    ⇒ aws S3를 사용하기로 했습니다.

이슈 2. 이미지를 어떻게 저장소에 보관할 것인가

이미지 저장소 보관 시 이슈

  1. 로그인한 유저만 업로드가 가능해야 합니다. 즉 인증 인가 절차를 거쳐야 합니다.
  2. 업로드 된 이미지들은 속해있는 산책일지와 관계를 가져야 합니다.
  3. .jpg, .png, 등 이미지 파일만 업로드 할 수 있도록 해야 합니다.

1, 2번은 서버와의 API 통신을 통해 처리해야 합니다.

일반적인 접근방법

  1. 브라우저에서 서버에 이미지를 보낸다(이 과정에서 validation 등 수행)
  2. 서버에서 임시 저장소에 이미지를 저장합니다.
  3. 이미지를 S3에 전송합니다.
  4. 서버의 임시 저장소에 저장되어 있는 이미지를 삭제합니다.

단점

서버에서 이미지를 처리하는 과정에서 CPU 리소스를 많이 소모하게 됩니다.
유저가 적을 때는 괜찮겠지만, 늘어날수록 서버에 부하가 커지게 됩니다.

해결책 : 서버가 아닌 클라이언트가 S3에 이미지를 전송한다

  1. 클라이언트는 서버에게 이미지 업로드가 필요함을 알립니다. 파일 이름과 파일 타입을 명시해 API 요청을 날립니다. 이 때 인증 / 인가가 자연스럽게 수행됩니다.

  2. 서버는 S3에 특정 파일 이름 / 타입의 이미지를 업로드 할 수 있는 특수한 URL인 Presigned URL을 요청합니다.

  3. S3가 presigned URL을 전송하면, 서버가 이를 클라이언트에 다시 전송합니다.

  4. presigned URL을 통해 클라이언트에서 직접 S3에 이미지를 업로드합니다. 이미지 처리 과정에서 생기는 CPU 부하를 클라이언트에서 처리하도록 합니다.

  5. 성공시 서버에 성공함을 알립니다. 서버는 새로운 이미지가 저장된 URL을 DB에 저장합니다.

Presigned URL이란

S3에 작업 명령을 내릴 수 있는 URL입니다.
원래 Key를 통해 인증된 사용자만 S3와 통신할 수 있지만, 이 URL은 인증되지 않은 사용자도 사용할 수 있습니다.
대신 URL을 발급받을 때 인증 절차를 거칩니다.
또 특정한 리소스에 대해서만 접근할 수 있게 제한이 걸려있으며, URL을 사용할 수 있는 시간 제한도 존재합니다.

Presigned URL에 담긴 정보

  • Domain(S3 버킷 주소)
  • 파일 이름
  • AWS Access Key Id
  • Content-Type
  • Expires - 일정 기간이 지나면 이 URL을 쓸 수 없게 됨
  • Signature

Presigned URL 보안

  • URL을 통해 하나의 파일만 업로드 할 수 있다
    • 많은 파일을 s3 파일에 넣을 수 없습니다.
  • 파일명과 파일 타입을 암호화한다
    • 요청한 파일과 다른 파일을 업로드 할 수 없습니다.
  • 유효기간이 있다
    • 다른 유저로부터 URL을 가져오려는 시도를 방지할 수 있습니다.
  • URL이 우리 서버와 AWS간 보안 요청을 통해 만들어진다
    • 유저가 자신이 만든 가짜 URL로 속일 수 없습니다.
  • URL을 만든 버킷에서만 동작한다
    • 다른 버킷에 URL을 사용할 수 없습니다.

구현

백엔드 - aws에 presigned URL 요청하기

1. s3 클라이언트 생성

export class S3Service {
    private readonly s3Client;
    constructor(
        private readonly configService: ConfigService,
        private readonly logger: WinstonLoggerService,
    ) {
        this.s3Client = new S3Client({ region: this.configService.getOrThrow('AWS_S3_REGION') });
    }
  • AWS_S3_REGION 을 인자로 넣어 s3 Client 인스턴스를 만든다.

2. presignedURL 생성

    async createPresignedUrlWithClientForPut(userId: number, type: FileType[]): Promise<PresignedUrlInfo[]> {
        const filenameArray = this.makeFileName(userId, type);
        const presignedUrlInfoPromises = filenameArray.map(async (curFileName) => {
            const command = new PutObjectCommand({
                Bucket: BUCKET_NAME,
                ContentType: `image/${type}`,
                Key: curFileName,
            }); //커맨드 생성
            const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); //presigned url 요청
            return { filename: curFileName, url };
        });
        return Promise.all(presignedUrlInfoPromises);
    }
  • PutObjectCommand 함수에 버킷 이름, 이미지 타입, 파일 이름을 인자로 넣어 이미지 생성 커맨드를 만듭니다.

  • getSignedUrl 함수를 호출해 S3에 presigned URL 발급 요청을 보냅니다.

파일 이름 지정

파일 이름 = s3에 이미지를 저장할 경로인데,

나중에 특정 유저가 회원 탈퇴를 하거나 해서 정보를 지워야 할 때를 고려하면 유저별로 저장하는 게 좋기 때문에

경로를 {userId}/{random file name(UUID)}.{fileType}으로 만들었습니다.


    makeFileName(userId: number, type: string): string {
        return `${userId}/${generateUuid()}.${type}`;
    }

산책일지 API에 적용

이후 프론트엔드에서 S3에 사진을 업로드하고, 산책일지 API의 산책 사진 필드에 업로드가 성공한 URL을 전송하면 서버는 이 URL만 저장해둡니다.

이 때, URL 전체가 아닌 도메인 부분을 제외한 문자열만 보냅니다:

https://mybucket.s3.ap-northeast-2.amazonaws.com/ + {파일경로}
  • 위와 같은 URL이 있을 때, https://mybucket.s3.ap-northeast-2.amazonaws.com/를 제외한 뒷부분(파일 경로)만 DB에 저장합니다.
  • 추후 버킷이 바뀌거나 할 때, 프론트에서 이미지 조회시 경로는 그대로 두고 앞부분의 URL만 변경하면 됩니다.

reference: 유데미 Node JS: Advanced Concepts 강의

0개의 댓글

관련 채용 정보