온프레미스 서버 부하 한계, AWS Presigned URL로 트래픽 95% 절감한 이야기

Seung Hyeon ·2025년 12월 21일
post-thumbnail

이미지/영상 업로드 시 RTC 서버에 과도한 트래픽이 발생하는 문제가 있었다.

온프레미스 환경 → API 서버를 통한 S3 업로드 → 서버를 거치지 않고 AWS Presigned URL로 S3에 직접 업로드 까지 과정을 경험하며, 트래픽을 평균 95% 절감한 이야기를 담았다

(🤕 Problem) 공채 시즌마다 반복되는 RTC 서버 과부하

수시 채용 기간에는 괜찮았지만, 상하반기 공채 시즌에는 하루 최대 5,000명이 응시하고 한 타임에 최대 1,200명이 동시에 접속한다.

문항 이미지 관리와 감독관 API는 모두 온프레미스 환경의 RTC 서버에서 처리되는데, 각 과목이 종료되는 시점에 모든 응시자가 웹캠과 화면공유 영상을 동시에 송신하면서 디스크 사용률(%util)이 90~100%까지 치솟았다.

이 과정에서 몇몇 인원의 화상 연결이 끊어지고 문항 이미지가 안보이거나 녹화 영상 저장이 실패하는 이슈가 계속 발생했다.


(🤔 Why) 원인이 뭘까

<당시 사용되었던 인프라 구조의 문제점>

응시자들의 녹화 영상이 저장되는 Flow는 아래와 같다.

  1. RTC 서버로 녹화 영상 업로드
  2. 녹화 영상을 단일 SDD에 쓰기
  3. 녹화 영상이 저장된 주소 반환
  4. DB에 영상 주소 저장 요청
  5. DB에 영상 주소 저장

1번 과정에서 RTC 서버로 응시자의 녹화 영상 업로드 되며 이 과정에서 영상을 SSD에 쓰는 과정에서 높은 부하가 발생된다. (iostat 명령어 확인시 %util 컬럼이 100%로 치솟는 상황)

  • 단일 SSD 1개에 모든 응시자들의 녹화 영상이 저장되는 구조이므로, 쓰기 버퍼 허용량 이상의 요청을 받는 경우에는 RTC 서버의 CPU 부하가 가중되는 상황이었다.

※ 해당 현상은 보통 1,000명 이상 시 발생


(☝🏻How) 검사운영 API 서버를 통한 S3 업로드 방식

<변경된 인프라 구조>

Flow는 아래와 같다.

  1. 녹화 영상을 API 서버로 보낸다.
  2. 녹화 영상을 S3에 업로드한다.
  3. S3는 녹화 영상을 bucket에 업로드한 뒤, 저장 주소(url)을 반환한다.
  4. 녹화 영상 주소를 DB에 저장한다.

필자는 NestJS를 사용하고 있다.
흔히 사용하는 방식으로, multer-s3 라이브러리를 통해 binary 파일 데이터를 받아 buffer로 변환한 뒤, @aws-sdk/client-s3 라이브러리로 S3에 업로드했다.

(client로부터 binary 파일을 multer-s3로 받으면,)

@UseInterceptors(FileInterceptor('file'))
@Patch('id')
async saveRecord(
  @UploadedFile() file: Express.Multer.File): Promise<ResponseDto> {
    const testerStatus: any = await this.examService.saveRecordFile(file);

    const result: ResponseDto = {
      message: `응시자 녹화영상 저장 완료`,
      data: testerStatus,
    };

	return result;
}

(S3 인증 후, S3에 업로드)

import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';

export class AwsService {
  s3Client: S3Client;

  constructor(private configService: ConfigService) {
    // AWS S3 클라이언트 초기화. 환경 설정 정보를 사용하여 AWS 리전, Access Key, Secret Key를 설정.
    this.s3Client = new S3Client({
      region: this.configService.get<string>('S3_REGION'),
      credentials: {
        accessKeyId: this.configService.get<string>('S3_ACCESS_KEY_ID'),
        secretAccessKey: this.configService.get<string>('S3_SECRET_ACCESS_KEY'),
      },
    });
  }
  
  async uploadFileToS3(bucketName: string, filePath: string, file: Express.Multer.File): Promise<string> {
    // AWS S3에 이미지 업로드 명령을 생성합니다. 파일 이름, 파일 버퍼, 파일 접근 권한, 파일 타입 등을 설정합니다.
    const command: PutObjectCommand = new PutObjectCommand({
      Bucket: bucketName, // S3 버킷 이름
      Key: filePath, // 업로드될 파일의 경로 및 이름
      Body: file.buffer, // 업로드할 파일(버퍼형식)
      ACL: 'public-read', // 파일 접근 권한
      ContentType: `image/${file.mimetype}`, // 파일 타입
      ContentDisposition: 'inline',
    });
    // 생성된 명령을 S3 클라이언트에 전달하여 이미지 업로드
    await this.s3Client.send(command);
    // 업로드된 이미지의 URL을 반환
    const s3Region: string = this.configService.get<string>('S3_REGION');

    return `https://${bucketName}.s3.${s3Region}.amazonaws.com/${filePath}`;
  }

<해당 인프라 구조에서 발생한 문제점>
1. 높은 부하를 갖는 트래픽이 이중으로 발생되는 현상이 발생했다.

