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

성능 모니터링과 프로파일링


이제 9장의 마지막 절로, 시스템의 성능을 지속적으로 측정하고 분석하여 병목 현상을 식별하고 최적화하는 데 필수적인 성능 모니터링(Performance Monitoring)프로파일링(Profiling) 에 대해 NestJS 애플리케이션 환경을 중심으로 살펴보겠습니다.

애플리케이션이 배포된 후에도 성능은 계속해서 관리해야 하는 중요한 요소입니다. 사용량 패턴의 변화, 데이터 증가, 코드 변경 등으로 인해 언제든지 성능 문제가 발생할 수 있으며, 이를 조기에 발견하고 해결하기 위해서는 체계적인 모니터링과 심층적인 프로파일링이 필수적입니다.


성능 모니터링이란?

성능 모니터링은 애플리케이션과 인프라의 핵심 지표(메트릭)를 지속적으로 수집, 시각화하고, 비정상적인 패턴이나 임계값 초과 시 알림을 발생시키는 활동입니다. 이는 시스템의 현재 상태를 파악하고, 잠재적인 문제를 사전에 감지하며, 장애 발생 시 원인을 빠르게 진단하는 데 도움을 줍니다.

모니터링의 주요 지표 (Metrics)

  • 시스템 리소스 (System Resources)
    • CPU 사용률: 프로세서가 얼마나 바쁜지 나타냅니다.
    • 메모리 사용량: 애플리케이션이 사용하는 RAM의 양입니다. 메모리 누수는 치명적일 수 있습니다.
    • 디스크 I/O: 디스크 읽기/쓰기 작업량입니다. 데이터베이스나 파일 시스템에 많이 의존하는 경우 중요합니다.
    • 네트워크 I/O: 네트워크를 통한 데이터 송수신량입니다.
  • 애플리케이션 성능 (Application Performance)
    • 응답 시간(Latency): 클라이언트 요청이 서버에 도달하여 응답을 받는 데 걸리는 시간입니다.
    • 처리량(Throughput): 단위 시간당 처리되는 요청의 수 (QPS: Queries Per Second, RPS: Requests Per Second).
    • 에러율(Error Rate): 전체 요청 중 실패한 요청의 비율입니다.
    • 동시 사용자 수: 동시에 시스템을 사용하고 있는 사용자 또는 연결 수.
    • 큐 길이: 메시지 큐나 작업 큐에 대기 중인 항목의 수.
  • 데이터베이스 성능 (Database Performance)
    • 쿼리 실행 시간: 각 쿼리가 실행되는 데 걸리는 시간입니다. 느린 쿼리 식별에 중요합니다.
    • 연결 수: 데이터베이스에 연결된 클라이언트 수.
    • 잠금(Locks): 데이터베이스 잠금 발생 여부 및 시간.
    • 캐시 히트율: 데이터베이스 캐시의 효율성 지표.

모니터링 도구

  • 클라우드 제공업체 모니터링 서비스: AWS CloudWatch, Google Cloud Monitoring, Azure Monitor 등.
  • APM (Application Performance Monitoring) 도구: New Relic, Datadog, Dynatrace, Elastic APM 등. 코드 레벨에서의 성능 분석, 분산 트레이싱 등을 제공합니다.
  • 오픈 소스 모니터링 스택: Prometheus (메트릭 수집), Grafana (시각화), Alertmanager (알림).

NestJS 성능 모니터링 구현

NestJS 애플리케이션의 성능을 모니터링하기 위해 메트릭을 노출하고 이를 Prometheus와 Grafana로 시각화하는 방법을 예시로 들어보겠습니다.

메트릭 노출

Node.js 애플리케이션의 메트릭을 Prometheus가 수집할 수 있는 형태로 노출하려면 prom-client 라이브러리를 사용할 수 있습니다.

단계 1: 필요한 패키지 설치

npm install prom-client
npm install --save-dev @types/prom-client

단계 2: 메트릭 서비스 생성

src/metrics/metrics.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import * as client from 'prom-client';

@Injectable()
export class MetricsService implements OnModuleInit {
  private register: client.Registry;
  private httpRequestDurationMicroseconds: client.Histogram<string>;
  private totalRequests: client.Counter<string>;

  constructor() {
    this.register = new client.Registry();
    // 기본 메트릭 수집 활성화 (Node.js 프로세스 관련)
    client.collectDefaultMetrics({ register: this.register });
  }

