-
CPU는 남는데 서비스가 멈추는 현상: DB 커넥션 병목Spring 2026. 2. 1. 13:26
백엔드 서버를 운영하다 보면 CPU 사용률은 10%도 안 되는데 서비스가 먹통이 되는 이상한 상황을 만나게 된다. 얼핏 보면 모순처럼 느껴지지만, 이것은 전형적인 DB 커넥션 병목 현상이다.
문제 상황
상황을 한 문장으로 요약하면 이렇다:
DB 커넥션 병목으로 웹 쓰레드가 BLOCKED 상태가 되어 CPU는 유휴 상태지만 서비스는 정지된 상황
왜 CPU는 놀고 있는가?
CPU는 기본적으로 RUNNABLE 상태의 쓰레드만 실행한다.
그런데 이 상황에서는:
- 웹 쓰레드 대부분이 DB 커넥션을 기다리며 BLOCKED/WAITING 상태
- BLOCKED 상태의 쓰레드는 CPU 스케줄링 대상이 아님
- 결과적으로 CPU는 실행할 쓰레드가 없어서 유휴 상태
따라서 CPU가 놀고 있다는 건 일이 없다는 뜻이 아니라, 실행 가능한 일이 없다는 뜻이다.
쓰레드 풀 고갈이 서비스 정지를 의미하는 이유
Tomcat 쓰레드 풀은 HTTP 요청을 처리하는 작업자 풀이다.
중요한 특징은
- 요청 하나당 쓰레드 하나가 반드시 필요
- 쓰레드가 없으면 요청은 처리 시작조차 못함
예시로 설명하면:
설정: - Tomcat 쓰레드 풀: 200개 - DB 커넥션 풀: 10개 흐름: 1. 200개의 요청이 동시에 들어옴 2. Tomcat은 200개의 쓰레드를 모두 할당 3. 모든 쓰레드가 DB 접근 시도 4. 10개만 DB 커넥션을 획득 5. 나머지 190개는 DB 커넥션을 기다리며 BLOCKED 6. 사용 가능한 Tomcat 쓰레드 = 0 결과: 새로운 요청은 대기하거나 타임아웃으로 실패이것이 바로 서비스 완전 정지 상태다.
왜 CPU나 쓰레드를 늘리는 것은 해결책이 아닐까?
CPU 증설이 효과 없는 이유
문제의 본질은 CPU 연산 부족이 아니라 I/O 대기다.
- CPU를 늘려도 BLOCKED 쓰레드는 여전히 BLOCKED
- CPU는 여전히 실행할 쓰레드가 없음
쓰레드 증가가 오히려 악화시키는 이유
Tomcat 쓰레드를 200개에서 400개로 늘리면
- DB 커넥션은 여전히 10개
- 390개의 쓰레드가 DB 커넥션 대기
- 메모리 사용 증가
- 컨텍스트 스위칭 비용 증가
- 장애 복구 시간 증가
병목을 해결하지 않고 대기 인원만 늘린 것이다.
해결 전략: 대기 쓰레드 줄이기
해결의 핵심은 단순하다
웹 쓰레드가 DB를 오래 기다리지 않게 만든다
구체적인 해결 방법을 살펴보자.
1. DB 커넥션 풀 튜닝
1-1) maximum-pool-size 설정
DB 커넥션은 단순한 연결이 아니다:
- DB 내부 쓰레드 소비
- 락 자원 점유
- 메모리 사용
- CPU 자원 사용
따라서 커넥션 수는 DB가 감당 가능한 최대 동시 쿼리 수에 맞춰야 한다.
- 너무 적으면 → 웹 쓰레드가 대기
- 너무 많으면 → DB 자체가 과부하
1-2) connection-timeout: 빠른 실패 전략
DB 커넥션을 무한히 기다리게 하면:
- 웹 쓰레드가 계속 BLOCKED
- 쓰레드 풀 고갈
- 장애가 전체로 전파
타임아웃을 설정하면:
- 일정 시간 이상 기다리면 예외 발생
- 쓰레드 즉시 반환
- 다른 요청을 처리할 기회 확보
느리게 죽는 것보다, 빨리 실패하는 게 낫다는 전략이다.
2. 쿼리 최적화
웹 쓰레드가 BLOCKED되는 시간의 대부분은 DB 응답 시간이다.
쿼리가 느리면
- 커넥션 점유 시간 증가
- 대기 쓰레드 증가
대표적인 최적화
인덱스 추가
- 풀 스캔 제거
- 쿼리 시간 단축
- 커넥션 점유 시간 감소
N+1 문제 제거
- 요청 1번에 쿼리 N번 실행 방지
- 커넥션 사용 횟수 급감
불필요한 트랜잭션 제거
- 트랜잭션 = 락 유지 시간
- 락이 줄면 대기 시간 감소
쿼리가 빨라지면 자연스럽게 병목도 완화된다.
3. 비동기/논블로킹 구조 전환
Spring WebFlux나 Netty 같은 비동기 프레임워크를 사용하면
- 쓰레드가 I/O 대기를 하지 않음
- 이벤트 루프 방식으로 동작
- 적은 쓰레드로 많은 커넥션 처리 가능
하지만 DB 접근이 블로킹이면 효과가 반감되므로 R2DBC 같은 리액티브 드라이버가 필요하다.
4. 구조적으로 DB 덜 쓰기
근본적으로 DB 접근 자체를 줄이는 방법:
- Redis 캐시: 자주 조회되는 데이터 캐싱
- 로컬 캐시: 애플리케이션 메모리 캐시
- 읽기 전용 복제본: 조회 부하 분산
- 이벤트 기반 아키텍처: 동기 DB 호출 최소화
정리
CPU가 놀고 있는데 서비스가 멈춘 이유는:
연산 문제가 아니라 DB 커넥션이라는 공유 자원을 기다리며 웹 쓰레드가 BLOCKED 되었기 때문이다
해결 방법은 세 가지로 정리된다:
- DB 접근 시간을 줄이거나 (쿼리 최적화, 인덱스)
- 오래 기다리지 않게 하거나 (타임아웃, 커넥션 풀 튜닝)
- 아예 DB를 덜 쓰게 만든다 (캐시, 비동기)
이 문제를 이해하는 것은 단순히 기술적 지식을 넘어, 시스템의 병목이 어디서 발생하고 어떻게 전파되는지를 이해하는 것이다.
CPU 사용률이 낮다고 무조건 여유가 있는 것이 아니며, 쓰레드를 늘린다고 문제가 해결되는 것도 아니다.
진짜 병목을 찾아 해결하는 것이 중요하고 생각한다.
'Spring' 카테고리의 다른 글
[Spring Study] 토비의 스프링 3주차 스터디 회고: 예외를 던지는 이유를 이해 (0) 2025.11.18 [Spring Study] 토비의 스프링 2주차 스터디 회고: JDBC 리팩토링과 JdbcTemplate의 발전 (0) 2025.11.05 [Spring Study] 토비의 스프링 1주차 스터디 회고: 스프링의 핵심 철학 (0) 2025.11.03 @MemberId 도입기: 스프링 MVC 요청 흐름과 커스ArgumentResolver 활용 (0) 2025.09.08 왜 ResponseEntity를 써야 할까? (2) 2025.08.05