데드 락
두 개 이상의 트랜잭션이 서로가 보유한 락을 기다리며 영원히 대기 상태에 빠지는 현상입니다.
비유
[ 좁은 골목에서 마주친 두 차 ]
차 A: "비켜, 내가 먼저 갈게"
차 B: "아니, 네가 비켜"
차 A: 뒤로 못 감 (후진 공간 없음)
차 B: 뒤로 못 감 (후진 공간 없음)
→ 둘 다 영원히 멈춤
→ 누군가 강제로 한 대를 치우지 않으면 해결 불가
데드락 발생 구조
트랜잭션 A 트랜잭션 B
─────────── ───────────
① 행 1에 X락 획득 ② 행 2에 X락 획득
│ │
▼ ▼
③ 행 2에 X락 요청 → 대기 ④ 행 1에 X락 요청 → 대기
│ │
└──────── 서로 대기 ────────────┘
│
영원히 해결 안 됨
(DEADLOCK!)
A는 B가 가진 락을 기다림
B는 A가 가진 락을 기다림
→ 서로 상대방이 락을 놓아줘야 진행 가능
→ 둘 다 락을 안 놓음
→ 무한 대기
SQL로 보는 데드락 발생
-- 트랜잭션 A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 행 1에 X락 획득
-- ... 다른 작업 ...
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 행 2에 X락 요청 → 대기!
-- 트랜잭션 B (동시에)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 행 2에 X락 획득
-- ... 다른 작업 ...
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 행 1에 X락 요청 → 대기!
시간 흐름:
T1 A: 행1 X락 획득
T2 B: 행2 X락 획득
T3 A: 행2 X락 요청 → B가 보유 중 → 대기
T4 B: 행1 X락 요청 → A가 보유 중 → 대기
T5 ────── DEADLOCK! ──────
데드락 발생 조건 (4가지 모두 충족 시)
① 상호 배제 (Mutual Exclusion)
자원(락)을 동시에 공유할 수 없음
→ 배타 락은 한 트랜잭션만 보유 가능
② 점유 대기 (Hold and Wait)
이미 락을 보유한 상태에서 다른 락을 기다림
→ A가 행1 락을 들고 행2 락을 대기
③ 비선점 (No Preemption)
다른 트랜잭션의 락을 강제로 뺏을 수 없음
→ B의 락을 A가 강제로 뺏기 불가
④ 순환 대기 (Circular Wait)
락 대기가 원형으로 연결됨
→ A→B→A 또는 A→B→C→A
┌──── 대기 ────┐
│ │
▼ │
트랜잭션 A ────────→ 트랜잭션 B
보유: 행1 보유: 행2
대기: 행2 대기: 행1
│ ▲
└──── 대기 ────┘
순환 대기 형성 → 데드락!
DB의 데드락 처리
1. 데드락 감지 (Detection)
DB는 주기적으로 대기 그래프(Wait-for Graph)를 검사
사이클이 발견되면 데드락으로 판단
대기 그래프:
A → B (A가 B를 기다림)
B → A (B가 A를 기다림)
→ 사이클 발견! → 데드락!
2. 희생자 선택 & 롤백
데드락 감지 시:
1. DB가 "희생자(Victim)" 트랜잭션 선택
2. 희생자 트랜잭션 강제 롤백
3. 롤백된 트랜잭션의 락 해제
4. 다른 트랜잭션이 진행 가능해짐
선택 기준 (DBMS마다 다름):
- 작업량이 적은 트랜잭션
- 실행 시간이 짧은 트랜잭션
- 롤백 비용이 적은 트랜잭션
-- 트랜잭션 A (희생자로 선택됨)
ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction
-- 트랜잭션 B
-- 정상 진행됨 (A의 락이 해제되었으므로)
데드락 예방 방법
1. 락 순서 일관되게 유지 (가장 중요)
-- X 데드락 위험
-- 트랜잭션 A: 행1 → 행2 순서로 락
-- 트랜잭션 B: 행2 → 행1 순서로 락
-- O 안전 — 항상 같은 순서 (예: id 오름차순)
-- 트랜잭션 A: 행1 → 행2 순서로 락
-- 트랜잭션 B: 행1 → 행2 순서로 락
-- 예시: 계좌 이체 시 항상 작은 id부터 락
BEGIN;
-- id 1 → id 2 순서로 통일
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
SELECT * FROM accounts WHERE id = 2 FOR UPDATE;
-- 이체 처리
COMMIT;
순서 통일 시:
트랜잭션 A: 행1 락 → 행2 락 요청
트랜잭션 B: 행1 락 요청 → 대기 (A가 보유 중)
A가 행2까지 완료 후 COMMIT → 락 해제
B가 행1 락 획득 → 행2 락 획득 → 완료
→ 순차 처리되어 데드락 없음!
2. 트랜잭션을 짧게 유지
-- X 나쁜 예 — 불필요하게 긴 트랜잭션
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- ... 복잡한 비즈니스 로직 (5초) ...
-- ... 외부 API 호출 (3초) ...
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;
-- 8초 동안 락 보유 → 다른 트랜잭션 8초 대기
-- O 좋은 예 — 짧은 트랜잭션
-- 1. 먼저 필요한 데이터 조회 (락 없이)
SELECT * FROM accounts WHERE id = 1;
-- 2. 비즈니스 로직 처리
-- 3. 외부 API 호출
-- 4. 최소한의 시간만 트랜잭션 사용
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;
-- 수십 ms만 락 보유
3. 락 타임아웃 설정
-- MySQL
SET innodb_lock_wait_timeout = 5; -- 5초 대기 후 타임아웃
-- PostgreSQL
SET lock_timeout = '5s';
-- 5초 대기해도 락 못 얻으면 에러 발생
-- ERROR: Lock wait timeout exceeded
4. NOWAIT / SKIP LOCKED 사용
-- NOWAIT: 락 못 얻으면 즉시 실패
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- 이미 락 걸려 있으면 즉시 에러
-- SKIP LOCKED: 락 걸린 행 건너뛰기
SELECT * FROM jobs WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 1;
-- 락 걸린 행 제외하고 다른 행 반환
5. 낙관적 락 사용
-- 락을 안 걸고 버전으로 충돌 감지
SELECT id, balance, version FROM accounts WHERE id = 1;
-- balance = 1000, version = 5
-- 수정 시 버전 확인
UPDATE accounts
SET balance = 500, version = version + 1
WHERE id = 1 AND version = 5;
-- 다른 트랜잭션이 먼저 수정했으면 version이 달라져서 0 rows affected
-- → 재시도
데드락 발생 시 애플리케이션 처리
// 데드락 발생 시 재시도 로직
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
transferMoney(fromId, toId, amount);
break; // 성공 시 종료
} catch (DeadlockException e) {
retryCount++;
if (retryCount >= maxRetries) {
throw e; // 최대 재시도 초과
}
Thread.sleep(100 * retryCount); // 잠시 대기 후 재시도
}
}
데드락 모니터링
-- MySQL: 최근 데드락 정보 확인
SHOW ENGINE INNODB STATUS;
-- 출력 예시:
-- LATEST DETECTED DEADLOCK
-- *** (1) TRANSACTION:
-- *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
-- *** (2) TRANSACTION:
-- *** (2) HOLDS THE LOCK(S):
-- *** WE ROLL BACK TRANSACTION (1)
-- PostgreSQL: 락 대기 상황 확인
SELECT * FROM pg_locks WHERE NOT granted;
데드락 vs 라이브락
[ 데드락 (Deadlock) ]
아무것도 안 하고 멈춤
→ 양쪽 다 대기 상태로 굳어버림
[ 라이브락 (Livelock) ]
계속 뭔가 하지만 진전 없음
→ 양쪽이 계속 양보하다가 아무도 못 지나감
비유:
데드락 = 좁은 골목에서 둘 다 안 비킴 → 정지
라이브락 = 복도에서 둘이 "먼저 가세요" "아니 먼저" 반복 → 계속 움직이지만 진전 없음
핵심 정리
데드락은 두 트랜잭션이 서로의 락을 기다리며 영원히 대기하는 상태입니다. 상호 배제, 점유 대기, 비선점, 순환 대기 4가지 조건이 모두 충족될 때 발생합니다. DB는 데드락을 감지하면 한쪽 트랜잭션을 강제 롤백하여 해결합니다. 예방하려면 락 순서를 일관되게 유지하고, 트랜잭션을 짧게 가져가며, 타임아웃이나 NOWAIT 옵션을 활용합니다. 애플리케이션에서는 데드락 에러 발생 시 재시도 로직을 구현해야 합니다.