로드 밸런싱과 수평적 확장
수직·수평 확장을 비교하고 헬스 체크와 세션 상태를 고려해 여러 NestJS 인스턴스로 트래픽을 분산합니다.
로드 밸런싱과 수평 확장은 인스턴스 풀, 헬스 체크, 세션 상태, 오토스케일 기준으로 읽습니다.
요청 분산만으로는 부족합니다. 인스턴스 풀, 헬스 체크, 상태 분리, 스케일 기준이 함께 맞아야 안정적으로 늘어납니다.
- Client
트래픽은 한 지점으로 모이고 로드 밸런서가 첫 판단자가 됩니다.
- Load balancer
round robin, least connection, IP hash 중 서비스 특성에 맞게 보냅니다.
- Instance pool
여러 Nest 인스턴스가 같은 API를 제공하고 하나가 빠져도 응답합니다.
- External state
세션, 캐시, 파일, rate limit 상태는 외부 저장소로 분리합니다.
- Health
준비된 인스턴스만 요청을 받게 합니다.
- Pool
DB connection과 외부 API quota를 같이 봅니다.
- Metric
CPU뿐 아니라 RPS, latency, queue lag를 봅니다.
- Failover
한 대 제거 실험으로 세션과 작업이 안전한지 확인합니다.
이 절에서는 NestJS 애플리케이션의 로드 밸런싱(Load Balancing)과 수평적 확장(Horizontal Scaling)을 다룹니다.
단일 서버로 운영되는 애플리케이션은 처리할 수 있는 동시 요청 수에 한계가 있으며, 서버에 장애가 발생하면 서비스 전체가 중단될 위험이 있습니다.
로드 밸런싱과 수평적 확장은 이러한 한계를 극복하고 고가용성 및 확장 가능한 시스템을 구축하는 데 필수적인 기술입니다.
아래 다이어그램은 수직 확장과 수평 확장을 용량, 장애 범위, 운영 난이도 관점에서 비교합니다.
성능만 보면 수직 확장이 쉬워 보이지만, 장애 격리와 비용 효율까지 보면 수평 확장의 설계 조건이 핵심입니다.
- Vertical
CPU와 메모리를 올려 단일 서버 처리량을 늘립니다. 장점 구현이 단순하고 코드 변경이 적습니다. 한계 물리 한계와 단일 장애 지점이 남습니다.
- Horizontal
같은 역할의 인스턴스를 추가해 요청을 나눕니다. 장점 장애 격리와 가용성이 좋아집니다. 조건 상태 공유와 헬스 체크 설계가 필요합니다.
- CPU 코어
단일 이벤트 루프만 믿으면 코어를 충분히 쓰기 어렵습니다.
- I/O 부하
DB, cache, 외부 API 대기 시간을 여러 인스턴스로 분산합니다.
- 장애 격리
프로세스 하나가 죽어도 전체 서비스가 멈추지 않게 합니다.
스케일링(Scaling)이란 무엇인가?
스케일링(Scaling)은 애플리케이션이 증가하는 부하를 처리할 수 있도록 시스템의 용량을 늘리는 것을 의미합니다.
스케일링에는 크게 두 가지 방식이 있습니다.
수직적 확장
- 정의: 단일 서버의 하드웨어 자원(CPU, 메모리, 디스크 등)을 업그레이드하여 성능을 향상시키는 방식입니다.
- 장점: 구현이 비교적 간단하고, 애플리케이션 코드를 변경할 필요가 거의 없습니다.
-
단점
- 물리적 한계: 서버 한 대가 가질 수 있는 자원에는 물리적인 한계가 있습니다.
- 단일 장애 지점(Single Point of Failure): 여전히 서버 한 대에 의존하므로, 해당 서버에 장애가 발생하면 서비스 전체가 중단됩니다.
- 비용 효율성 감소: 일정 수준 이상으로 자원을 늘리는 것은 비용 효율성이 급격히 떨어집니다.
수평적 확장
- 정의: 서버의 개수를 늘려 여러 서버가 부하를 분산하여 처리하도록 하는 방식입니다.
-
장점
- 무한한 확장성: 이론적으로 서버를 계속 추가하여 무한정으로 용량을 늘릴 수 있습니다.
- 고가용성: 한 서버에 장애가 발생해도 다른 서버들이 서비스를 계속 제공하여 중단 없이 운영됩니다.
- 비용 효율성: 비교적 저렴한 여러 대의 서버를 조합하여 고성능을 얻을 수 있습니다.
-
단점
- 복잡성 증가: 여러 서버 간의 부하 분산, 세션 관리, 데이터 일관성 유지 등 아키텍처의 복잡성이 증가합니다.
- 상태 비저장(Stateless) 아키텍처 요구: 서버 간 상태를 공유하지 않도록 애플리케이션을 설계해야 합니다.
NestJS와 같은 Node.js 기반 애플리케이션은 I/O 바운드(I/O Bound) 작업에 강하며, 단일 스레드 모델이기 때문에 CPU 바운드(CPU Bound) 작업에는 취약합니다.
따라서 CPU를 최대한 활용하고 높은 처리량을 달성하기 위해서는 수평적 확장이 필수적입니다.
여러 NestJS 애플리케이션 인스턴스를 실행하고 이들 간에 트래픽을 분산해야 합니다.
로드 밸런싱(Load Balancing)이란?
로드 밸런싱은 수평적 확장된 여러 서버 인스턴스에 들어오는 네트워크 트래픽을 효율적으로 분산하는 기술입니다.
로드 밸런서는 클라이언트의 요청을 받아, 내부 알고리즘에 따라 부하가 적거나 응답이 빠른 서버로 요청을 전달합니다.
로드 밸런싱의 주요 목적- 부하 분산: 모든 서버가 균등하게 요청을 처리하도록 하여 특정 서버의 과부하를 방지하고 전체 시스템의 처리량을 높입니다.
- 고가용성: 특정 서버에 장애가 발생하면, 해당 서버로의 트래픽 전송을 중단하고 정상 서버로만 요청을 보내 서비스 중단을 방지합니다.
- 세션 지속성(Session Persistence/Sticky Sessions): 특정 클라이언트의 모든 요청이 항상 동일한 서버로 전송되도록 하여 세션 기반 애플리케이션의 문제를 해결합니다 (상태 비저장 아키텍처가 더 권장되지만, 경우에 따라 필요).
- SSL/TLS 오프로딩: 로드 밸런서에서 SSL/TLS 암복호화를 처리하여 백엔드 서버의 CPU 부하를 줄일 수 있습니다.
- 라운드 로빈(Round Robin): 요청을 서버들에게 순차적으로 분배합니다. (가장 간단하고 흔함)
- 가중치 기반 라운드 로빈(Weighted Round Robin): 서버의 성능이나 용량에 따라 가중치를 부여하여 더 많은 요청을 처리할 수 있는 서버에 더 많은 요청을 보냅니다.
- 최소 연결(Least Connection): 현재 가장 적은 활성 연결을 가진 서버로 요청을 보냅니다.
- IP 해시(IP Hash): 클라이언트의 IP 주소를 해시하여 항상 동일한 서버로 요청을 보냅니다. (세션 지속성 구현에 유용)
- 하드웨어 로드 밸런서: F5 BIG-IP, A10 Networks 등 물리적 장비 형태의 로드 밸런서입니다. 고성능이지만 비용이 비쌉니다.
- 소프트웨어 로드 밸런서: Nginx, HAProxy, LVS 등 소프트웨어 형태로 구현됩니다. 유연하고 비용 효율적입니다.
- 클라우드 기반 로드 밸런서: AWS ELB (ALB, NLB, CLB), Google Cloud Load Balancing, Azure Load Balancer 등 클라우드 서비스 제공업체에서 관리형 서비스로 제공합니다. 가장 많이 사용되는 방식입니다.
아래 다이어그램은 여러 NestJS 인스턴스 앞에서 분산 정책, 상태 관리, 장애 감지를 함께 판단하는 기준을 정리합니다.
로드 밸런서는 트래픽을 나누는 장치이면서 장애 인스턴스를 제외하고 연결 유지 요구를 조정하는 운영 지점입니다.
- Round Robin
요청 비용과 인스턴스 성능이 비슷할 때 기본값으로 좋습니다.
- Weighted
고사양 노드나 canary 단계에 더 많은 요청을 보냅니다.
- Least Conn
긴 연결이나 WebSocket처럼 요청 시간이 들쭉날쭉할 때 유리합니다.
- IP Hash
같은 클라이언트를 같은 인스턴스로 보내지만 상태 외부화가 더 낫습니다.
- Health
readiness 실패는 새 요청 차단, liveness 실패는 재시작 판단입니다.
- TLS
SSL/TLS 오프로딩과 HTTP/TCP 라우팅 성격을 구분합니다.
- State
sticky session은 임시방편이고 로그인 세션은 Redis 같은 외부 저장소가 우선입니다.
NestJS 애플리케이션의 수평적 확장 및 로드 밸런싱 구현
NestJS 애플리케이션은 Node.js 기반이므로, 단일 스레드 이벤트 루프 모델을 따릅니다.
따라서 CPU 코어를 최대한 활용하기 위해서는 여러 NestJS 인스턴스를 실행하고 이를 로드 밸런싱하는 것이 일반적입니다.
Node.js cluster 모듈 활용
Node.js의 cluster 모듈은 단일 서버 내에서 여러 개의 Node.js 프로세스를 생성하여 CPU 코어를 최대한 활용할 수 있도록 합니다.
이는 실제 로드 밸런서보다 앞단에서 프로세스 간 부하를 분산합니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as cluster from 'cluster'; // Node.js cluster 모듈 임포트
import * as os from 'os'; // OS 정보 모듈 임포트
// CPU 코어 수만큼 워커 프로세스 생성
const numCPUs = os.cpus().length;
async function bootstrap() {
if (cluster.isMaster) { // 마스터 프로세스
console.log(`Master ${process.pid} is running`);
// 워커 프로세스 생성
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 워커 프로세스 종료 시 재시작 (선택 사항, 안정성 향상)
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
cluster.fork(); // 새로운 워커 생성
});
} else { // 워커 프로세스
const app = await NestFactory.create(AppModule);
await app.listen(3000); // 각 워커는 동일한 포트에서 수신 대기 (마스터가 요청을 분배)
console.log(`Worker ${process.pid} started and listening on port 3000`);
}
}
bootstrap();cluster.isMaster: 현재 프로세스가 마스터 프로세스인지 워커 프로세스인지 구분합니다.cluster.fork(): 새로운 워커 프로세스를 생성합니다.- 워커 프로세스들은 동일한 포트(예: 3000)를 공유하며, 마스터 프로세스가 요청을 워커들에게 분배합니다.
- 주의: 이 방식은 단일 서버 내에서의 확장이며, 서버 자체의 장애에는 취약합니다. 클라우드 환경에서는 보통 컨테이너 오케스트레이션 도구(Kubernetes)나 클라우드 로드 밸런서를 통해 여러 서버에 걸쳐 수평 확장을 구현하는 것이 일반적입니다.
Docker 컨테이너 및 오케스트레이션
가장 일반적인 수평 확장 방법입니다.
NestJS 애플리케이션을 Docker 이미지로 빌드하고, 이를 여러 컨테이너 인스턴스로 실행한 다음, Kubernetes와 같은 컨테이너 오케스트레이션 도구를 사용하여 로드 밸런싱과 자동 스케일링을 구현합니다.
단계 1: NestJS 애플리케이션 Dockerfile 작성# 빌드 환경 설정 (멀티 스테이지 빌드)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # 의존성 설치 (npm install 대신 npm ci 사용 권장)
COPY . .
RUN npm run build # NestJS 프로젝트 빌드
# 실행 환경 설정
FROM node:20-alpine
WORKDIR /app
# 빌드 단계에서 생성된 node_modules와 dist 디렉토리 복사
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./ # package.json 필요 (시작 스크립트 등)
EXPOSE 3000 # NestJS 애플리케이션이 수신 대기할 포트
CMD ["node", "dist/main"] # 애플리케이션 시작 명령어docker build -t your-username/nestjs-app:latest .
docker push your-username/nestjs-app:latestKubernetes는 Pods(애플리케이션 컨테이너의 최소 단위), Deployments(Pod 관리), Services(로드 밸런싱)를 통해 애플리케이션을 배포하고 관리합니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nestjs-app-deployment
labels:
app: nestjs-app
spec:
replicas: 3 # NestJS 애플리케이션 인스턴스 3개 실행 (수평 확장)
selector:
matchLabels:
app: nestjs-app
template:
metadata:
labels:
app: nestjs-app
spec:
containers:
- name: nestjs-app
image: your-username/nestjs-app:latest # 빌드한 Docker 이미지
ports:
- containerPort: 3000 # 컨테이너 내부 포트
# 리소스 요청 및 제한 설정 (매우 중요)
resources:
requests:
cpu: "100m" # 최소 CPU 0.1 코어
memory: "128Mi" # 최소 메모리 128MB
limits:
cpu: "500m" # 최대 CPU 0.5 코어
memory: "256Mi" # 최대 메모리 256MB
env: # 환경 변수 설정
- name: DATABASE_URL
value: "postgres://user:password@db-host:5432/mydb"
# - name: REDIS_HOST
# value: "redis-service" # Redis 서비스명apiVersion: v1
kind: Service
metadata:
name: nestjs-app-service
spec:
selector:
app: nestjs-app # 이 서비스가 트래픽을 보낼 Pods를 선택
ports:
- protocol: TCP
port: 80 # 클라이언트가 접근할 서비스 포트 (로드 밸런서 포트)
targetPort: 3000 # Pod 컨테이너의 포트
type: LoadBalancer # 클라우드 환경에서 외부 로드 밸런서 자동 프로비저닝
# 또는 ClusterIP (내부용), NodePort (노드 IP:Port)Deployment:replicas를 통해 NestJS 애플리케이션 Pod를 여러 개 띄웁니다. Kubernetes는 이 Pod들을 자동으로 관리하고, 하나가 죽으면 새로운 Pod를 생성합니다.Service:LoadBalancer타입으로 설정하면 클라우드 환경에서 자동으로 외부 접근 가능한 로드 밸런서를 프로비저닝하여, 클라이언트의 요청을 여러 NestJS Pods로 분산시킵니다.
컨테이너 기반 수평 확장에서는 Deployment, Service, 로드 밸런서, 헬스 체크, 오토스케일링이 한 흐름으로 맞물려야 합니다.
Deployment로 같은 앱 Pod를 늘리고, Service와 로드 밸런서가 안정적인 접근점을 제공합니다.
- 1Image
멀티 스테이지 빌드로 같은 실행 단위를 만듭니다.
- 2Deployment
replicas로 같은 NestJS Pod를 여러 개 유지합니다.
- 3Service / LB
Pod IP 변화와 무관한 하나의 접근점으로 분산합니다.
- 4Readiness
준비되지 않은 Pod는 새 요청을 받지 않게 합니다.
- 5resources
CPU와 memory request/limit이 HPA 판단의 기준입니다.
- 6readiness
앱 시작과 DB 연결 준비를 분리해야 합니다.
- 7pool
replica 수만큼 DB connection 총량도 늘어납니다.
- 8metric
CPU만으로는 queue lag나 외부 API 지연을 설명하지 못합니다.
클라우드 서비스 활용 (AWS, GCP, Azure)
클라우드 환경에서는 관리형 서비스를 통해 로드 밸런싱과 수평 확장을 더욱 쉽게 구현할 수 있습니다.
-
AWS (Amazon Web Services)
- Elastic Load Balancing (ELB): ALB(Application Load Balancer), NLB(Network Load Balancer)를 사용하여 HTTP/HTTPS 트래픽 또는 TCP/UDP 트래픽을 EC2 인스턴스, 컨테이너(ECS/EKS), Lambda 함수 등으로 분산합니다.
- Auto Scaling Groups (ASG): EC2 인스턴스 그룹을 생성하고, CPU 사용량이나 네트워크 트래픽 같은 지표에 따라 자동으로 인스턴스 수를 늘리거나 줄여 수평 확장을 구현합니다.
- ECS (Elastic Container Service) / EKS (Elastic Kubernetes Service): 컨테이너화된 NestJS 애플리케이션을 배포하고, 로드 밸런싱 및 자동 스케일링을 쉽게 설정할 수 있습니다.
-
GCP (Google Cloud Platform)
- Cloud Load Balancing: 글로벌, 리전별 로드 밸런싱을 제공하여 다양한 트래픽을 Compute Engine, GKE(Google Kubernetes Engine), Cloud Run 등으로 분산합니다.
- Managed Instance Groups (MIGs): VM 인스턴스 그룹을 관리하고, 자동 스케일링을 통해 VM 수를 조절합니다.
- GKE (Google Kubernetes Engine): Kubernetes 클러스터를 관리형 서비스로 제공하여 컨테이너화된 애플리케이션의 배포, 로드 밸런싱, 스케일링을 자동화합니다.
-
Azure (Microsoft Azure)
- Azure Load Balancer / Azure Application Gateway: 트래픽 분산 및 고급 라우팅 기능을 제공합니다.
- Azure Virtual Machine Scale Sets (VMSS): VM 인스턴스 그룹을 자동으로 스케일링합니다.
- Azure Kubernetes Service (AKS): Kubernetes 클러스터를 관리형 서비스로 제공합니다.
수평 확장은 인스턴스 수를 늘리는 작업보다 먼저 상태, 헬스 체크, 연결 풀, 오토스케일링 조건을 검증해야 안정적으로 동작합니다.
수평 확장은 병목을 없애기보다 다른 곳으로 옮길 수 있습니다. 상태, 헬스 체크, 연결 풀, 오토스케일 기준을 먼저 검증합니다.
- 1. Stateless
세션은 인스턴스 메모리에 두지 않습니다. 업로드 파일은 공유 가능한 저장소로 보냅니다. rate limit 카운터는 Redis 등으로 분리합니다.
- 2. Health
프로세스 생존과 요청 준비 상태를 분리합니다. DB, cache, queue 의존성 실패 시 새 요청을 차단합니다. 무거운 헬스 체크는 별도 주기로 제한합니다.
- 3. Pool
인스턴스 수 x DB pool 크기를 다시 계산합니다. 외부 API quota와 timeout을 확인합니다. 큐 소비자 중복 처리를 막습니다.
- 4. Autoscale
CPU, RPS, latency, queue lag 중 병목을 설명하는 지표를 고릅니다. scale out/in 냉각 시간을 둡니다. 배포 직후 지표 급등을 따로 봅니다.
다음 다이어그램은 로드 밸런싱과 수평 확장에서 상태를 어디로 분리하고 어떤 지표를 봐야 하는지 다시 묶어 정리합니다.
모든 인스턴스가 교체 가능해야 안정적으로 늘릴 수 있습니다. 동시에 어떤 자원이 먼저 막히는지 계층별로 측정해야 합니다.
- Instance
세션과 파일을 오래 붙들면 교체 가능성이 깨집니다.
- Redis
세션, 캐시, rate limit처럼 빠르게 바뀌는 공유 상태를 둡니다.
- Storage
업로드 파일은 object storage나 공유 볼륨으로 보냅니다.
- Queue
작업 대기열과 consumer lag를 보고 처리량을 조절합니다.
- API
RPS와 p95 latency가 같이 오르는지 봅니다.
- Error
5xx와 timeout이 특정 인스턴스 문제인지 나눕니다.
- DB
pool wait와 slow query가 새 병목인지 확인합니다.
- Queue
lag, retry, consumer 수가 균형인지 확인합니다.
로드 밸런싱과 수평 확장은 고성능, 고가용성 애플리케이션 구축에 필수적인 전략입니다.
NestJS 애플리케이션은 Node.js 특성상 CPU 코어를 최대한 활용하고 안정성을 확보하기 위해 수평 확장이 특히 중요합니다.
Docker, Kubernetes 같은 컨테이너 기술이나 클라우드 관리형 서비스를 활용하면 NestJS 애플리케이션을 효율적으로 확장/운영할 수 있습니다.
이 전략은 증가하는 트래픽에 유연하게 대응하고 안정적인 서비스를 제공하는 데 큰 도움이 됩니다.
마지막으로 다음 다이어그램은 로컬 상태 제거, 헬스 체크, 연결 풀 점검을 절 마무리 체크리스트로 정리합니다.
요청이 어디로 가든 같은 결과가 나오고, 장애 인스턴스가 빠지며, 하위 자원이 버티는지 확인합니다.
- State
세션은 Redis나 DB로 이동 파일은 object storage로 이동 인메모리 카운터는 공유 저장소로 이동
- Health
readiness 실패 시 새 요청 차단 liveness 실패 시 재시작 의존성 체크 깊이와 주기 조절
- Pool
DB pool 총량 재계산 queue consumer 중복 처리 방지 외부 API quota와 timeout 점검
- 한 대 종료
드 밸런서가 실패 인스턴스로 새 요청을 보내지 않는지 확인합니다.
- 부하 증가
replica 증가 후 p95 latency와 error rate가 실제로 낮아지는지 봅니다.
- DB 압박
connection wait가 늘면 앱이 아니라 DB가 새 병목입니다.
이것으로 9장 성능 최적화와 스케일링의 세 번째 절을 마칩니다.