16장. 서로를 붙든 채 멈추다 — 데드락

만화로 보는 요약 — 먼저 읽어보세요
각자 락을 잡는다 → 서로가 가진 락을 기다리며 순환이 생긴다 → DB가 감지해 하나를 롤백하고 순서를 맞춘다.
면접 실전 질문: ① 데드락이 생기는 조건 ② wait-for graph의 순환이 의미하는 것 ③ 락 순서 통일이 데드락을 줄이는 이유
배경 — 현장으로
제4부까지 우리는 장치들을 하나씩 뜯어봤어요. 이제 그 장치들이 현장에서 어떻게 사고를 내는지 볼 차례입니다. 12장 락에서 예고했던 게 하나 있죠. “잘못하면 서로를 기다리다 영영 멈춘다.” 그 영영 멈춤 — 데드락(deadlock) — 이 제5부, 현장의 첫 사고입니다.
데드락은 장치가 고장 나서 생기는 게 아니에요. 오히려 각자 규칙을 완벽히 지켰는데도 터집니다. 그래서 더 골치 아프죠.
스토리 — 서로의 자물쇠를 기다리다
두 트랜잭션이 계좌 A와 B를 각각 반대 순서로 잠그는 상황을 봅시다. (이체를 양방향으로 동시에 처리한다고 생각하면 돼요.)
[T1] 계좌 A 잠금 (X락) → 성공
[T2] 계좌 B 잠금 (X락) → 성공
[T1] 계좌 B도 잠그려 함 → T2가 쥐고 있음 → 대기...
[T2] 계좌 A도 잠그려 함 → T1이 쥐고 있음 → 대기...보이시나요? T1은 T2가 쥔 B를 기다리고, T2는 T1이 쥔 A를 기다립니다. 서로가 서로의 자물쇠를 기다려요. 둘 다 상대가 먼저 놔주길 바라지만, 둘 다 안 놔줍니다. 영원히. 이게 데드락입니다. 두 사람이 서로의 손목을 붙잡은 채 “네가 먼저 놔”만 반복하는 거죠.
핵심은 잠그는 순서가 엇갈렸다는 거예요. T1은 A→B, T2는 B→A. 이 엇갈림이 원(circle)을 만듭니다. T1이 기다리는 자원의 주인(T2)이, 다시 T1이 쥔 자원을 기다리는 순환 대기.
핵심 — DB는 어떻게 풀고, 나는 어떻게 막나
다행히 DB는 이 상황을 방치하지 않습니다. 데드락을 감지(detection) 해요. 내부적으로 “누가 누구를 기다리는가” 그래프(wait-for graph)를 그리고, 거기에 원(사이클)이 생기면 데드락으로 판정합니다.
감지하면? 둘 다 영원히 멈출 순 없으니, DB가 한쪽을 희생양(victim)으로 골라 강제 롤백시킵니다. 보통 되돌릴 게 적은(한 일이 적은) 쪽을 죽여요. 희생된 트랜잭션은 이런 에러를 받죠.
MySQL: ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction
PostgreSQL: ERROR: deadlock detected에러 메시지가 친절하게도 **“try restarting transaction”**이라 말해줍니다. 즉 데드락으로 죽은 트랜잭션은 잘못이 아니라 재수가 없던 것이라, 앱은 보통 그냥 재시도하면 됩니다 (14장 낙관적 제어의 재시도와 똑같은 패턴이에요).
하지만 매번 죽고 재시도하는 건 낭비죠. 예방이 더 낫습니다. 데드락의 급소는 아까 그 “엇갈린 순서”였어요. 그러니 규칙은 하나입니다.
여러 자원을 잠글 땐, 모두가 같은 순서로 잠근다.
T1도 T2도 항상 A 먼저, 그다음 B로 잠그면? T2는 A에서 이미 막혀 기다리고, T1은 A·B를 다 얻어 끝냅니다. 원이 안 생겨요. 실무에선 “계좌 번호가 작은 것부터 잠근다”처럼 일관된 정렬 기준으로 자원을 잠그는 게 데드락 예방의 정석입니다.
// 두 계좌를 잠글 때, 항상 id가 작은 쪽부터 — 순서를 통일해 순환을 차단
// (fromId == toId 인 자기 이체는 상위에서 걸러진다고 가정)
Long first = Math.min(fromId, toId);
Long second = Math.max(fromId, toId);
accountRepository.findByIdForUpdate(first);
accountRepository.findByIdForUpdate(second);한 가지 더. 트랜잭션을 짧게 가져가는 것도 강력한 예방책이에요. 락을 오래 쥘수록 남과 부딪칠 확률이 커지니까요. 이건 다음 장(롱 트랜잭션·커넥션 풀)과 곧장 이어집니다.
정리
- 데드락 = 두 트랜잭션이 서로가 쥔 자물쇠를 기다리며 영영 멈추는 것(순환 대기). 장치 고장이 아니라, 각자 규칙을 지켰는데도 잠그는 순서가 엇갈려 생긴다.
- DB는 wait-for graph에서 사이클을 감지해, 한쪽을 victim으로 롤백시킨다
(
ERROR 1213/deadlock detected). 죽은 쪽은 보통 재시도하면 된다. - 예방: ① 여러 자원을 같은 순서로 잠근다(예: id 작은 것부터). ② 트랜잭션을 짧게.
생각해볼 질문: 데드락은 그래도 DB가 감지해서 한쪽을 죽여줬어요. 그런데 아무도 안 죽고, 에러도 안 나는데 서비스가 서서히 마비되는 사고가 있습니다. 트랜잭션을 열어둔 채 오래 붙잡고 있으면 벌어지는 일 — 커넥션이 바닥나 결국 모두가 멈추는 그 조용한 재앙으로 갑니다.