  • 1번 과정에서 IDC로 수신 트래픽
  • 2번 과정에서 AWS로 송신 트래픽)

(붉은 동그라미 = 교시가 종료되는 시점) (920명 동시 시행 시 트래픽 리포트)

IDC의 트래픽 총량이 증가함에 따라 검사 운영서버의 응답 속도가 지연되는 현상이 발생되어, 몇몇 응시자들이 검사가 이유 없이 중단되는 현상들이 있었다.

  • 트래픽 병목에 구간에 걸린 응시자들로 확인
  • 해당 현상은 보통 800명 이상 시 발생

2. 이중 트래픽으로 AWS S3 사용료 및 IDC 서버의 트래픽 사용료가 과도하게 청구되었다.



(☝🏻How) AWS Lambda 함수를 통한 S3 업로드 방식

RTC 서버를 통한 업로드 방식과 유사하다.
다만, AWS Lambda 서버가 영상 업로드 기능만을 대신 수행한다.

Flow는 아래와 같다.

  1. client는 녹화 영상을 AWS Lambda으로 보내면, Lambda는 영상을 S3에 업로드한다.
  2. S3에 저장된 영상 저장 주소를 Lambda를 통해 다시 client에게 전달한다.
  3. DB에 영상 주소 저장 요청
  4. DB에 영상 주소 저장

<해당 인프라의 기대효과 및 선택하지 않은 이유>
응시자들의 녹화 영상 원본을 저장하는 트래픽(붉은 점)을 AWS가 감당하므로 IDC로 들어오는 트래픽의 총량이 감소할 것으로 예상되었다.
→ IDC에서 내부적으로 수행해야할 로직이 불필요해지므로 가장 합리적인 인프라로 판단되었다.

하지만, 업로드 용량 제한(아래 이미지 참고)으로 인하여 해당 아키텍처는 부적합하다 판단했다.

현재 존재하는 검사의 평균 업로드 용량은 과목당 15mb ~ 30mb이므로
영상을 쪼개서 저장하지 않는 이상 이 방식은 적용이 어렵다고 판단하여 최종 방법으로 채택하지 않았다.


(☝🏻How) 서명된 S3 Url을 통한 클라이언트 직접 업로드 방식

"Presigned URL이란?"
Presigned URL은 S3 접근을 위한 임시 URL로, 일정 시간이 지나면 만료되어 접근이 불가능해진다.
일반적으로 S3 버킷에 객체를 업로드하려면 AWS 보안 자격 증명이 필요하지만, Presigned URL을 사용하면 별도의 자격 증명 없이도 임시 권한이 부여된 URL을 통해 파일을 업로드할 수 있다.

이를 활용하여 client가 서버를 거치지 않고 미리 서명된 URL을 통해 직접 S3에 파일을 업로드하도록 구현했다. (최대 단일 파일 5TB까지 업로드가 가능)

AWS 공식 doc - 미리 서명된 URL을 통해 객체 다운로드 및 업로드


<변경된 인프라 구조>

복잡해 보이지만 Flow는 아래와 같다.

  1. client는 검사 운영 API 서버에게 pre-signed URL 발급을 요청한다.
  2. 검사 운영 서버는 AWS 인증키로 S3에게 pre-signed URL을 발급받고(+URL 만료기간 설정), 해당 URL을 client에게 반환한다.
  3. client는 발급받은 URL로 S3에 PUT 메소드로 녹화 영상을 업로드한다.
  4. S3는 영상을 업로드한 뒤, 영상 주소를 client에게 반환한다.
  5. client는 영상 주소를 API 서버에 보내고, API 서버는 영상 주소를 DB에 저장한다.

AWS 직접 업로드는 보안상의 이유로 제한을 두지만,
위와 같은 flow를 사용하면
보안 절차는 백엔드 서버에서 수행하고 실질적인 업로드는 client에서 수행하게끔 가능해진다.

ℹ️ AWS Presigned URL을 발급하는 API (NestJs)

@Controller('aws')
export class AwsController {
  constructor(private readonly awsService: AwsService) {}

