Skip to content

✍️ 필사 모드: 성능 엔지니어링 완전 가이드 — 프로파일링·Flame Graph·JIT·메모리·벤치마킹을 2025년 기준으로 한 번에 정리

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

프롤로그 — "API 응답 500ms → 50ms, 돈으로 환산하면 얼마?"

2025년 4월, 당신의 API는 한 요청 500ms.

  • 월 1억 요청 × 500ms = 1,389시간의 CPU 시간
  • AWS EC2 m5.xlarge 140/×몇대?140/월 × 몇 대? → 월 2,000-5,000
  • **50ms로 줄이면 200500.200-500**. 연 20K-50K 절감
  • 더 중요한 건 사용자 체감 — 100ms 이내면 "즉시", 1초 넘으면 "느림"

성능은 비용이자 UX다. 그런데 대부분 팀이 측정 없이 최적화한다 — 가장 흔한 실수.

테스트(Ep 17)가 정확성을 지키면, 성능은 경제성과 경험을 지킨다. 이 글은 Season 2 Ep 18 — 성능 엔지니어링. 프로파일링 도구, Flame Graph 읽는 법, 메모리 관리, 벤치마킹 방법론, 저수준 최적화(Cache Line, Data-oriented Design)까지.

"Make it correct, make it clear, make it fast" — Kent Beck. 순서가 중요하다.


1부 — 성능 엔지니어링의 3대 법칙

Amdahl's Law

병렬 가능한 부분 p, 병렬도 N:
speedup = 1 / ((1 - p) + p/N)

: 95% 병렬 가능, 코어 16speedup = 1 / (0.05 + 0.95/16) = 9.1x (16x 아님!)

교훈: 직렬 부분이 상한을 결정. 99.5% 병렬이라도 최대 200x.

Little's Law

L = λ × W
L = 시스템 내 평균 요청  (concurrency)
λ = 도착률 (RPS)
W = 평균 체류 시간 (latency)

예시: RPS 1000, latency 100ms → 동시 100개 요청 처리 중 필요. 함의: 처리량 늘리려면 latency 줄이거나 concurrency 늘려야.

USE Method (Brendan Gregg)

리소스(CPU, 메모리, 디스크, 네트워크)마다 확인:
- U(tilization): 얼마나 사용 중
- S(aturation): 대기 큐 길이 (병목?)
- E(rrors): 에러 발생률

진단 순서: CPU → 메모리 → 디스크 IO → 네트워크 → 앱 → DB.


2부 — 측정 없이 최적화하지 말라

성능 최적화 5단계

1. 측정 (Measure)
2. 병목 식별 (Identify)
3. 가설 세움 (Hypothesize)
4. 수정 (Fix)
5. 재측정 (Re-measure)

Donald Knuth: "Premature optimization is the root of all evil" — 하지만 **"at least 97% of the time"**이라는 뒤 문장을 다들 생략함. 성능 중요한 3%는 측정하고 최적화해야.

성능 지표 5가지

지표설명
Latency단일 요청 처리 시간
Throughput단위 시간 처리량 (RPS)
Error rate실패 비율
Saturation리소스 포화도
Efficiency단위 작업당 비용

Percentile — 평균 대신

p50 (중간값): 보통 사용자 경험
p95, p99: 꼬리 지연 (문제 발견)
p99.9: 대규모 서비스 핵심 지표

평균(average)은 거짓말한다:
    10 요청 중 9개는 10ms, 1개는 1000ms
    평균 109ms — 마치 전체가 "100ms대" 같음
    실제로는 10%가 심각하게 느림

3부 — 프로파일링 도구 2025

언어별 프로파일러

언어도구
Node.js--prof, Clinic.js, 0x, Chrome DevTools
PythoncProfile, py-spy, Scalene, Pyinstrument
Gopprof, Fgprof, perf
Rustperf, cargo-flamegraph, samply
JavaJFR (Java Flight Recorder), async-profiler
RubyStackProf, ruby-prof, Vernier
.NETdotnet-trace, PerfView

시스템 프로파일러

도구용도
perf (Linux)CPU, 이벤트, 커널
bpftraceeBPF 스크립팅
py-spy, rbspy, async-profiler샘플링 (프로덕션 OK)
Pyroscope / ParcaContinuous profiling (연속)
pprofGo 표준, 다언어

샘플링 vs 계측

샘플링 (Sampling):
- 주기적으로 스택 캡처 (99Hz 등)
- 오버헤드 1% 이내 → 프로덕션 OK
- 짧은 함수 놓칠 수 있음

