이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 하나의 API 요청은 DB까지 어떤 경로를 거치는가?
- 백엔드 코드의 Repository 호출 뒤에서 어떤 비용이 발생하는가?
- DB 병목을 애플리케이션 병목과 어떻게 연결해서 봐야 하는가?
개요
백엔드 코드에서 DB는 Repository나 DAO 메서드 하나로 보인다. 그러나 실제 요청은 HTTP 스레드, 커넥션 풀, DB 세션, SQL 파싱과 최적화, 락, 버퍼 캐시, 디스크, 로그 기록을 거친다.
DB를 단순 저장소로 보면 장애를 늦게 이해한다. DB는 API 응답 시간과 정합성을 결정하는 실행 엔진이다.
원리
하나의 Repository 호출은 “DB에 갔다 온다”가 아니라 여러 대기열과 실행 단계를 통과한다. 백엔드에서 가장 먼저 나눠야 하는 것은 애플리케이션 처리 시간, 커넥션 획득 시간, DB 실행 시간, lock 대기 시간, 결과 매핑 시간이다.
주문 상세 API 흐름은 다음처럼 볼 수 있다.
Client
Controller
Service
Repository
HikariCP connection 획득
DB session
parse/bind/execute
lock 확인
buffer cache 또는 disk read
result set 반환
connection 반납
ResponseRepository 호출이 느리면 원인은 여러 곳에 있다. 커넥션을 못 얻었을 수도 있고, DB에서 lock을 기다릴 수도 있고, 실행 계획이 많은 행을 읽을 수도 있다.
운영에서 “DB가 느리다”라고 말할 때는 최소한 다음을 분리한다.
- pool pending이 있는가?
- SQL 자체 실행 시간이 긴가?
- lock wait 또는 deadlock이 있는가?
- 같은 요청에서 SQL이 몇 번 호출되는가?
- 결과 row 수와 payload가 갑자기 커졌는가?
이 구분이 있어야 인프라/DBA도 DB 내부 지표와 애플리케이션 맥락을 연결할 수 있다.
코드에서 보이는 모습
@Transactional(readOnly = true)
public OrderDetail getOrder(Long orderId, Long memberId) {
Order order = orderRepository.findByIdAndMemberId(orderId, memberId)
.orElseThrow();
return OrderDetail.from(order);
}코드는 단순하지만 DB에는 다음 쿼리가 갈 수 있다.
SELECT id, member_id, status, created_at, total_price
FROM orders
WHERE id = ?
AND member_id = ?;orders.id가 PK라면 빠를 가능성이 높다. 그러나 연관 객체를 lazy loading하거나 권한 조건이 복잡해지면 추가 쿼리와 조인이 생길 수 있다.
개발 환경에서 실제 SQL을 확인할 때는 endpoint와 trace id를 함께 남기는 방식이 유용하다. 운영에서는 개인정보와 SQL 로그 용량을 조심하면서 slow query, query fingerprint, trace id를 연결하는 편이 좋다.
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.connection-timeout=3000
spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=300이 설정만으로 운영 관측이 완성되지는 않는다. 하지만 “요청 하나가 몇 ms 동안 DB 커넥션을 붙잡았는지”를 추적하는 출발점이 된다.
DB가 담당하는 것
DB는 다음을 담당한다.
- 영속 저장
- 트랜잭션
- 동시성 제어
- 제약 조건 검증
- 인덱스 탐색
- 조인과 정렬
- 복구 로그
- 권한과 세션 관리
애플리케이션이 save를 호출해도 DB는 제약을 검사하고, 로그를 기록하고, 인덱스를 갱신하고, lock을 조정한다.
실전 팁
- 느린 Repository 호출은 SQL, pool, lock, 실행 계획을 나눠 본다.
- API trace에 connection acquire time과 DB query 시간을 가능하면 분리해 포함한다.
- 쿼리 로그에는 endpoint, trace id, query fingerprint를 연결해 slow query를 사용자 요청과 코드 경로로 되돌릴 수 있게 한다.
- DB 호출은 네트워크 왕복이므로 반복 호출을 조심한다.
- Repository 코드를 볼 때 실제 SQL과 인덱스를 함께 떠올린다.
- 장애 공유 때는 “느립니다”보다 “
GET /orders/{id}p99가 300ms에서 2s로 올랐고, pool pending이 증가했으며, 같은 시각 slow query가 이 SQL fingerprint에 집중됩니다”처럼 말한다.
위험 신호!
- DB 시간을 애플리케이션 전체 시간에서 분리해 보지 않는다.
- Repository 메서드가 몇 번 호출되는지 모른다.
- 커넥션 풀 지표가 없다.
- pool 획득 대기와 SQL 실행 시간을 같은 DB 시간으로 뭉뚱그린다.
- API timeout이 DB query timeout보다 짧거나 길게 어긋나 있다.
- SQL 로그와 실행 계획을 본 적이 없다.
확인 질문
- Repository 호출 뒤에서 발생할 수 있는 비용은 무엇인가?
- 커넥션 획득, SQL 실행, 락 대기, 인덱스 탐색, 디스크 I/O, 결과 전송, 인덱스 갱신, 로그 기록 비용이 있다.
- DB 병목을 분석할 때 계층을 나눠야 하는 이유는 무엇인가?
- 같은 API 지연도 pool 대기, lock 대기, 실행 계획 문제, DB 리소스 부족 등 원인이 다르기 때문이다.
- DB가 단순 저장소가 아닌 이유는 무엇인가?
- 트랜잭션, 동시성, 제약, 실행 계획, 복구까지 담당해 API의 정합성과 성능을 결정하기 때문이다.