  // [POST] 'aws/pre-signed-url' API
  @UseGuards(AuthGuard('tester'))
  @Post('pre-signed-url')
  async getPreSignedUrl(
    @Body('fileName') fileName: string,
  ): Promise<ResponseDto> {
    const urlInfo: UrlDto = await this.awsService.getPreSignedUrl(fileName);

    const result: ResponseDto = {
      message: 'S3 서명된 URL 생성 완료',
      data: urlInfo,
    };

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

export class AwsService {
  private readonly NODE_ENV: string;
  private readonly S3_CLIENT: S3Client;
  private readonly S3_BUCKET: string;
  private readonly S3_REGION: string;
  private readonly S3_ACCESS_KEY_ID: string;
  private readonly S3_SECRET_ACCESS_KEY: string;

  constructor(private configService: ConfigService) {
    // 환경 변수를 가져와서 변수에 할당
    this.NODE_ENV = this.configService.get<string>('NODE_ENV');
    // AWS S3 환경 설정 정보를 가져와서 변수에 할당
    this.S3_BUCKET = this.configService.get<string>('S3_BUCKET');
    this.S3_REGION = this.configService.get<string>('S3_REGION');
    this.S3_ACCESS_KEY_ID = this.configService.get<string>('S3_ACCESS_KEY_ID');
    this.S3_SECRET_ACCESS_KEY = this.configService.get<string>('S3_SECRET_ACCESS_KEY');
    // AWS S3 클라이언트 초기화. 환경 설정 정보를 사용하여 AWS 리전, Access Key, Secret Key를 설정.
    this.S3_CLIENT = new S3Client({
      region: this.S3_REGION,
      credentials: {
        accessKeyId: this.S3_ACCESS_KEY_ID,
        secretAccessKey: this.S3_SECRET_ACCESS_KEY,
      },
    });
  }

  async getPreSignedUrl(filePath: string): Promise<string> {
    // S3 버킷에 대한 URL을 생성하는 명령 생성
    const command = new PutObjectCommand({
      Bucket: this.S3_BUCKET, // S3 버킷 이름
      Key: filePath, // 업로드될 파일의 경로 및 이름
    });
    // 명령을 S3 클라이언트에 전달하여 서명된 URL을 생성 (10분 유효)
    const preSignedUrl = await getSignedUrl(this.S3_CLIENT, command, { expiresIn: 600 });

    const result: UrlDto = {
      uploadUrl: `https://${this.S3_BUCKET}.s3.${this.S3_REGION}.amazonaws.com/${filePath}`,
      preSignedUrl, // 서명된 URL을 반환
    };

    return result;
  }
}

API 결과를 통해 전달받은 URL 예시

https://<버킷명>.s3.<리전명>.amazonaws.com/<객체명>?response-content-disposition=inline&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Security-Token=<임시 인증 정보>&X-Amz-Algorithm=<서명 방식>&X-Amz-Credential=<AWS 인증 정보>&X-Amz-Date=202501102T123988Z&X-Amz-Expires=60&X-Amz-SignedHeaders=host&X-Amz-Signature=<서명 >
  • 해당 URL을 통해 PUT 메소드로 파일 업로드 시 성공 메시지 확인
  • 5.5mb 영상 업로드시 1.6초 소요시간 확인

※ I AM role 설정, 버킷 세팅 등의 자세한 방법은 하단의 Reference 블로그 글을 참조

🎉 트래픽 95% 절감 성공

리소스(신분증 이미지, 녹화 영상 등) 업로드를 AWS에서 직접 진행하므로 IDC 서버 트래픽은 전체적으로 감소되었다.

  • 녹화 영상이 저장되는 시점에서 동시에 발생하던 응시자 펜딩 현상도 발견되지 않았다.
  • 최대 송수신 약 900mb ⇒ 40mb 수준으로 약 95% 감소되었다.

(1000명 동시 시행 시 트래픽 리포트)


또한 (어쩌면 당연하지만), RTC 서버의 평균 CPU 사용량은 5% 이내로 유지되었고
디스크 I/O 사용률(%util)도 최대 1% 이내로 감소했다.



마무리하며

비용 관점에서만 보면 클라우드 도입이 항상 긍정적인 선택이라고 보기는 어렵다.
따라서 현재 운영 중인 온프레미스 환경을 전면적으로 대체하는 결정은 결코 쉽지 않았다. (실제로, AWS 도입 필요성을 설득하기까지 여간 쉽지 않았다)

다만 트래픽 지표를 통해 확인했듯이, 온프레미스 인프라는 급격한 트래픽 증가와 과부하에 대응하는 데는 분명한 한계가 있었다.
그래서 관련 레퍼런스를 하나씩 찾아보고 내용을 정리해, 전면 전환이 아닌 부분적인 도입부터 시도해보자는 방향으로 이야기를 꺼내며 설득해 나갔다.

향후에는 변경된 인프라 환경을 운영하며 추가적인 개선 과제들도 순차적으로 해결해 나갈 계획이다.
예를 들어, 데이터 보관 기간에 따른 비용을 줄이기 위해 AWS Lambda 기반의 스케줄링 자동화 등을 생각해보고 있다.
계속해서 여러 AWS 서비스들을 찾아보며 지금보다 더 안정적이고 효율적인 인프라 환경을 만들어 나갈 계획이다 😌



Reference

AWS-📚-S3-Pre-signed-URL-공유하기

S3 Presigned Url 도입하기

[AWS S3]Presigned URL과 생성방식

Nest.js : AWS S3 Presigned url 사용하기

profile
대기업 채용 인적성 검사 시스템을 개발하고 있습니다.

0개의 댓글