이 글에서 얻는 것

  • “부하 테스트를 한다”를 도구 실행이 아니라, 목표 설정 → 시나리오 → 측정 → 병목 분석 → 재검증 루프로 설계할 수 있습니다.
  • 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 로그/슬로우 로그로 원인을 좁혀보는 연습해보기
  • 개선을 하나만 적용(예: 인덱스 추가)한 뒤 같은 시나리오로 재테스트해 “정말 좋아졌는지” 확인해보기