본문으로 건너뛰기

안동민 개발노트

본문 시작

로드 밸런싱과 수평적 확장

확장 한계와 상태·헬스 체크·연결 예산을 함께 계산하고 여러 NestJS 인스턴스로 트래픽을 안전하게 분산합니다.

로드 밸런싱과 수평 확장은 서버 수 자체가 아니라 교체 가능한 실행 단위, 준비된 엔드포인트, 외부 상태, 하위 자원 예산을 함께 설계하는 문제입니다.

클라이언트 요청을 로드 밸런서가 준비된 NestJS 복제본으로 분산하고 상태와 하위 자원은 외부화하는 아키텍처, 수직 확장과 수평 확장의 경계, 알고리즘별 동작과 제약

NestJS · architecture

복제본보다 먼저 준비 상태와 공유 자원 경계를 그린다

로드 밸런서는 준비된 실행 단위에 정책을 적용할 뿐, 무한한 용량이나 무중단을 만들지는 않습니다. 세션과 작업 상태를 외부화하고 데이터베이스·큐·외부 API의 예산 안에서 복제본을 늘립니다.

로드 밸런서와 교체 가능한 NestJS 복제본 아키텍처 클라이언트가 로드 밸런서에 요청하면 준비 상태를 통과한 NestJS 복제본이 처리한다. 복제본은 외부 세션 저장소와 데이터베이스, 큐, 외부 API를 공유하며 총 연결과 할당량이 확장 상한을 만든다. EDGE READY APP POOL EXTERNAL STATE request ready only shared budget 클라이언트 HTTP · HTTPS 로드 밸런서 policy · health 교체 가능한 NestJS 복제본 Pod A · ready · stateless Pod B · ready · stateless Pod C · not ready → 제외 세션 · 캐시 파일 · rate limit 하위 자원 DB · queue · API pool · quota · lag 확장 상한 max replica × process/Pod × pool/process 가장 먼저 포화되는 연결·할당량·처리량이 실제 용량을 제한한다
확장 방식과 분산 정책의 실제 의미
선택무엇을 바꾸는가주의할 경계
scale up한 실행 단위의 CPU·메모리단일 노드 한계와 장애 범위는 남음
scale out준비된 동일 역할 복제본 수외부 상태와 하위 자원 예산 필요
round robin요청을 순서대로 배정요청 비용·실제 지연은 측정하지 않음
weighted설정 가중치 비율로 배정관측 결과에 맞춘 가중치 조정 필요
least connections활성 연결이 적은 대상으로 배정연결 수가 작업 비용을 항상 대표하지 않음
source/IP hash관측 소스의 해시로 대상 선택NAT·프록시·풀 변경 때 매핑이 달라질 수 있음
request 준비된 복제본만 분산 대상
edge

로드 밸런서

health와 readiness를 통과한 대상에 선택한 정책을 적용합니다.

pool

교체 가능한 NestJS 복제본

세션·파일·rate limit 상태를 외부화해 어느 복제본도 요청을 처리하게 합니다.

scale up

한 실행 단위의 자원 증가

적용은 단순할 수 있지만 노드 한계와 장애 범위가 남습니다.

scale out

준비된 복제본 수 증가

DB·queue·API 예산과 상태 분리가 실제 상한을 만듭니다.

round robin

순차 또는 가중 배정

요청 비용을 직접 측정하지 않으며 가중치는 관측으로 조정합니다.

least conn

활성 연결 수 기준

연결 수가 실제 작업 비용이나 응답 시간을 항상 뜻하지는 않습니다.

source hash

조건부 세션 지속성

NAT·프록시·풀 변경으로 매핑이 달라질 수 있어 외부 상태가 우선입니다.

로드 밸런싱, 상태 분리, 준비 상태, replica × process × pool 예산을 하나의 아키텍처 결정으로 검증합니다.

스케일링과 가용성의 경계

수직 확장(scale up) 은 한 실행 단위의 CPU·메모리 같은 자원을 늘립니다. 적용은 단순할 수 있지만 한 노드의 물리적·비용 한계와 장애 범위는 남습니다.

수평 확장(scale out) 은 같은 역할의 실행 단위를 추가합니다. 다만 용량은 무한하지 않습니다. 데이터베이스 연결, 외부 API 할당량, 큐 처리량, 캐시와 세션 상태, 조정 비용 가운데 가장 먼저 포화되는 자원이 상한이 됩니다. 여러 장애 도메인에 준비된 복제본을 두고 상태를 외부화했을 때 가용성도 개선됩니다. 단순히 프로세스 수만 늘린다고 무중단이 보장되지는 않습니다.

