- Authors

- Name
- Youngju Kim
- @fjvbn20031
프롤로그 — "API 응답 500ms → 50ms, 돈으로 환산하면 얼마?"
2025년 4월, 당신의 API는 한 요청 500ms.
- 월 1억 요청 × 500ms = 1,389시간의 CPU 시간
- AWS EC2 m5.xlarge 2,000-5,000
- **50ms로 줄이면 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% 병렬 가능, 코어 16개
speedup = 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 |
| Python | cProfile, py-spy, Scalene, Pyinstrument |
| Go | pprof, Fgprof, perf |
| Rust | perf, cargo-flamegraph, samply |
| Java | JFR (Java Flight Recorder), async-profiler |
| Ruby | StackProf, ruby-prof, Vernier |
| .NET | dotnet-trace, PerfView |
시스템 프로파일러
| 도구 | 용도 |
|---|---|
perf (Linux) | CPU, 이벤트, 커널 |
bpftrace | eBPF 스크립팅 |
py-spy, rbspy, async-profiler | 샘플링 (프로덕션 OK) |
| Pyroscope / Parca | Continuous profiling (연속) |
| pprof | Go 표준, 다언어 |
샘플링 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가지 패턴
- 넓은 막대: 시간 많이 쓰는 함수 — 최적화 후보
- 높은 스택: 재귀 / 깊은 호출 — 알고리즘 개선 여지
- 평평한 꼭대기: CPU-bound
- 깊은 꼭대기가 시스템콜: 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 |
| Generational | JVM, .NET, V8 | Young/Old heap, Young GC 빠름 |
| G1 (JVM) | Region 기반 | 예측 가능한 pause |
| ZGC, Shenandoah (JVM) | Concurrent | sub-ms pause, 큰 heap |
| CMS | JVM 구식 | deprecated |
| V8 Orinoco | Node, Chrome | Generational + Concurrent |
| Go GC | Go | Concurrent Mark-Sweep, tricolor |
| ARC | Swift, Objective-C | Reference 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)
흔한 누수 패턴
- 전역 변수에 축적 — 캐시 한도 없음
- 이벤트 리스너 제거 안 함 — DOM/Node EventEmitter
- 타이머 취소 안 함 — setInterval, setTimeout
- Closure 캡처 — 큰 객체가 작은 클로저에 걸림
- 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 Image | Java AOT 컴파일 → 50ms start |
| .NET Native AOT (7+) | .NET AOT 지원 |
| Dart AOT | Flutter mobile은 AOT |
| Bun | JS JIT 개선, 빠른 start |
| Ruby YJIT (3.2+) | Ruby JIT Rust로 재작성, 실전 가용 |
| PyPy | Python 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부 — 벤치마킹 방법론
나쁜 벤치마크의 특징
- Warmup 없음 — JIT 안 데워짐
- CPU 주파수 고정 안 함 — Turbo boost 변동
- OS noise — 다른 프로세스 영향
- 단일 측정 — 외부 요인 영향
- 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, 커널, 언어 버전)
도구
| 도구 | 언어/용도 |
|---|---|
| hyperfine | CLI 명령어 벤치마크 |
| wrk, wrk2 | HTTP 벤치마크 |
| k6 | Load testing (JS 스크립트) |
| Locust | Python 기반 load test |
| Criterion (Rust) | 통계적 micro-benchmark |
| Google Benchmark (C++) | 표준 |
| JMH (Java) | JVM의 micro-benchmark 표준 |
| Benchmark.js | JS |
| go test -bench | Go 내장 |
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가지
- DB 쿼리 최적화 — N+1, 인덱스, EXPLAIN
- Connection pool — DB/HTTP 둘 다
- Caching — Redis, CDN, in-memory
- Compression — Brotli, gzip
- HTTP/2, HTTP/3 — multiplexing
- Batch API — N requests → 1 request
- Pagination — 전체 로딩 대신
- Streaming — large response → SSE, NDJSON
- Async I/O — 스레드 대기 제거
- 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가지 체크
- EXPLAIN ANALYZE 읽기 배우기
- 인덱스: WHERE/JOIN/ORDER BY 컬럼
- 복합 인덱스 순서: 선택도 높은 컬럼 먼저
- N+1 제거: JOIN, batch, DataLoader
- Connection pool: pgBouncer (Postgres)
- 읽기 복제본: read-only를 replica로
- 파티셔닝: 시계열은 월별/일별
- Vacuum: Postgres bloat 관리
- 쿼리 재작성: subquery → JOIN, OR → UNION
- 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가지
- 측정 없이 최적화 → 엉뚱한 곳 고침
- 평균만 봄 → p99 참사 놓침
- Production DB로 부하 테스트 → 장애 유발
- Warmup 없이 벤치마크 → 거짓 수치
- Premature optimization → 가독성 희생
- 인덱스 남발 → 쓰기 느려짐
- Cache를 맹신 → stale data, invalidation 지옥
- Connection pool 작게 → 대기 큐 폭발
- Compression 없이 큰 JSON → 대역폭 낭비
- 모든 최적화 직접 → 언어/런타임 발전 따라가야 (V8, YJIT 등)
마무리 — "느린 건 측정되지 않은 것"
2025년 성능 엔지니어링의 교훈:
- 측정 → 병목 → 수정 → 재측정 — 루프는 과학
- p50, p95, p99 — 평균은 거짓말
- 프로파일러가 친구 — Continuous Profiling 필수
- 저수준은 마지막에 — 쿼리/알고리즘이 먼저
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는 사랑받는다. 다음 글에서.