본문 바로가기

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

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

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

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

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

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

측정 근거를 누적하는 성능 개선 루프

관측·범위·가설·표적 profile·최소 변경·동일 조건 검증에서 중앙 운영 근거가 누적된다.

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

Nest · Performance evidence loop

관측·프로파일·재측정의 루프

관측으로 범위를 좁히고 표적 프로파일로 가설을 확인한 뒤 같은 조건에서 다시 측정합니다. 이 그림은 운영 설계이며, 본문 인터셉터는 Nest handler를 거치는 요청의 기본 지표 예제입니다.

운영 근거를 공유하는 여섯 단계 성능 개선 루프 관측부터 범위 고정, 가설 분리, 표적 프로파일, 최소 변경, 같은 조건 검증까지 시계 방향으로 반복한다. 모든 단계가 중앙 운영 근거에 증거를 쓰고 마지막 결정과 기준선이 다음 관측으로 돌아간다. 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의 다음 관측을 시작한다
운영 근거를 남기고 다시 관측하는 성능 루프 관측→범위고정→가설분리→표적프로파일→최소변경→같은조건검증을반복한다. 각단계는오른쪽점선으로공유운영근거를남기고검증결정은왼쪽되먹임으로새관측기준이된다. 이는추가계측을포함한운영설계이며본문인터셉터는진입이후finish된응답만기록한다. CPU/heap/trace는가설에맞게선택하고Clinic의현재유지보수및런타임호환조건을확인한다. 1. 관측 p95 · RPS · 오류 2. 범위 고정 route · time · release 3. 가설 분리 DB · runtime · infra 4. 표적 프로파일 CPU · heap · trace 5. 최소 변경 한 원인 · canary 6. 같은 조건 검증 전후 · keep/rollback 공유 운영 근거 metrics · profiles baseline · decision
운영 근거 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를 대조합니다.

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

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 관측부터 범위 고정, 가설 분리, 표적 프로파일, 최소 변경, 같은 조건 검증까지 시계 방향으로 반복한다. 모든 단계가 중앙 운영 근거에 증거를 쓰고 마지막 결정과 기준선이 다음 관측으로 돌아간다.
  • 관측→범위 고정→가설 분리→표적 프로파일→최소 변경→같은 조건 검증을 반복한다. 각 단계는 오른쪽 점선으로 공유 운영 근거를 남기고 검증 결정은 왼쪽 되먹임으로 새 관측 기준이 된다. 이는 추가 계측을 포함한 운영 설계이며 본문 인터셉터는 진입 이후 finish된 응답만 기록한다. CPU/heap/trace는 가설에 맞게 선택하고 Clinic의 현재 유지보수 및 런타임 호환 조건을 확인한다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1측정 근거를 누적하는 성능 개선 루프
26관측→범위 고정→가설 분리→표적 프로파일→최소 변경→같은 조건 검증을 반복한다. 각 단계는 오른쪽 점선으로 공유 운영 근거를 남기고 검증 결정은 왼쪽 되먹임으로 새 관측 기준이 된다. 이는 추가 계측을 포함한 운영 설계이며 본문 인터셉터는 진입 이후 finish된 응답만 기록한다. CPU/heap/trace는 가설에 맞게 선택하고 Clinic의 현재 유지보수 및 런타임 호환 조건을 확인한다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
관측·범위·가설

관측에서 범위를 고정하고 가설을 분리합니다.

근거: 관측 · 범위: 고정 · 가설: 분리여러 원인을 한 변경에 섞지 않습니다.
표적 profile·변경

표적 profile로 확인하고 최소한으로 변경합니다.

근거: profile · 변경: 최소관측한 원인에 맞게 수정합니다.
동일 조건 검증

같은 조건에서 검증하고 결정·기준선을 기록합니다.

조건: 동일 · 기록: 운영 근거개선 근거가 다음 관측으로 이어집니다.

측정 근거를 누적하는 성능 개선 루프

관측·범위·가설·표적 profile·최소 변경·동일 조건 검증에서 중앙 운영 근거가 누적된다.

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

Nest · Performance evidence loop

관측·프로파일·재측정의 루프

관측으로 범위를 좁히고 표적 프로파일로 가설을 확인한 뒤 같은 조건에서 다시 측정합니다. 이 그림은 운영 설계이며, 본문 인터셉터는 Nest handler를 거치는 요청의 기본 지표 예제입니다.