계측 (Instrumentation):
- 모든 함수 진입/종료 기록
- 정확함
- 오버헤드 20-50% → 프로덕션 X

2025 추세: Continuous Profiling — Pyroscope, Parca, Grafana Phlare로 상시 샘플링.


4부 — Flame Graph 읽는 법

Flame Graph란

    [main]
   /      \
  [foo]   [bar]
 / \         \
[a][b]       [baz]

X축: CPU 시간 비율 (너비 = 해당 함수에서 소비한 시간) Y축: 콜 스택 (위로 갈수록 더 깊은 호출) 색: 무작위 (가독성)

읽는 4가지 패턴

  1. 넓은 막대: 시간 많이 쓰는 함수 — 최적화 후보
  2. 높은 스택: 재귀 / 깊은 호출 — 알고리즘 개선 여지
  3. 평평한 꼭대기: CPU-bound
  4. 깊은 꼭대기가 시스템콜: IO-bound

예시 분석

[web server]  [handle_request]
    [parse_json]40% 차지 → 최적화 1순위
    [query_db]30% (IO)
      [pg_connect]
      [parse_result]
    [serialize]20%

행동: parse_json → 빠른 파서(simdjson) 도입 → 40% → 10%.

Differential Flame Graph

Before/After 비교. 빨강 = 느려짐, 파랑 = 빨라짐. 쓰임: 릴리즈 간 회귀 감지.

생성 방법

# Linux perf + FlameGraph 스크립트
sudo perf record -F 99 -g -- ./myapp
sudo perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > flame.svg

# Go pprof
go tool pprof -http=:8080 cpu.prof
# 웹 UI에서 Flame Graph 뷰

# Node.js
node --prof-process isolate-*.log
# 또는 0x
npx 0x -- node app.js

# Pyroscope (Continuous)
# SDK 심어놓고 대시보드에서 확인

5부 — 메모리 이해하기

가비지 컬렉터 (GC) 종류

GC언어특징
Mark-Sweep기본 알고리즘Stop-the-world
GenerationalJVM, .NET, V8Young/Old heap, Young GC 빠름
G1 (JVM)Region 기반예측 가능한 pause
ZGC, Shenandoah (JVM)Concurrentsub-ms pause, 큰 heap
CMSJVM 구식deprecated
V8 OrinocoNode, ChromeGenerational + Concurrent
Go GCGoConcurrent Mark-Sweep, tricolor
ARCSwift, Objective-CReference counting, compile-time

메모리 누수 찾기

Node.js Heap Snapshot

// Chrome DevTools에서 Memory → Take Snapshot
// 비교(Comparison) 뷰로 delta 확인
// "Retained Size" 큰 것부터 조사

Go pprof heap

go tool pprof http://localhost:6060/debug/pprof/heap
(pprof) top
(pprof) list <함수명>

Python tracemalloc

import tracemalloc
tracemalloc.start()
# ... 코드 실행
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics('lineno')[:10]:
    print(stat)

흔한 누수 패턴

  1. 전역 변수에 축적 — 캐시 한도 없음
  2. 이벤트 리스너 제거 안 함 — DOM/Node EventEmitter
  3. 타이머 취소 안 함 — setInterval, setTimeout
  4. Closure 캡처 — 큰 객체가 작은 클로저에 걸림
  5. Map/Set 계속 추가 — WeakMap/WeakSet 고려

RSS, Heap, Stack 구별

RSS (Resident Set Size): 프로세스가 쓰는 물리 메모리
Heap: 동적 할당 영역
Stack: 함수 호출 스택
Anonymous mmap: mmap으로 할당된 영역

함정: Node 프로세스 RSS는 커졌는데 Heap은 안 커졌다면 → C++ native 메모리 누수 (buffer, native module).


6부 — Cache와 저수준 최적화

Memory Hierarchy (2025)

Register      < 1 ns    bytes
L1 cache      1-2 ns    32-64 KB
L2 cache      4-10 ns   256 KB - 1 MB
L3 cache      10-30 ns  수십 MB
DRAM          80-100 ns GB
NVMe SSD      10-100 μs TB
HDD           ms        TB
Network       ms-s      ∞

1 ns 차이가 100x 차이 — cache miss는 치명적.

Cache Line (64 bytes)

CPU는 byte 단위가 아니라 cache line(보통 64B) 단위로 메모리 읽음
→ 인접 데이터도 같이 읽힘 → 순차 접근이 빠름

