성능 테스트는 왜 유형이 다른가
Load / Stress / Spike / Soak 테스트가 각각 다른 질문에 답하는 이유부터, SLO 기반 p99 목표 설정과 Baseline 자동화를 통한 회귀 감지까지 추적한다.
- 01 성능 테스트는 왜 유형이 다른가
- 02 성능 테스트 도구는 왜 아키텍처가 결과를 결정하는가
- 03 성능 병목은 어디서 오는가 — USE 방법론부터 스레드 덤프까지
- 04 JVM 성능 문제는 어디서 오는가
- 05 DB 성능 튜닝은 어디서 시작해야 하는가
- 06 성능 튜닝은 과학이다 — 하나씩, 측정하고, 번역하라
- 07 성능 테스트가 거짓말을 하는 이유
“성능 테스트 통과”라는 말은 생각보다 아무 의미가 없을 수 있다. VU 50명, 5분, 에러율 0% — 숫자는 나왔지만, 그게 무엇을 보장하는지는 아무도 모른다. 성능 테스트가 의미 있으려면, 먼저 어떤 질문에 답하려는 테스트인지를 결정해야 한다.
네 가지 질문, 네 가지 테스트
Load, Stress, Spike, Soak — 이 네 유형은 같은 API에 부하를 넣는 행위처럼 보이지만, 각각 완전히 다른 질문에 답한다.
Load Test는 “정상 트래픽에서 SLO를 달성하는가”를 묻는다. 목표 동시 사용자 수 기준으로 Warm-up → Sustained → Cool-down 패턴을 유지하며 p95/p99 응답시간과 에러율을 측정한다. 가장 기본적이며 매 배포마다 실행해야 한다.
Stress Test는 “어디서 무너지기 시작하는가”를 묻는다. VU를 단계적으로 올려가며 에러율이 급증하거나 응답시간이 비선형으로 치솟는 Breaking Point를 데이터로 특정한다. 이 수치는 운영 용량 계획의 근거가 된다.
Spike Test는 “급증 후 살아남는가, 살아남는다면 얼마나 빨리 회복하는가”를 묻는다. 이벤트 시작이나 마케팅 메일 발송처럼 예측 가능한 급증을 재현하며, Spike 해소 후 회복 시간이 핵심 지표다.
Soak Test는 “장시간 운영에서 무엇이 새는가”를 묻는다. 낮은 부하를 수 시간 유지하면서 힙 사용량이 단조 증가하거나 HikariCP 커넥션이 반환되지 않는 패턴을 찾는다. 4시간 동안 힙이 180 → 680 MB로 증가했다면 메모리 누수 의심이다.
Load Test를 통과했다고 Soak Test를 생략하면, 출시 12시간 후 메모리 OOM으로 자동 재시작이 반복될 수 있다. 네 테스트는 대체 관계가 아니라 보완 관계다.
평균 응답시간이 SLO 기준으로 부적합한 이유
목표 없이 테스트하면 결과를 해석할 수 없다. 그리고 SLO 기준을 잘못 설정하면 결과가 나와도 실제 사용자 경험을 보호하지 못한다.
평균 응답시간이 145ms인 서비스에서 p99가 4,800ms일 수 있다. 하루 방문자 10만 명이라면, 1,000명이 매 요청마다 5초 가까이 기다린다. 평균은 소수의 극단적으로 느린 요청을 숨긴다.
p99를 쓰는 이유는 명확하다. 100명 중 99명의 경험을 수치로 보호할 수 있기 때문이다. k6 thresholds에 이렇게 명시하면 테스트가 자동으로 합격/실패를 판정한다.
thresholds: {
'http_req_duration': ['p(95)<300', 'p(99)<800'],
'http_req_failed': ['rate<0.001'],
}
Long Tail Latency가 발생하는 원인은 구체적이다. GC Stop-The-World, DB Lock 경합, Connection Pool 고갈, 외부 API Timeout — 이 중 어느 것이든 특정 요청만 극단적으로 느려지게 만들고 p99를 끌어올린다. 평균은 이 신호를 보이지 않게 한다.
SLI, SLO, SLA의 계층도 이 맥락에서 이해해야 한다. SLI는 측정 지표 자체(응답시간, 에러율), SLO는 내부 목표치(p99 < 800ms), SLA는 외부 계약이다. SLA는 항상 SLO보다 느슨하게 설정한다. SLO를 지키면 SLA는 자동으로 달성된다.
현실적인 부하 시나리오를 만드는 법
VU 100명이 Think Time 없이 단일 API를 반복 호출하는 테스트는 실제 트래픽이 아니다. Think Time 없는 VU 100명은 Think Time 2초를 포함한 VU 1,600명과 동일한 압박을 만들어낸다. 결과가 실제와 10배 이상 차이 날 수 있다.
현실적인 VU 수는 Little’s Law로 계산한다.
L = λ × W
피크 TPS = 300 req/s
평균 응답시간 = 200ms, Think Time = 3초
W = 0.2 + 3 = 3.2s
L = 300 × 3.2 = 960 VU
Access Log에서 엔드포인트별 비율을 추출해 가중치로 반영하면 실제 트래픽 분포를 재현할 수 있다. 상품 목록 60%, 상품 상세 27%, 검색 10% — 이 비율 없이 단일 엔드포인트만 테스트하면 실제 병목이 보이지 않는다.
Warm-up도 빠져선 안 된다. JVM Cold Start 상태에서 바로 최대 부하를 투입하면 초기 2분의 측정값이 JIT 미완료와 캐시 비어있음으로 심각하게 왜곡된다. 2분 이상의 Warm-up 단계를 포함하고 이 구간 결과를 측정에서 제외해야 한다.
환경이 다르면 결과는 무의미하다
“로컬에서는 빠른데 운영에서 느려요”는 테스트 환경과 운영 환경의 차이에서 나온다. 가장 흔하게 과소평가되는 요소는 데이터 볼륨이다.
인덱스 없는 쿼리가 1,000건 데이터에서는 2ms에 실행된다. 5,000만 건에서는 9,200ms다. 로컬 테스트에서는 인덱스 필요성 자체가 드러나지 않는다. 운영 배포 후에야 Full Table Scan이 장애를 만든다.
JVM 힙 설정도 동일하게 맞춰야 한다. 테스트 환경에서 -Xmx4g이고 운영이 -Xmx1g이면, 테스트에서 거의 발생하지 않던 GC가 운영에서 빈번해져 Stop-The-World가 p99를 끌어올린다.
Baseline과 CI 자동화
성능 회귀(Regression)는 새 기능 추가, 의존성 업그레이드, 설정 변경 어디서든 발생한다. Baseline이 없으면 회귀를 발견하는 시점이 사용자 불만이나 장애가 발생한 후가 된다.
Baseline은 측정 조건(날짜, Git 커밋 해시, 환경 설정)과 결과(p50/p95/p99, 에러율, TPS)의 조합이다. CI Pipeline에 통합하면 코드 리뷰 단계에서 회귀를 차단할 수 있다.
실용적인 전략은 세 단계다. PR마다 Smoke Test(5분, VU 20)로 SLO 최소 기준을 검증한다. develop 머지마다 Full Load Test를 실행해 Baseline 대비 p99가 10% 이상 상승하면 경고, 30% 이상이면 배포 파이프라인을 차단한다. 배포 성공 후에는 현재 결과를 새 Baseline으로 저장한다.
절대 기준(SLO만)은 오탐이 적지만 서서히 느려지는 회귀를 놓친다. Baseline 대비 상대 기준은 점진적 회귀를 잡지만 측정 변동성으로 오탐이 발생할 수 있다. 통상 10~20% 경고, 30% 실패가 오탐과 미탐의 균형점이다.
정리
- Load / Stress / Spike / Soak은 각각 다른 질문에 답한다. 하나로 나머지를 대체할 수 없다.
- SLO는 p99 기반으로 정의하라. 평균 응답시간은 Long Tail을 숨긴다.
- VU 수는 Little’s Law로, 트래픽 비율은 Access Log로 계산하라. 임의로 설정하면 결과를 신뢰할 수 없다.
- 테스트 환경은 데이터 볼륨, JVM 설정, 네트워크 레이턴시 모두 Prod-like해야 한다.
- Baseline을 CI에 통합하면 회귀 발견 시점을 장애 후에서 코드 리뷰 단계로 앞당길 수 있다.
다음 글에서는 k6의 내부 아키텍처 — Goja 런타임과 Go goroutine 기반 VU 구현이 어떻게 수천 VU를 단일 프로세스에서 처리하는지 추적한다.