운영 근거를 공유하는 여섯 단계 성능 개선 루프 관측부터 범위 고정, 가설 분리, 표적 프로파일, 최소 변경, 같은 조건 검증까지 시계 방향으로 반복한다. 모든 단계가 중앙 운영 근거에 증거를 쓰고 마지막 결정과 기준선이 다음 관측으로 돌아간다. 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의 다음 관측을 시작한다
운영 근거를 남기고 다시 관측하는 성능 루프 관측→범위고정→가설분리→표적프로파일→최소변경→같은조건검증을반복한다. 각단계는오른쪽점선으로공유운영근거를남기고검증결정은왼쪽되먹임으로새관측기준이된다. 이는추가계측을포함한운영설계이며본문인터셉터는진입이후finish된응답만기록한다. CPU/heap/trace는가설에맞게선택하고Clinic의현재유지보수및런타임호환조건을확인한다. 1. 관측 p95 · RPS · 오류 2. 범위 고정 route · time · release 3. 가설 분리 DB · runtime · infra 4. 표적 프로파일 CPU · heap · trace 5. 최소 변경 한 원인 · canary 6. 같은 조건 검증 전후 · keep/rollback 공유 운영 근거 metrics · profiles baseline · decision
운영 근거 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를 대조합니다.

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

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 관측부터 범위 고정, 가설 분리, 표적 프로파일, 최소 변경, 같은 조건 검증까지 시계 방향으로 반복한다. 모든 단계가 중앙 운영 근거에 증거를 쓰고 마지막 결정과 기준선이 다음 관측으로 돌아간다.
  • 관측→범위 고정→가설 분리→표적 프로파일→최소 변경→같은 조건 검증을 반복한다. 각 단계는 오른쪽 점선으로 공유 운영 근거를 남기고 검증 결정은 왼쪽 되먹임으로 새 관측 기준이 된다. 이는 추가 계측을 포함한 운영 설계이며 본문 인터셉터는 진입 이후 finish된 응답만 기록한다. CPU/heap/trace는 가설에 맞게 선택하고 Clinic의 현재 유지보수 및 런타임 호환 조건을 확인한다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1측정 근거를 누적하는 성능 개선 루프
26관측→범위 고정→가설 분리→표적 프로파일→최소 변경→같은 조건 검증을 반복한다. 각 단계는 오른쪽 점선으로 공유 운영 근거를 남기고 검증 결정은 왼쪽 되먹임으로 새 관측 기준이 된다. 이는 추가 계측을 포함한 운영 설계이며 본문 인터셉터는 진입 이후 finish된 응답만 기록한다. CPU/heap/trace는 가설에 맞게 선택하고 Clinic의 현재 유지보수 및 런타임 호환 조건을 확인한다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
관측·범위·가설

관측에서 범위를 고정하고 가설을 분리합니다.

근거: 관측 · 범위: 고정 · 가설: 분리여러 원인을 한 변경에 섞지 않습니다.
표적 profile·변경

표적 profile로 확인하고 최소한으로 변경합니다.

근거: profile · 변경: 최소관측한 원인에 맞게 수정합니다.
동일 조건 검증

같은 조건에서 검증하고 결정·기준선을 기록합니다.

조건: 동일 · 기록: 운영 근거개선 근거가 다음 관측으로 이어집니다.

성능 모니터링이란?

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

상관관계 ID로 잇는 느린 요청 시퀀스

현재 HttpMetricsInterceptor의 Guard 허용→listener 등록→finish 기록 한 경로만 추적합니다. Guard 거절·미선택 404는 제외하고 correlation·Edge·dependency 전체 tracing은 추가 설계이며 Node finish는 클라이언트 수신 완료가 아닙니다.

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

Nest · Correlated request sequence

요청을 잇는 경계별 시간 기록

