Skip to content

Bottleneck Identification

성능 테스트의 궁극적인 목적은 단순히 지표를 수집하는 것이 아니라, 시스템의 한계를 결정짓는 병목 지점(Bottleneck)을 찾아내고 이를 해소하는 것이다.

부하가 증가함에 따라 응답 시간은 비선형적으로 증가하면서 특정 시점에 도달하면 자원 포화로 인해 응답 시간이 급격히 치솟는데, 이 지점을 Knee Point이라고 한다.

graph TD
classDef point fill: #f96, stroke: #333, stroke-width: 2px, color: #000
A[부하 증가] --> B{자원 사용률 확인}
B -- 정상 --> C[응답 시간 선형 유지]
B -- 임계치 도달 --> D[Knee Point 발생]:::point
D --> E[응답 시간 기하급수적 증가]
E --> F[시스템 마비 및 요청 거절]
  • 처리량 한계: Knee Point 이후에는 부하를 늘려도 처리량이 더 이상 증가하지 않음
  • 대기 큐 발생: 처리를 기다리는 요청이 쌓이면서 지연 시간(Latency) 폭증

물리적 자원의 한계로 인해 발생하는 병목 현상이다.

자원 유형현상원인진단 방법
CPU SaturationCPU 사용률이 일정 비 이상 지속 및 Context Switching 비용 증가복잡한 연산, 과도한 직렬화/역직렬화, 빈번한 GC 발생top, vmstat (User/System 비중 확인)
Memory & GCHeap 부족으로 인한 빈번한 Full GC 및 STW 지연메모리 누수, 부적절한 캐시 사용, 과도한 객체 생성jstat, GC 로그 (회수 패턴 확인)
Disk & Network I/OI/O Wait 수치 상승 및 데이터 입출력 속도 저하대량 로그 기록, DB 디스크 한계, 네트워크 대역폭 포화iostat, sar (I/O 대기 시간 측정)

애플리케이션 설정이나 코드 로직에서 발생하는 병목 현상이다.

DB 커넥션은 한정된 자원이므로, 이를 효율적으로 관리하지 못하면 시스템 전체가 마비된다.

  • HikariCP 고갈: 비동기 처리 시 동시 실행 제어가 없으면 순식간에 커넥션이 바닥남
  • 커넥션 대기: 요청이 커넥션을 얻기 위해 getConnection() 단계에서 블로킹됨
  • 해결: 적절한 Pool Size 설정 및 트랜잭션 범위 최소화
  • Tomcat 스레드 고갈: 외부 API 호출 대기 시간이 길어지면 HTTP 스레드 모두 점유
  • 블로킹 전파: 특정 서비스의 지연이 전체 시스템의 요청 수용 능력 저하로 이어짐
  • 해결: 비동기 논블로킹 아키텍처 도입 또는 가상 스레드 활용

실제 결제 시스템 구축 과정에서 마주친 대표적인 병목 지점과 해결 방법이다.

신규 배포 직후 트래픽 유입 초기에 JDBCConnectionException: Connection is not available, request timed out 에러가 발생하는 현상이다.

  • 원인: JVM의 지연 클래스 로딩(Lazy Loading)과 커넥션 점유의 상관관계가 핵심
    • 요청 처리를 위해 DB 커넥션을 풀에서 먼저 획득
    • 커넥션을 점유한 상태에서 Jackson, Hibernate, QueryDSL 등의 클래스를 동적으로 로딩
    • 클래스 로딩 및 인터프리터 실행이 CPU 집약적이므로 커넥션 반환이 지연
    • 후속 요청들이 커넥션 대기열에 쌓이다가 타임아웃 초과
  • 증상
    • Connection Usage Time: 평소 5ms 미만 → 배포 직후 1s~1.5s로 급증
    • Connection Acquire Time: 평소 1ms 미만 → 타임아웃 임계치(10s)까지 도달
    • CPU 사용률 및 Young GC 발생 빈도 급증
  • 해결: JVM 웜업(Warm-up)을 통해 실제 트래픽 유입 전에 클래스 로딩과 JIT 컴파일을 사전 완료하고, Kubernetes startupProbe로 웜업 완료 전까지 트래픽 유입 차단

Java 21 가상 스레드 사용 시 특정 상황에서 플랫폼 스레드가 고정되어 확장성이 저하되는 현상이다.

  • 원인: JDBC 드라이버(MySQL Connector/J 8.x) 내 synchronized 블록 사용
  • 증상: I/O 작업 시 가상 스레드가 캐리어 스레드를 반납하지 못하고 함께 블로킹
  • 해결: synchronizedReentrantLock으로 교체된 최신 드라이버 버전으로 업데이트

단순히 @Async를 사용하는 것만으로는 병목을 완전히 해결할 수 없다.

  • 현상: SimpleAsyncTaskExecutor의 동시 실행 제한 도달 시 호출 스레드가 블로킹
  • 결과: 비동기로 호출했음에도 불구하고 HTTP 응답 시간이 외부 API 지연 시간 동기화
  • 해결: LinkedBlockingQueue를 이용한 명시적 버퍼링과 백그라운드 워커 구조 채택

병목 지점을 체계적으로 찾기 위해 다음 항목을 점검한다.

  • 시스템 부하가 증가할 때 CPU 사용률이 임계치에 도달하는가?
  • DB 커넥션 풀의 활성 상태와 대기 큐 크기가 안정적인가?
  • 특정 API의 응답 시간이 외부 의존성(Third-party API)에 비례하여 늘어나는가?
  • GC 발생 빈도와 Stop-The-World 시간이 서비스 요구사항을 충족하는가?
  • 가상 스레드 사용 시 Pinning 현상으로 인해 캐리어 스레드가 고갈되지 않는가?

Last updated:

Performance Engineering