Node.js는 JavaScript 콜백을 이벤트 루프에서 실행하므로 긴 CPU 작업이 루프를 막을 수 있습니다. I/O 중심 API는 비동기 I/O의 이점을 얻지만, CPU 중심 작업은 다음 대안 가운데 측정 결과에 맞는 것을 선택합니다.

  • 작은 작업으로 나누거나 알고리즘을 개선합니다.
  • worker_threads 또는 별도 작업 큐로 CPU 작업을 격리합니다.
  • 한 머신의 여러 프로세스 또는 여러 Pod로 처리량과 장애 범위를 나눕니다.

로드 밸런싱 정책을 읽는 법

로드 밸런서는 준비된 엔드포인트 집합에 정책을 적용합니다. 정책 이름만으로 “가장 빠른 서버”가 선택된다고 단정할 수 없습니다.

  • 라운드 로빈은 사용 가능한 엔드포인트에 요청을 순서대로 보냅니다. 요청 비용 차이와 실제 지연을 직접 측정하지는 않습니다.
  • 가중 라운드 로빈은 설정한 가중치 비율에 따라 더 큰 용량의 엔드포인트에 더 많은 요청을 배정합니다. 가중치는 관측 결과에 맞게 조정해야 합니다.
  • 최소 연결은 활성 연결 수가 가장 적은 엔드포인트를 고릅니다. 연결 수가 작업 비용이나 응답 시간을 항상 대표하는 것은 아닙니다.
  • 소스/IP 해시는 로드 밸런서가 관측한 소스 값을 해시해 대상을 고릅니다. 프록시·NAT, 풀 구성 변경, 제품별 구현에 따라 매핑이 바뀔 수 있으므로 “항상 같은 서버”를 보장하는 일반 규칙이 아닙니다.

세션 지속성(sticky session)은 특정 제품이 쿠키나 소스 해시 등으로 제공하는 구성 가능한 동작입니다. 장애나 스케일 변경 때 재매핑될 수 있으므로, 가능하면 세션·rate limit·업로드 파일 같은 상태를 외부 저장소에 두고 어느 복제본도 요청을 처리할 수 있게 합니다.

구현 위치는 요구에 따라 달라집니다. NGINX·HAProxy 같은 소프트웨어, 하드웨어 장비, Kubernetes Gateway/Ingress 또는 Service 구현, AWS Elastic Load Balancing의 Application Load Balancer·Network Load Balancer 같은 관리형 제품을 사용할 수 있습니다. 각 제품이 지원하는 계층, 알고리즘, 상태 확인, 세션 지속성의 의미를 문서에서 확인해야 합니다.

NestJS 실행 단위 선택

Kubernetes에서는 Pod당 NestJS 프로세스 하나를 기본 모델로 두면 자원 요청, 재시작, readiness, HPA의 관측 단위가 일치합니다. 벤치마크와 연결 예산이 허용할 때만 Pod당 소수 프로세스를 선택합니다. 최대 데이터베이스 연결 수는 다음처럼 전체 배수를 계산합니다.

최대 앱 연결 = maxReplicas × processesPerPod × poolSizePerProcess

예를 들어 아래 구성은 12 × 1 × 8 = 96개까지 앱 연결을 만들 수 있습니다. 마이그레이션·관리 연결과 장애 시 여유분을 제외한 데이터베이스 예산 안에 들어오도록 세 값을 함께 제한해야 합니다.

단일 VM의 대안: Node.js cluster

cluster는 한 머신에서 여러 Node.js 프로세스가 포트를 공유하는 대안입니다. 프로세스 분배 방식은 플랫폼과 cluster.schedulingPolicy 설정의 영향을 받으므로 외부 로드 밸런서 알고리즘과 동일하다고 가정하지 않습니다. 다음 예시는 현재 API인 node: 임포트, availableParallelism(), cluster.isPrimary를 사용하고 짧은 시간의 무한 재시작을 막습니다.

src/main.ts (단일 VM의 cluster 대안)
import cluster from 'node:cluster';
import { availableParallelism } from 'node:os';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

const workerTarget = Math.max(1, availableParallelism());
const restartWindowMs = 60_000;
const maxRestartsPerWindow = 5;
const restartTimes: number[] = [];
const pendingRestarts = new Set<NodeJS.Timeout>();
let shuttingDown = false;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}