Edge·Nest·Provider·DB의 시간을 연결하면 느린 구간을 좁힐 수 있습니다. correlation id 전파와 계층별 span은 추가 계측이 필요하며 본문의 지표 인터셉터만으로 전체 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
현재 계측 시작점과 앞선 거절 경로 Client와Edge추적은전체요청관측의추가설계이다. NestGuard가허용해야본문인터셉터가시작하고Pipe/controller/provider/DB/API를거친응답finish를기록한다. Guard거절과미선택route404는본문인터셉터를거치지않아전체카운터에포함되지않는다. 점선왼쪽우회는Guard거절응답이고오른쪽반환은추가상관관계추적이있을때Edge와Client로전체구간을잇는다. Nodefinish는클라이언트수신확인이아니고미완료연결종료는별도관측조건이다. 본문 지표 코드 범위 Client URL · 전체 대기시간 Edge · ingress correlation ID Nest Guard 거절은 여기서 응답 인터셉터 진입 여기부터 서버 duration Pipe · handler controller · service DB · 외부 API pool · query · call handler 결과 값 또는 처리 중 예외 응답 finish 최종 status · 한 번 지표 기록 code · duration · count Edge → Client 별도 전체 구간 추적
  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 순으로 한 경계씩 좁히면 모든 지표가 함께 오른 결과와 실제 병목 원인을 분리할 수 있습니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • Client 요청이 Edge와 Nest middleware 및 interceptor를 지나 provider에서 실행되고 DB 또는 외부 API를 호출한다. 반환은 dependency span, handler status, route duration, 전체 latency를 같은 correlation id로 연결한다.
  • Client와 Edge 추적은 전체 요청 관측의 추가 설계다. Nest Guard가 허용해야 본문 인터셉터가 시작하고 Pipe/controller/provider/DB/API를 거친 응답 finish를 기록한다. Guard 거절과 미선택 route 404는 본문 인터셉터를 거치지 않아 전체 카운터에 포함되지 않는다. 점선의 왼쪽 우회는 Guard 거절 응답이고 오른쪽 반환은 추가 상관관계 추적이 있을 때 Edge와 Client로 전체 구간을 잇는다. Node finish는 클라이언트 수신 확인이 아니고 미완료 연결 종료는 별도 관측 조건이다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1상관관계 ID로 잇는 느린 요청 시퀀스
28Client와 Edge 추적은 전체 요청 관측의 추가 설계다. Nest Guard가 허용해야 본문 인터셉터가 시작하고 Pipe/controller/provider/DB/API를 거친 응답 finish를 기록한다. Guard 거절과 미선택 route 404는 본문 인터셉터를 거치지 않아 전체 카운터에 포함되지 않는다. 점선의 왼쪽 우회는 Guard 거절 응답이고 오른쪽 반환은 추가 상관관계 추적이 있을 때 Edge와 Client로 전체 구간을 잇는다. Node finish는 클라이언트 수신 확인이 아니고 미완료 연결 종료는 별도 관측 조건이다.
57status·exception과 반환 시각을 Edge에서 Client까지 이어 전체 latency 측정을 마칩니다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
허용된 요청의 계측 진입

Guard가 허용해 인터셉터에 진입한 요청에서 startedAt과 route label을 잡고 finish listener를 등록합니다.

진입 조건: Guard 허용·인터셉터 진입 · 제외: Guard 거절·handler 미선택 404 · 등록: response.once(finish)·시작 시간계측 시작점은 앞선 middleware·Guard 뒤입니다. correlation·Edge·dependency 전체 span은 이 코드가 자동 생성하는 값이 아닙니다.
handler 처리와 finish 대기

next.handle()로 Pipe·handler 이후 처리를 진행하며 응답의 finish를 기다립니다.

처리: next.handle()·정상 또는 진입 뒤 예외 응답 · 계측 기록: finish 전에는 해당 listener 기록 없음 · 연결 중단: finish가 없을 수 있어 별도 종료 설계 필요정상·예외 여부만으로 즉시 카운터를 올리지 않고 finish 때의 최종 status를 읽습니다. 전체 요청과 클라이언트 지연은 추가 계측 설계입니다.
finish에서 한 번 기록

finish가 발생하면 최종 status와 인터셉터 진입 이후 duration을 기록하고 요청 카운터를 한 번 증가시킵니다.

counter 변화: 해당 finish listener에서 +1 · histogram: (Date.now−startedAt) / 1000 초 · label: method·bounded route·최종 code · finish 의미: 서버가 OS에 넘김, 클라이언트 수신 확인 아님once listener가 이 응답의 기록을 한 번 만들며 그 범위는 인터셉터 진입부터 서버 finish까지입니다. Edge·Guard·전체 dependency span 구현을 뜻하지 않습니다.

상관관계 ID로 잇는 느린 요청 시퀀스

현재 HttpMetricsInterceptor의 Guard 허용→listener 등록→finish 기록 한 경로만 추적합니다. Guard 거절·미선택 404는 제외하고 correlation·Edge·dependency 전체 tracing은 추가 설계이며 Node finish는 클라이언트 수신 완료가 아닙니다.

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

Nest · Correlated request sequence

요청을 잇는 경계별 시간 기록