  onModuleInit() {
    // HTTP 요청 응답 시간 히스토그램
    this.httpRequestDurationMicroseconds = new client.Histogram({
      name: 'http_request_duration_seconds',
      help: 'Duration of HTTP requests in seconds',
      labelNames: ['method', 'route', 'code'],
      buckets: [0.1, 0.5, 1, 2, 5], // 100ms, 500ms, 1s, 2s, 5s 버킷
      registers: [this.register],
    });

    // 총 요청 수 카운터
    this.totalRequests = new client.Counter({
      name: 'http_requests_total',
      help: 'Total number of HTTP requests',
      labelNames: ['method', 'route', 'code'],
      registers: [this.register],
    });

    console.log('Prometheus metrics initialized.');
  }

  // HTTP 요청을 기록하는 메서드
  recordHttpRequest(method: string, route: string, code: number, duration: number) {
    this.httpRequestDurationMicroseconds
      .labels(method, route, code.toString())
      .observe(duration / 1000); // 밀리초를 초로 변환
    this.totalRequests
      .labels(method, route, code.toString())
      .inc();
  }

  // Prometheus가 스크랩할 수 있도록 메트릭을 문자열로 반환
  async getMetrics(): Promise<string> {
    return this.register.metrics();
  }
}

단계 3: 메트릭 컨트롤러 생성

Prometheus 서버가 메트릭을 가져갈 수 있는 엔드포인트를 제공합니다.

src/metrics/metrics.controller.ts
import { Controller, Get, Res } from '@nestjs/common';
import { MetricsService } from './metrics.service';
import { Response } from 'express'; // Express Response 타입

@Controller('metrics')
export class MetricsController {
  constructor(private readonly metricsService: MetricsService) {}

  @Get()
  async getMetrics(@Res() res: Response) {
    // Prometheus 메트릭 포맷으로 응답
    res.set('Content-Type', this.metricsService.register.contentType);
    res.end(await this.metricsService.getMetrics());
  }
}

단계 4: 요청을 기록하는 인터셉터 생성

src/metrics/http-metrics.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { MetricsService } from './metrics.service';
import { Request, Response } from 'express';

@Injectable()
export class HttpMetricsInterceptor implements NestInterceptor {
  constructor(private readonly metricsService: MetricsService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now(); // 요청 시작 시간 기록
    const httpContext = context.switchToHttp();
    const request = httpContext.getRequest<Request>();
    const response = httpContext.getResponse<Response>();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - now; // 응답 시간 계산
        this.metricsService.recordHttpRequest(
          request.method,
          request.route ? request.route.path : request.url, // 라우트 경로 또는 URL
          response.statusCode,
          duration,
        );
      }),
    );
  }
}

단계 5: AppModule에 모듈 및 인터셉터 등록

src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core'; // APP_INTERCEPTOR 임포트
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MetricsModule } from './metrics/metrics.module';
import { HttpMetricsInterceptor } from './metrics/http-metrics.interceptor';

@Module({
  imports: [MetricsModule], // MetricsModule 임포트
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_INTERCEPTOR, // 전역 인터셉터로 HttpMetricsInterceptor 등록
      useClass: HttpMetricsInterceptor,
    },
  ],
})
export class AppModule {}

단계 6: Prometheus 및 Grafana 설정 및 실행

  • Prometheus: prometheus.yml 설정 파일에 NestJS 애플리케이션의 /metrics 엔드포인트를 스크랩하도록 타겟을 추가합니다.

    prometheus.yml
    scrape_configs:
      - job_name: 'nestjs_app'
        static_configs:
          - targets: ['localhost:3000'] # NestJS 애플리케이션 주소

    Docker로 Prometheus 실행: docker run -p 9090:9090 -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

  • Grafana: Prometheus를 데이터 소스로 추가하고, NestJS 애플리케이션의 메트릭을 시각화하는 대시보드를 생성합니다. http_request_duration_seconds_bucket, http_requests_total 등의 메트릭을 활용하여 응답 시간, 요청 수 등을 시각화할 수 있습니다. Docker로 Grafana 실행: docker run -p 3001:3000 grafana/grafana-oss (기본 로그인: admin/admin)


프로파일링(Profiling)이란?

프로파일링은 애플리케이션의 코드 실행을 분석하여 특정 함수나 코드 블록이 CPU 시간, 메모리, I/O 등 리소스를 얼마나 사용하는지 상세하게 측정하는 과정입니다. 모니터링이 시스템의 "전반적인 건강 상태"를 확인한다면, 프로파일링은 "문제가 발생한 코드 라인"을 정확히 찾아내는 데 사용됩니다.

프로파일링의 주요 목적

  • 병목 현상 식별: 느린 함수, 과도한 CPU 사용, 메모리 누수 등을 정확히 파악합니다.
  • 최적화 대상 선정: 성능 개선 작업의 우선순위를 정하는 데 도움을 줍니다.
  • 리소스 사용 패턴 이해: 애플리케이션이 런타임에 리소스를 어떻게 사용하는지 이해합니다.

