junyeokk
Blog
NestJS·2025. 11. 16

NestJS와 AWS Lambda

NestJS는 기본적으로 Express나 Fastify 위에서 항상 실행되는 서버다. 요청이 없어도 프로세스가 살아있다. 반면 AWS Lambda는 요청이 올 때만 실행되고, 끝나면 꺼진다. 이 두 모델을 연결하는 것이 serverless-express 어댑터의 역할이다.

일반 서버 vs Lambda

일반 서버 (EC2, ECS)

main.ts에서 NestJS 애플리케이션을 생성하고 포트에 바인딩한다. 프로세스가 계속 실행되면서 HTTP 요청을 기다린다.

typescript
// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors({ origin: '*' });
  createSwagger(app);
  await app.listen(3000);  // 포트에 바인딩하고 계속 실행
}
bootstrap();

서버가 한 번 시작되면 모든 Module과 Provider가 초기화된 상태로 유지된다. DB 연결 풀도 열려있고, DI 컨테이너도 구성 완료 상태다. 요청이 오면 즉시 처리할 수 있다.

Lambda

Lambda는 다르다. HTTP 요청이 API Gateway를 통해 Lambda 함수로 전달되면, AWS가 함수를 실행한다. 함수가 응답을 반환하면 실행이 끝난다. 일정 시간 요청이 없으면 Lambda 인스턴스가 사라진다.

NestJS를 Lambda에 올리려면 app.listen() 대신, 요청을 받아서 Express 앱에 전달하는 핸들러 함수를 만들어야 한다.

typescript
// lambda.ts
import serverlessExpress from '@codegenie/serverless-express';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

let server: Handler;

async function bootstrap(): Promise<Handler> {
  const app = await NestFactory.create(AppModule);
  app.enableCors({ origin: '*' });
  createSwagger(app);
  await app.init();  // listen이 아니라 init

  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({ app: expressApp }) as Handler;
}

export const handler: Handler = async (event, context, callback) => {
  server = server ?? (await bootstrap());
  return server(event, context, callback);
};

app.listen(3000) 대신 app.init()을 호출한다. 포트에 바인딩하지 않고, NestJS 애플리케이션만 초기화한다. 그리고 Express 인스턴스를 serverlessExpress로 감싸서 Lambda 핸들러를 만든다.

serverless-express 어댑터

@codegenie/serverless-express가 하는 일은 API Gateway의 이벤트 형식을 Express가 이해하는 HTTP 요청으로 변환하는 것이다.

text
API Gateway Event (JSON)
  → serverless-express (변환)
    → Express Request/Response
      → NestJS Controller
        → Service → Repository → DB
      → Response
    → Express Response
  → serverless-express (변환)
→ API Gateway Response (JSON)

API Gateway는 HTTP 요청을 JSON 형태의 이벤트 객체로 변환해서 Lambda에 전달한다. 이 이벤트에는 HTTP 메서드, 경로, 헤더, 본문 등이 포함되어 있다. serverless-express는 이 JSON을 Express의 Request 객체로 변환해서, NestJS가 일반 HTTP 요청처럼 처리할 수 있게 한다. 응답도 마찬가지로 Express Response를 API Gateway가 이해하는 형식으로 변환한다.

Cold Start

Lambda에서 가장 중요한 이슈가 cold start다. Lambda 인스턴스가 새로 생성될 때 NestJS 애플리케이션 전체를 초기화해야 한다. 모든 Module을 로드하고, DI 컨테이너를 구성하고, DB 연결을 만들어야 한다.

typescript
export const handler: Handler = async (event, context, callback) => {
  // 첫 요청에만 bootstrap 실행 (cold start)
  server = server ?? (await bootstrap());
  return server(event, context, callback);
};

server를 모듈 스코프 변수에 캐시하는 패턴이 핵심이다. 첫 번째 요청에서 bootstrap()이 실행되고(cold start), 이후 요청에서는 캐시된 server를 재사용한다(warm start). Lambda 인스턴스가 살아있는 동안은 warm start로 빠르게 응답한다.

cold start 시간은 NestJS 애플리케이션의 크기에 비례한다. Module이 많고, DB 연결이 필요하고, 외부 서비스 초기화가 있으면 수 초가 걸릴 수 있다. 일반 서버에서는 시작 시간이 한 번이지만, Lambda에서는 인스턴스가 꺼질 때마다 반복된다.

