학습 목표
데드락이 순환 대기로 발생함을 설명할 수 있다.
대기 그래프(wait-for graph) 로 교착 사이클을 찾는 방법을 설명할 수 있다.
감지·예방·회피와 lock timeout·NOWAIT 차이를 구분할 수 있다.
애플리케이션에서 데드락·직렬화 실패 시 재시도 패턴을 설계할 수 있다.
문제 상황
- 주문 A·B가 동시에 서로의 행을
FOR UPDATE— 둘 다 무한 대기- API는 30초 후 504, DB 세션은 락을 쥔 채 남음
- 재고 차감·포인트 차감을 순서 없이 두 테이블에 락 — 가끔 deadlock detected
- 같은 코드인데 부하 올리면 재현
lock_timeout없이 긴 txn — 슬로우 쿼리 하나가 전체 결제를 막음- ORM
@Transactional안에서 외부 HTTP 호출- 락 오래 보유 → 교착·대기 확률 상승
- MySQL
1213, PostgreSQL40P01— 어느 txn을 롤백할지 앱이 처리해야 함
앞서 격리 수준·2PL·FOR UPDATE로 동시성을 제어했다. 락이 많아지면 서로 기다리는 고리가 생긴다. 이번 편은 교착과 얼마나 기다릴지다.
1. 데드락이란
두 개 이상 txn이 서로가 가진 자원을 기다리며 영원히 진행하지 못하는 상태.
Coffman 조건 — 네 가지가 동시에 성립하면 데드락 가능:
| 조건 | 의미 |
|---|---|
| Mutual exclusion | 자원을 동시에 한 txn만 사용 |
| Hold and wait | 잡은 락을 놓지 않고 다른 락 대기 |
| No preemption | DBMS가 락을 강제로 빼앗지 않음 (일반적) |
| Circular wait | T1→T2→…→T1 순환 대기 |
T1: lock row A
T2: lock row B
T1: wait for row B ← T2가 보유
T2: wait for row A ← T1이 보유 → cycle
- 2PL·비관적 락·Serializable일수록 락 경합↑ → 데드락 빈도↑
- MVCC 읽기는 보통 블로킹 없음 — 쓰기·범위 락에서 주로 발생
2. 대기 그래프
DBMS는 누가 누구를 기다리는지 그래프로 추적한다.

| 요소 | 의미 |
|---|---|
| 노드 | 활성 txn (또는 자원 — 구현마다 다름) |
| 간선 T1 → T2 | T1이 T2가 보유한 락을 기다림 |
| 사이클 | 데드락 — 감지 알고리즘의 핵심 |
| txn | 보유 | 대기 |
|---|---|---|
| T1 | row A (X) | row B |
| T2 | row B (X) | row A |
- 주기적으로(또는 대기 발생 시) DFS로 사이클 탐색
- 희생자(victim) 하나 ROLLBACK — 보통 비용 적은 txn (짧은 txn, 적은 작업량)
3. 감지 · 예방 · 회피