if (cluster.isPrimary) {
  const forkWorker = () => cluster.fork();
  const stopCluster = () => {
    if (shuttingDown) return;
    shuttingDown = true;
    for (const timer of pendingRestarts) clearTimeout(timer);
    pendingRestarts.clear();
    cluster.disconnect(() => process.exit(1));
    setTimeout(() => process.exit(1), 10_000).unref();
  };

  for (let index = 0; index < workerTarget; index += 1) {
    forkWorker();
  }

  cluster.on('exit', (worker, code, signal) => {
    if (shuttingDown) return;
    const now = Date.now();
    while (restartTimes[0] && now - restartTimes[0] > restartWindowMs) {
      restartTimes.shift();
    }
    restartTimes.push(now);

    console.error('worker exited', {
      pid: worker.process.pid,
      code,
      signal,
    });

    if (restartTimes.length > maxRestartsPerWindow) {
      console.error('restart budget exhausted; let the process supervisor intervene');
      stopCluster();
      return;
    }

    const delayMs = Math.min(1_000 * 2 ** (restartTimes.length - 1), 30_000);
    const timer = setTimeout(() => {
      pendingRestarts.delete(timer);
      if (!shuttingDown) forkWorker();
    }, delayMs);
    pendingRestarts.add(timer);
  });
} else {
  void bootstrap();
}

이 코드는 단일 머신의 CPU 활용 예시일 뿐 고가용성 구성이 아닙니다. 종료 신호 전달, graceful shutdown, 로그·메트릭, 프로세스 감독 정책도 별도로 설계해야 합니다.

버전 고정 이미지 만들기

Node.js 공식 지원 표에서 현재 LTS인 v24를 기준으로 예시를 작성합니다. 개발 의존성은 빌드 단계에만 두고, 런타임에는 프로덕션 의존성과 빌드 산출물만 복사합니다. Dockerfile 명령 뒤의 #는 줄 중간 주석이 아니라 인수가 될 수 있으므로 설명은 별도 줄에 둡니다.

Dockerfile
FROM node:24.19-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:24.19-alpine AS production-deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

FROM node:24.19-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --chown=node:node package*.json ./
COPY --from=production-deps --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]

latest 대신 버전 태그를 사용하면 의도하지 않은 버전 이동을 줄일 수 있지만, 태그 자체는 레지스트리에서 다시 가리킬 수 있습니다. 바이트 단위 재현성이 필요하면 레지스트리에서 확인한 이미지 digest를 고정하고, 애플리케이션 release tag에는 레지스트리의 immutability 정책을 적용하거나 배포 명세에서 digest를 사용합니다.

docker build -t ghcr.io/example/nestjs-app:2026.08.25-1 .
docker push ghcr.io/example/nestjs-app:2026.08.25-1

실행 가능한 헬스 엔드포인트

startup은 애플리케이션 시작 완료를, liveness는 프로세스 자체의 생존을, readiness는 새 요청을 받아도 되는지를 답합니다. liveness에 데이터베이스 같은 외부 의존성을 넣으면 의존성 장애 때 모든 Pod를 재시작하는 연쇄 장애가 생길 수 있습니다. 아래 컨트롤러를 모듈의 controllers에 등록합니다.

src/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { DataSource } from 'typeorm';

@Controller('health')
export class HealthController {
  constructor(private readonly dataSource: DataSource) {}

  @Get('startup')
  startup() {
    return { status: 'ok' };
  }

  @Get('live')
  live() {
    return { status: 'ok' };
  }

  @Get('ready')
  async ready() {
    await this.dataSource.query('SELECT 1');
    return { status: 'ok' };
  }
}

Deployment, Service, HPA 연결

다음 예시는 하나의 NestJS 프로세스를 각 Pod에서 실행합니다. DATABASE_URL 값은 미리 만든 Secret을 참조하고, startup 성공 전에는 readiness와 liveness 판단을 시작하지 않습니다.

kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nestjs-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nestjs-app
  template:
    metadata:
      labels:
        app: nestjs-app
    spec:
      containers:
        - name: app
          image: ghcr.io/example/nestjs-app:2026.08.25-1
          ports:
            - name: http
              containerPort: 3000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: nestjs-app-secrets
                  key: database-url
            - name: DB_POOL_SIZE
              value: "8"
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: "1"
              memory: 512Mi
          startupProbe:
            httpGet:
              path: /health/startup
              port: http
            periodSeconds: 2
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health/live
              port: http
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
kubernetes/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nestjs-app
spec:
  type: LoadBalancer
  selector:
    app: nestjs-app
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: http

type: LoadBalancer가 외부 로드 밸런서를 만드는지는 클러스터의 클라우드 공급자 통합 또는 별도 컨트롤러 지원에 달려 있습니다. 지원이 없으면 외부 주소가 할당되지 않을 수 있으며, 환경에 맞는 Gateway/Ingress나 다른 노출 방식을 선택해야 합니다. Service는 readiness를 통과한 Pod만 엔드포인트로 사용하고, liveness 실패는 kubelet이 해당 컨테이너를 재시작하게 합니다.

kubernetes/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nestjs-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nestjs-app
  minReplicas: 3
  maxReplicas: 12
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65