Data-oriented Design 예시

// ❌ AoS (Array of Structs) — cache 비효율
struct Particle { float x, y, z; char active; ... };
Particle particles[1000];
// x 값만 갱신해도 전체 struct가 cache line에 들어감

// ✅ SoA (Struct of Arrays) — cache 효율
struct Particles {
    float x[1000];
    float y[1000];
    float z[1000];
    char active[1000];
};
// x만 접근하면 x 배열이 빽빽하게 cache에 들어감 → SIMD 가능

False Sharing

두 쓰레드가 서로 다른 변수지만 같은 cache line
→ 한 쪽 쓰기 시 다른 쪽 cache invalidation
→ 심각한 성능 저하

해결: padding 또는 alignas(64).

Branch Prediction

// 정렬된 배열 순회 + if → 분기 예측 성공 → 빠름
// 무작위 배열 + if → 분기 예측 실패 → 느림 (10x)

교훈: 가능하면 정렬 → 분기 예측 친화적.

SIMD

// SSE/AVX/NEON — 벡터 명령어
for (int i = 0; i < n; i += 4) {
    __m128 v = _mm_load_ps(&a[i]);
    __m128 w = _mm_add_ps(v, _mm_set1_ps(1.0));
    _mm_store_ps(&a[i], w);
}
// 4개 float 동시 처리 → 4x 빠름

고수준 언어: Rust std::simd, Go math/bits 부분 지원, .NET Vector<T>.


7부 — JIT vs AOT

JIT (Just-In-Time)

언어: JavaScript (V8), Java, C#, Lua, Ruby(YJIT)
흐름: 바이트코드 → 실행 중 hot path 감지 → native code 컴파일
장점: 런타임 정보로 최적화 (inline caching, type specialization)
단점: warmup 필요, 메모리 오버헤드

AOT (Ahead-of-Time)

언어: C/C++/Rust/Go
흐름: 소스 → 컴파일 → native binary → 실행
장점: 시작 빠름, 메모리 적음
단점: 런타임 정보 없음

흥미로운 하이브리드 2025

언어/런타임특징
GraalVM Native ImageJava AOT 컴파일 → 50ms start
.NET Native AOT (7+).NET AOT 지원
Dart AOTFlutter mobile은 AOT
BunJS JIT 개선, 빠른 start
Ruby YJIT (3.2+)Ruby JIT Rust로 재작성, 실전 가용
PyPyPython JIT, 실전 사용
Node.js --experimental-sea단일 실행파일

Cold start의 실체

Lambda Cold start 비교:
- Python / Node.js: 150-500ms
- Go / Rust: 50-100ms
- Java (JVM): 1000-3000ms
- Java (GraalVM Native): 50-100ms
- C# AOT: 100-200ms

교훈: 서버리스에서 JIT 언어는 불리. AOT 또는 Provisioned Concurrency.


8부 — 벤치마킹 방법론

나쁜 벤치마크의 특징

  1. Warmup 없음 — JIT 안 데워짐
  2. CPU 주파수 고정 안 함 — Turbo boost 변동
  3. OS noise — 다른 프로세스 영향
  4. 단일 측정 — 외부 요인 영향
  5. Outlier 처리 안 함 — 극단값으로 왜곡

좋은 벤치마크 절차

1. CPU 주파수 고정 (cpufreq-set -g performance)
2. Background 프로세스 죽임
3. Warmup 10 (JIT, cache)
4. 본 측정 100+
5. Outlier 제거 (2σ 밖)
6. p50, p95, p99 보고
7. 환경 기록 (CPU, OS, 커널, 언어 버전)

도구

도구언어/용도
hyperfineCLI 명령어 벤치마크
wrk, wrk2HTTP 벤치마크
k6Load testing (JS 스크립트)
LocustPython 기반 load test
Criterion (Rust)통계적 micro-benchmark
Google Benchmark (C++)표준
JMH (Java)JVM의 micro-benchmark 표준
Benchmark.jsJS
go test -benchGo 내장

hyperfine 예시

hyperfine --warmup 3 \
  --prepare 'sync && echo 3 | sudo tee /proc/sys/vm/drop_caches' \
  'grep pattern big-file.log' \
  'rg pattern big-file.log'

결과:

Benchmark 1: grep pattern big-file.log
  Time (mean ± σ):     1.234 s ±  0.045 s
Benchmark 2: rg pattern big-file.log
  Time (mean ± σ):     0.156 s ±  0.012 s
Summary
  'rg' ran 7.91 ± 0.38 times faster than 'grep'

