이 글에서 얻는 것
- “부하 테스트를 한다”를 도구 실행이 아니라, 목표 설정 → 시나리오 → 측정 → 병목 분석 → 재검증 루프로 설계할 수 있습니다.
- p95/p99 레이턴시, 에러율, 포화(saturation)를 함께 보고 “어디가 병목인지”를 분류할 수 있습니다.
- 테스트가 잘못 설계돼서(캐시 워밍업/데이터/환경) 결과가 왜곡되는 흔한 함정을 피할 수 있습니다.
0) 부하 테스트는 ‘튜닝’이 아니라 ‘사실 확인’이다
부하 테스트의 목적은 보통 세 가지입니다.
- 현재 용량(capacity)에서 SLO를 만족하는가?
- 어느 지점에서 무너지는가(breakpoint)?
- 병목이 어디인가(DB/락/커넥션 풀/CPU/GC/네트워크)?
즉, 먼저 “어디까지 되는지”를 알고, 그 다음에 튜닝을 합니다.
1) 테스트 종류를 구분하면 계획이 쉬워진다
- Load test: 예상 트래픽에서 SLO를 만족하는지
- Stress test: 한계를 넘어가면 어떻게 무너지는지(그레이스풀?)
- Spike test: 갑자기 트래픽이 튀면 버티는지(캐시/큐/오토스케일)
- Soak test: 오랜 시간(수 시간~수일) 돌리면 누수/성능 저하가 생기는지
“어떤 테스트인지”가 정해지면, 지표/시나리오/기간이 자연스럽게 결정됩니다.
2) 목표(SLI/SLO)를 먼저 고정하라
대체로 아래 3가지는 필수입니다.
- 레이턴시: p95/p99(엔드포인트별)
- 에러율: 5xx(그리고 의미 있는 4xx)
- 처리량/포화: CPU, 메모리, 스레드, 커넥션 풀, 큐 길이 등
SLO 예시(개념):
- “주문 조회 API p95 < 200ms, p99 < 500ms, 5xx < 0.1%”
SLO가 없으면 부하 테스트는 “느린데요?”로 끝나기 쉽습니다.
3) 시나리오 설계: 실제 사용자 경로를 모델링
좋은 시나리오의 조건:
- 핵심 플로우(주요 API 3~5개)를 포함한다
- 요청 비율(읽기/쓰기, 엔드포인트 mix)을 현실적으로 둔다
- think time(사용자 간격)과 데이터 분포를 반영한다
자주 빠뜨리는 것:
- 캐시 히트/미스 비율(전부 히트면 너무 낙관적, 전부 미스면 너무 비관적)
- DB 데이터 규모(작으면 인덱스/플랜 문제가 안 드러남)
- 워밍업(초반엔 JIT/캐시/커넥션 풀 때문에 결과가 흔들림)
4) 환경: 프로덕션과 비슷해야 의미가 있다
가능하면:
- 비슷한 인프라(스펙/네트워크/DB)
- 비슷한 데이터 크기
- 같은 설정(커넥션 풀/캐시/타임아웃)
가 필요합니다.
테스트 환경이 너무 약하면 “환경 병목”을 애플리케이션 문제로 착각할 수 있습니다.
5) 도구(k6) 예시: 램프업 + 체크
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 50 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('https://api.example.com/orders');
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1);
}
포인트:
- 램프업(점진 증가) 없이 바로 폭주하면 “워밍업/초기화 비용”이 결과를 더럽힐 수 있습니다.
- 실패율(threshold)을 함께 보면 “언제부터 실패가 늘어나는지”를 빠르게 잡을 수 있습니다.
6) 측정: 부하 테스트는 ‘관측성 테스트’이기도 하다
테스트 동안 최소한 아래를 같이 봅니다.
- 애플리케이션: 레이턴시 분포(p95/p99), 에러율, 스레드 풀/큐
- JVM: GC pause, 힙 사용량, 할당률
- DB: 쿼리 시간/슬로우 로그, 락 대기, 커넥션 수
- 캐시: hit ratio, latency, eviction
- 인프라: CPU/메모리/네트워크/디스크, 오토스케일 이벤트
가능하면 traceId/트레이싱으로 “느린 요청의 경로”를 함께 봐야 원인 규명이 빨라집니다.
7) 튜닝 루틴: 원인별로 접근하라
대표 병목과 흔한 대응:
- DB가 느리다 → 인덱스/플랜/슬로우 쿼리/락/커넥션 풀부터
- CPU가 포화 → 핫 코드 프로파일링, 불필요한 변환/직렬화/암호화 확인
- GC가 길다 → 할당률/객체 생존율/캐시 구조 확인(플래그는 마지막)
- 스레드/큐가 막힌다 → 타임아웃/벌크헤드/외부 의존성 고립, 풀 크기 근거화
“풀 크기만 키우기”는 많은 경우 문제를 늦출 뿐, 근본 원인을 해결하지 못합니다.
8) 결과를 남겨라(다음 테스트를 위해)
부하 테스트는 한 번으로 끝나지 않습니다.
- 목표(SLO), 시나리오, 데이터 셋, 환경, 설정
- 결과(레이턴시/에러율/포화), 병목 원인
- 변경 사항과 재검증 결과
를 짧게라도 남기면 “성능 개선”이 반복 가능한 작업이 됩니다.
연습(추천)
- 핵심 API 3개를 골라 “요청 비율/데이터 분포/캐시 히트율 가정”을 문서로 만들고 부하 테스트를 설계해보기
- p95/p99가 튀는 구간에서 스레드 덤프/GC 로그/슬로우 로그로 원인을 좁혀보는 연습해보기
- 개선을 하나만 적용(예: 인덱스 추가)한 뒤 같은 시나리오로 재테스트해 “정말 좋아졌는지” 확인해보기
💬 댓글