CPU averageUtilization은 Pod의 실제 CPU를 resources.requests.cpu와 비교한 비율입니다. 요청값이 없으면 CPU utilization이 정의되지 않아 HPA가 해당 리소스 metric으로 확장하지 못하고, 비현실적인 요청값은 판단을 왜곡합니다. 리소스 metric에는 metrics.k8s.io 제공자(보통 Metrics Server)가 별도로 필요합니다. CPU가 병목을 대표하지 않는 서비스는 metrics API 어댑터를 통해 RPS, 지연, 큐 길이 같은 Pods/Object/External 사용자 정의 지표를 사용하고, 데이터베이스 연결 예산과 외부 할당량을 별도 상한으로 둡니다.

release tag 또는 digest로 식별한 NestJS 산출물을 Deployment가 단일 프로세스 Pod로 배포하고 Service가 HTTP 3000 포트의 준비된 Pod로 요청을 보내며, CPU 요청 기준 메트릭이 HPA를 거쳐 복제본 수로 되먹임되는 Kubernetes 배포 구조

NestJS · deployment · HPA feedback

Pod의 상태 신호와 스케일 신호를 분리한다

Pod당 한 프로세스를 기본으로 두고 startup, readiness, liveness를 서로 다른 질문에 연결합니다. HPA의 CPU 비율은 request를 기준으로 계산하며, 최대 복제본은 데이터베이스 연결 예산을 넘지 않아야 합니다.

Kubernetes NestJS 배포와 HPA 피드백 경로 release tag 또는 digest로 식별한 산출물을 Deployment가 Pod 세 개로 유지한다. Service는 HTTP 3000 포트에서 readiness를 통과한 Pod에만 요청을 보낸다. Pod는 Secret의 DATABASE_URL을 참조하고 DB TCP 연결 풀을 프로세스당 8개 이하로 제한한다. CPU 메트릭은 Metrics API와 HPA를 거쳐 Deployment의 desired replicas에 되먹임된다. KUBERNETES CLUSTER 80/TCP 3000·ready rollout DB/TCP usage metrics recommend scale feedback Client HTTP · HTTPS Service LoadBalancer* target HTTP:3000 Deployment desired replicas 2026.08.25-1 tag · registry policy NestJS Pod pool Pod A · process 1 startup → ready Pod B · process 1 live · ready Pod C · process 1 not ready → drain Secret ref · DATABASE_URL pool/process ≤ 8 DB URL port pool≤8 budget≤96 Metrics API CPU / request · custom HPA 3 ≤ replicas ≤ 12 * 외부 LB 프로비저닝은 지원되는 cloud provider 또는 controller가 있을 때만 성립 readiness는 트래픽 제외 · liveness는 컨테이너 재시작 · startup은 초기화 시간 보호
deploy release tag·digest → Pod당 1 프로세스
artifact

2026.08.25-1

release tag에는 레지스트리 정책을 적용하고, 정확한 재현성이 필요하면 검증한 digest로 배포합니다.

Pod pool

Pod당 NestJS 프로세스 하나

Service는 HTTP 3000 포트에서 readiness를 통과한 Pod만 엔드포인트로 사용합니다.

health

서로 다른 세 질문

startup은 초기화, readiness는 새 요청 수용, liveness는 프로세스 생존을 확인합니다.

LoadBalancer*

공급자·컨트롤러 조건

지원 환경에서만 외부 주소가 생기며, 그 밖에는 Gateway/Ingress 등을 선택합니다.

HPA

메트릭 → 권고 → desired replicas

CPU utilization은 request 대비 비율이며 metrics.k8s.io 제공자가 필요합니다. 필요하면 RPS와 queue 같은 사용자 지표를 씁니다.

DB budget

12 × 1 × 8 ≤ 96

관리·마이그레이션·장애 여유를 뺀 연결 예산 안에서 최대 복제본을 정합니다.

Secret ref

설정과 자격 증명 분리

Pod 명세에는 값 대신 Secret의 이름과 key를 참조합니다.

HPA 피드백이 복제본을 늘려도 readiness와 하위 자원 예산이 요청을 안전하게 받을 수 있는 범위를 결정합니다.

관리형 환경에서도 원리는 같습니다. AWS의 Application Load Balancer·Network Load Balancer, GCP Cloud Load Balancing, Azure Application Gateway·Load Balancer와 EKS/GKE/AKS 같은 오케스트레이션 서비스를 조합할 수 있지만, 구체적인 계층·상태 확인·스케일 지표는 선택한 제품 문서에 맞춰야 합니다.

마지막으로 한 복제본을 제거하는 연습, 세션과 작업 상태의 외부화, readiness 실패 시 트래픽 제외, replica × process × pool 연결 예산, 최대 복제본에서의 하위 서비스 한계를 함께 검증합니다.

이것으로 9장 성능 최적화와 스케일링의 세 번째 절을 마칩니다.