들어가며
"느리다"는 리포트를 받았을 때, 어디서부터 봐야 할까? 로그? APM? 프로파일러?
가장 빠르고 확실한 방법 중 하나는 Thread Dump다. 특히 Kubernetes Pod 환경에서, APM이 없거나 설정이 안 된 상태에서 즉시 할 수 있는 것이 Thread Dump다.
이 글에서는 JVM 기반 애플리케이션(Spring Boot 등)의 성능 병목을 Thread Dump로 찾는 실전적인 방법을 정리한다.
Thread Dump란?
Thread Dump는 JVM의 모든 스레드가 "지금 이 순간" 무엇을 하고 있는지를 스냅샷으로 찍은 것이다. 각 스레드의 호출 스택(Stack Trace)이 포함되어 있어, 어떤 메서드에서 시간을 소비하고 있는지 알 수 있다.
Pod 환경에서 Thread Dump 뜨기
방법 1: jstack
# Pod 내부에 접속
kubectl exec -it <pod-name> -- /bin/bash
# Java PID 확인
jps
# 또는
ps aux | grep java
# Thread Dump 생성
jstack <pid> > /tmp/thread-dump.txt
Bash
복사
방법 2: kill -3
kill -3 <pid>
Bash
복사
표준 출력으로 Thread Dump가 출력된다. 컨테이너 로그에서 확인할 수 있다.
팁: 여러 번 떠라
한 번의 Thread Dump는 "순간"만 보여준다. 3~5초 간격으로 3번 이상 떠야 "지속적으로 같은 위치에 멈춰 있는 스레드"를 찾을 수 있다.
스택 트레이스 읽는 법
핵심: BLOCKED / WAITING 스레드를 찾아라
•
RUNNABLE: 정상적으로 실행 중
•
BLOCKED: 다른 스레드가 잡고 있는 락을 기다리는 중
•
WAITING / TIMED_WAITING: 어떤 조건을 기다리는 중
패턴 인식
패턴 1: 같은 메서드에 여러 스레드가 BLOCKED
여러 스레드가 같은 메서드에서 BLOCKED 상태라면, 그 메서드에 동기화 병목이 있다.
패턴 2: 예외 처리에 스레드가 몰려 있음
fillInStackTrace 같은 예외 관련 메서드에 많은 스레드가 있다면, 예외가 대량 발생하고 있을 수 있다. 예외 생성 자체가 비용이 높은 작업이다(스택 트레이스 수집).
패턴 3: DB 커넥션 대기
getConnection 관련 메서드에서 WAITING이 많다면 커넥션 풀이 부족하다.
예외 누적 병목 — 흔하지만 간과되는 패턴
Thread Dump에서 자주 발견되는 패턴 중 하나가 "예외 누적"이다.
로직 내부에서 정상 흐름의 일부로 예외를 사용하는 경우, 데이터 양이 많아지면 예외 생성 비용이 누적된다. 특히 중첩 구조(예: 10개 필드 × 10개 하위 필드 = 100번의 예외 생성)에서는 기하급수적으로 증가한다.
해결 방향
•
예외를 "정상 흐름 제어"에 사용하지 말라
•
문자열 파싱 대신 타입 안전한 방식(클래스 필드 이름, enum 등)을 사용하라
•
불가피한 경우 스택 트레이스를 생략하는 경량 예외를 사용하라
Thread Dump 이후: 최적화 방향
Pod 스펙 튜닝 — 고성능 소수 vs 저성능 다수
같은 총 리소스(예: 16 CPU, 32GB RAM)를 어떻게 배분할 것인가?
전략 | 장점 | 단점 |
저성능 × 다수 (e.g. 0.5CPU × 32개) | 하나가 죽어도 영향 적음 | JVM 오버헤드 비율 높음, GC 비효율 |
고성능 × 소수 (e.g. 2CPU × 8개) | JVM 오버헤드 비율 낮음, G1GC 효과적 | 하나가 죽으면 영향 큼 |
일반적으로 JVM 애플리케이션은 어느 정도의 메모리가 확보되어야 G1GC가 효과적으로 동작한다. Pod 스펙이 너무 작으면 GC 오버헤드가 상대적으로 커진다.
Bulk 처리
대량 데이터 처리 시, 개별 insert를 Bulk Operations로 전환하면 네트워크 라운드트립이 대폭 줄어든다.
Update/Insert 분리
모든 요청을 upsert로 처리하면, 이미 존재하는 데이터에 대해서도 불필요한 존재 여부 확인이 발생한다. 신규 데이터와 기존 데이터를 분리하면 이 오버헤드를 제거할 수 있다.
결론
Thread Dump는 성능 문제의 "어디를 봐야 하는지"를 빠르게 알려주는 도구다. APM이 없어도, 프로파일러가 없어도, kubectl과 jstack만 있으면 시작할 수 있다.
핵심은:
1.
여러 번 떠서 지속적인 병목을 찾고
2.
BLOCKED/WAITING 스레드를 우선 확인하고
3.
예외 누적 패턴을 놓치지 말 것
병목을 찾은 뒤에는 Pod 스펙, Bulk 처리, 쿼리 분리 등 상황에 맞는 최적화를 적용하면 된다.