본문으로 건너뛰기

안동민 개발노트

본문 시작

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

응답 시간·처리량·오류율 메트릭을 수집하고 런타임 프로파일로 CPU와 메모리 병목의 증거를 찾아 검증합니다.

이 절에서는 NestJS 애플리케이션의 성능 모니터링(Performance Monitoring)프로파일링(Profiling)을 다룹니다.

애플리케이션이 배포된 후에도 성능은 계속해서 관리해야 하는 중요한 요소입니다.

사용량 패턴의 변화, 데이터 증가, 코드 변경 등으로 인해 언제든지 성능 문제가 발생할 수 있으며, 이를 조기에 발견하고 해결하기 위해서는 체계적인 모니터링과 심층적인 프로파일링이 필수적입니다.

먼저 아래 다이어그램은 성능 문제를 지표 수집, 병목 가설, 프로파일링 증거, 개선 검증의 운영 루프로 읽는 기준을 잡아줍니다.

관측, 범위 고정, 가설 분리, 표적 프로파일, 최소 변경, 같은 조건 검증을 반복하며 메트릭, trace, profile, 결정과 기준선을 중앙 운영 근거에 누적하는 NestJS 성능 개선 루프

NestJS · monitoring → profiling · true loop

모니터링 신호를 근거로 좁히고, 검증 결과를 다음 기준선으로 돌린다

모니터링은 어디서 언제 변했는지를 계속 보고, 프로파일링은 그 시간대와 경로의 함수·호출·보유 객체를 깊게 봅니다. 각 단계의 증거와 결정은 중앙 기록에 쌓이고, 마지막 검증의 기준선이 다음 관측을 시작합니다.

운영 근거를 공유하는 여섯 단계 성능 개선 루프 관측부터 범위 고정, 가설 분리, 표적 프로파일, 최소 변경, 같은 조건 검증까지 시계 방향으로 반복한다. 모든 단계가 중앙 운영 근거에 증거를 쓰고 마지막 결정과 기준선이 다음 관측으로 돌아간다. 1. 관측 p95 · RPS · error 2. 범위 고정 route · time · release 3. 가설 분리 DB · runtime · infra 4. 표적 프로파일 flame · heap · trace 5. 최소 변경 one cause · canary 6. 같은 조건 검증 전 · 후 · 결정 운영 근거 metrics · spans · profiles baseline · decision 6의 결정과 기준선이 1의 다음 관측을 시작한다
운영 근거 metrics · spans · profiles · baseline · decision
  1. 관측

    latency, throughput, error rate와 CPU, memory, DB 신호를 기준선과 비교합니다.

  2. 범위 고정

    알림 시각, route, release, correlation id를 정해 같은 요청을 추적할 수 있게 합니다.

  3. 가설 분리

    route에서 provider, DB, runtime, infra 순으로 긴 구간과 원인 후보를 좁힙니다.

  4. 표적 프로파일

    CPU flame, event-loop 진단, heap snapshot, trace 가운데 가설을 확인할 도구만 짧게 캡처합니다.

  5. 최소 변경

    관측한 한 원인을 겨냥한 변경을 canary에 배포하고 다른 조건은 고정합니다.

  6. 같은 조건 검증

    전후 p95, error, resource를 비교해 keep·rollback을 기록합니다. 새 기준선이 1번 관측으로 돌아갑니다.

p95 · trace

특정 route 또는 외부 대기가 길다

access log와 같은 correlation id로 ingress, Nest 경계, handler, DB·외부 API span을 나눕니다.

CPU · loop

계산 또는 event loop 지연을 의심한다

DevTools나 CPU flame으로 넓은 함수 frame을 보고, Clinic Doctor로 동기 I/O와 큰 JSON 처리 신호를 확인합니다.

memory

요청 뒤에도 메모리가 회수되지 않는다

시간차 heap snapshot의 retained object를 비교하고 GC pause와 무한 캐시 같은 보유 경로를 맞춥니다.

DB · release

pool 대기나 배포 직후 오류가 늘었다

pool wait와 query plan을 함께 보고, error rate 급등은 release 시각과 exception log를 대조합니다.

실선 고리는 실제 개선 순서, 안쪽 점선은 각 단계가 중앙 운영 근거에 쓰는 증거입니다. 마지막 결정이 다음 기준선을 갱신하므로 이 경로는 한 번 끝나는 점검표가 아니라 실제 운영 루프입니다.


성능 모니터링이란?

성능 모니터링과 프로파일링에서는 요청이 들어오는 위치, 책임을 맡는 계층, 실패 응답으로 나가는 지점을 분리합니다.

클라이언트에서 Edge, Nest 경계, Provider 런타임, 데이터베이스와 외부 API까지 같은 correlation id로 요청과 반환 시간을 이어 각 계층의 지표 책임과 느린 구간을 좁히는 NestJS 시퀀스

NestJS · correlated request · sequence

같은 요청을 경계별 시간 조각으로 이어 실제 병목을 찾는다

전체 latency 하나만으로는 Edge, Nest 파이프라인, Provider 런타임, dependency 대기를 구분할 수 없습니다. 같은 correlation id와 정규화된 route를 사용해 호출과 반환의 status·duration을 한 trace로 닫습니다.

