이 글에서 얻는 것
- DB가 느린 원인을 단순히 “쿼리가 느리다"로 보지 않고, 락 경합(lock contention) 문제로 분해해 진단하는 방법을 익힙니다.
- p95 지연시간, lock wait time, deadlock 발생률 같은 지표를 기준으로 인덱스 추가 vs 트랜잭션 분리 vs 재시도 정책 중 무엇을 먼저 할지 결정할 수 있습니다.
- 장애가 터진 뒤 대응이 아니라, 배포 전/후 점검으로 경합을 줄이는 운영 루틴을 정리합니다.
핵심 개념/이슈
1) 락은 나쁜 게 아니라, 긴 락이 문제다
동시성 제어가 필요한 시스템에서 락은 필수입니다. 문제는 락 자체가 아니라, 다음 상황에서 락이 길게 점유될 때입니다.
- 한 트랜잭션이 너무 많은 행을 잡고 오래 유지
- 인덱스가 부정확해 필요한 범위를 넘어 스캔/잠금
- 외부 호출(HTTP, 메시지 발행)을 트랜잭션 내부에 넣어 커밋 지연
실무에서는 “CPU는 여유인데 응답이 느림” 현상이 보이면 락 대기를 먼저 의심해야 합니다. 특히 주문/재고/정산처럼 동일 키에 쓰기가 몰리는 도메인에서 자주 보입니다.
관련 기초는 MySQL 격리수준과 락과 JPA 성능 글을 먼저 보고 오면 더 빠르게 이해됩니다.
2) 경합은 세 가지 패턴으로 나타난다
핫 로우(Hot Row) 경합
같은 PK/UK를 여러 요청이 동시에 갱신할 때 발생합니다. 예: 동일 상품 재고 차감.범위 잠금(Range Lock) 경합
인덱스 설계가 애매하면 필요 이상 범위를 잠그고, unrelated 요청까지 같이 대기시킵니다.교차 갱신(Deadlock) 경합
트랜잭션 A/B가 서로 다른 순서로 리소스를 잡아 순환 대기가 생깁니다.
이 세 패턴은 대응 방식이 다릅니다. 핫 로우는 키 분산/큐잉, 범위 잠금은 인덱스와 쿼리 조건 정리, 데드락은 접근 순서 통일이 우선입니다.
3) “느림"을 숫자로 바꾸는 최소 지표
아래 4개는 반드시 대시보드에 있어야 합니다.
- lock wait p95/p99: 100ms를 넘기기 시작하면 사용자 체감이 빠르게 나빠짐
- deadlock count/min: 0이 이상적, 1/min 이상이면 즉시 분석 대상
- row lock time sum: 배포 전후 증감 추적
- tx duration p95: 트랜잭션 길이가 늘면 경합 확률이 비선형 증가
권장 초기 경보 기준(팀 상황에 맞게 조정):
- lock wait p95 > 150ms, 10분 이상 지속
- deadlock > 5회/10분
- write API 에러율 > 0.5%
이 기준은 부하 테스트 전략과 연결해 검증해야 의미가 있습니다.
실무 적용
1) 의사결정 순서: 인덱스 → 트랜잭션 범위 → 재시도
락 경합이 보일 때 바로 재시도부터 붙이면 DB 부하가 더 커지는 경우가 많습니다. 우선순위는 다음이 안전합니다.
1순위: 인덱스와 실행 계획 확인
WHERE조건이 인덱스를 정확히 타는지- 불필요한 범위 스캔이 있는지
- 갱신 대상 행 수가 예상보다 큰지
예상보다 많은 행을 잠그고 있다면, 재시도보다 인덱스 정리가 먼저입니다.
2순위: 트랜잭션 길이 줄이기
- 트랜잭션 내부 외부 API 호출 제거
- 대용량 쓰기 작업을 배치/청크로 분리
- 읽기-검증-쓰기 단계를 최소화
실무 경험상 트랜잭션 p95를 900ms→250ms로 줄이면 lock wait가 절반 이하로 내려가는 경우가 흔합니다.
3순위: 제한적 재시도
- 데드락/락 타임아웃에만 재시도
- 지수 백오프 + jitter
- 최대 2회, 총 재시도 시간 300ms 이내
무제한 재시도는 “실패를 지연"할 뿐입니다. 타임아웃/재시도/백오프 정책과 반드시 맞춰야 합니다.
2) 트랜잭션 설계 규칙(팀 합의용)
팀 규칙은 문서가 아니라 코드리뷰 기준으로 강제되어야 합니다.
- 동일 도메인 엔티티 접근 순서 통일(예:
account -> order -> ledger) - 트랜잭션 내부 네트워크 I/O 금지
- 한 트랜잭션에서 갱신 가능한 행 수 상한(예: 500행)
- 대량 UPDATE는 점진 배치 + 커밋 분할
“예외 허용"도 숫자로 관리하세요.
- 예외 PR 비율이 20%를 넘으면 규칙 재설계 필요
- 월간 deadlock 상위 쿼리 3개는 고정 개선 항목으로 지정
3) 운영 중 대응: 완화 버튼을 미리 준비
장애 시 가장 먼저 필요한 건 “원인 분석"보다 “즉시 완화"입니다.
- 핫키 요청 레이트 제한(429 + Retry-After)
- 쓰기 경로를 임시 큐잉으로 우회
- 비핵심 쓰기 기능 토글 OFF
- DB 파라미터 임시 변경은 런북 승인 절차 후 적용
이런 버튼이 없으면 매번 애플리케이션 재배포에 의존하게 되고, 평균 복구 시간(MTTR)이 급격히 늘어납니다. 배포 대응 구조는 배포 런북과 함께 설계하는 게 안전합니다.
트레이드오프/주의점
락을 줄이려다 정합성을 깰 수 있다
잠금 범위를 줄인다고 트랜잭션을 무리하게 쪼개면 데이터 불일치가 생깁니다. 특히 금전/재고 도메인은 정합성 우선입니다.낙관적 락이 만능은 아니다
충돌률이 높은 핫 로우에서는 재시도 폭증으로 오히려 성능이 악화될 수 있습니다. 충돌률 5% 이상이면 비관적 전략이나 큐잉을 검토하세요.읽기 성능 최적화가 쓰기 경합을 악화시킬 수 있다
인덱스를 많이 늘리면 쓰기 비용이 증가합니다. 쓰기 QPS가 높은 테이블은 인덱스 추가 전후 write latency를 반드시 비교해야 합니다.지표 없는 튜닝은 재현되지 않는다
“체감상 좋아짐"으로 끝나면 다음 장애에서 다시 같은 논쟁을 반복합니다. 개선 전/후 수치를 한 문서로 남겨야 팀 자산이 됩니다.
체크리스트 또는 연습
배포 전 체크리스트
- 핵심 쓰기 API의 lock wait p95/p99 대시보드가 있다.
- deadlock 발생 시 SQL/트랜잭션 정보를 남기는 로그가 있다.
- 트랜잭션 내부 외부 I/O 금지 규칙을 코드리뷰에서 확인한다.
- 핫키 완화(레이트 리밋/큐잉/토글) 버튼이 런북에 문서화되어 있다.
- 재시도는 데드락/타임아웃 전용이며 최대 횟수가 고정돼 있다.
연습 과제
- 최근 2주간 lock wait 상위 API 3개를 뽑아 공통 패턴을 분류해보세요.
- 가장 긴 트랜잭션 1개를 선택해 “트랜잭션 내부 I/O"를 제거한 개선안을 작성해보세요.
- 개선 전/후로 lock wait p95, write API 에러율, DB CPU를 비교해 숫자로 결론을 내보세요.
핵심은 단순합니다. 락 경합은 “DB가 느려서"가 아니라 설계와 운영 기준이 없어서 커집니다. 기준을 숫자로 고정하면, 튜닝은 감각이 아니라 반복 가능한 작업이 됩니다.
💬 댓글