감지 (Detection) — PostgreSQL·InnoDB 기본
- 대기 중 주기적 사이클 검사
- 발견 시 victim 롤백 — 나머지 txn 진행
- 오버헤드는 있지만 동시성 유지
| DB | 에러 | 의미 |
|---|---|---|
| PostgreSQL | 40P01 deadlock_detected |
victim txn 전체 abort |
| MySQL InnoDB | 1213 Deadlock found |
victim 롤백 |
예방 (Prevention)
| 방법 | 설명 |
|---|---|
| 락 순서 통일 | 항상 (user_id 작은 쪽) → (큰 쪽) 순으로 FOR UPDATE |
| 한 번에 잠금 | SELECT ... WHERE id IN (1,2) ORDER BY id FOR UPDATE |
| 락 범위 축소 | txn 짧게, 필요한 행만 |
- 애플리케이션 설계로 순환 자체를 없애는 접근
회피 (Avoidance)
| 방법 | 설명 |
|---|---|
| lock timeout | SET lock_timeout = '2s' — 초과 시 에러·대기 중단 |
| NOWAIT | FOR UPDATE NOWAIT — 대기 없이 즉시 실패 |
| SKIP LOCKED | 큐·워커 — 잠긴 행 건너뜀 |
- 데드락을 기다리지 않음 — 앱이 재시도·다른 행 처리
4. lock timeout vs statement timeout
| 설정 | PostgreSQL | 효과 |
|---|---|---|
| lock_timeout | SET lock_timeout = '3s' |
락 대기 상한 — 초과 시 취소 |
| statement_timeout | SET statement_timeout = '30s' |
쿼리 전체 실행 상한 |
| idle_in_transaction_session_timeout | 세션 설정 | txn 열린 채 idle 상한 |
-- session-level (PostgreSQL)
SET lock_timeout = '2s';
SET statement_timeout = '30s';
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- waits max 2s for lock
-- ...
COMMIT;
-- no wait
SELECT * FROM seats WHERE id = 42 FOR UPDATE NOWAIT;
-- queue worker: skip locked rows
SELECT * FROM jobs
WHERE status = 'PENDING'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;
| lock_timeout | statement_timeout | |
|---|---|---|
| 측정 | 락 기다린 시간 | 실행 전체 시간 |
| 용도 | 교착·장기 대기 차단 | 슬로우 쿼리·runaway 차단 |
| 데드락 | 직접 해결 안 함 — 포기 후 재시도 | victim 전에도 긴 대기 가능 |
- 둘 다 설정하는 경우 많음 —
lock_timeout≤statement_timeout권장
5. 애플리케이션 재시도
DB가 victim을 롤백해도 클라이언트는 에러를 받는다. 멱등·재시도가 필요하다.

| 단계 | 처리 |
|---|---|
| 1 | txn 시작 · 비즈니스 로직 |
| 2 | deadlock / lock timeout / serialization_failure |
| 3 | ROLLBACK (자동 또는 명시) |
| 4 | 지수 백오프 + 재시도 (상한 N회) |
| 5 | 초과 시 사용자에게 실패 |
-- PostgreSQL: serialization failure (SSI)
-- SQLSTATE 40001
| 재시도 대상 | SQLSTATE (PG) | 비고 |
|---|---|---|
| Deadlock | 40P01 |
victim txn |
| Lock timeout | 55P03 lock_not_available |
lock_timeout |
| Serialization failure | 40001 |
Serializable 충돌 |
// Spring — declarative retry (concept)
@Retryable(
retryFor = { PSQLException.class, CannotAcquireLockException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 50, multiplier = 2)
)
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) { ... }
- 멱등 키 — 재시도 시 이중 출금 방지
- 락 순서 — 재시도만으로 근본 해결 안 됨 — 설계 병행
6. 실무 체크리스트
| 항목 | 권장 |
|---|---|
| 다중 테이블 락 | PK·비즈니스 키 기준 정렬 후 잠금 |
| txn 길이 | DB 작업만 — HTTP·메시지 밖으로 |
| 모니터링 | pg_locks, innodb_locks, deadlock 로그 |
| 기본값 | lock_timeout 설정 (0=무한 대기 주의) |
| Serializable | 재시도 필수 — 40001 처리 |
| 워커 큐 | SKIP LOCKED — 경쟁 consumer |
-- PostgreSQL: who blocks whom
SELECT blocked.pid AS blocked_pid,
blocking.pid AS blocking_pid,
left(blocked.query, 60) AS blocked_query,
left(blocking.query, 60) AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
7. 정리
- 데드락 = 순환 대기 — Coffman 네 조건
- 대기 그래프 사이클 → victim 롤백 (감지)
- 예방 — 락 순서·txn 짧게·범위 최소
- 회피 —
lock_timeout,NOWAIT,SKIP LOCKED - lock_timeout ≠ statement_timeout
- 앱 재시도 + 멱등 —
40P01·40001·lock timeout
다음에 다룰 것
- WAL과 crash recovery
- redo/undo, checkpoint
해당 내용은 Database System Concepts, 7/E (Avraham Silberschatz, Henry F. Korth, S. Sudarshan) 의 내용을 기반으로 합니다.
'데이터베이스 따라잡기' 카테고리의 다른 글
| MVCC (0) | 2026.06.30 |
|---|---|
| 로깅과 복구 (0) | 2026.06.29 |
| 동시성 제어 (0) | 2026.06.27 |
| 트랜잭션과 ACID (0) | 2026.06.26 |
| 실행 계획 읽기 (0) | 2026.06.25 |