Edge·Nest·Provider·DB의 시간을 연결하면 느린 구간을 좁힐 수 있습니다. correlation id 전파와 계층별 span은 추가 계측이 필요하며 본문의 지표 인터셉터만으로 전체 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
현재 계측 시작점과 앞선 거절 경로 Client와Edge추적은전체요청관측의추가설계이다. NestGuard가허용해야본문인터셉터가시작하고Pipe/controller/provider/DB/API를거친응답finish를기록한다. Guard거절과미선택route404는본문인터셉터를거치지않아전체카운터에포함되지않는다. 점선왼쪽우회는Guard거절응답이고오른쪽반환은추가상관관계추적이있을때Edge와Client로전체구간을잇는다. Nodefinish는클라이언트수신확인이아니고미완료연결종료는별도관측조건이다. 본문 지표 코드 범위 Client URL · 전체 대기시간 Edge · ingress correlation ID Nest Guard 거절은 여기서 응답 인터셉터 진입 여기부터 서버 duration Pipe · handler controller · service DB · 외부 API pool · query · call handler 결과 값 또는 처리 중 예외 응답 finish 최종 status · 한 번 지표 기록 code · duration · count Edge → Client 별도 전체 구간 추적
  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 순으로 한 경계씩 좁히면 모든 지표가 함께 오른 결과와 실제 병목 원인을 분리할 수 있습니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • Client 요청이 Edge와 Nest middleware 및 interceptor를 지나 provider에서 실행되고 DB 또는 외부 API를 호출한다. 반환은 dependency span, handler status, route duration, 전체 latency를 같은 correlation id로 연결한다.
  • Client와 Edge 추적은 전체 요청 관측의 추가 설계다. Nest Guard가 허용해야 본문 인터셉터가 시작하고 Pipe/controller/provider/DB/API를 거친 응답 finish를 기록한다. Guard 거절과 미선택 route 404는 본문 인터셉터를 거치지 않아 전체 카운터에 포함되지 않는다. 점선의 왼쪽 우회는 Guard 거절 응답이고 오른쪽 반환은 추가 상관관계 추적이 있을 때 Edge와 Client로 전체 구간을 잇는다. Node finish는 클라이언트 수신 확인이 아니고 미완료 연결 종료는 별도 관측 조건이다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1상관관계 ID로 잇는 느린 요청 시퀀스
28Client와 Edge 추적은 전체 요청 관측의 추가 설계다. Nest Guard가 허용해야 본문 인터셉터가 시작하고 Pipe/controller/provider/DB/API를 거친 응답 finish를 기록한다. Guard 거절과 미선택 route 404는 본문 인터셉터를 거치지 않아 전체 카운터에 포함되지 않는다. 점선의 왼쪽 우회는 Guard 거절 응답이고 오른쪽 반환은 추가 상관관계 추적이 있을 때 Edge와 Client로 전체 구간을 잇는다. Node finish는 클라이언트 수신 확인이 아니고 미완료 연결 종료는 별도 관측 조건이다.
57status·exception과 반환 시각을 Edge에서 Client까지 이어 전체 latency 측정을 마칩니다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
허용된 요청의 계측 진입

Guard가 허용해 인터셉터에 진입한 요청에서 startedAt과 route label을 잡고 finish listener를 등록합니다.

진입 조건: Guard 허용·인터셉터 진입 · 제외: Guard 거절·handler 미선택 404 · 등록: response.once(finish)·시작 시간계측 시작점은 앞선 middleware·Guard 뒤입니다. correlation·Edge·dependency 전체 span은 이 코드가 자동 생성하는 값이 아닙니다.
handler 처리와 finish 대기

next.handle()로 Pipe·handler 이후 처리를 진행하며 응답의 finish를 기다립니다.

처리: next.handle()·정상 또는 진입 뒤 예외 응답 · 계측 기록: finish 전에는 해당 listener 기록 없음 · 연결 중단: finish가 없을 수 있어 별도 종료 설계 필요정상·예외 여부만으로 즉시 카운터를 올리지 않고 finish 때의 최종 status를 읽습니다. 전체 요청과 클라이언트 지연은 추가 계측 설계입니다.
finish에서 한 번 기록

finish가 발생하면 최종 status와 인터셉터 진입 이후 duration을 기록하고 요청 카운터를 한 번 증가시킵니다.

counter 변화: 해당 finish listener에서 +1 · histogram: (Date.now−startedAt) / 1000 초 · label: method·bounded route·최종 code · finish 의미: 서버가 OS에 넘김, 클라이언트 수신 확인 아님once listener가 이 응답의 기록을 한 번 만들며 그 범위는 인터셉터 진입부터 서버 finish까지입니다. Edge·Guard·전체 dependency span 구현을 뜻하지 않습니다.

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

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

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