프로파일링 도구

  • Node.js 내장 프로파일러: --inspect 플래그를 사용하여 Chrome DevTools 또는 VS Code에서 프로파일링할 수 있습니다.
  • clinic.js: Node.js 애플리케이션의 성능 병목을 시각적으로 분석해주는 강력한 도구 (Flamegraphs, Doctor, Bubbleprof 등).
  • APM 도구: Datadog, New Relic 등은 프로파일링 기능을 함께 제공하여 코드 레벨에서의 상세한 분석을 지원합니다.

Node.js 내장 프로파일러

가장 간단하게 Node.js 애플리케이션을 프로파일링하는 방법입니다.

단계 1: NestJS 애플리케이션을 디버그 모드로 실행

node --inspect dist/main # 또는 nest start --debug

콘솔에 Debugger listening on ws://127.0.0.1:9229/... 와 같은 메시지가 출력됩니다.

단계 2: Chrome 브라우저에서 chrome://inspect 접속

"Remote Target" 섹션에 실행 중인 Node.js 인스턴스가 나타납니다. "inspect" 링크를 클릭합니다.

단계 3: Chrome DevTools에서 "Performance" 탭 사용

  • DevTools가 열리면 "Performance" 탭으로 이동합니다.
  • 좌측 상단의 원형 "Record" 버튼을 클릭합니다.
  • 이제 NestJS 애플리케이션에 트래픽을 발생시킵니다 (예: Postman으로 여러 번 요청 보내기, Jmeter로 부하 테스트).
  • 충분한 트래픽이 발생한 후 "Stop" 버튼을 클릭합니다.

단계 4: 프로파일링 결과 분석

  • 기록된 데이터가 로드되면 Flame Chart를 통해 함수 호출 스택과 각 함수의 실행 시간을 시각적으로 확인할 수 있습니다. 넓은 블록은 해당 함수가 많은 시간을 소비했음을 의미합니다.
  • Bottom-Up 탭에서는 각 함수가 자체적으로(Self Time) 또는 자식 함수를 포함하여(Total Time) 얼마나 많은 시간을 소비했는지 표 형태로 볼 수 있습니다. 이를 통해 CPU 시간을 많이 소모하는 병목 함수를 식별할 수 있습니다.
  • Call Tree 탭에서는 함수 호출 흐름을 트리 형태로 탐색할 수 있습니다.

clinic.js 활용

clinic.js는 Node.js 애플리케이션의 성능 문제를 진단하고 시각화하는 데 특화된 도구입니다.

단계 1: clinic 설치

npm install -g clinic

단계 2: clinic doctor로 일반적인 문제 진단

clinic doctor는 CPU, 이벤트 루프 지연, 메모리, GC(Garbage Collection) 등 다양한 지표를 수집하여 일반적인 Node.js 성능 병목을 진단하고 보고서를 생성합니다.

clinic doctor -- node dist/main.js # NestJS 앱 실행 명령

clinic doctor가 실행되는 동안 애플리케이션에 부하를 발생시킵니다. 분석이 완료되면 HTML 보고서가 자동으로 열립니다. 이 보고서는 병목 현상을 시각적으로 보여주고 개선을 위한 제안을 제공합니다.

단계 3: clinic flame으로 CPU 사용량 분석 (Flamegraph)

clinic flame은 CPU 사용량을 상세하게 분석하여 Flamegraph를 생성합니다. 이는 특정 함수가 CPU를 얼마나 많이 소모하는지 시각적으로 파악하는 데 매우 효과적입니다.

clinic flame -- node dist/main.js

clinic doctor와 마찬가지로 실행 중 애플리케이션에 부하를 준 후, 완료되면 Flamegraph가 포함된 HTML 보고서가 열립니다.


성능 모니터링과 프로파일링은 애플리케이션의 건강 상태를 지속적으로 확인하고, 문제 발생 시 신속하게 원인을 찾아 해결하며, 잠재적인 성능 병목을 선제적으로 제거하는 데 필수적인 활동입니다. NestJS와 Node.js 생태계는 이를 위한 다양한 도구와 기능을 제공하므로, 개발 및 운영 단계에서 이들을 적극적으로 활용하여 고성능의 안정적인 애플리케이션을 유지하시길 바랍니다.

이것으로 9장 "성능 최적화와 스케일링"을 모두 마칩니다. 이제 여러분은 NestJS 애플리케이션의 성능을 개선하고, 증가하는 트래픽에 대응하며, 지속적으로 시스템 상태를 모니터링하고 분석하는 방법을 이해하게 되었습니다.