성능 최적화 실전 고수 팁
캐시·압축·HTTP/2 설정이 끝났다고 끝이 아닙니다. 실제로 얼마나 빨라졌는지 부하 테스트로 검증하고, 병목 지점을 정확히 찾아야 진정한 성능 최적화가 완성됩니다.
부하 테스트 도구 비교
| 도구 | 특징 | 프로토콜 | 권장 용도 |
|---|---|---|---|
ab (Apache Bench) | 단순, 빠른 설치 | HTTP/1.1 | 빠른 기준선 측정 |
wrk | 고성능, Lua 스크립트 | HTTP/1.1 | 처리량·지연 측정 |
k6 | JS 시나리오, 시각화 | HTTP/1.1~2 | 시나리오 부하 테스트 |
h2load | HTTP/2 전용 | HTTP/2 | HTTP/2 성능 검증 |
locust | Python, 분산 가능 | HTTP | 복잡한 사용자 시나리오 |
ab (Apache Bench)
# 기본 사용법
# -n: 총 요청 수, -c: 동시 연결 수, -k: Keep-Alive 활성화
ab -n 10000 -c 100 -k https://example.com/api/products
# 출력 해석
# Requests per second: 1234.56 [#/sec] ← 처리량 (높을수록 좋음)
# Time per request: 81.00 [ms] ← 평균 응답 시간
# Time per request: 0.81 [ms] ← 동시 요청 기준 평균
# Transfer rate: 456.78 [Kbytes/sec]
# Percentile 분포 (p99 > 1000ms면 개선 필요)
# 50% 45 ms
# 75% 78 ms
# 90% 120 ms
# 95% 180 ms
# 99% 450 ms
# 100% 1200 ms (longest request)
# POST 요청 테스트
ab -n 5000 -c 50 -k \
-H "Content-Type: application/json" \
-p /tmp/payload.json \
https://example.com/api/orders
wrk — 고성능 벤치마크
# 설치
sudo apt install wrk # 또는 소스 빌드
# 기본 사용법
# -t: 스레드 수, -c: 연결 수, -d: 지속 시간
wrk -t4 -c100 -d30s https://example.com/api/products
# Running 30s test @ https://example.com/api/products
# 4 threads and 100 connections
#
# Thread Stats Avg Stdev Max +/- Stdev
# Latency 45.67ms 12.34ms 234.56ms 87.65%
# Req/Sec 567.89 45.67 678.90 78.90%
# 68145 requests in 30s, 12.34MB read
# Requests/sec: 2271.50 ← 처리량
# Latency: 45ms ← 평균 응답 시간
# Lua 스크립트로 POST 요청
cat > /tmp/post.lua << 'EOF'
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"
wrk.body = '{"userId": 1, "productId": 100, "qty": 2}'
EOF
wrk -t4 -c50 -d30s -s /tmp/post.lua https://example.com/api/orders
# 연결별 응답 시간 분포
wrk -t4 -c100 -d30s --latency https://example.com/
k6 — 시나리오 기반 부하 테스트
# 설치
sudo apt install k6
# 또는: https://k6.io/docs/getting-started/installation/
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const errorRate = new Rate('errors');
// 테스트 시나리오 정의
export const options = {
stages: [
{ duration: '1m', target: 50 }, // 1분간 50명으로 램프업
{ duration: '3m', target: 200 }, // 3분간 200명 유지
{ duration: '1m', target: 0 }, // 1분간 종료
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95%가 500ms 이내
errors: ['rate<0.01'], // 에러율 1% 미만
},
};
export default function () {
// 상품 목록 조회
const res = http.get('https://example.com/api/products', {
headers: { 'Accept': 'application/json' },
});
check(res, {
'status 200': (r) => r.status === 200,
'response < 500ms': (r) => r.timings.duration < 500,
});
errorRate.add(res.status !== 200);
sleep(1);
}
# 실행
k6 run load-test.js
# 결과 출력
# ✓ status 200
# ✓ response < 500ms
#
# checks.........................: 99.80% ✓ 5988 ✗ 12
# http_req_duration..............: avg=87.23ms min=12.34ms med=71.45ms
# max=1.23s p(90)=156.78ms p(95)=234.56ms
# http_reqs......................: 6000 199.89/s
# Grafana 연동으로 실시간 시각화
k6 run --out influxdb=http://localhost:8086/k6 load-test.js
h2load — HTTP/2 성능 측정
# 설치 (nghttp2 패키지)
sudo apt install nghttp2-client
# HTTP/2 부하 테스트
# -n: 총 요청, -c: 연결 수, -m: 연결당 동시 스트림
h2load -n 10000 -c 50 -m 10 https://example.com/api/products
# 결과
# finished in 5.23s, 1912.04 req/s, 2.34MB/s
# requests: 10000 total, 10000 started, 10000 done, 10000 succeeded
# status codes: 10000 2xx, 0 3xx, 0 4xx, 0 5xx
# traffic: 12.23MB total, 1.23MB headers, 10.78MB data
#
# time for request: min=5ms max=234ms mean=52ms sd=23ms cv=44.23%
병목 지점 분석 방법론
1단계: 응답 시간 분해
# curl으로 각 단계 시간 측정
curl -o /dev/null -s -w "
DNS: %{time_namelookup}s
TCP Connect: %{time_connect}s
TLS: %{time_appconnect}s
TTFB: %{time_starttransfer}s ← Time to First Byte
Total: %{time_total}s
Size: %{size_download} bytes
" https://example.com/api/products
2단계: 서버별 처리 시간 분리
# Nginx upstream_response_time 로깅
log_format perf '$remote_addr [$time_local] '
'"$request" $status '
'req=$request_time ' # Nginx 총 처리 시간
'upstream=$upstream_response_time ' # 백엔드 처리 시간
'cache=$upstream_cache_status';
# Nginx 처리 시간 = 총 시간 - upstream 시간
# 이 차이가 크면 Nginx 자체가 병목 (캐시, 압축 설정 확인)
3단계: 병목 위치 판단
응답이 느린 경우 체크리스트:
──────────────────────────────────────
1. DNS 조회 느림? → DNS 캐시 TTL 확인
2. TCP/TLS 느림? → Keep-Alive 활성화, TLS 세션 캐시 확인
3. TTFB 느림? → 백엔드 로직, DB 쿼리 확인
4. 전송 느림? → 응답 크기 (압축 여부), 네트워크 대역폭 확인
──────────────────────────────────────
TTFB가 느린 경우 (백엔드 병목):
- DB 슬로우 쿼리 확인: EXPLAIN ANALYZE
- N+1 쿼리 문제 확인
- 외부 API 호출 타임아웃 미설정
- 스레드 풀 고갈 (Tomcat maxThreads 초과)
전송이 느린 경우 (네트워크/크기 병목):
- 압축 미설정 (gzip/brotli 확인)
- 캐시 미적용 (X-Cache-Status: MISS)
- 응답 페이로드 과다 (페이지네이션 확인)
성능 지표 목표값 (SLO 기준)
| 지표 | 목표값 | 개선 방법 |
|---|---|---|
| TTFB | < 200ms | 캐시, DB 쿼리 최적화 |
| p95 응답 시간 | < 500ms | 스레드 풀 튜닝, 캐시 |
| p99 응답 시간 | < 1000ms | 타임아웃 설정, 서킷 브레이커 |
| 에러율 | < 0.1% | 헬스체크, 자동 재시도 |
| 압축률 | > 60% (텍스트) | gzip/brotli 설정 확인 |
| 캐시 히트율 | > 80% | proxy_cache 설정 최적화 |
성능 테스트 자동화 (CI/CD 통합)
#!/bin/bash
# performance-gate.sh — 배포 전 성능 기준 통과 확인
ENDPOINT="https://staging.example.com/api/products"
MAX_P95_MS=500
MIN_RPS=100
echo "=== Performance Gate Check ==="
# wrk으로 30초 부하 테스트
result=$(wrk -t4 -c100 -d30s --latency "$ENDPOINT" 2>&1)
# p95 추출
p95=$(echo "$result" | grep "99%" | awk '{print $2}' | sed 's/ms//')
rps=$(echo "$result" | grep "Requests/sec" | awk '{print $2}')
echo "p95: ${p95}ms (기준: ${MAX_P95_MS}ms)"
echo "RPS: ${rps} (기준: ${MIN_RPS})"
if (( $(echo "$p95 > $MAX_P95_MS" | bc -l) )); then
echo "❌ 성능 기준 미달: p95 ${p95}ms > ${MAX_P95_MS}ms"
exit 1
fi
echo "✅ 성능 기준 통과"