cold start 완화 방법

Provisioned Concurrency를 설정하면 지정한 수만큼의 Lambda 인스턴스를 미리 초기화해둔다. cold start 없이 즉시 응답할 수 있지만, 사용하지 않는 시간에도 비용이 발생한다.

Lambda의 메모리를 늘리면 CPU도 비례해서 늘어난다. 메모리를 512MB에서 1024MB로 올리면 초기화 속도가 빨라진다.

Lambda vs ECS Fargate

NestJS를 AWS에 배포하는 방식은 크게 두 가지다.

Lambda (서버리스)

Lambda에 배포하면 요청이 없을 때 비용이 발생하지 않는다. 트래픽이 갑자기 늘어도 AWS가 자동으로 인스턴스를 추가한다.

장점:

  • 요청이 없으면 비용 없음 (pay-per-request)
  • 자동 스케일링
  • 인프라 관리 불필요

단점:

  • cold start 지연
  • 실행 시간 제한 (최대 15분)
  • 메모리 제한 (최대 10GB)
  • WebSocket 같은 장시간 연결 불가

ECS Fargate (컨테이너)

Docker 컨테이너로 NestJS를 실행한다. 항상 실행되는 서버지만, 서버 관리는 AWS가 한다.

장점:

  • cold start 없음
  • 실행 시간 제한 없음
  • WebSocket, SSE 등 장시간 연결 가능
  • 더 많은 리소스 사용 가능

단점:

  • 항상 실행되므로 기본 비용 발생
  • 스케일링 설정 필요

키오스크 시스템처럼 응답 지연에 민감하고, 파일 업로드나 영상 합성 같은 무거운 작업이 있는 경우 ECS Fargate가 적합하다. Lambda의 cold start가 사용자 경험에 영향을 줄 수 있고, 영상 합성은 15분 제한에 걸릴 수 있기 때문이다.

두 방식을 모두 준비해두는 것도 가능하다. main.ts는 일반 서버용, lambda.ts는 Lambda용으로 두면, 같은 NestJS 애플리케이션을 어느 환경에든 배포할 수 있다. 비즈니스 로직(Module, Service, Controller)은 동일하고, 진입점(entry point)만 다르다.

Serverless Framework

AWS Lambda 배포를 관리하는 도구다. serverless.yml에 인프라를 코드로 정의하고, 명령어 한 줄로 배포한다.

yaml
service: chiki-backend
provider:
  name: aws
  runtime: nodejs20.x
  region: ap-northeast-2
  stage: ${opt:stage, 'dev'}
bash
# 로컬 개발
npx serverless offline

# 배포
npx serverless deploy --stage dev

serverless offline은 로컬에서 API Gateway + Lambda 환경을 시뮬레이션한다. 실제 AWS에 배포하지 않고도 Lambda 핸들러를 테스트할 수 있다.

프론트엔드의 서버리스 배포

NestJS 백엔드와 달리, React SPA(Single Page Application)는 Lambda가 필요 없다. 빌드된 정적 파일을 S3에 업로드하고 CloudFront CDN으로 서빙한다.

text
사용자 → CloudFront (CDN) → S3 (정적 파일)

                          index.html, JS, CSS

S3는 파일 저장소이고, CloudFront는 전 세계 엣지 서버에 파일을 캐싱해서 빠르게 전달한다. 서버 실행이 필요 없으므로 비용이 매우 저렴하다.

SPA는 클라이언트에서 라우팅을 처리하므로, /admin/stores 같은 URL로 직접 접속하면 S3에서 해당 경로의 파일을 찾지 못한다. CloudFront에서 404/403 에러를 index.html로 리다이렉트하는 설정이 필요하다. React Router가 URL을 해석해서 올바른 페이지를 렌더링한다.

정리

  • NestJS를 Lambda에 올리려면 app.listen() 대신 app.init() + serverless-express 어댑터로 진입점만 바꾸면 되고, 비즈니스 로직은 그대로 재사용할 수 있다
  • cold start는 모듈 스코프 캐싱으로 완화하되, 응답 지연에 민감하거나 장시간 연결이 필요하면 ECS Fargate가 적합하다
  • 프론트엔드 SPA는 Lambda 없이 S3 + CloudFront 정적 배포가 가장 경제적이고, CloudFront의 에러 페이지 리다이렉트로 SPA 라우팅을 처리한다

관련 문서