(4/6) 홈페이지 스트레스 테스트 — 백엔드·DB 병목 찾기
Summary
앞의 두 편은 “밖에서” 홈페이지를 눌렀어요. 2편은 서버에 부하를, 3편은 브라우저 렌더링을 쟀죠. 그런데 부하를 걸었을 때 “느려졌다”까지는 알아도, 왜 느려졌는지 — 어느 쿼리가, 어느 자원이 병목인지 — 는 서버 바깥에서 안 보여요. 이번 편은 백엔드 안쪽으로 들어갑니다.
부하 테스트와 백엔드 모니터링은 한 쌍이에요. k6 로 부하를 거는 동안 서버 내부 지표를 같이 봐야, “동시 접속 500 에서 DB 커넥션이 고갈되면서 응답이 튀었다”처럼 원인까지 짚을 수 있어요. 이번 편은 그 내부 지표를 어떻게 보고, 가장 흔한 병목 세 가지(느린 쿼리·N+1·커넥션 풀)를 어떻게 잡는지 다룹니다.
💡 이 글에서 다루는 것
- 부하를 걸며 동시에 서버 내부를 보는 이유
- 네 골든 시그널과 USE 방법론으로 병목 좁히기
- DB 병목 — 느린 쿼리, N+1, 인덱스 부재
- 커넥션 풀 고갈 — 소크 테스트에서 자주 터지는 문제
- APM 으로 요청 하나의 시간 분해하기
1. 부하와 모니터링은 한 쌍
핵심 원칙부터 짚을게요. 부하 테스트는 반드시 서버 내부 모니터링과 동시에 돌려야 합니다. k6 결과만 보면 “p95 가 갑자기 튀었다”는 증상만 알 수 있어요. 그 순간 서버의 CPU·메모리·DB 커넥션 그래프를 겹쳐 봐야 원인이 보입니다.
예를 들어 이런 식으로 원인이 드러나요.
- 응답 시간이 튄 순간 CPU 가 100% → 연산이 병목. 코드 최적화나 스케일아웃 필요.
- CPU 는 여유로운데 DB 커넥션이 상한에 붙음 → 커넥션 풀이 병목. 응답을 기다리며 줄 서는 중.
- CPU·DB 다 여유로운데 느림 → 외부 API 호출이나 잠금(lock) 대기 의심.
즉 부하 도구는 “증상”을, 서버 모니터링은 “원인”을 알려줍니다. 둘을 같은 시간축에 겹쳐 보는 게 병목 찾기의 출발점이에요.
2. 무엇을 볼까 — 네 신호와 USE
1편에서 말한 네 골든 시그널(지연·트래픽·에러·포화도)을 서버 자원에 적용하면, 각 자원마다 무엇을 볼지가 정해져요. 이걸 체계화한 게 USE 방법론입니다. 자원마다 사용률(Utilization)·포화도(Saturation)·에러(Errors) 세 가지를 보라는 거예요.
| 자원 | 사용률 | 포화도(대기) | 에러 |
|---|---|---|---|
| CPU | 사용 % | 실행 큐 길이 (load average) |
— |
| 메모리 | 사용 % | 스와핑 발생 | OOM 킬 |
| 디스크 | I/O 사용 % | I/O 대기 큐 | 읽기/쓰기 실패 |
| 네트워크 | 대역폭 % | 재전송 | 드롭된 패킷 |
| DB 커넥션 | 사용 중 커넥션 % | 대기 중인 요청 | 획득 타임아웃 |
여기서 초보가 자주 놓치는 게 포화도(대기) 예요. CPU 사용률이 70% 라고 여유로워 보여도, 실행 큐에 작업이 길게 줄 서 있으면 이미 병목입니다. 사용률만 보지 말고 “얼마나 줄 서 있나(대기)” 를 꼭 같이 보세요. 대기가 생기는 순간이 곧 응답 시간이 튀는 순간이에요.
이 지표들은 보통 Prometheus 로 수집해서 Grafana 대시보드로 봅니다. k6 결과까지 같은 Grafana 로 흘려보내면, 부하 곡선과 서버 자원을 한 화면에서 겹쳐 볼 수 있어요. 이게 스트레스 테스트의 이상적인 관측 구성입니다.
3. DB 병목 — 가장 흔한 범인
부하를 걸었을 때 병목의 상당수는 데이터베이스에서 나옵니다. 애플리케이션 서버는 늘리기 쉽지만, DB 는 보통 한 대라서 여기가 먼저 막혀요. 대표적인 세 가지를 정리합니다.
느린 쿼리 (Slow Query)
인덱스가 없어서 테이블을 통째로 훑는 쿼리가 대표적이에요. 데이터가 적을 땐 안 보이다가, 운영 규모 데이터에서 갑자기 몇 초씩 걸립니다. 대부분의 DB 는 느린 쿼리를 자동으로 기록하는 기능이 있어요. MySQL 이면 이렇게 켭니다.
-- 1초 넘게 걸린 쿼리를 로그로 기록
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
그리고 의심되는 쿼리 앞에 EXPLAIN 을 붙이면 DB 가 어떻게 실행하는지 보여줘요.
EXPLAIN SELECT * FROM orders WHERE user_id = 42;
type: ALL ← 'ALL' 은 풀 테이블 스캔. 인덱스를 안 탐 (위험 신호)
rows: 1840000 ← 184만 행을 전부 훑는 중
type: ALL 과 큰 rows 값이 보이면 인덱스가 필요하다는 뜻이에요. user_id 에 인덱스를 걸면 이 쿼리는 순식간에 빨라집니다.
N+1 쿼리
목록 하나를 가져오려고 쿼리를 수백 번 날리는 문제예요. 게시글 20개를 가져오면서 각 글의 작성자를 따로 조회하면, 1(목록) + 20(작성자) = 21번의 쿼리가 나갑니다. 목록이 100개면 101번이에요. 한 요청에서 나가는 쿼리 개수를 세어보면 금방 드러나요. ORM 을 쓴다면 이 문제가 특히 잘 숨어 있어서, 부하 테스트 중 DB 쿼리 수를 꼭 확인하세요.
잠금 경합 (Lock Contention)
같은 행을 여러 요청이 동시에 수정하려 하면 줄을 서서 기다립니다. 재고 차감, 좋아요 수 증가처럼 인기 있는 한 행에 쓰기가 몰릴 때 터져요. 부하를 걸었을 때 특정 요청만 유독 느리고 DB 에 잠금 대기가 쌓인다면 이걸 의심합니다.
⚠️ DB 병목은 운영 규모의 데이터에서만 드러납니다. 1편에서 “텅 빈 DB 는 무조건 빠르다”고 한 이유가 이거예요. 테스트 DB 에 운영과 비슷한 양의 데이터를 채워두지 않으면, 느린 쿼리도 N+1 도 인덱스 부재도 전혀 안 보이다가 오픈 후에 터집니다.
4. 커넥션 풀 고갈
이건 소크 테스트(2편)에서 특히 잘 터지는 문제라 따로 짚을게요. 애플리케이션은 DB 커넥션을 매번 새로 만들지 않고 풀(pool) 에 미리 만들어두고 빌려 씁니다. 문제는 이 풀의 크기가 정해져 있다는 거예요.
풀이 20개인데 동시에 30개의 요청이 DB 를 쓰려 하면, 10개는 커넥션이 반납되기를 기다립니다. 이때 나타나는 증상이 특이해요. CPU·메모리는 멀쩡한데 응답 시간만 치솟습니다. 자원은 놀고 있는데 다들 커넥션을 기다리며 줄 서 있는 거예요.
체크할 지점은 이렇습니다.
- 풀 크기가 적정한가 — 너무 작으면 대기가 생기고, 너무 크면 DB 가 감당을 못 해요. DB 의 최대 커넥션 수와 함께 봐야 합니다.
- 커넥션을 제때 반납하나 — 쿼리 후 커넥션을 안 돌려주는 버그가 있으면, 부하가 일정해도 풀이 서서히 고갈돼요. 소크 테스트에서 “사용 중 커넥션”이 우상향하면 이걸 의심합니다.
- 획득 타임아웃이 걸려 있나 — 커넥션을 무한정 기다리면 요청이 쌓여 서버가 통째로 멈춰요. “N초 기다려도 못 얻으면 실패” 설정이 있어야 합니다.
🚨 커넥션 풀 고갈은 오픈 직후엔 멀쩡하다가 트래픽이 조금만 늘면 갑자기 전체가 먹통이 되는, 아주 흔하고 무서운 사고예요. 부하 테스트를 걸며 “사용 중 커넥션 수”를 반드시 그래프로 지켜보세요.
5. APM — 요청 하나를 시간으로 분해
지금까지가 자원별로 보는 방식이라면, APM(Application Performance Monitoring) 은 반대로 요청 하나가 어디서 시간을 썼는지를 분해해서 보여줘요. 느린 요청 하나를 클릭하면 이런 식으로 쪼개집니다.
요청: GET /products/42 총 1,240 ms
├─ 라우팅·미들웨어 30 ms
├─ DB 쿼리 (상품 조회) 180 ms
├─ DB 쿼리 (리뷰 목록 N+1 ×50) 820 ms ← 여기가 범인
├─ 외부 API (배송비 계산) 160 ms
└─ 템플릿 렌더링 50 ms
이 그림 한 장이면 “1.2초 중 0.8초가 리뷰 조회의 N+1 때문”이라는 게 바로 보여요. 자원 그래프로 “DB 가 병목”까지 좁혔다면, APM 으로 “어느 요청의 어느 쿼리”까지 정확히 짚는 거예요. Datadog·New Relic 같은 상용 도구나, OpenTelemetry 로 직접 구축하는 방법이 있습니다.
💡 병목을 좁혀가는 순서를 정리하면 이래요. 부하 도구(어디서 느려지나) → 자원 모니터링(어느 자원이 병목) → APM(어느 요청·쿼리) → EXPLAIN(왜 그 쿼리가 느린가). 위에서 아래로 내려가며 범위를 좁히면 헤매지 않아요.
6. 정리
- 부하 테스트는 서버 내부 모니터링과 동시에 — 증상과 원인을 겹쳐 본다
- 자원마다 사용률·포화도(대기)·에러(USE) — 특히 대기를 놓치지 말 것
- DB 병목 3종 — 느린 쿼리(EXPLAIN)·N+1·잠금 경합, 운영 규모 데이터에서만 드러남
- 커넥션 풀 고갈 — CPU 는 멀쩡한데 응답만 튀면 이걸 의심
- APM 으로 요청 하나를 시간 분해해 정확한 범인을 짚는다
여기까지가 “정상 부하에서 어디가 느린가”를 찾는 과정이었어요. 그런데 진짜 무서운 건 뭔가 고장 났을 때 입니다. DB 가 잠깐 죽거나, 외부 API 가 느려지거나, 서버 한 대가 빠졌을 때 홈페이지가 통째로 무너지지 않고 버티는지 — 다음 편에서는 일부러 장애를 주입하는 카오스 엔지니어링으로 복원력을 시험할게요.
일단 오늘은 여기까지….. 다음 글에서는 카오스 엔지니어링으로 복원력을 시험하는 법을 정리해볼게요.
← 이전 글: (3/6) 홈페이지 스트레스 테스트 — 프론트엔드 성능과 코어 웹 바이탈 | 다음 글 →: (5/6) 홈페이지 스트레스 테스트 — 복원력과 카오스 엔지니어링