간헐적으로 특정 API에서 Deadlock found when trying to get lock; try restarting transaction 에러가 올라왔습니다. 하루에 몇 건, 그것도 트래픽이 몰리는 시간대에만 나타났고, 같은 요청을 다시 보내면 멀쩡히 성공했습니다. 재현이 되지 않아 며칠을 헤맸는데, 원인은 코드의 버그가 아니라 여러 트랜잭션이 같은 데이터를 서로 다른 순서로 잠근다는, 눈에 잘 안 보이는 동시성 문제였습니다.
데드락은 락 대기와 비슷해 보이지만 성격이 다릅니다. 락 대기는 시간이 지나면 풀리지만, 데드락은 두 트랜잭션이 서로가 쥔 락을 영원히 기다리는 교착 상태라 스스로 풀리지 않습니다. 그래서 데이터베이스가 한쪽을 강제로 롤백시켜 버립니다.
이 글에서는 데드락이 정확히 어떤 상황에서 생기는지, InnoDB에서 원인을 어떻게 진단하는지, 그리고 락 순서 통일과 재시도로 어떻게 근본적으로 푸는지를 실전 코드와 함께 정리합니다.
데드락이란: 서로가 서로를 기다리는 교착

데드락은 두 개 이상의 트랜잭션이 상대가 쥐고 있는 락을 서로 기다리며 아무도 나아가지 못하는 상태입니다. 가장 흔한 형태는 이렇습니다.
트랜잭션 T1이 A행을 잠근 뒤 B행을 잠그려 하고, 동시에 트랜잭션 T2가 B행을 잠근 뒤 A행을 잠그려 합니다. 이제 T1은 T2가 쥔 B를 기다리고, T2는 T1이 쥔 A를 기다립니다. 서로가 서로를 기다리는 **순환 대기(circular wait)**가 만들어지고, 둘 다 영원히 진행할 수 없습니다.
이 상황을 방치하면 두 트랜잭션이 모두 멈추므로, InnoDB 같은 데이터베이스는 데드락을 감지하면 둘 중 하나를 희생자(victim)로 골라 강제 롤백시킵니다. 롤백된 쪽 애플리케이션은 데드락 예외를 받게 됩니다. 자원이 시간에 따라 잠겨가다 문제가 되는 서버 교체 후 1시간마다 죽는 서비스, 원인은 DB 락의 락 대기와는 결이 다릅니다. 그건 순서대로 풀리는 대기 행렬이고, 데드락은 스스로 못 푸는 교착입니다.
왜 생기나: 락을 잡는 ‘순서’가 다를 때
데드락의 뿌리는 대부분 하나입니다. 여러 트랜잭션이 같은 자원들을 서로 다른 순서로 잠그는 것. 계좌 이체 코드를 예로 보면 명확합니다.
@Transactional
public void transfer(Long fromId, Long toId, long amount) {
Account from = accountRepo.findByIdForUpdate(fromId); // 1) from 행 잠금
Account to = accountRepo.findByIdForUpdate(toId); // 2) to 행 잠금
from.withdraw(amount);
to.deposit(amount);
}
이 코드에서 A→B로 이체하는 요청과 B→A로 이체하는 요청이 동시에 들어오면, 앞의 요청은 A를 잠그고 B를 기다리고, 뒤의 요청은 B를 잠그고 A를 기다립니다. 정확히 순환 대기가 만들어집니다. 코드 자체는 멀쩡해 보이지만, 잠그는 순서가 인자에 따라 달라진다는 점이 함정입니다.
여기에 인덱스 문제가 겹치면 확률이 더 올라갑니다. 인덱스 없는 조건으로 갱신하면 넓은 범위에 락이 걸려, 원래는 충돌하지 않을 행끼리도 서로 얽힙니다. 인덱스 튜닝으로 락 범위와 성능을 함께 잡은 사례는 MySQL 쿼리 지연 해결: 3개월 클라우드 DB 분투기에서 다뤘습니다.
어떻게 찾나: SHOW ENGINE INNODB STATUS
데드락은 순간적으로 일어나 재현이 어렵지만, InnoDB는 가장 최근에 감지한 데드락의 상세 내역을 기록해 둡니다.
SHOW ENGINE INNODB STATUS;
출력의 LATEST DETECTED DEADLOCK 섹션을 보면, 어떤 두 트랜잭션이 각각 어떤 락을 쥐고 어떤 락을 기다렸는지, 그리고 데이터베이스가 어느 쪽을 희생자로 롤백했는지가 그대로 찍혀 있습니다. 여기서 두 트랜잭션이 잠근 자원과 순서를 비교하면, 어느 코드 경로들이 서로 엇갈린 순서로 락을 잡았는지 역추적할 수 있습니다.
애플리케이션 쪽에서는 데드락이 특정 예외로 올라옵니다. 스프링·JPA 환경이라면 DeadlockLoserDataAccessException(내부적으로 SQL 에러 코드 1213)으로 잡히므로, 이 예외를 로깅해 발생 빈도와 대상 쿼리를 모으는 것이 진단의 출발점입니다. 동시성 문제를 애플리케이션 레벨에서 다루는 접근은 Redis 분산락 구현하기에서도 정리한 바 있습니다.
데드락 vs 단순 락 대기
둘을 구분하면 대응이 달라집니다.
| 구분 | 단순 락 대기(blocking) | 데드락(deadlock) |
|---|---|---|
| 상황 | 한쪽이 락을 놓기를 기다림 | 서로가 서로의 락을 기다림(순환) |
| 해소 | 앞 트랜잭션이 끝나면 자동 해소 | 스스로 못 풀림 → DB가 한쪽 강제 롤백 |
| 대표 에러 | (지연 후 진행) 또는 lock wait timeout | Deadlock found … (error 1213) |
| 핵심 대응 | 트랜잭션 단축·인덱스 최적화 | 락 순서 통일 + 재시도 |
락 대기는 “느려지는” 문제라 트랜잭션을 짧게 하고 인덱스를 손보면 완화됩니다. 데드락은 “실패하는” 문제라, 순환 자체를 없애고 실패한 트랜잭션을 다시 시도하는 두 방향으로 접근해야 합니다.
어떻게 푸나: 순서 통일 + 짧은 트랜잭션 + 재시도
가장 근본적인 해결은 모든 트랜잭션이 자원을 같은 순서로 잠그게 만드는 것입니다. 순환이 생기려면 순서가 엇갈려야 하므로, 순서를 통일하면 데드락의 조건 자체가 사라집니다. 앞의 이체 코드는 항상 작은 ID부터 잠그도록 바꾸면 됩니다.
@Transactional
public void transfer(Long fromId, Long toId, long amount) {
Long first = Math.min(fromId, toId);
Long second = Math.max(fromId, toId);
accountRepo.findByIdForUpdate(first); // 항상 작은 ID 먼저
accountRepo.findByIdForUpdate(second); // 그다음 큰 ID
// 모든 트랜잭션이 같은 순서로 잠그므로 순환이 생기지 않는다
...
}
두 번째는 트랜잭션을 짧게 유지하는 것입니다. 락을 쥐고 있는 시간이 길수록 충돌 확률이 커지므로, 트랜잭션 안에서 외부 API 호출이나 긴 연산을 하지 않고 꼭 필요한 갱신만 빠르게 끝냅니다. 세 번째는 적절한 인덱스로 락 범위를 좁혀, 갱신이 넓은 범위를 잠그지 않게 하는 것입니다.
마지막은 재시도입니다. 데드락은 순간적인 타이밍 문제라, 희생자로 롤백된 트랜잭션을 잠시 후 다시 실행하면 대개 성공합니다. 스프링에서는 재시도를 선언적으로 걸 수 있습니다.
@Retryable(
retryFor = DeadlockLoserDataAccessException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100))
@Transactional
public void transfer(Long fromId, Long toId, long amount) { ... }
단, 재시도는 그 작업이 **여러 번 실행돼도 안전(멱등)**할 때만 안전합니다. 재시도는 남은 소수의 데드락을 흡수하는 안전망일 뿐, 근본 해결은 어디까지나 락 순서 통일이라는 점을 기억해야 합니다.
재발을 막으려면
이번 데드락은 코드를 아무리 들여다봐도 버그가 없어 오래 헤맸습니다. 각 요청은 완벽히 정상이었고, 다만 두 요청이 동시에 엇갈린 순서로 같은 자원을 잠글 때만 터졌기 때문입니다. 결국 문제는 한 줄의 로직이 아니라 동시에 실행됐을 때의 순서였습니다.
SHOW ENGINE INNODB STATUS로 실제 락 순서를 확인하고, 자원 접근 순서를 통일하고 나니 데드락은 사라졌습니다. 여러 행을 함께 갱신하는 코드를 짤 때는 처음부터 “이 자원들을 항상 어떤 순서로 잠글 것인가”를 정해두는 것이, 나중에 재현 안 되는 데드락으로 밤을 새우지 않는 가장 확실한 방법입니다.
핵심 요약
- 데드락은 여러 트랜잭션이 같은 자원을 서로 다른 순서로 잠글 때 생기는 순환 대기다
- 진단은
SHOW ENGINE INNODB STATUS의 LATEST DETECTED DEADLOCK + 애플리케이션 예외 로깅- 해결은 락 순서 통일(근본) + 짧은 트랜잭션·인덱스 + 멱등한 재시도(안전망)