icon
9장 : 성능 최적화와 스케일링

로드 밸런싱과 수평적 확장


이제 9장의 세 번째 절로, 급증하는 트래픽에 대응하고 시스템의 안정성을 높이기 위한 핵심 전략인 로드 밸런싱(Load Balancing)수평적 확장(Horizontal Scaling) 에 대해 NestJS 애플리케이션 환경을 중심으로 살펴보겠습니다.

단일 서버로 운영되는 애플리케이션은 처리할 수 있는 동시 요청 수에 한계가 있으며, 서버에 장애가 발생하면 서비스 전체가 중단될 위험이 있습니다. 로드 밸런싱과 수평적 확장은 이러한 한계를 극복하고 고가용성 및 확장 가능한 시스템을 구축하는 데 필수적인 기술입니다.


스케일링(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 애플리케이션의 수평적 확장 및 로드 밸런싱 구현

NestJS 애플리케이션은 Node.js 기반이므로, 단일 스레드 이벤트 루프 모델을 따릅니다. 따라서 CPU 코어를 최대한 활용하기 위해서는 여러 NestJS 인스턴스를 실행하고 이를 로드 밸런싱하는 것이 일반적입니다.

Node.js cluster 모듈 활용

Node.js의 cluster 모듈은 단일 서버 내에서 여러 개의 Node.js 프로세스를 생성하여 CPU 코어를 최대한 활용할 수 있도록 합니다. 이는 실제 로드 밸런서보다 앞단에서 프로세스 간 부하를 분산합니다.

src/main.ts (cluster 모듈 적용 예시)
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 작성

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"] # 애플리케이션 시작 명령어

단계 2: Docker 이미지 빌드 및 푸시 (Docker Hub 또는 Private Registry)

docker build -t your-username/nestjs-app:latest .
docker push your-username/nestjs-app:latest

단계 3: Kubernetes Deployment 및 Service 정의 (간략화된 예시)

Kubernetes는 Pods(애플리케이션 컨테이너의 최소 단위), Deployments(Pod 관리), Services(로드 밸런싱)를 통해 애플리케이션을 배포하고 관리합니다.

kubernetes/deployment.yaml (예시)
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 서비스명
kubernetes/service.yaml (예시)
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로 분산시킵니다.

클라우드 서비스 활용 (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 클러스터를 관리형 서비스로 제공합니다.

로드 밸런싱과 수평적 확장은 고성능, 고가용성 애플리케이션을 구축하는 데 필수적인 전략입니다. NestJS 애플리케이션은 Node.js의 특성상 CPU 코어를 최대한 활용하고 안정성을 확보하기 위해 수평적 확장이 매우 중요합니다. Docker와 Kubernetes 같은 컨테이너 기술, 또는 클라우드 제공업체의 관리형 서비스를 활용하여 NestJS 애플리케이션을 효율적으로 확장하고 운영할 수 있습니다. 이는 증가하는 사용자 트래픽에 유연하게 대응하고, 안정적인 서비스를 제공하는 데 큰 도움이 될 것입니다.

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