데드락(교착 상태) — 락을 거는 순서만 통일하면 사라집니다

@JavaPark · 2026년 8월 23일 · 11 min read

데드락 커버 — 두 트랜잭션이 서로가 쥔 락을 기다리며 고리를 이루는 구조
데드락 커버 — 두 트랜잭션이 서로가 쥔 락을 기다리며 고리를 이루는 구조

안녕하세요, 자바파커입니다.

요청이 멈췄습니다. CPU도 한가하고 DB 부하도 평소와 같습니다. 그리고 로그에 이게 찍힙니다.

Deadlock found when trying to get lock; try restarting transaction

멈춘 게 아닙니다. 서로를 기다리고 있는 겁니다.

결론부터 말씀드리면 — 데드락은 고리입니다. T1이 T2를 기다리고, T2가 다시 T1을 기다리면 둘 다 영원히 못 갑니다. 그리고 이 고리는 락을 거는 순서만 통일하면 만들어지지 않습니다.


어떻게 생기나

계좌 이체가 교과서 사례입니다. 두 요청이 동시에 들어옵니다.

T1: A → B 로 이체
T2: B → A 로 이체

각자 출금 계좌부터 잠급니다.

시각 T1 T2
1 A 잠금 성공 B 잠금 성공
2 B 잠금 시도 → 대기 A 잠금 시도 → 대기
3 T2가 B를 놓기를 기다림 T1이 A를 놓기를 기다림

T1은 T2를, T2는 T1을 기다립니다. 아무도 자기가 쥔 걸 놓지 않으니 영원히 안 풀립니다.

여기서 중요한 건 둘 다 정상적인 코드라는 점입니다. 버그가 없습니다. 순서만 엇갈렸습니다.


네 가지 조건 — 그중 하나만 깨면 된다

데드락은 아래 넷이 동시에 성립할 때만 생깁니다.

조건 실무에서 깰 수 있나
상호 배제 한 번에 하나만 쓸 수 있다 ✗ 락의 존재 이유
점유와 대기 쥔 채로 다음 걸 기다린다 △ 어렵다
비선점 남의 것을 뺏을 수 없다 ✗ DB가 정한다
순환 대기 기다림이 고리를 이룬다 ○ 여기를 깬다

앞의 셋은 락이라는 장치의 성질이라 우리가 못 바꿉니다. 실무에서 손댈 수 있는 건 순환 대기 하나뿐입니다.

그리고 고리를 없애는 방법은 단순합니다 — 모두가 같은 순서로 잠그면 됩니다.

T1: A 잠금 → B 잠금
T2: A 잠금 → (T1이 놓을 때까지 대기) → B 잠금

T2는 기다리지만 T1은 안 기다립니다. 고리가 아니라 줄입니다. 줄은 언젠가 끝납니다.


DB는 이걸 어떻게 처리하나

다행히 DB가 알아서 끊어 줍니다. 다만 끊는 방식이 다릅니다.

DB 동작
MySQL (InnoDB) 대기 그래프에서 고리를 즉시 탐지하고 한쪽을 롤백합니다
PostgreSQL deadlock_timeout(기본 1초)만큼 기다린 뒤 검사해서 한쪽을 중단합니다

MySQL은 작업량이 적은 쪽(변경한 행이 적은 트랜잭션)을 골라 롤백합니다. 그쪽이 되돌리기 싸니까요.

에러를 받은 쪽은 이렇게 나옵니다.

MySQL       ERROR 1213 (40001): Deadlock found when trying to get lock
PostgreSQL  ERROR: deadlock detected (SQLSTATE 40P01)

40001은 "다시 시도하면 될 수도 있다"는 뜻의 표준 에러 클래스입니다. 그래서 데드락은 재시도가 정상적인 대응입니다. 로직 버그처럼 500으로 던지지 마세요.

직전 데드락의 전문은 이렇게 봅니다.

-- MySQL
SHOW ENGINE INNODB STATUS;   -- LATEST DETECTED DEADLOCK 절

실무에서 자주 만드는 네 가지

1. 락 순서가 코드마다 다르다

가장 흔합니다. 위의 이체 사례가 그대로입니다. 어떤 메서드는 A→B로, 다른 메서드는 B→A로 잠급니다.

고치는 법 — 순서를 규칙으로 못 박습니다.

// 항상 id 가 작은 쪽을 먼저 잠근다
long first  = Math.min(fromId, toId);
long second = Math.max(fromId, toId);

accountRepo.lockById(first);
accountRepo.lockById(second);

무엇을 기준으로 삼든 상관없습니다. 모두가 같은 기준을 쓰기만 하면 고리가 안 생깁니다.

2. 인덱스 없는 UPDATE

이건 덜 알려져 있는데 효과가 큽니다.

UPDATE orders SET status = 'X' WHERE user_email = 'a@b.c';