Criterion (Rust)

use criterion::{criterion_group, criterion_main, Criterion};

fn bench_fib(c: &mut Criterion) {
    c.bench_function("fib 20", |b| b.iter(|| fibonacci(20)));
}

criterion_group!(benches, bench_fib);
criterion_main!(benches);

특징: 자동 warmup, 통계 분석, regression 감지, HTML 리포트.


9부 — HTTP/API 성능 최적화

측정: RED Method

  • Rate — RPS
  • Errors — 에러율
  • Duration — 지연

최적화 10가지

  1. DB 쿼리 최적화 — N+1, 인덱스, EXPLAIN
  2. Connection pool — DB/HTTP 둘 다
  3. Caching — Redis, CDN, in-memory
  4. Compression — Brotli, gzip
  5. HTTP/2, HTTP/3 — multiplexing
  6. Batch API — N requests → 1 request
  7. Pagination — 전체 로딩 대신
  8. Streaming — large response → SSE, NDJSON
  9. Async I/O — 스레드 대기 제거
  10. Horizontal Scale — 적절할 때만

N+1 쿼리 해결

// ❌ N+1: 1 + N 쿼리
const orders = await db.order.findMany();
for (const order of orders) {
  order.user = await db.user.findUnique({ where: { id: order.userId } });
}

// ✅ JOIN 또는 eager loading
const orders = await db.order.findMany({ include: { user: true } });

Caching 계층

Browser cache (Cache-Control)
  ↓ miss
CDN cache (Cloudflare, Fastly)
  ↓ miss
Application cache (Redis)
  ↓ miss
DB

Cache key 전략: URL + query param + user context (user-specific은 주의)


10부 — 데이터베이스 성능

실전 10가지 체크

  1. EXPLAIN ANALYZE 읽기 배우기
  2. 인덱스: WHERE/JOIN/ORDER BY 컬럼
  3. 복합 인덱스 순서: 선택도 높은 컬럼 먼저
  4. N+1 제거: JOIN, batch, DataLoader
  5. Connection pool: pgBouncer (Postgres)
  6. 읽기 복제본: read-only를 replica로
  7. 파티셔닝: 시계열은 월별/일별
  8. Vacuum: Postgres bloat 관리
  9. 쿼리 재작성: subquery → JOIN, OR → UNION
  10. Materialized view: 무거운 집계 미리

Postgres 설정 체크 (postgres.conf)

shared_buffers = 25% of RAM
effective_cache_size = 75% of RAM
work_mem = RAM / max_connections / 2
maintenance_work_mem = 256MB
wal_compression = on
random_page_cost = 1.1  (SSD)
max_wal_size = 2GB

pg_stat_statements — 느린 쿼리 찾기

SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

11부 — 프론트엔드 성능

Core Web Vitals 최적화

LCP (Largest Contentful Paint)

  • 이미지 최적화: WebP/AVIF, lazy, priority
  • 서버 렌더 (SSR/SSG) → 빠른 초기 콘텐츠
  • Preload critical resources

CLS (Cumulative Layout Shift)

  • 이미지/비디오 width/height 지정
  • Font load 안정 (font-display: optional 주의)
  • Ad/embed에 예약 공간

INP (Interaction to Next Paint) — 2024부터 FID 대체

  • JS 실행 시간 줄이기
  • Heavy work를 Web Worker로
  • Debounce/throttle

Bundle 분석

# Webpack
npx webpack-bundle-analyzer

# Vite
npx vite-bundle-visualizer

# Next
ANALYZE=true next build

찾을 것: moment.js → date-fns, lodash → lodash-es(tree-shakable), 중복 패키지.

React 특화 최적화

// 1. Code splitting
const HeavyChart = lazy(() => import('./HeavyChart'));

// 2. Memoization (React Compiler 쓰면 자동)
const memoized = useMemo(() => expensive(data), [data]);

// 3. Virtualization (긴 리스트)
import { FixedSizeList } from 'react-window';

// 4. RSC로 서버로 이동 (Next App Router)
// 5. Server Actions로 round-trip 줄이기

12부 — 사례 연구

사례 1: API 500ms → 50ms

증상: /api/dashboard p95 500ms 프로파일링: pprof로 확인 → queryUserStats가 80% 차지 원인: N+1 쿼리, 사용자마다 5개 쿼리 해결:

  • JOIN으로 1 쿼리
  • Redis 캐시 30초
  • 결과: p95 50ms (10x)

