서버리스 NestJS (AWS Lambda 통합)
지난 11장에서는 NestJS 애플리케이션의 배포와 안정 운영을 위한 Docker 컨테이너화, CI/CD 파이프라인, 클라우드 배포 전략, 모니터링/로깅 시스템을 다뤘습니다.
이제 12장에서는 NestJS 고급 주제와 최신 개발 트렌드를 다루며, 첫 주제로 서버리스(Serverless) 환경에서의 NestJS를 살펴보겠습니다.
서버리스는 클라우드 컴퓨팅의 패러다임 중 하나로, 개발자가 서버 인프라를 직접 관리할 필요 없이 애플리케이션 코드 작성에만 집중할 수 있게 해줍니다. NestJS는 유연한 구조 덕분에 이러한 서버리스 환경에서도 효과적으로 작동할 수 있습니다.
서버리스(Serverless)란 무엇인가?
서버리스 컴퓨팅은 클라우드 서비스 제공업체가 서버 인프라의 프로비저닝, 스케일링, 패치, 유지보수 등 모든 운영 업무를 대신 처리하는 컴퓨팅 실행 모델입니다. 개발자는 자신의 애플리케이션 코드를 함수나 컨테이너 이미지 형태로 배포하고, 클라우드 플랫폼이 코드를 실행하고 자동으로 확장합니다.
서버리스의 주요 특징- 서버 관리 불필요: 개발자는 서버를 직접 프로비저닝하거나 관리할 필요가 없습니다. 클라우드 공급자가 모든 인프라를 처리합니다.
- 이벤트 기반(Event-Driven): 코드는 HTTP 요청, 데이터베이스 변경, 파일 업로드, 메시지 큐 이벤트 등 특정 이벤트에 의해 트리거될 때만 실행됩니다.
- 자동 스케일링: 트래픽 증가에 따라 자동으로 인스턴스가 확장되고, 트래픽 감소 시 자동으로 축소됩니다. 유휴 상태에서는 인스턴스 수가 0으로 줄어들 수 있습니다.
- 사용량 기반 과금: 코드가 실행되는 동안에만 비용을 지불합니다. 유휴 상태에서는 거의 또는 전혀 비용이 발생하지 않습니다.
- 고가용성 내장: 클라우드 공급자가 기본적으로 고가용성과 내결함성을 제공합니다.
- 개발 생산성 향상: 인프라 관리 부담이 줄어들어 개발자가 핵심 비즈니스 로직에 집중할 수 있습니다.
- 비용 효율성: 사용한 만큼만 지불하므로, 트래픽 변동이 심하거나 간헐적으로 사용되는 애플리케이션에 매우 효과적입니다.
- 뛰어난 확장성: 트래픽 급증에도 자동으로 확장되어 안정적인 서비스를 제공합니다.
- 운영 부담 감소: 인프라 패치, 보안 업데이트 등을 클라우드 공급자가 처리합니다.
- 콜드 스타트(Cold Start): 유휴 상태에서 첫 요청이 들어올 때 컨테이너나 런타임이 시작되는 초기 지연 시간(밀리초~수 초)이 발생할 수 있습니다.
- 벤더 종속성: 특정 클라우드 서비스의 API 및 생태계에 종속될 가능성이 있습니다.
- 제한 사항: 함수 실행 시간 제한, 메모리 제한, 파일 시스템 접근 제한 등이 있을 수 있습니다.
- 복잡한 상태 관리: 서버리스는 기본적으로 상태 비저장(Stateless) 아키텍처를 지향하므로, 상태 관리가 필요한 경우 외부 데이터베이스나 캐시를 사용해야 합니다.
NestJS와 서버리스의 통합
NestJS는 Express (또는 Fastify) 기반으로 동작하며, 미들웨어, 가드, 인터셉터, 파이프 등 강력한 모듈 시스템을 갖추고 있어 서버리스 환경에서도 유연하게 적응할 수 있습니다. 특히 @nestjs/platform-express 패키지의 어댑터 패턴 덕분에 다양한 런타임 환경에 맞게 애플리케이션을 조절할 수 있습니다.
NestJS 애플리케이션을 서버리스 환경에 배포하는 주요 방법은 다음과 같습니다.
AWS Lambda & API Gateway
가장 보편적인 서버리스 NestJS 배포 모델입니다. HTTP 요청을 API Gateway가 수신하고, 이를 AWS Lambda 함수로 전달하여 NestJS 애플리케이션 코드를 실행하는 방식입니다. Serverless Framework는 이러한 배포 과정을 자동화하는 데 매우 유용합니다.
구성 요소- AWS Lambda: NestJS 코드를 실행하는 서버리스 컴퓨팅 서비스.
- API Gateway: HTTP 요청을 받아 Lambda 함수를 트리거하는 완전 관리형 서비스.
- Serverless Framework: Lambda, API Gateway 등 AWS 리소스를 쉽게 정의하고 배포할 수 있는 CLI 툴.
npm install -g serverlessNestJS 프로젝트 설정: NestJS 프로젝트를 생성하고, main.ts 파일을 Lambda 환경에 맞게 수정합니다. NestJS는 기본적으로 Express(또는 Fastify) 어댑터를 사용하는데, Lambda 환경에서는 이 어댑터를 @codegenie/serverless-express와 함께 사용하는 구성이 현재 기준으로 일반적입니다.
npm install @codegenie/serverless-express
npm install --save-dev @types/aws-lambdaimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ExpressAdapter } from '@nestjs/platform-express';
import { Callback, Context, Handler } from 'aws-lambda';
import serverlessExpress from '@codegenie/serverless-express';
import * as express from 'express'; // express 직접 임포트
let cachedServer: Handler;
async function bootstrap(): Promise<Handler> {
if (!cachedServer) {
const expressApp = express();
const app = await NestFactory.create(AppModule, new ExpressAdapter(expressApp));
app.enableCors(); // CORS 설정 (필요시)
await app.init();
cachedServer = serverlessExpress({ app: expressApp });
}
return cachedServer;
}
// AWS Lambda 핸들러 함수
// 이 함수가 Lambda의 진입점 (serverless.yml의 handler 설정)
export const handler = async (event: any, context: Context, callback: Callback) => {
// Warm-up 요청 처리 (콜드 스타트 방지)
if (event.source === 'serverless-plugin-warmup') {
console.log('WarmUp - Lambda is warm!');
return callback(null, 'Lambda is warm!');
}
const server = await bootstrap();
return server(event, context, callback);
};bootstrap()함수는 NestJS 애플리케이션 인스턴스를 한 번만 생성하여cachedServer에 저장합니다. 이는 콜드 스타트를 줄이는 데 도움이 됩니다. 첫 요청 시에는 초기화 시간이 걸리지만, 이후 요청들은 캐시된 인스턴스를 사용하여 빠르게 처리됩니다.handler함수는 AWS Lambda가 호출하는 실제 진입점입니다.@codegenie/serverless-express를 사용하여 Express 앱을 Lambda 이벤트와 통합합니다.serverless-plugin-warmup은 콜드 스타트 방지를 위해 주기적으로 Lambda 함수를 호출하여 활성화 상태를 유지하는 플러그인입니다.
serverless.yml 파일 작성: 프로젝트 루트에 serverless.yml 파일을 생성하여 AWS 리소스와 NestJS Lambda 함수를 정의합니다.
service: nestjs-serverless-app # 서비스 이름
# 플러그인 설정
plugins:
- serverless-offline # 로컬 개발용 (선택 사항)
- serverless-plugin-warmup # 콜드 스타트 방지 (선택 사항)
- serverless-dotenv-plugin # .env 파일 로드 (선택 사항)
provider:
name: aws
runtime: nodejs20.x # Node.js 런타임 버전
stage: dev # 배포 스테이지 (dev, prod 등)
region: ap-northeast-2 # AWS 리전
memorySize: 256 # Lambda 함수 메모리 (MB)
timeout: 30 # Lambda 함수 타임아웃 (초)
logRetentionInDays: 7 # CloudWatch Logs 보존 기간
apiGateway:
minimumCompressionSize: 1024 # API Gateway 압축 설정 (gzip)
environment: # Lambda 환경 변수 (민감 정보는 AWS Secrets Manager 사용 권장)
NODE_ENV: ${self:provider.stage}
DATABASE_HOST: ${env:DATABASE_HOST} # .env 파일에서 가져오기
JWT_SECRET: ${env:JWT_SECRET}
# ... 기타 환경 변수
package:
individually: true # 각 함수별로 패키징
patterns:
- '!node_modules/**' # 필요한 모듈은 포함
- 'node_modules/@nestjs/**'
- 'node_modules/reflect-metadata/**'
- 'node_modules/class-validator/**'
# ... NestJS 앱에 필요한 모든 node_modules를 정확히 명시해야 함
# 또는, dist 폴더만 포함하고 나머지 node_modules를 배포 후 설치하는 방식도 있음
- '!src/**' # 빌드된 JS 코드만 포함
functions:
api:
handler: dist/main.handler # Lambda 핸들러 함수 경로 (빌드 후의 JS 파일)
events: # 이 함수를 트리거할 이벤트 (API Gateway)
- http: # HTTP API Gateway (더 빠르고 저렴)
path: /{any+} # 모든 경로를 이 Lambda 함수로 라우팅
method: ANY # 모든 HTTP 메서드 허용
cors: true # CORS 활성화 (필요시)
private: false # 공개 API
# .env 파일 로드를 위한 플러그인 설정 (serverless-dotenv-plugin)
custom:
dotenv:
path: ./.env.${self:provider.stage} # 스테이지별 .env 파일 경로 지정- 로컬에서 NestJS 앱 빌드:
npm run build - Serverless Framework로 배포:
sls deploy --stage dev - 로컬에서 테스트 (serverless-offline 플러그인 사용):
sls offline
- 콜드 스타트 최적화: 위에서 언급한 캐싱 전략 외에도
serverless-plugin-warmup사용, 충분한 메모리 할당, Node.js 런타임 버전 최신화 등을 통해 콜드 스타트 영향을 줄일 수 있습니다. - 상태 관리: Lambda 함수는 상태 비저장이므로, 세션, 사용자 데이터 등 상태가 필요한 경우 AWS RDS(데이터베이스), DynamoDB, S3, ElastiCache(Redis) 등 외부 관리형 서비스를 사용해야 합니다.
- 환경 변수 관리: 민감한 정보는
serverless.yml에 직접 명시하지 않고, AWS Secrets Manager 또는 Parameter Store와 같은 서비스를 통해 런타임에 가져오도록 구현하는 것이 안전합니다. - 로깅 및 모니터링: Lambda는 CloudWatch Logs에 자동으로 로그를 보내고, CloudWatch Metrics를 통해 실행 시간, 에러 수 등을 모니터링합니다. NestJS 내부 로거(Winston 등)를 CloudWatch Logs와 연동하도록 설정하면 좋습니다.
- 파일 시스템 접근: Lambda 함수는
/tmp디렉토리만 쓰기 가능하며, 파일 시스템 접근에 제한이 있습니다. 파일 업로드 등은 S3와 같은 객체 스토리지를 사용해야 합니다. - 배포 아키텍처: MSA(Microservice Architecture)에서 각 서비스를 별도의 Lambda 함수로 배포하는 것이 일반적입니다.
AWS Fargate (ECS/EKS)
Lambda는 함수 기반이지만, Fargate는 Docker 컨테이너를 서버리스 형태로 실행합니다. NestJS가 이미 Docker 이미지로 컨테이너화되어 있다면, Fargate는 Lambda보다 더 자연스러운 선택일 수 있습니다.
개념: 개발자는 Docker 이미지만 제공하고, Fargate가 컨테이너를 실행하고 스케일링하는 EC2 인스턴스를 관리합니다. ECS(Elastic Container Service) 또는 EKS(Elastic Kubernetes Service)의 런치 타입으로 Fargate를 선택할 수 있습니다.
장점- 컨테이너 기반: 기존 Docker 컨테이너 이미지를 거의 그대로 사용할 수 있어 마이그레이션이 용이합니다.
- 유연성: Lambda보다 더 큰 메모리와 긴 실행 시간을 지원하며, 일반적인 웹 서버처럼 동작합니다.
- 서버 관리 불필요: Lambda와 마찬가지로 서버 인스턴스를 직접 관리할 필요가 없습니다.
- Lambda보다는 비용이 더 발생할 수 있습니다 (유휴 상태에서도 최소 리소스 유지).
- Lambda보다 콜드 스타트가 더 길 수 있습니다.
NestJS Docker 이미지 빌드: 이전 11장 1절에서 설명한 대로 NestJS 애플리케이션의 Docker 이미지를 빌드하고 ECR에 푸시합니다.
ECS 클러스터 생성: Fargate 런치 타입으로 ECS 클러스터를 생성합니다.
- 컨테이너 이미지 URI, 포트 매핑(3000), CPU/메모리 할당 등을 정의합니다.
- NestJS에 필요한 환경 변수를 설정합니다. 민감 정보는 Secrets Manager와 연동하거나, 직접 주입합니다.
- 헬스 체크(Health Check) 경로(예:
/health)를 설정합니다.
- 생성된 태스크 정의를 기반으로 ECS 서비스를 생성합니다.
- 네트워크 구성:
awsvpc모드를 사용하고, 필요한 서브넷과 보안 그룹을 선택합니다. - 로드 밸런싱: ALB(Application Load Balancer)를 서비스에 연결하여 외부 트래픽을 컨테이너로 라우팅합니다.
- 자동 스케일링: CPU 사용량, 요청 수 등 지표에 따라 서비스의 태스크(컨테이너 인스턴스) 수를 자동으로 조절하도록 설정합니다.
Google / Azure
- Google Cloud Run: GCP의 완전 관리형 서버리스 컨테이너 플랫폼입니다. Docker 이미지만 제공하면 자동으로 스케일링되고, HTTP 요청 또는 Cloud Pub/Sub 이벤트에 의해 트리거됩니다.
- Azure Container Apps: Azure의 서버리스 컨테이너 서비스로, 마이크로서비스, 이벤트 기반 처리 등에 최적화되어 있습니다. Kubernetes 기반이지만 복잡한 오케스트레이션 없이 컨테이너를 배포할 수 있습니다.
이러한 서비스들은 AWS Fargate와 유사하게 NestJS Docker 이미지를 그대로 사용하여 서버리스 환경의 이점을 누릴 수 있도록 합니다.
NestJS는 유연하고 확장 가능한 아키텍처 덕분에 서버리스 환경에서도 매우 효과적으로 동작할 수 있습니다.
AWS Lambda/API Gateway 조합은 함수 기반 서버리스의 대표 예시이고, AWS Fargate, Google Cloud Run, Azure Container Apps는 컨테이너 기반 서버리스의 훌륭한 대안입니다.
서비스별 장단점과 프로젝트 요구사항을 고려해 가장 적합한 서버리스 전략을 선택하는 것이 중요합니다.
서버리스 NestJS는 운영 부담을 줄이고 비용을 최적화하며, 뛰어난 확장성을 제공해 현대 웹 애플리케이션 개발에 큰 이점을 줍니다.