이 인터셉터의 계측 범위는 인터셉터까지 진입해 finish가 발생한 응답입니다. Nest 요청 수명에서 Guard는 인터셉터보다 앞에 실행되므로 Guard가 거절한 403이나 handler를 찾지 못한 404는 이 코드로 전체 집계되지 않습니다. 진입 뒤 Pipe·handler에서 생긴 예외 응답은 응답 종료 때 기록할 수 있습니다. 따라서 코드의 성공·예외 기록 주석은 이 진입 조건 안에서 읽어야 합니다. 전체 ingress 요청 수를 원하면 더 앞의 middleware·HTTP adapter 경계에서 완료 계측을 설계하고 중복 기록을 막습니다.

또한 Node의 응답 finish는 서버가 응답을 운영체제에 넘긴 시점이며 클라이언트 수신 완료를 뜻하지 않습니다. 중간 연결 종료는 finish가 없을 수 있으므로 별도 종료·중단 조건이 필요합니다. 이 코드의 duration은 인터셉터 진입 이후의 서버 구간으로, 앞선 middleware·Guard·네트워크까지 포함하는 사용자 전체 지연과 구분합니다.

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

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

먼저 앞에서 만든 서비스와 컨트롤러를 MetricsModule에 묶습니다. AppModule에서 만드는 인터셉터도 같은 서비스를 받아야 하므로 MetricsService를 exports에 넣습니다.

src/metrics/metrics.module.ts
import { Module } from '@nestjs/common';
import { MetricsController } from './metrics.controller';
import { MetricsService } from './metrics.service';

@Module({
  controllers: [MetricsController],
  providers: [MetricsService],
  exports: [MetricsService],
})
export class MetricsModule {}

서비스 등록은 MetricsModule 한곳에 두면 컨트롤러와 인터셉터가 같은 인스턴스의 Registry를 사용합니다. 아래 AppModule은 이 모듈을 가져오고 HttpMetricsInterceptor를 전역 인터셉터로 등록합니다.

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 앱

    위 YAML은 prefix가 없는 /metrics route 기준입니다. 8장의 api prefix를 유지한다면 수집 job에 metrics_path: /api/metrics를 추가하거나 metrics route를 prefix에서 제외하는 설정을 선택해야 합니다. URI versioning도 사용하면 해당 실제 URL을 확인합니다. 앱이 정상인데 scrape가 404라면 먼저 수집 경로와 route 계약을 비교하세요.

    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)


직접 판단하기 동일 label의 기록 하나만 놓고 판단합니다. 이 인터셉터에 진입한 요청이 250 ms 뒤 finish를 발생시켰습니다. 관측값은 몇 초이며, 위의 classic histogram에서 le="0.1", le="0.5", le="1", le="2", le="5", +Inf 중 어느 누적 bucket이 1씩 늘까요? Guard가 인터셉터 전에 거절한 별도 요청도 이 counter에 더해질까요?

단위·누적·계측 경계 확인

250 ÷ 1_000 = 0.25초입니다. Classic histogram bucket은 상한 이하의 관측을 누적하므로 0.1은 늘지 않고 0.5·1·2·5·+Inf는 각각 1씩 늘어납니다. count와 http_requests_total도 이 기록으로 1씩 증가합니다. 이 요청 하나가 여러 bucket에 들어가는 것은 여러 요청이 있었다는 뜻이 아닙니다. 별도의 Guard 거절 요청은 인터셉터에 진입하지 않았으므로 이 counter에는 기록되지 않습니다. 전체 요청 수라고 보고하려면 계측 시작 경계를 바꿔야 합니다.

프로파일링(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 링크를 클릭합니다.

Chrome의 공식 Node 프로파일링 안내는 전용 Node DevTools의 Performance 패널을 사용합니다. 웹페이지의 전체 로딩 프로파일과 Node의 CPU 프로파일을 구분하고, 같은 endpoint·부하·기록 시간을 두고 비교합니다.

단계 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 내부 변화 때문에 실행이나 결과의 정확성이 영향을 받을 수 있다고 안내합니다. 아래 명령은 기존 도구의 사용 흐름을 설명한 예시입니다. 사용 중인 Node 버전에서 호환성을 확인하지 않은 채 새 분석의 기본 도구로 적용하지 말고, 먼저 위의 Node·DevTools 경로로 재현 가능한 CPU 증거를 확보하세요. 원 명령의 존재를 현재 Node 24에서 실행 검증됐다는 의미로 읽지 않습니다.

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

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