사례 2: Node.js 메모리 누수

증상: RSS 24시간마다 500MB 증가 프로파일링: heap snapshot 비교 → express-session 저장소 무한 증가 원인: session store에서 만료된 entry 삭제 안 됨 해결: Redis session store + TTL → 메모리 안정

사례 3: Rust 미시 최적화

증상: 파서가 400MB/s → 목표 1GB/s 프로파일링: cargo-flamegraph → 정규식 매칭이 60% 해결:

  • 정규식 → 상태 머신 직접 작성
  • SIMD 활용
  • 결과: 1.2GB/s

사례 4: 프론트엔드 LCP 4s → 1s

증상: 이미지 많은 페이지 LCP 4s 진단: Chrome DevTools Performance → 히어로 이미지가 늦게 로드 해결:

  • <Image priority> hero
  • AVIF 형식
  • Preload + fetchPriority
  • CDN Edge
  • 결과: LCP 0.9s

13부 — 6개월 로드맵

1개월차: 측정 기초. hyperfine, Chrome DevTools Performance, Lighthouse 2개월차: 언어별 프로파일러 (pprof, py-spy, Clinic.js). Flame Graph 읽기 3개월차: DB 쿼리 튜닝. EXPLAIN, 인덱스, pg_stat_statements 4개월차: 메모리 프로파일링. Heap snapshot, 누수 패턴 5개월차: 부하 테스트. k6, wrk, Locust. SLA 설정 6개월차: 저수준 최적화 탐구. Cache line, SIMD, SoA vs AoS (취미로)


14부 — 체크리스트 12개

  • 주요 엔드포인트 p50/p95/p99 측정
  • Continuous Profiling (Pyroscope/Parca) 설치
  • 느린 쿼리 로깅 (Postgres slow_query_log)
  • 인덱스 정기 리뷰
  • 캐시 계층 정의 (Browser, CDN, Redis)
  • HTTP/2 또는 HTTP/3 활성화
  • Connection pool 크기 튜닝
  • 프론트엔드 Core Web Vitals 모니터링
  • Bundle analyzer 정기 실행
  • 부하 테스트 CI에 통합
  • 회귀 감지 (Performance budget)
  • 성능 regression 알람

15부 — 안티패턴 10가지

  1. 측정 없이 최적화 → 엉뚱한 곳 고침
  2. 평균만 봄 → p99 참사 놓침
  3. Production DB로 부하 테스트 → 장애 유발
  4. Warmup 없이 벤치마크 → 거짓 수치
  5. Premature optimization → 가독성 희생
  6. 인덱스 남발 → 쓰기 느려짐
  7. Cache를 맹신 → stale data, invalidation 지옥
  8. Connection pool 작게 → 대기 큐 폭발
  9. Compression 없이 큰 JSON → 대역폭 낭비
  10. 모든 최적화 직접 → 언어/런타임 발전 따라가야 (V8, YJIT 등)

마무리 — "느린 건 측정되지 않은 것"

2025년 성능 엔지니어링의 교훈:

  1. 측정 → 병목 → 수정 → 재측정 — 루프는 과학
  2. p50, p95, p99 — 평균은 거짓말
  3. 프로파일러가 친구 — Continuous Profiling 필수
  4. 저수준은 마지막에 — 쿼리/알고리즘이 먼저

Donald Knuth의 원문 전체: "We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%."

중요한 3%를 찾는 것이 성능 엔지니어링의 본질.

다음 글은 Season 2 Ep 19 — API 디자인 완전 가이드. REST Maturity, OpenAPI, Versioning, Pagination, Rate Limiting, Idempotency, Webhooks, Async API까지. 느린 API는 고쳤으니, 좋은 API를 만들 차례.


다음 글 예고 — "API 디자인 완전 가이드: REST·OpenAPI·Versioning·Pagination·Idempotency·Webhooks"

Season 2 Ep 19는:

  • REST Maturity Model (Richardson Level)
  • OpenAPI 3.1 + Zod/Typebox 통합
  • Versioning 전략 (URL, Header, Content negotiation)
  • Pagination (Offset, Cursor, Keyset)
  • Idempotency Key 설계
  • Rate Limiting (Token bucket, Sliding window)
  • Webhook 재전송과 서명
  • Async API (CloudEvents, AsyncAPI)

좋은 API는 사랑받는다. 다음 글에서.

현재 단락 (1/373)

2025년 4월, 당신의 API는 한 요청 500ms.

작성 글자: 0원문 글자: 11,454작성 단락: 0/373