user_email에 인덱스가 없으면 InnoDB는 훑는 행마다 락을 겁니다. 한 행만 바꾸려던 쿼리가 훨씬 넓은 범위를 잠그고, 그만큼 다른 트랜잭션과 부딪힐 면적이 커집니다.

인덱스는 성능만의 문제가 아닙니다. 락 범위를 좁히는 장치이기도 합니다.

3. 트랜잭션이 길다

락은 트랜잭션이 끝나야 풀립니다. 트랜잭션 안에 외부 API 호출이나 파일 처리가 들어 있으면 그동안 계속 쥐고 있습니다. 부딪힐 시간이 길어지니 데드락 확률도 같이 올라갑니다.

4. 외래 키

자식 행을 INSERT 하면 InnoDB가 부모 행에 공유 락을 겁니다. 코드에 락을 건 적이 없어도 락이 생깁니다. 여러 자식이 같은 부모를 참조하면서 순서가 엇갈리면 데드락이 납니다.


애플리케이션 데드락도 같은 원리입니다

DB만의 이야기가 아닙니다. 자바에서도 똑같이 납니다.

// 스레드 1
synchronized (a) { synchronized (b) { ... } }

// 스레드 2  ← 순서가 반대다
synchronized (b) { synchronized (a) { ... } }

그리고 이쪽이 더 위험합니다. DB는 탐지해서 끊어 주지만, JVM은 안 끊어 줍니다. 두 스레드가 영원히 멈춰 있고 재시작 말고는 답이 없습니다.

발견은 스레드 덤프로 합니다.

jcmd <pid> Thread.print | grep -A5 "Found one Java-level deadlock"

여기서도 답은 같습니다 — 잠그는 순서를 통일하세요.


정리 — 순서대로 볼 것

  1. 에러 코드를 봅니다. 40001 계열이면 데드락이고, 재시도가 정상 대응입니다
  2. SHOW ENGINE INNODB STATUS로 어느 두 쿼리가 물렸는지 봅니다
  3. 그 둘의 락 순서를 맞춥니다. 대부분 여기서 끝납니다
  4. 안 끝나면 인덱스를 봅니다. 훑는 범위가 넓으면 락도 넓습니다
  5. 재시도는 넣되, 원인을 덮는 용도로 쓰지 마세요

자주 묻는 질문 (FAQ)

데드락과 락 대기(lock wait timeout)는 다른가요?

다릅니다. 락 대기는 앞사람이 끝나면 풀립니다 — 그냥 느린 겁니다. 데드락은 고리라서 기다려도 안 풀립니다. 그래서 DB가 강제로 하나를 죽입니다. Lock wait timeout exceeded(1205)를 보고 데드락으로 오해하는 경우가 많은데, 그건 긴 트랜잭션 문제입니다.

재시도만 넣으면 되지 않나요?

재시도는 필요하지만 충분하지 않습니다. 데드락이 잦으면 그만큼 롤백된 작업이 버려지고 응답이 느려집니다. 재시도는 안전망이고, 락 순서를 맞추는 게 해결입니다. 그리고 재시도할 때는 약간의 지연을 두고 하세요 — 즉시 재시도하면 같은 충돌이 반복됩니다.

격리 수준을 낮추면 데드락이 줄어드나요?

줄어들 수 있지만 권할 방법이 아닙니다. MySQL에서 REPEATABLE READ를 READ COMMITTED로 낮추면 갭 락이 줄어 충돌 면적이 작아지긴 합니다. 다만 격리 수준을 낮추면 다른 이상 현상이 새로 들어옵니다. 데드락을 고치려고 정확성을 파는 셈입니다.

락을 아예 안 쓰면 안 되나요?

낙관적 락(버전 확인)으로 바꾸면 데드락은 사라집니다. 대신 충돌 시 실패하므로 재시도 로직이 필수가 되고, 충돌이 잦은 자원에서는 오히려 처리량이 떨어집니다. 충돌이 드물면 낙관적 락, 잦으면 비관적 락 + 순서 통일이 정석입니다.


정리하며

데드락은 고리입니다.

  • 네 조건이 동시에 성립할 때만 생기고, 그중 깰 수 있는 건 순환 대기 하나입니다
  • 모두가 같은 순서로 잠그면 고리가 줄이 됩니다. 줄은 끝납니다
  • 인덱스가 없으면 락 범위가 넓어져 부딪힐 면적이 커집니다
  • 40001은 재시도하라는 뜻입니다. 다만 재시도는 안전망이지 해결이 아닙니다
  • JVM 데드락은 아무도 안 끊어 줍니다. 여기가 DB보다 위험합니다

데드락 잡아 보신 적 있으신가요? 원인이 락 순서였나요, 아니면 다른 것이었나요?

참고 자료

@JavaPark
AI 시대의 개발자 도구, 실전 경험을 공유합니다