← all posts
DEV 2026.05.02 · 12 min read Intermediate

성능 튜닝은 과학이다 — 하나씩, 측정하고, 번역하라

변수 격리 원칙부터 p99 비즈니스 번역, CI 자동화, 실전 DB 커넥션 고갈 케이스까지, 성능 튜닝을 반복 가능한 과학으로 만드는 방법을 추적한다.


네 개의 설정을 동시에 바꿔서 p99가 절반으로 줄었다. 좋은 일이다. 그런데 어느 설정이 효과 있었는가? 6개월 뒤 메모리 문제가 생겨 롤백해야 할 때, 무엇을 되돌려야 하는가?

하나씩 바꾸는 이유

성능 튜닝이 “경험과 감”의 영역으로 여겨지는 이유는 단순하다 — 여러 변수를 동시에 건드리면 인과관계를 알 수 없다. 변수 격리 원칙(Isolation of Variables) 은 단순하지만 실무에서 가장 많이 위반된다.

시스템은 비선형이다. DB 커넥션 풀 확장 효과와 캐시 TTL 증가 효과를 따로 측정하면 각각 -150ms, -100ms지만, 동시에 적용하면 -400ms가 될 수도 있고 -180ms에 그칠 수도 있다. 경합(contention) 패턴이 바뀌거나 리소스 한계가 다른 곳으로 이동하기 때문이다.

올바른 흐름은 단순하다: 가설 수립 → Baseline 측정 → 단 하나의 변경 → 재측정 → 기록 → 다음 가설. 이 사이클을 반복하면 6개월 뒤에도 “왜 이 설정인가”에 답할 수 있다.

좋은 가설의 조건

“APM 분석 결과 DB I/O 대기 시간이 전체의 60%를 차지한다. 커넥션 풀을 30에서 50으로 늘리면 p99가 200ms 이상 감소할 것이다.” — 증거 기반, 구체적, 측정 가능. “성능이 느린 것 같다, 캐시를 크게 해보자”는 가설이 아니다.

p99를 비즈니스 언어로 번역하라

개발팀은 “p99 550ms 개선”이라고 말한다. 경영진은 “그래서 매출이 얼마나 늘어요?”라고 묻는다. 이 간극이 메워지지 않으면 성능 개선은 개발팀 내부의 만족으로 끝난다.

번역의 핵심은 tail latency가 사용자 경험을 결정한다는 사실을 수치로 연결하는 것이다. 평균 응답시간이 250ms라도 p99가 3,200ms라면, 100명 중 1명은 주문 버튼을 누르고 3초 넘게 기다린다. Amazon과 Google의 연구는 응답시간 100ms 단축이 전환율 약 1% 향상과 연관됨을 보여준다.

p99 3,200ms → 720ms는 25배 100ms 단축이다. 전환율 +25%가 월간 사용자 100만 명과 만나면 추가 전환 25만 건, 평균 주문액 5만 원 기준으로 월 125억 원 규모의 임팩트가 된다. 물론 이 수치에는 “응답시간-전환율 관계가 우리 서비스에도 적용된다”는 가정이 있다. 가정은 명시해야 한다. 하지만 가정을 명시한 채로도 이 번역은 경영진이 우선순위를 결정하는 데 필요한 언어다.

성능 회귀는 자동으로 막아라

힘들게 튜닝한 성능은 다음 배포에서 조용히 무너진다. 합리적인 이유로 캐시 설정을 바꾼 신입 개발자, 타임아웃 값을 수정한 핫픽스 — 각각의 변경은 이유가 있지만 전체 성능 프로파일에 미치는 영향은 측정되지 않는다.

k6의 thresholds는 이 문제를 CI 단계에서 차단한다.

export const options = {
  thresholds: {
    'http_req_duration': [
      'p(99)<715',   // baseline(650ms) + 10% 여유
      'p(95)<483',
    ],
    'http_req_failed': [
      'rate<0.0012',
    ],
  },
};

Threshold를 초과하면 k6가 non-zero로 종료되고, GitHub Actions가 빌드를 실패시킨다. Slack 웹훅으로 즉시 알림이 간다. 탐지 지연이 2-3주에서 1시간 이내로 줄어든다.

트레이드오프

경보 기준이 너무 엄격하면 거짓 경보가 잦아 개발자가 알림을 무시하기 시작한다. 결제 API처럼 크리티컬한 엔드포인트는 baseline 그대로, 일반 조회 API는 +15~20% 여유를 두는 식으로 API 중요도별로 임계값을 다르게 설정하는 것이 현실적이다.

실전 케이스 — DB 커넥션 고갈

이론이 실전에서 어떻게 작동하는지 하나의 스토리로 살펴보자.

대규모 프로모션 당일, 오후 3시. p99가 3,200ms, 에러율 8.2%로 치솟았다. 에러 메시지는 Cannot get a connection, pool error Timeout waiting for idle object. 커넥션 풀 고갈이다.

1단계: 부하 재현. k6로 동시 사용자 300명을 시뮬레이션해 문제를 통제된 환경에서 재현한다. “느린 것 같다”에서 “p99 3,200ms, 에러율 8.2%“로 수치화된다.

2단계: USE 방법론. CPU 45%, 메모리 60%, 네트워크 정상 — 인프라 레벨은 문제없다. APM 도구를 열면 DB 커넥션 획득 대기가 전체 응답시간의 98.3%를 차지한다. 30개 커넥션이 모두 사용 중이고, 280개 요청이 큐에서 기다리고 있다.

3단계: 근본 원인. 슬로우 쿼리 로그를 보면 orders 테이블에 인덱스가 없어 100만 행 full scan이 0.7초씩 걸린다. 커넥션 하나가 0.7초를 붙잡고 있으니, 30개 커넥션이 21초짜리 병목을 만들고 있었다.

4단계: 하나씩 해결. 인덱스 추가 → p99 1,800ms (-44%). 커넥션 풀 30→80 → p99 850ms (-53%). 추가 인덱스 → p99 720ms (-12%). 각 단계마다 k6를 재실행해 기여도를 측정했다. 총 3시간, 목표 달성.

정리

  • 변수를 격리하지 않으면 효과 측정이 불가능하고, 롤백도 불가능해진다.
  • p95/p99는 사용자 경험을 결정한다. 평균은 문제를 숨긴다.
  • 성능 수치는 비즈니스 언어로 번역할 때 의사결정에 쓰인다.
  • k6 thresholds를 CI에 통합하면 성능 회귀를 코드 리뷰 단계에서 차단할 수 있다.

성능 튜닝은 일회성 이벤트가 아니다. 가설을 세우고, 하나씩 실험하고, 기록을 남기는 반복 사이클이 쌓일 때, “왜 이 설정인가”라는 질문에 6개월 뒤에도 답할 수 있다.