서버리스 NestJS (AWS Lambda 통합)
NestJS를 AWS Lambda 핸들러에 연결하고 콜드 스타트·상태 저장·연결 재사용을 고려해 서버리스로 배포합니다.
11장에서는 Docker 컨테이너화, CI/CD 파이프라인, 클라우드 배포, 모니터링과 로깅을 다뤘습니다. 12장에서는 NestJS의 고급 주제를 다루며, 첫 글인 이 글에서는 서버리스(serverless) 환경의 NestJS를 설명합니다.
서버리스는 개발자가 서버 인프라를 직접 관리하지 않고 애플리케이션 코드에 집중할 수 있게 해 주는 클라우드 실행 방식입니다. NestJS는 HTTP 어댑터 위에서 동작하는 구조 덕분에 서버리스 환경에서도 쓸 수 있지만, 콜드 스타트, handler 어댑터, 상태를 둘 곳, 연결 재사용을 먼저 따져 봐야 합니다.
Lambda 적합성 판단부터 cold·warm 실행과 상태 외부화까지
Lambda를 선택한 사례에서 cold 초기화·처리·외부 상태·다음 warm 재사용의 실행 상태를 구별할 수 있다.
Nest · Lambda lifecycle
Lambda 선택과 cold·warm 실행
원문의 Node20 handler 예제로 초기화 완료 후 캐시와 실행 환경 재사용을 구분합니다. 외부 업무 상태·연결 예산은 추가 운영 설계입니다. 현재 배포에는 지원 runtime과 handler·adapter 계약을 별도로 맞춰야 합니다.
-
실행 형태를 먼저 선택
함수가 직접 지속 연결을 소유하거나 작업 시간이 함수 예산을 넘으면 컨테이너·잡을 검토합니다. Lambda response streaming 지원은 별도로 확인합니다.
-
비동기 이벤트 진입
원문 handler는 event·context·callback을 받아 serverless-express handler로 전달합니다.
-
cold 초기화
cachedServer가 없으면 bootstrap을 실행하고, 완료된 handler만 저장합니다. 동시 최초 호출을 합치는 Promise cache는 없습니다.
-
warm 재사용
준비된 실행 환경은 handler와 연결을 재사용할 수 있지만 그 메모리는 내구 상태가 아닙니다.
-
요청 처리 예산
예약 동시성과 환경별 DB pool을 함께 제한하고 외부 호출 timeout을 함수 예산보다 짧게 둡니다.
-
상태 외부화
세션·파일·작업 진행 상태는 DB, cache, object storage, queue에 기록해 freeze나 폐기 후에도 남깁니다.
원문의 nodejs20.x는 deprecated 예시입니다. Node24의 Lambda async handler는 event·context를 받으며 callback을 지원하지 않습니다. 현재 배포는 runtime·adapter 계약과 전체 의존성 패키징을 다시 검증해야 합니다. 원문 warm-up은 bootstrap 전에 반환해 Nest 초기화를 보장하지 않습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 기존 두 영역, 아홉 노드와 아홉 화살표다. 지속 연결·예산 초과는 컨테이너로, 나머지는 Lambda로 향한다. cold 초기화와 warm 재사용은 각각 요청 처리로 합쳐진다. 상태 저장은 외부 단계이며 메모리 재사용과 다르다.
- 지속 연결과 실행 예산으로 Lambda 또는 서버리스 컨테이너를 고른다. Lambda에서는 API Gateway 이벤트가 cold 초기화 또는 warm handler 재사용으로 갈라진 뒤 하나의 Nest 요청 처리로 합쳐지고, 내구 상태는 외부 저장소에 기록된다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| cold 또는 warm 시작 Lambda 선택 경로에서 cold는 초기화하고 warm은 기존 실행 환경을 재사용합니다. | cold: 초기화 · warm: 재사용 · 지속 연결·예산 초과: 컨테이너 별도 선택 | 요청 처리 전 준비 상태가 실행 환경에 따라 다릅니다. |
| 요청 처리 cold와 warm 경로가 요청 처리로 합쳐집니다. | 실행: 요청 처리 · 메모리: 실행 환경 내부 | 준비된 실행 환경에서 업무가 진행됩니다. |
| 외부 상태 보관 지속할 상태는 외부 저장 경계에 둡니다. | 지속 상태: 외부 · 메모리 재사용: 지속성 보장 아님 | 요청 결과의 보관 위치가 실행 환경과 분리됩니다. |
Lambda 적합성 판단부터 cold·warm 실행과 상태 외부화까지
Lambda를 선택한 사례에서 cold 초기화·처리·외부 상태·다음 warm 재사용의 실행 상태를 구별할 수 있다.
Nest · Lambda lifecycle
Lambda 선택과 cold·warm 실행
원문의 Node20 handler 예제로 초기화 완료 후 캐시와 실행 환경 재사용을 구분합니다. 외부 업무 상태·연결 예산은 추가 운영 설계입니다. 현재 배포에는 지원 runtime과 handler·adapter 계약을 별도로 맞춰야 합니다.
-
실행 형태를 먼저 선택
함수가 직접 지속 연결을 소유하거나 작업 시간이 함수 예산을 넘으면 컨테이너·잡을 검토합니다. Lambda response streaming 지원은 별도로 확인합니다.
-
비동기 이벤트 진입
원문 handler는 event·context·callback을 받아 serverless-express handler로 전달합니다.
-
cold 초기화
cachedServer가 없으면 bootstrap을 실행하고, 완료된 handler만 저장합니다. 동시 최초 호출을 합치는 Promise cache는 없습니다.
-
warm 재사용
준비된 실행 환경은 handler와 연결을 재사용할 수 있지만 그 메모리는 내구 상태가 아닙니다.
-
요청 처리 예산
예약 동시성과 환경별 DB pool을 함께 제한하고 외부 호출 timeout을 함수 예산보다 짧게 둡니다.
-
상태 외부화
세션·파일·작업 진행 상태는 DB, cache, object storage, queue에 기록해 freeze나 폐기 후에도 남깁니다.
원문의 nodejs20.x는 deprecated 예시입니다. Node24의 Lambda async handler는 event·context를 받으며 callback을 지원하지 않습니다. 현재 배포는 runtime·adapter 계약과 전체 의존성 패키징을 다시 검증해야 합니다. 원문 warm-up은 bootstrap 전에 반환해 Nest 초기화를 보장하지 않습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 기존 두 영역, 아홉 노드와 아홉 화살표다. 지속 연결·예산 초과는 컨테이너로, 나머지는 Lambda로 향한다. cold 초기화와 warm 재사용은 각각 요청 처리로 합쳐진다. 상태 저장은 외부 단계이며 메모리 재사용과 다르다.
- 지속 연결과 실행 예산으로 Lambda 또는 서버리스 컨테이너를 고른다. Lambda에서는 API Gateway 이벤트가 cold 초기화 또는 warm handler 재사용으로 갈라진 뒤 하나의 Nest 요청 처리로 합쳐지고, 내구 상태는 외부 저장소에 기록된다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| cold 또는 warm 시작 Lambda 선택 경로에서 cold는 초기화하고 warm은 기존 실행 환경을 재사용합니다. | cold: 초기화 · warm: 재사용 · 지속 연결·예산 초과: 컨테이너 별도 선택 | 요청 처리 전 준비 상태가 실행 환경에 따라 다릅니다. |
| 요청 처리 cold와 warm 경로가 요청 처리로 합쳐집니다. | 실행: 요청 처리 · 메모리: 실행 환경 내부 | 준비된 실행 환경에서 업무가 진행됩니다. |
| 외부 상태 보관 지속할 상태는 외부 저장 경계에 둡니다. | 지속 상태: 외부 · 메모리 재사용: 지속성 보장 아님 | 요청 결과의 보관 위치가 실행 환경과 분리됩니다. |
위 그림은 Lambda가 이 서비스에 맞는지 판단하는 데서 시작해, 처음 요청을 받는 cold 실행과 이미 준비된 환경을 다시 쓰는 warm 실행의 차이, 그리고 상태를 외부로 옮기는 흐름을 보여 줍니다. 핵심은 초기화를 마친 handler를 같은 실행 환경 안에서만 다시 쓸 수 있다는 점과, 업무 상태와 데이터베이스 연결 수는 실행 환경 바깥에서 따로 관리해야 한다는 점입니다. 다만 그림에서 '원문'이라고 부르는 handler는 이 글의 이전 판에 있던 Node.js 20(nodejs20.x)용 예제입니다. 그 예제는 event, context, callback을 받고, warm-up 호출은 초기화 전에 바로 돌려보냈으며, 초기화를 마친 handler만 cachedServer에 저장했습니다. 그래서 그림에 'Promise cache는 없습니다'와 warm-up에 대한 주의가 적혀 있습니다. 아래 코드는 Node.js 24(nodejs24.x)에 맞춰 event와 context만 받고, warm-up 분기 없이 초기화 중인 Promise를 serverPromise에 보관합니다.
서버리스란
서버리스 컴퓨팅은 클라우드 공급자가 서버 인프라의 준비, 확장, 패치, 유지보수를 대신 처리하는 실행 모델입니다. 개발자는 코드를 함수나 컨테이너 이미지로 배포하고, 클라우드 플랫폼이 그 코드를 실행하며 자동으로 확장합니다.
- 서버 관리 불필요: 서버를 직접 준비하거나 관리하지 않습니다.
- 이벤트 기반: HTTP 요청, 데이터베이스 변경, 파일 업로드, 메시지 큐 이벤트 같은 이벤트가 생길 때만 코드가 실행됩니다.
- 자동 확장: 트래픽이 늘면 인스턴스가 늘고 줄면 줄어들며, 쓰지 않을 때는 0까지 줄어들 수 있습니다.
- 사용량 기반 과금: 코드가 실행되는 동안에만 비용을 냅니다.
- 고가용성: 클라우드 공급자가 기본적인 고가용성과 장애 대응을 제공합니다.
장점으로는 인프라 관리 부담이 줄어 핵심 로직에 집중할 수 있다는 점, 트래픽 변동이 크거나 가끔 쓰이는 서비스에서 비용을 아낄 수 있다는 점, 트래픽이 갑자기 늘어도 자동으로 확장된다는 점, 패치와 보안 업데이트를 공급자가 처리한다는 점이 있습니다.
단점도 있습니다. 쉬고 있던 상태에서 첫 요청이 들어오면 런타임과 애플리케이션을 시작하는 콜드 스타트(cold start) 지연이 생기고, 특정 클라우드의 API와 생태계에 묶이기 쉬우며, 실행 시간·메모리·파일 시스템에 제한이 있습니다. 또 기본적으로 상태를 두지 않는 구조를 지향하므로, 상태가 필요하면 외부 데이터베이스나 캐시를 써야 합니다.
AWS Lambda와 API Gateway로 NestJS 실행하기
가장 널리 쓰이는 서버리스 NestJS 구성은 API Gateway가 HTTP 요청을 받아 Lambda 함수로 넘기고, Lambda 안의 NestJS가 요청을 처리하는 방식입니다. 이 글은 배포 도구로 Serverless Framework를 씁니다.
- AWS Lambda: NestJS 코드를 실행하는 서버리스 컴퓨팅 서비스입니다.
- API Gateway: HTTP 요청을 받아 Lambda 함수를 호출하는 관리형 서비스입니다.
- Serverless Framework: Lambda와 API Gateway 같은 AWS 자원을 정의하고 배포하는 CLI 도구입니다. 현재 버전은 4입니다.
AWS의 런타임 표에서 Node.js 런타임은 nodejs22.x, nodejs24.x, 그리고 공개 미리 보기(public preview) 단계인 nodejs26.x가 있습니다. 미리 보기 런타임은 SLA와 기술 지원 대상이 아니어서 AWS가 운영에 쓰지 말라고 안내하므로, 이 글은 앞 장들과 같은 Node.js 24를 씁니다. AWS의 Node.js handler 문서에 따르면 Node.js 24부터 비동기 handler는 event와 context만 받고 callback을 지원하지 않으므로, handler는 결과를 Promise로 돌려줘야 합니다.
패키지 설치
Serverless Framework CLI를 전역으로 설치합니다. 설치하면 serverless 명령과 줄임말 sls를 쓸 수 있고, 아래 배포 단계에서는 sls deploy로 씁니다. 이 CLI는 배포할 때만 쓰는 도구이므로 NestJS 프로젝트의 의존성에는 넣지 않습니다.
npm install -g serverlessNestJS 프로젝트에는 Express 앱을 Lambda 이벤트와 연결하는 @codegenie/serverless-express를 설치합니다. 현재 버전 5는 Node.js 24 이상을 대상으로 하며, 4.x에 있던 resolutionMode 옵션과 callback 매개변수를 없애 결과를 Promise로만 돌려줍니다. 코드에서 express를 직접 가져오므로 express도 직접 의존성으로 설치합니다.
npm install @codegenie/serverless-express express
npm install --save-dev @types/aws-lambda두 번째 줄의 @types/aws-lambda는 아래 진입 파일이 import type으로 가져오는 Context와 Handler의 타입 정의만 담고 있습니다. 타입은 컴파일한 뒤 사라지므로 개발 의존성으로 설치하고, 배포 패키지에는 들어가지 않아도 됩니다.
Lambda용 진입 파일
main.ts 대신 Lambda 전용 진입 파일을 만듭니다. 같은 프로젝트를 일반 HTTP 서버로도 실행할 수 있도록 main.ts는 그대로 둡니다.
import serverlessExpress from '@codegenie/serverless-express';
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import type { Context, Handler } from 'aws-lambda';
import express from 'express';
import { AppModule } from './app.module';
// 실행 환경 하나에서 초기화를 한 번만 하도록 Promise를 모듈 변수에 보관합니다.
let serverPromise: Promise<Handler> | undefined;
async function bootstrap(): Promise<Handler> {
const expressApp = express();
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressApp),
);
await app.init();
return serverlessExpress({ app: expressApp });
}
export const handler = async (event: unknown, context: Context) => {
serverPromise ??= bootstrap().catch((error: unknown) => {
// 초기화에 실패하면 다음 요청이 다시 시도하도록 비웁니다.
serverPromise = undefined;
throw error;
});
const server = await serverPromise;
// Promise 방식이므로 callback은 쓰지 않습니다.
return server(event, context, () => undefined);
};bootstrap()은 Express 앱 위에 NestJS를 만들고app.init()으로 모듈과 route를 초기화한 뒤,serverlessExpress()가 만든 Lambda용 handler를 돌려줍니다.listen()은 호출하지 않습니다.serverPromise는 모듈 변수이므로 같은 실행 환경이 다시 쓰이는 동안 유지됩니다. 그래서 그 환경의 두 번째 요청부터는 초기화를 건너뜁니다. 하지만 새 실행 환경은 각자 처음부터 초기화하므로, 이 캐시가 콜드 스타트 자체를 없애지는 않습니다.- 초기화에 실패하면
serverPromise를 비워, 같은 환경의 다음 요청이 실패한 Promise를 계속 받지 않고 다시 초기화를 시도하게 합니다. handler는async함수이므로 Node.js 24의 규칙대로event와context만 받고 결과를 Promise로 돌려줍니다.server()에 넘기는 세 번째 인수() => undefined는 빈 함수입니다.serverlessExpress()의 반환 타입인aws-lambda의Handler가 세 번째 매개변수를 요구하므로 타입 검사를 통과하려고 넣었으며, 버전 5의 handler는event와context만 쓰고 이 인수를 무시합니다.
serverless.yml 작성
프로젝트 루트에 serverless.yml을 만들어 AWS 자원과 Lambda 함수를 정의합니다. 파일은 세 부분으로 나눠 읽으면 됩니다. provider는 모든 함수에 공통인 런타임, 리전, 메모리, 제한 시간, 환경 변수를 정하고, package.patterns는 앞에 !를 붙인 파일을 배포 묶음에서 뺍니다. functions는 함수 이름과 진입점, 그리고 그 함수를 호출할 HTTP 이벤트를 정합니다.
service: nestjs-serverless-app
provider:
name: aws
runtime: nodejs24.x
stage: dev
region: ap-northeast-2
memorySize: 512 # MB, 측정해서 조정
timeout: 29 # 초, API Gateway REST의 기본 통합 제한(29초) 이하
logRetentionInDays: 7
environment:
NODE_ENV: production
# Lambda가 꺼 둔 require(esm)을 다시 켭니다(CommonJS 앱이 NestJS 12를 불러오는 데 필요).
NODE_OPTIONS: --experimental-require-module
# 비밀 값은 여기에 쓰지 않고 실행 시점에 Secrets Manager에서 읽습니다.
APP_SECRET_ID: nestjs-app/${sls:stage}
package:
patterns:
- '!src/**'
- '!test/**'
- '!.env*'
functions:
api:
handler: dist/lambda.handler # dist/lambda.js의 handler 내보내기
events:
# 루트 경로와 그 아래의 모든 경로를 각각 연결합니다.
- http:
path: /
method: ANY
- http:
path: /{proxy+}
method: ANYhandler: dist/lambda.handler는 “빌드 결과의 dist/lambda.js 모듈에서 handler 내보내기를 호출한다”는 뜻입니다. 소스에 함수가 있다고 배포 파일이 완성되는 것은 아니므로, npm run build로 dist/lambda.js가 만들어지는지 먼저 확인합니다.
environment의 NODE_OPTIONS 줄은 빼면 안 됩니다. 이 교재의 프로젝트는 CommonJS이고, ESM으로만 배포되는 NestJS 12 패키지를 require(esm)으로 불러옵니다. 그런데 Lambda의 Node.js 24 런타임은 이 기능을 꺼 둔 채 시작하므로, 이 줄이 없으면 함수가 시작할 때 @nestjs/core를 불러오지 못합니다. NestJS 이전 안내도 같은 설정을 안내합니다.
Serverless Framework는 기본적으로 devDependencies를 빼고 프로덕션 의존성의 node_modules를 배포 패키지에 넣습니다. 그래서 @nestjs/core, @nestjs/platform-express, express, @codegenie/serverless-express와 그 전이 의존성이 모두 dependencies에 있어야 합니다. Lambda는 시작할 때 빠진 패키지를 설치해 주지 않으며, AWS Node.js 배포 패키지 문서는 런타임에 없는 의존성을 배포 패키지나 레이어로 제공하라고 안내합니다. 패키지 크기와 콜드 스타트가 문제라면 esbuild 같은 번들러로 하나의 파일로 묶는 방법도 검토합니다.
REST API의 /{proxy+}는 경로 조각이 하나 이상인 요청만 받으므로 루트 /는 포함하지 않습니다. 그래서 위 설정은 루트 경로를 따로 연결합니다. Nest에 루트 controller를 추가하는 것만으로 API Gateway에 없는 루트 연결이 생기지는 않습니다. 자세한 동작은 API Gateway의 리소스·메서드 설명에서 확인할 수 있습니다.
빌드와 배포
npm run build
sls deploy --stage dev로컬에서 Lambda 환경을 흉내 내려면 serverless-offline 플러그인을 설치하고 plugins에 추가한 뒤 sls offline을 실행합니다.
더 깊이: Lambda의 Node.js 런타임에서 꺼져 있는 require(esm)
Lambda의 Node.js 문서에 따르면 Lambda는 원래의 Node.js가 기본으로 켜 둔 몇몇 실험 기능을 런타임의 안정성을 위해 끄며, Node.js 20, 22, 24 런타임에서는 --no-experimental-require-module 플래그로 require(esm)도 끕니다. 그래서 로컬의 Node.js 24에서 잘 돌던 CommonJS 앱이 같은 버전의 Lambda 런타임에서는 시작하지 못할 수 있습니다.
앞의 serverless.yml이 NODE_OPTIONS에 --experimental-require-module을 넣은 것이 이 기능을 다시 켜는 방법이며, 이미 다른 플래그가 있다면 그 뒤에 덧붙입니다. Lambda는 이 값을 감지해 자신이 붙인 끄기 플래그를 뺍니다. 다만 AWS 문서는 실험 기능을 쓰는 함수가 Lambda의 SLA와 AWS Support 대상이 아니며, 이런 기능은 이후 Node.js 버전에서 바뀌거나 사라질 수 있다고 경고합니다. 문서의 표에는 Node.js 20, 22, 24 런타임만 올라 있으므로, 다른 런타임을 고를 때는 그 시점의 표를 다시 확인합니다.
같은 실행 환경 안에서 handler 추적하기
serverPromise는 모듈 변수라는 점을 기억해야 합니다. API Gateway 요청 하나의 응답과, 이후 요청에도 다시 쓸 수 있는 실행 환경의 메모리는 서로 다른 것입니다.
빈 실행 환경 A에 첫 요청이 오면 bootstrap()이 실행되어 초기화가 1번 일어나고, 그 결과가 serverPromise에 저장됩니다. 같은 A에 다음 요청이 오면 저장된 Promise를 그대로 쓰므로 초기화는 여전히 1번입니다. 하지만 Lambda가 동시 요청을 처리하려고 새 실행 환경 B를 만들면, B는 자기만의 모듈 변수를 가지므로 처음부터 다시 초기화합니다. A가 준비되어 있다는 사실이 B의 준비를 뜻하지는 않습니다.
콜드 스타트의 영향을 줄이려면 초기화 코드와 의존성을 가볍게 하고, 메모리를 늘려 CPU 할당을 키우는 방법을 먼저 측정합니다. 지연이 꼭 일정해야 한다면 AWS의 프로비저닝된 동시성(provisioned concurrency)으로 미리 초기화된 실행 환경을 유지할 수 있지만, 그만큼 비용이 듭니다. 주기적으로 함수를 호출해 따뜻하게 유지하는 방법은 호출한 그 환경만 준비시킬 뿐 콜드 스타트를 없애 주지는 않습니다.
서버리스 NestJS를 운영할 때 고려할 점
- 상태 관리: Lambda 실행 환경은 언제든 사라질 수 있으므로, 세션과 사용자 데이터 같은 상태는 RDS, DynamoDB, S3, ElastiCache(Redis) 같은 외부 서비스에 둡니다.
- 비밀 관리: 민감한 정보는
serverless.yml에 직접 적지 않고, 10장에서처럼 AWS Secrets Manager나 Parameter Store에서 실행 시점에 가져옵니다. Lambda 함수의 실행 역할에는 그 비밀을 읽을 최소한의 권한만 줍니다. - 로그와 지표: Lambda는 표준 출력을 CloudWatch Logs로 보내고, 실행 시간과 오류 수를 CloudWatch 지표로 제공합니다. 10장의 구조화 JSON 로그를 그대로 쓰면 됩니다.
- 파일 시스템: Lambda 함수는
/tmp만 쓸 수 있으므로 파일 업로드는 S3 같은 객체 저장소를 씁니다. - 데이터베이스 연결: 실행 환경마다 별도의 연결 풀을 가지므로, 동시 실행 수가 늘면 연결 수도 함께 늘어납니다.
예를 들어 실행 환경마다 별도의 DB 풀이 있고, 동시에 살아 있는 환경이 8개, 풀당 최대 연결이 5개라면 설정상의 연결 상한 합계는 8 × 5 = 40입니다. 환경이 12개로 늘면 같은 설정에서 12 × 5 = 60이 됩니다. 이 애플리케이션에 허용한 연결 예산이 50개이고 동시 실행을 12개로 제한한다면, 다른 연결 수요가 없다는 단순한 가정에서 풀당 4개로 줄여 12 × 4 = 48로 맞출 수 있습니다. 실제로 열린 연결 수, 환경 재사용, RDS Proxy 같은 연결 관리 도구, 다른 애플리케이션의 연결은 따로 측정해야 하며, 이 계산은 처리량이나 늘 열려 있는 연결 수가 아니라 설정 상한의 합입니다.
컨테이너 기반 서버리스
Lambda는 함수 단위의 서버리스이지만, Docker 컨테이너를 그대로 서버리스처럼 실행하는 서비스도 있습니다. NestJS를 이미 Docker 이미지로 만들어 두었다면, 이쪽이 더 자연스러운 선택일 수 있습니다.
AWS Fargate는 ECS나 EKS에서 컨테이너를 실행하는 방식으로, 개발자는 이미지만 제공하고 그 아래의 서버는 AWS가 관리합니다. 11장에서 만든 이미지를 거의 그대로 쓸 수 있고, Lambda보다 큰 메모리와 긴 실행 시간을 지원하며 일반 웹 서버처럼 동작합니다. 대신 최소 태스크를 늘 유지하므로 쉬는 동안에도 비용이 들고, 새 태스크를 시작하는 데는 Lambda의 콜드 스타트보다 오래 걸릴 수 있습니다.
Fargate on ECS로 배포하는 흐름은 다음과 같습니다. 먼저 11장처럼 Docker 이미지를 빌드해 ECR에 올립니다. 다음으로 Fargate를 쓰는 ECS 클러스터를 만들고, 태스크 정의에 이미지 digest, 포트 3000, CPU와 메모리, 환경 변수, Secrets Manager 연동, 헬스 확인 경로를 정합니다. 마지막으로 그 태스크 정의로 ECS 서비스를 만들어 awsvpc 네트워크 모드의 서브넷과 보안 그룹, ALB(Application Load Balancer) 연결, CPU 사용률이나 요청 수에 따른 자동 확장을 설정합니다.
Google Cloud Run은 이미지를 받아 HTTP 요청이나 Pub/Sub 이벤트에 따라 자동으로 확장하는 GCP의 관리형 컨테이너 서비스이고, Azure Container Apps는 복잡한 오케스트레이션 없이 컨테이너를 배포하는 Azure의 서비스입니다. 둘 다 Fargate처럼 NestJS Docker 이미지를 그대로 쓸 수 있습니다.
함수 기반의 Lambda와 컨테이너 기반의 Fargate, Cloud Run, Container Apps 가운데 무엇이 맞는지는 실행 시간, 트래픽 모양, 콜드 스타트 허용 범위, 상태와 연결 관리, 운영 지표를 함께 보고 정합니다.
정리
- 서버리스는 서버 관리를 공급자에게 맡기고 사용량만큼 비용을 내는 모델이지만, 콜드 스타트, 실행 제한, 상태 외부화라는 대가가 있습니다.
- Node.js 24 Lambda의 비동기 handler는
event와context만 받고 callback을 지원하지 않으므로,@codegenie/serverless-express5의 Promise 방식으로 결과를 돌려줍니다. - 초기화한 NestJS handler는 모듈 변수에 보관해 같은 실행 환경 안에서만 다시 쓰며, 새 실행 환경은 각자 초기화합니다.
- 배포 패키지에는
dist의 handler와 모든 프로덕션 의존성이 들어 있어야 하고, REST API의/{proxy+}와 루트/는 따로 연결합니다. - 실행 환경마다 DB 풀이 생기므로 연결 상한은
동시 실행 수 × 풀 크기로 계산하고, 상태는 외부 저장소에 둡니다.
연습 문제
1. 두 실행 환경의 초기화
같은 실행 환경 A에 요청 두 개가 차례로 성공했습니다. 이어서 새로 만들어진 환경 B의 첫 요청이 app.init()에서 실패했고, 같은 B에 요청을 한 번 더 보냈습니다. 각 환경의 초기화 시도 횟수와 완료된 handler가 있는지 추적해 보세요. A의 handler에 보관한 사용자 진행 상태를 B에서도 읽을 수 있을까요?
해설 보기
A는 첫 요청에서 초기화를 1번 시도해 성공했고, 두 번째 요청은 저장된 Promise를 그대로 썼으므로 완료된 handler가 있고 시도는 1번입니다.
B는 첫 요청에서 초기화가 실패했고, catch에서 serverPromise를 비웠으므로 완료된 handler가 없습니다. 두 번째 요청은 다시 초기화를 시도하므로 누적 시도는 2번이며, 이번에 성공했다면 그때 handler가 저장됩니다. 모듈 메모리는 실행 환경마다 따로 있으므로, A에 보관한 사용자 상태가 B에 공유되거나 환경이 사라진 뒤에도 남는다고 볼 수 없습니다. 그런 상태는 외부 저장소에 두어야 합니다.
2. 연결 예산 맞추기
동시 실행 환경 10개, 풀당 최대 연결 6개, 이 애플리케이션에 할당한 연결 예산이 55개입니다. 설정 상한의 합계를 계산하고, 환경 수를 유지할 때 예산 안에 드는 풀 크기를 구해 보세요. 이 결과로 실제 DB 연결 수를 확정할 수 있을까요?
해설 보기
상한 합계는 10 × 6 = 60으로 55개를 넘습니다. 풀당 정수 상한을 5개로 줄이면 10 × 5 = 50으로 예산 안에 들어옵니다. 이는 다른 연결 수요가 없고 동시 실행 환경이 10개 이하라는 가정 아래의 설정 계산입니다.
실제 연결 수는 풀이 연결을 언제 만드는지, 유휴 연결을 얼마나 오래 두는지, 실행 환경이 얼마나 다시 쓰이는지에 따라 달라지므로 따로 관측해야 합니다. 동시 실행 수 자체를 Lambda의 예약 동시성으로 제한하거나 RDS Proxy로 연결을 모으는 방법도 함께 검토합니다.
3. 배포 패키지에 빠진 모듈
배포 패키지에 dist/lambda.js는 있지만 @codegenie/serverless-express가 devDependencies에 들어가 있어서 패키지에서 빠졌습니다. 개발 PC에서는 실행되지만 Lambda에서는 모듈을 불러오지 못해 실패합니다. serverPromise 캐시나 메모리 설정을 바꾸면 해결될까요?
해설 보기
해결되지 않습니다. 이 실패는 모듈을 불러오는 단계의 문제이며, 캐시와 메모리 설정은 애플리케이션을 성공적으로 불러온 뒤의 초기화와 재사용에 관한 것입니다.
Serverless Framework는 기본적으로 devDependencies를 배포 패키지에서 빼므로, 실행에 필요한 @codegenie/serverless-express를 dependencies로 옮기고 다시 배포해야 합니다. 배포 전에 패키지를 풀어 보거나 번들러로 묶은 결과를 확인해, handler 경로에서 필요한 모든 모듈을 불러올 수 있는지 점검합니다.
다음 글 WebSocket과 실시간 애플리케이션에서는 요청과 응답을 넘어, 서버와 클라이언트가 연결을 유지하며 실시간으로 메시지를 주고받는 방법을 다룹니다.