NestJS 요청 경계와 의존성 시간을 잇는 시퀀스 Client 요청이 Edge와 Nest middleware 및 interceptor를 지나 provider에서 실행되고 DB 또는 외부 API를 호출한다. 반환은 dependency span, handler status, route duration, 전체 latency를 같은 correlation id로 연결한다. URL · payload · retry corr id · ingress guard · pipe · route handler · serialize · CPU pool · query · API result · dep span status · exception route · code · duration response · total ms Client Edge LB · Gateway Nest 경계 미들웨어 · 인터셉터 Provider service · runtime DB · API pool · query · call
  1. Client

    URL, payload, retry 여부와 사용자가 기다린 전체 시간을 기록합니다.

  2. Edge · ingress

    LB/Gateway가 도착 시각, 요청 크기, upstream timeout과 correlation id를 남깁니다.

  3. Nest 경계

    middleware, guard, pipe, interceptor가 정규화된 route와 status, duration을 연결합니다.

  4. Provider · runtime

    controller/service 처리, 직렬화, CPU와 event-loop 구간을 dependency 대기와 분리합니다.

  5. DB · 외부 API

    pool wait, query, 외부 왕복을 span으로 나누고 결과를 같은 trace로 반환합니다.

  6. Response

    status·exception과 반환 시각을 Edge에서 Client까지 이어 전체 latency를 닫습니다.

Edge · route

진입부터 Nest 경계가 길다

access log, 요청 크기, TLS·routing, rate limit, upstream timeout과 특정 route p95를 확인합니다.

handler · runtime

Provider 내부가 길다

직렬화와 동기 계산을 나누고 CPU flame, heap, GC, event-loop delay를 같은 시간대에서 봅니다.

dependency

DB나 외부 대기가 길다

pool wait, slow query와 실행 계획, cache hit, 외부 API timeout을 dependency span과 맞춥니다.

점선은 동기 호출의 반환입니다. route → provider → DB/runtime → infra 순으로 한 경계씩 좁히면 모든 지표가 함께 오른 결과와 실제 병목 원인을 분리할 수 있습니다.

성능 모니터링은 애플리케이션과 인프라의 핵심 지표(메트릭)를 지속적으로 수집, 시각화하고, 비정상적인 패턴이나 임계값 초과 시 알림을 발생시키는 활동입니다.

이는 시스템의 현재 상태를 파악하고, 잠재적인 문제를 사전에 감지하며, 장애 발생 시 원인을 빠르게 진단하는 데 도움을 줍니다.

모니터링의 주요 지표 (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

prom-client는 TypeScript 선언을 함께 제공하므로 별도의 @types/prom-client 패키지는 필요하지 않습니다.

단계 2: 메트릭 서비스 생성
src/metrics/metrics.service.ts
import { Injectable } from '@nestjs/common';
import * as client from 'prom-client';

@Injectable()
export class MetricsService {
  private readonly register = new client.Registry();
  private readonly httpRequestDurationSeconds: client.Histogram<string>;
  private readonly totalRequests: client.Counter<string>;

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

    // HTTP 요청 응답 시간 히스토그램
    this.httpRequestDurationSeconds = new client.Histogram({
      name: 'http_request_duration_seconds',
      help: 'Duration of HTTP requests in seconds',
      labelNames: ['method', 'route', 'code'] as const,
      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'] as const,
      registers: [this.register],
    });
  }

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

  getContentType(): string {
    return this.register.contentType;
  }

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

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

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

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

  @Get()
  async getMetrics(@Res() response: Response): Promise<void> {
    // Prometheus 메트릭 포맷으로 응답
    response.set('Content-Type', this.metricsService.getContentType());
    response.end(await this.metricsService.getMetrics());
  }
}

이 컨트롤러는 Express 어댑터의 Response를 사용하는 예제입니다. 운영 환경에서는 /metrics를 내부 네트워크나 인증된 수집 경로로 제한합니다.

단계 4: 요청을 기록하는 인터셉터 생성
src/metrics/http-metrics.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { MetricsService } from './metrics.service';
import type { Request, Response } from 'express';

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

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const startedAt = Date.now();
    const httpContext = context.switchToHttp();
    const request = httpContext.getRequest<Request>();
    const response = httpContext.getResponse<Response>();
    // raw URL 대신 bounded route template을 label로 사용
    const route = request.route?.path ?? 'unmatched';

    // 성공과 예외 응답 모두 최종 status로 한 번만 기록
    response.once('finish', () => {
      this.metricsService.recordHttpRequest(
        request.method,
        route,
        response.statusCode,
        Date.now() - startedAt,
      );
    });

    return next.handle();
  }
}

사용자 ID나 query string을 포함한 raw URL을 label로 쓰면 시계열 cardinality가 빠르게 늘어납니다. 컨트롤러 prefix까지 구분해야 한다면 계측 계층에서 정규화한 route template을 조합합니다.

단계 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: ['host.docker.internal:3000'] # Docker Desktop의 호스트 Nest 앱

    Prometheus와 NestJS를 같은 Docker 네트워크에서 실행한다면 host.docker.internal 대신 NestJS 서비스 이름과 포트를 사용합니다. 컨테이너 안의 localhost는 Prometheus 컨테이너 자신을 가리킵니다.

    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 등은 프로파일링 기능을 함께 제공하여 코드 레벨에서의 상세한 분석을 지원합니다.

프로파일링 도구는 모두 같은 목적을 갖지만, CPU 병목, 이벤트 루프 지연, 메모리 누수, 분산 트레이스 중 무엇을 좁힐지에 따라 선택 기준이 달라집니다.

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장 성능 최적화와 스케일링을 모두 마칩니다.

프로파일링, 캐시, 데이터베이스 최적화, 수평 확장, 모니터링 지표를 기준으로 병목을 찾고 변경 효과를 확인하는 흐름을 정리했습니다.