처음 부하테스트를 할 때 JMeter로 어디까지 봐야 하는가
처음 부하테스트를 시작하면 보통 가장 먼저 이런 질문을 하게 된다.
- Thread는 몇으로 잡아야 할까?
- TPS는 어느 정도 나오면 괜찮은 걸까?
- 평균 응답시간만 보면 되는 걸까?
- 테스트를 돌렸는데 느려졌다. 이게 서버 문제일까, DB 문제일까?
기능 테스트는 보통 "정상 동작하는가"를 확인하는 데 초점이 있다. 반면 부하테스트는 여러 사용자가 동시에 들어왔을 때 시스템이 얼마나 안정적으로 버티는가를 보는 작업에 가깝다.
그래서 부하테스트는 단순히 숫자 하나를 보는 테스트가 아니다. 요청량이 늘어날수록 응답시간이 어떻게 바뀌는지, 에러가 언제부터 생기는지, 병목이 어디에서 시작되는지를 함께 봐야 의미가 있다.
이번 글은 처음 JMeter를 쓰는 사람을 기준으로 정리했다. 특정 회사 환경이나 특정 결과를 기준으로 설명하지 않고, 실제로 처음 테스트를 만들고 해석할 때 필요한 흐름 위주로 풀어보려고 한다.
왜 부하테스트를 먼저 해봐야 할까
API 하나가 로컬에서 잘 응답한다고 해서, 실제 운영 환경에서도 잘 버틴다고 볼 수는 없다. 특히 조회 API, 페이징 API, 정산성 API, 외부 연동 API는 사용자가 몰리는 순간 전혀 다른 모습을 보일 수 있다.
부하테스트를 하는 이유는 대체로 아래 질문에 답하기 위해서다.
- 동시에 몇 명 정도까지 안정적으로 처리할 수 있는가
- 초당 몇 건 정도를 감당할 수 있는가
- 응답속도가 일정하게 유지되는가
- 특정 부하 이상에서 에러가 발생하는가
- 성능이 무너질 때 병목이 서버인지, DB인지, GC인지 구분할 수 있는가
결국 핵심은 "얼마나 빠른가"보다 "어디까지 안정적인가" 에 가깝다.
부하테스트에서 가장 먼저 이해해야 하는 용어
JMeter를 처음 켜면 Thread, Ramp-up, Throughput, Error %, Aggregate Report 같은 용어가 한 번에 보인다. 처음에는 다 비슷해 보이지만, 실제 해석에서는 역할이 꽤 다르다.
TPS와 Throughput
TPS는 보통 초당 처리 건수라고 이해하면 된다.
JMeter에서는 Throughput 항목으로 보이는 경우가 많다.
Throughput: 120/sec
이 값은 초당 약 120개의 요청이 처리됐다는 뜻이다.
여기서 많이 헷갈리는 부분이 Thread 수 = TPS는 아니라는 점이다.
가상 사용자가 10명뿐이어도 응답이 매우 빠르면 한 사용자가 짧은 시간 안에 여러 번 요청을 보낼 수 있다.
반대로 Thread가 많아도 응답이 느리면 TPS는 기대만큼 올라가지 않을 수 있다.
TPS에는 아래 요소가 같이 영향을 준다.
응답속도
Thread 수
Ramp-up 설정
Loop Count
Think Time
서버 성능
DB 성능
JMeter 실행 PC 성능
네트워크 상태
즉 TPS는 단순히 "사용자 수"만의 결과가 아니라, 전체 시스템 상태를 압축해서 보여주는 숫자에 가깝다.
Latency와 Response Time
응답시간은 요청을 보낸 뒤 응답을 받을 때까지 걸린 시간이다.
JMeter에서는 보통 Average, Min, Max 같은 항목으로 먼저 보게 된다.
Average: 평균 응답시간
Min: 최소 응답시간
Max: 최대 응답시간
다만 평균만 보고 빠르다고 판단하면 위험하다.
실무에서는 평균보다 95%, 99% 구간을 더 중요하게 보는 경우가 많다.
예를 들어 아래와 같은 결과가 나왔다고 가정해보자.
Average: 25ms
95% Line: 280ms
99% Line: 1100ms
이 경우 평균은 좋아 보이지만, 일부 사용자는 1초 이상 대기하고 있다는 뜻이다. 그래서 부하테스트는 평균이 아니라 꼬리 구간이 얼마나 튀는지를 함께 봐야 한다.
Error %
Error %는 전체 요청 중 실패한 요청 비율이다.
Error %: 0.00%
이면 실패한 요청이 없다는 뜻이다.
대체로 아래 정도의 감각으로 보면 된다.
0% → 정상
1% 이상 → 원인 확인 필요
5% 이상 → 이미 한계를 넘었을 가능성 높음
조회 API라면 에러율이 조금만 올라가도 원인을 바로 확인하는 편이 좋고, 결제나 정산성 API라면 더 엄격하게 봐야 한다.
병목
병목은 전체 처리량이나 응답시간을 악화시키는 지점을 의미한다. 부하테스트의 핵심은 사실 TPS 숫자를 자랑하는 것이 아니라, 병목이 어디서 시작되는지 찾는 것이다.
대표적인 병목은 아래와 같다.
DB 쿼리 병목
DB 커넥션 풀 부족
Java GC 병목
CPU 병목
Thread Pool 병목
Lock 병목
외부 API 병목
네트워크 병목
JMeter 실행 PC 병목
JMeter를 어떻게 구성하면 되는가
처음 테스트를 만들 때는 복잡하게 시작할 필요가 없다. 가장 기본적인 구조는 아래 정도면 충분하다.
Test Plan
└─ Thread Group
├─ HTTP Header Manager
├─ HTTP Request
├─ Summary Report
├─ Aggregate Report
└─ View Results Tree
핵심은 Thread Group과 HTTP Request다.
Listener는 결과를 보는 용도이고, 실제 부하를 만드는 설정은 Thread Group 쪽에서 대부분 결정된다.
Thread Group 설정은 어떻게 이해하면 좋을까
부하테스트를 처음 할 때 가장 많이 건드리는 설정이 Thread Group이다. 여기서 중요한 것은 Thread 수를 무조건 크게 넣는 게 아니라, 실제 테스트 목적에 맞게 부하를 설계하는 것이다.
Number of Threads
Number of Threads는 가상 사용자 수라고 보면 된다.
Number of Threads: 50
이면 50명의 사용자가 테스트에 참여하는 것과 비슷한 상황을 만든다.
다만 이것이 "정확히 같은 시점에 50명이 동시에 한 번 요청한다"는 뜻은 아니다. 실제 시작 타이밍은 Ramp-up 설정에 따라 나뉜다.
Ramp-up period
Ramp-up period는 지정한 Thread를 몇 초 동안 나누어 시작할지를 의미한다.
Number of Threads: 50
Ramp-up period: 10
이면 10초 동안 50명의 사용자를 점진적으로 올린다는 뜻이다. 대략 0.2초마다 1명씩 진입하는 셈이다.
이 설정이 중요한 이유는, 처음부터 너무 짧게 잡으면 현실적인 부하라기보다 순간 폭격에 가까워지기 때문이다.
Ramp-up을 너무 짧게 잡으면 생기는 일
예를 들어 아래 설정은 거의 스파이크 테스트에 가깝다.
Number of Threads: 100
Ramp-up: 1초
이 경우 아래 현상이 한 번에 나타날 수 있다.
순간 CPU 급등
DB 커넥션 급증
Thread Pool 대기
응답시간 Max 급증
일부 요청 timeout
처음 부하테스트를 할 때는 이렇게 시작하기보다, 낮은 부하에서 천천히 올려보는 편이 해석하기 쉽다.
처음 잡기 쉬운 예시는 아래 정도다.
Threads 10 → Ramp-up 10초
Threads 30 → Ramp-up 20초
Threads 50 → Ramp-up 30초
Threads 100 → Ramp-up 60초
Loop Count
Loop Count는 각 Thread가 요청을 몇 번 반복할지를 의미한다.
Number of Threads: 50
Loop Count: 100
이면 대략 총 요청 수는 아래처럼 계산할 수 있다.
50 * 100 = 5000 요청
만약 Thread Group 안에 HTTP Request가 두 개 있다면 총 요청 수는 더 늘어난다.
50 * 100 * 2 = 10000 요청
기능 확인 단계에서는 Loop Count가 직관적이지만, 운영과 비슷한 부하를 길게 보려면 Duration 방식이 더 실용적인 경우가 많다.
Same user on each iteration
이 옵션은 같은 Thread가 반복 요청을 보낼 때 동일한 사용자 문맥을 유지할지 결정하는 옵션이다. 로그인 세션, 쿠키, 인증 흐름이 중요한 테스트라면 유지하는 편이 맞는 경우가 많다.
반대로 인증이 필요 없는 단순 조회 API 테스트라면 체감 차이가 크지 않을 수도 있다.
Loop Count보다 Duration이 실무에 더 잘 맞는 이유
실무에서는 "몇 번 보냈는가"보다 "얼마나 안정적으로 유지되는가"가 중요한 경우가 많다. 그래서 아래처럼 Duration을 쓰는 방식이 더 자주 사용된다.
Number of Threads: 50
Ramp-up: 30초
Loop Count: Infinite
Duration: 300초
이렇게 하면 5분 동안 계속 요청을 보내게 된다.
짧은 테스트는 초반 워밍업 구간 때문에 결과가 흔들릴 수 있다. 특히 아래 요소들은 테스트 초반 수치를 왜곡하기 쉽다.
JVM 워밍업
DB 버퍼 캐시
커넥션 풀 초기화
JIT 컴파일
OS 캐시
그래서 처음 기준을 잡을 때는 아래 정도를 많이 권장한다.
Warm-up: 1~2분
실측 구간: 3~5분 이상
HTTP Request는 무엇을 맞춰야 할까
HTTP Request에는 실제 테스트할 API 정보를 넣는다. 여기서 중요한 것은 JMeter 설정을 복잡하게 꾸미는 것이 아니라, 실제 클라이언트 요청과 최대한 비슷하게 맞추는 것이다.
주로 확인하는 항목은 아래와 같다.
Protocol: http 또는 https
Server Name or IP: 서버 주소
Port Number: 포트
Method: GET / POST / PUT / DELETE
Path: API 경로
Parameters 또는 Body Data
만약 JSON POST 요청이라면 Body Data에 JSON을 넣고, Header Manager에 헤더를 맞춰준다.
Content-Type: application/json
Accept: application/json
인증이 필요한 API라면 아래도 같이 확인해야 한다.
Authorization: Bearer 토큰
Cookie
API Key
처음 작성할 때는 View Results Tree를 켜두고 요청 Body, 응답 코드, 응답 Body가 기대한 값과 맞는지부터 먼저 확인하는 편이 좋다.
Listener는 많이 켜두면 안 된다
JMeter를 처음 쓰면 화면으로 결과를 많이 보고 싶어서 Listener를 이것저것 추가하게 된다. 그런데 Listener가 많아질수록 JMeter 자체가 느려질 수 있다.
특히 View Results Tree는 요청과 응답을 메모리에 계속 들고 있기 때문에, 실제 부하 측정 구간에서는 꺼두는 편이 좋다.
테스트 작성 중에 유용한 Listener
초기 설정을 확인할 때는 아래 Listener가 도움이 된다.
View Results Tree
Summary Report
Aggregate Report
Graph Results
실제 부하를 측정할 때 권장하는 Listener
실제 부하 측정 구간에서는 아래 정도만 두는 편이 무난하다.
Summary Report
Aggregate Report
필요하면 나중에 HTML Report를 따로 생성해서 보는 방식이 더 낫다.
Summary Report와 Aggregate Report는 어떻게 봐야 할까
부하테스트를 처음 하면 제일 먼저 평균 응답시간만 보게 된다. 그런데 실제로 더 중요한 것은 평균보다 분포와 흔들림이다.
Summary Report에서 볼 것
Summary Report에서는 아래 항목을 먼저 확인하면 된다.
# Samples : 총 요청 수
Average : 평균 응답시간
Min : 최소 응답시간
Max : 최대 응답시간
Std. Dev. : 응답시간 편차
Error % : 에러율
Throughput : 초당 처리량
Received KB/sec: 초당 수신 데이터량
Sent KB/sec : 초당 송신 데이터량
Avg. Bytes : 평균 응답 크기
해석은 대체로 이렇게 가져가면 된다.
Average 낮음 → 평균적으로 빠름
Max 높음 → 일부 요청이 크게 튐
Std. Dev 높음 → 응답속도가 불안정함
Error % 높음 → 실패 발생
Throughput 증가 → 처리량 증가
Throughput 정체/감소 → 병목 가능성
Aggregate Report에서 볼 것
Aggregate Report는 분포를 보기 더 좋다. 특히 아래 항목은 꼭 같이 보는 편이 좋다.
Average
Median
90% Line
95% Line
99% Line
Min
Max
Error %
Throughput
예를 들어 아래와 같은 결과를 보자.
Average: 45ms
95% Line: 220ms
99% Line: 980ms
이 수치는 "평균은 괜찮지만, 일부 요청은 꽤 오래 걸린다"는 신호다. 즉 평균이 낮다고 바로 안심할 수는 없다.
View Results Tree는 언제 보는가
View Results Tree는 디버깅용이다.
테스트 작성 중에는 매우 유용하지만, 부하 측정 도중에는 오히려 방해가 된다.
이 화면에서 주로 보는 것은 아래다.
Request Header
Request Body
Response Code
Response Body
Response Time
Error Message
즉 이 Listener는 "성능 측정"보다 "요청이 제대로 나가고 있는가"를 확인하는 데 더 적합하다.
처음 하는 사람에게 권하는 테스트 순서
처음부터 100 Thread로 시작하면 숫자는 많이 나오지만, 오히려 원인을 해석하기 어려워진다. 그래서 반드시 낮은 부하에서 시작해서 단계적으로 올리는 편이 좋다.
1단계. 기능 확인
Threads: 1
Ramp-up: 1초
Loop Count: 1
이 단계에서는 아래만 확인해도 충분하다.
응답코드 200
응답 Body 정상
Header 정상
인증 정상
DB 데이터 정상
2단계. 기준선 테스트
Threads: 10
Ramp-up: 10초
Duration: 3분
이 단계의 목적은 낮은 부하에서 기본 응답속도와 처리량을 확인하는 것이다. 이 값을 기준선으로 잡아두면, 이후 30, 50, 80 Thread로 올렸을 때 무엇이 달라졌는지 비교하기 쉬워진다.
3단계. 일반 부하 테스트
Threads: 30
Ramp-up: 20초
Duration: 3분
또는
Threads: 50
Ramp-up: 30초
Duration: 5분
이 구간에서는 보통 실제 사용자 상황과 비슷한 흐름을 가정해 응답시간 안정성과 Throughput 증가 여부를 본다.
4단계. 고부하 테스트
Threads: 80
Ramp-up: 40초
Duration: 5분
또는
Threads: 100
Ramp-up: 60초
Duration: 5분
이 단계부터는 응답시간의 꼬리 구간, DB 병목, CPU 포화, GC 영향이 눈에 띄게 드러나는 경우가 많다.
5단계. 순간 폭격 테스트
Threads: 100
Ramp-up: 1초
Loop Count: 100
이 테스트는 실제 사용자 패턴이라기보다, 갑작스러운 유입이 왔을 때 시스템이 얼마나 버티는지 보는 용도에 가깝다.
예를 들면 아래를 확인할 수 있다.
갑자기 트래픽이 몰렸을 때 서버가 버티는가
커넥션 풀이 고갈되는가
DB 쿼리가 쌓이는가
에러가 발생하는가
결과는 반드시 표로 남겨야 한다
부하테스트는 한 번 돌리고 끝나는 작업이 아니라, 개선 전후를 비교하는 작업에 가깝다. 그래서 결과를 기록하지 않으면 다음 테스트가 사실상 무의미해진다.
아래처럼 간단한 표만 있어도 충분하다.
| 단계 | Threads | Ramp-up | Duration | Avg | 95% | 99% | Max | TPS | Error | CPU | DB 상태 | GC 상태 |
|------|---------|---------|----------|-----|-----|-----|-----|-----|-------|-----|---------|---------|
| 1 | 10 | 10s | 3m | | | | | | | | | |
| 2 | 30 | 20s | 3m | | | | | | | | | |
| 3 | 50 | 30s | 5m | | | | | | | | | |
| 4 | 80 | 40s | 5m | | | | | | | | | |
| 5 | 100 | 60s | 5m | | | | | | | | | |
이 표가 있으면 "인덱스를 추가한 뒤 95% 응답시간이 얼마나 줄었는가", "GC 튜닝 전후로 Max가 얼마나 안정됐는가" 같은 비교가 쉬워진다.
TPS는 어느 정도면 좋은가
이 질문은 정말 자주 나오지만, 사실 절대값으로 답하기 어렵다. TPS는 서비스 성격, 쿼리 무게, 응답 크기, 인증 방식, 캐시 사용 여부에 따라 크게 달라진다.
그래서 "몇 TPS 이상이면 무조건 좋다"라고 말하는 방식보다는, 현재 API 특성에 비해 합리적인 수준인가를 보는 편이 낫다.
단순 조회 API
예를 들어 PK 기준 단건 조회나 캐시 기반 조회처럼 가벼운 API는 수백 TPS에서 수천 TPS까지도 나올 수 있다.
예: 단건 조회, 캐시 조회, 단순 SELECT
일반적인 DB 조회 API
조건 검색, 정렬, 페이징이 들어간 일반 조회 API는 그보다 낮게 나오는 것이 자연스럽다.
예: WHERE 조건 조회, 간단한 JOIN, 정렬, 페이징
무거운 조회 API
COUNT, SUM, GROUP BY, 여러 JOIN이 같이 들어가는 API는 TPS가 상대적으로 낮더라도 이상한 것이 아닐 수 있다.
예: 집계, 다중 JOIN, 정렬, 복합 검색 조건
결제/정산성 API
이쪽은 TPS보다 안정성이 더 중요하다. 조금 느려도 에러 없이 정합성을 지키는 것이 훨씬 중요하다.
그래서 이런 API는 아래 기준을 더 먼저 본다.
Error 0%
데이터 정합성 유지
중복 처리 방지
95%, 99% 응답시간 안정
DB Lock 없음
좋은 결과와 나쁜 결과는 어떻게 구분할까
처음 보면 숫자가 많아서 복잡해 보이지만, 사실은 아래 흐름만 보면 된다.
좋은 상태
Error % = 0%
Average 응답시간 안정
95% 응답시간 안정
99% 응답시간 과도하게 튀지 않음
Threads 증가 시 Throughput도 증가
DB에 쿼리가 쌓이지 않음
Full GC 없음
CPU가 계속 100%에 붙지 않음
병목이 시작되는 상태
Threads를 올렸는데 TPS가 더 이상 증가하지 않음
Average 증가
95% / 99% 증가
Max 응답시간 급증
DB Query가 쌓임
CPU가 높아짐
GC 시간 증가
한계를 넘은 상태
TPS 감소
Average 급증
95% / 99% 급증
Max가 수초 이상 튐
Error 발생
Timeout 발생
DB Lock 발생
Full GC 발생
즉 부하를 올렸을 때 Throughput은 더 이상 오르지 않는데 응답시간과 에러만 나빠진다면, 이미 병목이 시작된 것이다.
DB 병목은 어떻게 확인할까
부하테스트를 하면서 JMeter 숫자만 보는 것은 절반만 보는 것이다. 실제로는 DB 상태를 반드시 같이 봐야 한다.
MySQL이나 MariaDB 계열이라면 아래 명령으로 현재 실행 중인 세션을 볼 수 있다.
SHOW FULL PROCESSLIST;
PROCESSLIST에서 주로 보는 컬럼
Id : DB 커넥션 ID
User : DB 사용자
Host : 접속한 클라이언트
db : 사용 중인 DB
Command : Query / Sleep 등 상태
Time : 현재 상태가 지속된 시간
State : 세부 상태
Info : 실행 중인 SQL
정상 상태
대체로 아래와 비슷하면 DB가 크게 막히지 않는 경우가 많다.
Sleep이 많음
Query가 잠깐 보였다가 사라짐
Time이 0~1초 수준
Lock 관련 State 없음
DB 병목이 의심되는 상태
아래 State가 반복되거나 오래 유지되면 DB 병목 가능성이 있다.
Sending data
Creating sort index
Creating tmp table
Copying to tmp table
Waiting for table lock
Waiting for row lock
Opening tables
Statistics
여기서 중요한 것은 State 이름 자체보다, 이 상태가 짧게 지나가는지 오래 머무는지다.
실행 중인 쿼리는 설명용 샘플로 이렇게 볼 수 있다
Info 컬럼에 보이는 쿼리를 복사해서 원인을 분석할 수 있다.
아래는 설명용 샘플 쿼리다.
SELECT o.order_id,
o.user_id,
o.order_status,
o.created_at,
o.total_amount
FROM orders o
WHERE o.user_id = 1001
AND o.created_at BETWEEN '2026-04-01' AND '2026-04-24'
ORDER BY o.created_at DESC
LIMIT 20;
이 쿼리가 오래 걸린다면 그대로 EXPLAIN으로 확인해볼 수 있다.
EXPLAIN
SELECT o.order_id,
o.user_id,
o.order_status,
o.created_at,
o.total_amount
FROM orders o
WHERE o.user_id = 1001
AND o.created_at BETWEEN '2026-04-01' AND '2026-04-24'
ORDER BY o.created_at DESC
LIMIT 20;
EXPLAIN에서 볼 것
설명용 샘플 기준으로 보면 아래 컬럼이 중요하다.
type
possible_keys
key
rows
Extra
특히 아래는 주의해서 본다.
type = ALL → 풀스캔 가능성
key = NULL → 인덱스 미사용 가능성
rows 값이 과도하게 큼 → 읽는 데이터가 많을 가능성
Using filesort → ORDER BY 최적화 필요 가능성
Using temporary → 임시 테이블 사용 가능성
인덱스는 어떻게 봐야 할까
인덱스가 있다고 해서 무조건 해당 쿼리에 최적인 것은 아니다. 부하테스트 중 자주 보이는 오해 중 하나가 바로 이 부분이다.
테이블 인덱스는 아래처럼 확인할 수 있다.
SHOW INDEX FROM orders;
예를 들어 아래 같은 조회가 있다고 해보자.
SELECT o.order_id,
o.user_id,
o.created_at
FROM orders o
WHERE o.user_id = 1001
AND o.created_at BETWEEN '2026-04-01' AND '2026-04-24'
ORDER BY o.created_at DESC
LIMIT 20;
이 경우 단일 인덱스가 각각 따로 있는 것보다, 쿼리 흐름에 맞춘 복합 인덱스가 더 잘 맞을 수 있다.
CREATE INDEX idx_orders_user_created_at
ON orders (user_id, created_at);
복합 인덱스를 볼 때는 보통 아래 순서로 생각하면 이해가 쉽다.
동등 조건 컬럼
범위 조건 컬럼
정렬 컬럼
조인 컬럼
페이징 API는 왜 자주 느려질까
처음 부하테스트를 해보면 생각보다 페이징 API가 빨리 무너지는 경우가 많다. 이유는 API 한 번 호출이 실제로는 여러 쿼리를 유발하기 때문이다.
예를 들면 아래 흐름이 한 번에 나갈 수 있다.
목록 조회
COUNT 조회
SUM 조회
부가 정보 조회
즉 API는 1회 호출이지만, DB 입장에서는 부담이 훨씬 클 수 있다.
COUNT 쿼리
설명용 샘플로 보면 아래처럼 총 건수를 구하는 쿼리가 자주 붙는다.
SELECT COUNT(*)
FROM orders
WHERE user_id = 1001
AND order_status = 'COMPLETED';
이 쿼리가 매 요청마다 실행되면 페이지네이션 API는 생각보다 빨리 무거워진다.
실무에서는 아래 같은 방식으로 줄이는 경우가 많다.
첫 페이지에서만 COUNT 수행
검색 조건이 바뀔 때만 재조회
COUNT 결과 캐싱
정확한 총 개수 대신 다음 페이지 존재 여부만 제공
SUM 쿼리
합계도 마찬가지다.
SELECT SUM(total_amount)
FROM orders
WHERE user_id = 1001
AND created_at >= '2026-04-01';
이런 쿼리도 화면 진입마다 매번 수행하면 부담이 커진다.
OFFSET 기반 페이징
아래처럼 뒤 페이지로 갈수록 느려지는 패턴도 흔하다.
SELECT order_id,
created_at,
total_amount
FROM orders
ORDER BY created_at DESC
LIMIT 20 OFFSET 10000;
이 방식은 뒤로 갈수록 앞의 데이터를 건너뛰는 비용이 누적된다. 그래서 큰 트래픽이 있는 서비스에서는 커서 기반 페이징을 검토하는 경우가 많다.
예를 들면 아래처럼 마지막 기준값을 넘겨서 조회하는 방식이다.
SELECT order_id,
created_at,
total_amount
FROM orders
WHERE created_at < '2026-04-24 10:00:00'
ORDER BY created_at DESC
LIMIT 20;
서버 CPU는 어떻게 같이 봐야 할까
JMeter에서 응답시간이 늘었는데 원인이 서버 CPU인지, DB인지, GC인지 구분하려면 서버 상태를 같이 확인해야 한다.
Linux에서는 보통 아래 명령부터 많이 본다.
top
또는
htop
top에서 주로 보는 항목은 아래다.
CPU 사용률
load average
메모리 사용량
I/O wait
Java 프로세스 CPU
DB 프로세스 CPU
CPU 항목 해석
Linux 기준으로는 아래 정도로 이해하면 된다.
us : 사용자 영역 CPU
sy : 커널 영역 CPU
id : idle
wa : I/O wait
대략적인 해석은 아래와 같다.
us 높음 → 애플리케이션 코드나 DB 연산이 CPU 사용 중
sy 높음 → 커널, 네트워크, 시스템 콜 부담
wa 높음 → 디스크 I/O 대기
id 낮음 → CPU 여유 부족
load average
load average는 현재 시스템에 쌓여 있는 작업 대기 정도를 보여준다.
보통 CPU 코어 수와 비교해서 본다.
load average <= CPU 코어 수 → 대체로 안정
load average > CPU 코어 수 → 대기 발생 가능성
load average가 코어 수의 2배 이상 → 병목 가능성 높음
macOS에서 로컬 테스트할 때 주의할 점
macOS에서는 Linux처럼 wa 항목이 바로 보이지 않을 수 있다.
그래서 아래 명령을 함께 보는 편이 좋다.
top -o cpu
iostat -w 1
vm_stat 1
memory_pressure
무엇보다 로컬에서 JMeter, IDE, 애플리케이션 서버, DB를 모두 한 머신에서 같이 돌리면 결과가 쉽게 왜곡된다. 처음 연습 단계에서는 괜찮지만, 실제 기준값을 잡으려면 환경 분리가 훨씬 낫다.
JMeter 실행 PC
WAS 서버
DB 서버
vmstat은 왜 같이 보면 좋은가
Linux에서는 vmstat 1도 꽤 유용하다.
vmstat 1
여기서 자주 보는 항목은 아래다.
r : CPU 대기 프로세스 수
b : I/O 대기 프로세스 수
us : 사용자 CPU
sy : 시스템 CPU
id : idle
wa : I/O wait
해석은 아래 정도로 가져가면 된다.
r이 CPU 코어 수보다 큼 → CPU 대기
b가 계속 높음 → I/O 대기
wa가 높음 → 디스크 I/O 병목
us가 높음 → 애플리케이션/DB CPU 사용 많음
sy가 높음 → 커널/네트워크 부담
Java 서버라면 GC를 반드시 같이 봐야 한다
부하가 올라갈수록 Max 응답시간이 갑자기 튀는 경우가 있다. 이때 코드나 DB만 볼 것이 아니라 GC도 함께 확인해야 한다.
Java PID 확인
ps -ef | grep java
또는
jps -l
jstat으로 GC 확인
jstat -gcutil <PID> 1000
예를 들면 아래처럼 1초 간격으로 볼 수 있다.
jstat -gcutil 40253 1000
필요하면 30회만 확인할 수도 있다.
jstat -gcutil 40253 1000 30
jstat에서 특히 볼 것
보통 아래 컬럼을 주로 본다.
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
특히 O, FGC, FGCT, GCT가 중요하다.
O가 80% 이상 → 주의
O가 95% 이상 → 위험
O가 99% 근처 → Full GC 가능성 높음
FGC 증가 → 부하테스트 중 경계 신호
FGCT 증가 → Stop-the-world 시간 누적
GCT 빠르게 증가 → GC 부담 증가
정상 상태는 대체로 아래와 비슷하다.
YGC는 증가할 수 있음
YGCT 증가폭이 작음
FGC는 증가하지 않음
Old 영역이 계속 95% 이상 유지되지 않음
GCT 증가폭이 작음
반대로 아래처럼 보이면 GC 병목 가능성을 의심해볼 수 있다.
O 영역 95~99% 유지
FGC 증가
FGCT 증가
GCT 빠르게 증가
JMeter Max 응답시간 증가
GC 로그도 가능하면 남기는 편이 좋다
순간적인 jstat 관찰만으로는 부족할 수 있어서, 가능하면 GC 로그도 남기는 편이 좋다.
Java 8에서는 아래처럼 설정할 수 있다.
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
Java 9 이상에서는 아래처럼 쓸 수 있다.
-Xlog:gc*:file=/path/to/gc.log:time,uptime,level,tags
응답이 멈추거나 심하게 느리면 Thread Dump도 본다
느려지는 순간의 스레드 상태를 보면 병목이 어디에 걸려 있는지 더 명확해질 때가 많다.
jstack <PID> > thread.txt
한 번만 찍기보다 몇 초 간격으로 여러 번 찍는 편이 더 좋다.
jstack 40253 > thread1.txt
sleep 3
jstack 40253 > thread2.txt
sleep 3
jstack 40253 > thread3.txt
상태는 대체로 아래처럼 해석한다.
RUNNABLE : 실행 중 또는 CPU 사용 중
BLOCKED : Lock 대기
WAITING : 무기한 대기
TIMED_WAITING : 시간 제한 대기
예를 들어 아래처럼 보면 방향을 잡을 수 있다.
BLOCKED 많음 → synchronized, lock 병목 가능성
WAITING 많음 → 커넥션 풀, 큐, 외부 응답 대기 가능성
RUNNABLE 많음 + CPU 높음 → CPU 병목 가능성
socketRead 많음 → DB 또는 외부 API 응답 대기 가능성
getConnection 많음 → DB 커넥션 풀 부족 가능성
커넥션 풀 병목은 DB 병목과 어떻게 다를까
Spring Boot + HikariCP를 쓴다면 아래 설정을 자주 보게 된다.
spring.datasource.hikari.maximumPoolSize
spring.datasource.hikari.minimumIdle
spring.datasource.hikari.connectionTimeout
커넥션 풀 병목은 DB 병목과 비슷해 보이지만, 실제 양상은 조금 다르다.
커넥션 풀 병목 증상
DB PROCESSLIST는 한가함
그런데 API 응답은 느림
Thread Dump에 getConnection 대기 많음
Hikari timeout 로그 발생
예를 들면 이런 로그가 나온다.
Connection is not available, request timed out
DB 병목과의 차이
DB 병목은 보통 아래처럼 나타난다.
SHOW PROCESSLIST에 Query가 많이 보임
Sending data, Creating sort index, Lock wait가 오래 지속됨
DB CPU가 높음
반면 커넥션 풀 병목은 아래처럼 보인다.
SHOW PROCESSLIST에는 Query가 별로 없음
애플리케이션 Thread가 getConnection에서 대기
DB는 한가한데 API는 느림
즉 DB가 느린 것인지, DB에 가기도 전에 커넥션을 못 받아서 기다리는 것인지 분리해서 봐야 한다.
실제 부하테스트는 GUI보다 CLI가 낫다
GUI는 테스트를 만들고 확인할 때는 편하다. 하지만 실제로 부하를 측정할 때는 CLI가 훨씬 안정적이다.
jmeter -n -t test.jmx -l result.jtl -e -o report
각 옵션은 아래 의미다.
-n : Non-GUI 모드
-t : 테스트 파일
-l : 결과 파일
-e : HTML 리포트 생성
-o : HTML 리포트 출력 폴더
예를 들면 아래처럼 실행할 수 있다.
jmeter -n -t paging-test.jmx -l result_80.jtl -e -o report_80
이때 report_80 폴더는 비어 있어야 하고, 결과 파일도 덮어쓰지 않도록 관리하는 편이 좋다.
테스트 전, 중, 후 체크리스트
처음 부하테스트를 할 때는 이것저것 보기 시작하면 쉽게 놓치는 부분이 생긴다. 그래서 간단한 체크리스트를 만들어두면 꽤 도움이 된다.
테스트 전
JMeter 결과 Clear
View Results Tree 비활성화 준비
Graph Results 비활성화 준비
테스트 데이터 준비
DB 인덱스 확인
서버 로그 확인 준비
DB PROCESSLIST 확인 준비
Java PID 확인
GC 확인 명령 준비
CPU/메모리 확인 명령 준비
테스트 중
Error 발생 여부
Average 증가 여부
95%, 99% 증가 여부
TPS 증가 또는 정체 여부
Max 튐 여부
DB Query 누적 여부
CPU 포화 여부
FGC 증가 여부
테스트 후
Threads
Ramp-up
Duration
Samples
Average
95%
99%
Max
Std. Dev
Throughput
Error %
그리고 아래도 같이 정리해두면 좋다.
Creating sort index 발생 여부
Using filesort 여부
Lock wait 여부
COUNT / SUM 쿼리 비용
FGC 증가 여부
Old 영역 사용률
CPU 사용률
load average
I/O wait
처음 하는 사람에게 추천하는 순서
정리하면 처음 부하테스트를 시작할 때는 아래 순서대로 진행하는 것이 가장 무난하다.
1. Thread 1 / Loop 1로 API 정상 응답 확인
2. Thread 10 / Ramp-up 10초 / Duration 3분으로 기준선 측정
3. Thread 30 / Ramp-up 20초 / Duration 3분 테스트
4. Thread 50 / Ramp-up 30초 / Duration 5분 테스트
5. Thread 80 / Ramp-up 40초 / Duration 5분 테스트
6. Thread 100 / Ramp-up 60초 / Duration 5분 테스트
7. 별도로 Thread 100 / Ramp-up 1초 순간 폭격 테스트
8. 각 단계마다 TPS, Average, 95%, 99%, Max, Error 기록
9. 동시에 DB PROCESSLIST, CPU, GC 확인
10. 병목 구간이 나오면 EXPLAIN, jstack, GC 로그로 원인 분석
처음부터 완벽한 수치를 만들 필요는 없다. 오히려 중요한 것은 숫자를 예쁘게 만드는 것이 아니라, 어느 지점부터 시스템이 흔들리는지 재현 가능한 방식으로 찾는 것이다.
마무리
부하테스트는 단순히 "몇 TPS가 나왔는가"를 보는 작업이 아니다.
정확히는 아래 과정에 더 가깝다.
사용자 수를 단계적으로 늘려보면서
응답시간, TPS, 에러율 변화를 확인하고
DB, CPU, GC, Thread 상태를 함께 보면서
어디서부터 성능이 무너지기 시작하는지 찾는 과정
처음에는 JMeter 숫자만 보게 되지만, 몇 번만 해보면 결국 중요한 것은 숫자 하나가 아니라 그 숫자가 왜 그렇게 나왔는지 설명할 수 있는가라는 점을 알게 된다.
그래서 부하테스트를 처음 시작할 때는 복잡한 시나리오보다도, 작은 단계로 나누어 테스트를 만들고, 각 단계마다 서버와 DB 상태를 같이 기록하는 습관부터 잡는 것이 더 중요하다.
다음 단계로 넘어간다면, 이후에는 HTML Report 비교, 실제 운영과 비슷한 Think Time 적용, 여러 API를 섞은 시나리오, 읽기/쓰기 혼합 부하 같은 주제로 확장해볼 수 있다.