
결론부터 말씀드리면, deadlock detected 로그를 받았을 때 가장 먼저 할 일은 에러 메시지를 지우거나 재시도 로직을 추가하는 게 아니라 로그에 적힌 두 트랜잭션의 잠금 순서를 그대로 복원하는 것입니다. 어느 트랜잭션이 어떤 리소스를 먼저 잠그고 어떤 리소스를 기다리다가 되돌아갔는지를 종이에 그려보면, 코드에서 잠금 순서를 어디부터 맞춰야 하는지가 보입니다. 이 글에서는 MySQL InnoDB와 PostgreSQL을 기준으로 로그를 읽는 방법과, 같은 데드락이 반복되지 않도록 잠금 순서를 코드 레벨에서 고정하는 방법을 정리합니다.
deadlock detected 로그가 알려주는 것
MySQL InnoDB에서는 SHOW ENGINE INNODB STATUS를 실행하면 LATEST DETECTED DEADLOCK이라는 구역에 가장 최근 데드락 정보가 나타납니다. 이 구역은 트랜잭션을 TRANSACTION (1), TRANSACTION (2) 식으로 나눠서 각각 어떤 레코드 잠금을 들고 있었는지, 그리고 WAITING FOR THIS LOCK TO BE GRANTED 아래에 어떤 잠금을 기다리다 롤백됐는지를 보여줍니다.
문제는 이 명령이 가장 최근 발생한 데드락 하나만 보여준다는 점입니다. 같은 패턴이 반복되는지 확인하려면 innodb_print_all_deadlocks 변수를 ON으로 켜서 모든 데드락을 에러 로그에 누적시켜야 합니다. 이 변수는 MySQL 5.6.2부터 제공되고, 운영 중인 서버에서 SET GLOBAL innodb_print_all_deadlocks = ON;으로 즉시 적용할 수 있습니다. 이렇게 두 트랜잭션이 서로의 잠금을 기다리며 영원히 풀리지 않는 상태를 전산학에서는 교착상태라고 부릅니다.
잠금 순서를 복원하려면 로그의 어느 부분을 봐야 할까요
로그를 받으면 다음 네 가지만 뽑아서 표로 옮기는 게 가장 빠릅니다. 각 트랜잭션이 들고 있던 잠금(HOLDS)과 기다리던 잠금(WAITING)을 나란히 적으면 순환 구조가 바로 보입니다.
| 구분 | TRANSACTION (1) | TRANSACTION (2) |
|---|---|---|
| 들고 있던 잠금 | orders 테이블, id=101 레코드 락 | orders 테이블, id=205 레코드 락 |
| 기다리던 잠금 | id=205 레코드 락 | id=101 레코드 락 |
| 쿼리 | UPDATE … WHERE id=101 다음 id=205 | UPDATE … WHERE id=205 다음 id=101 |
위 예시처럼 두 트랜잭션이 같은 두 레코드를 서로 반대 순서로 잠그려 할 때 데드락이 발생합니다. 로그에서 RECORD LOCKS space id와 index 이름을 확인하면 정확히 어느 인덱스에서 충돌이 났는지도 알 수 있어서, 복합 인덱스를 쓰는 경우 어떤 인덱스 순서 때문에 락이 걸렸는지까지 추적할 수 있습니다.
MySQL과 PostgreSQL의 로그 구조는 서로 다릅니다
같은 “데드락 로그”라도 DBMS마다 저장되는 위치와 감지 방식이 다릅니다. 이 차이를 모르고 MySQL 기준 명령을 PostgreSQL에 그대로 쓰면 아무것도 안 나옵니다.
| 항목 | MySQL InnoDB | PostgreSQL |
|---|---|---|
| 로그 확인 명령 | SHOW ENGINE INNODB STATUS | 서버 로그 파일(log_lock_waits 활성 시) |
| 감지 방식 | 백그라운드 대기 그래프 자동 순회 | deadlock_timeout 경과 후 순환 검사 |
| 기본 대기 시간 | 즉시 감지(폴링 기반) | deadlock_timeout 기본 1초 |
| 전체 이력 보기 | innodb_print_all_deadlocks=ON | log_lock_waits=on, log_min_duration 조합 |
PostgreSQL은 deadlock_timeout(기본 1초)이 지나야 순환 대기 여부를 검사하기 때문에, 짧은 트랜잭션이 몰리는 구간에서는 데드락 대신 그냥 느린 응답처럼 보이는 경우도 있습니다. 블로킹 관계를 바로 확인하려면 아래 쿼리로 pg_locks와 pg_stat_activity를 조인해서 어느 pid가 어느 pid를 막고 있는지 볼 수 있습니다.
SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_query,
blocking_activity.query AS blocking_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.pid != blocked_locks.pid
AND NOT blocked_locks.granted
JOIN pg_catalog.pg_stat_activity blocked_activity
ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity
ON blocking_activity.pid = blocking_locks.pid;
잠금 순서를 고정해서 재발을 막는 방법
순환을 찾았다면 해결책은 결국 하나입니다. 모든 트랜잭션이 같은 리소스 집합을 항상 같은 순서로 잠그도록 코드를 바꾸는 것입니다. 가장 흔한 방법은 기본키(PK) 오름차순으로 잠그도록 쿼리 순서를 통일하는 것입니다.

-- 나쁜 예: 트랜잭션 A, B가 id를 서로 다른 순서로 잠금
-- 트랜잭션 A
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 트랜잭션 B
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
-- 고친 예: 두 트랜잭션 모두 id 오름차순으로 잠금 순서를 통일
-- 트랜잭션 A, B 공통
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
여러 행을 한꺼번에 갱신하는 배치 작업이라면 ORDER BY 없이 반복문을 돌리는 코드가 특히 위험합니다. 리스트 순서가 호출마다 뒤바뀌는 애플리케이션 코드(예: 해시맵 순회, 비정렬 배열)에서 이런 데드락이 잦게 나타나므로, 갱신 대상 id 목록을 정렬한 뒤 순서대로 처리하는 습관을 들이는 쪽이 안전합니다. MySQL InnoDB는 기본적으로 자동 감지 방식을 쓰고 있어서 순환이 생기면 둘 중 하나를 즉시 롤백시켜 주는데, 이 동작과 innodb_deadlock_detect 변수의 역할은 InnoDB 공식 문서에 상세히 설명돼 있습니다.
재시도 로직만으로는 데드락이 사라지지 않는 이유
애플리케이션에서 데드락 예외를 잡아 자동 재시도를 거는 패턴은 임시 처방일 뿐입니다. 트래픽이 적을 때는 재시도로 충돌을 가려줄 수 있지만, 동시 접속이 늘어나면 같은 두 트랜잭션이 계속 서로를 걷어차면서 재시도 횟수와 지연 시간만 쌓입니다. 특히 배치 작업과 실시간 트랜잭션이 같은 테이블을 건드리는 구조에서는 재시도가 오히려 락 경쟁을 더 늘리는 역효과를 낼 수 있습니다.
또 하나 짚을 점은, 잠금 순서를 고정하는 방법이 모든 경우에 적용되지는 않는다는 것입니다. 외부 서비스 호출이나 사용자 입력에 따라 잠글 리소스 목록 자체가 트랜잭션마다 동적으로 달라지는 구조라면, 정렬만으로 순서를 고정하기 어렵습니다. 이런 경우에는 애초에 트랜잭션 범위를 좁혀서 잠금을 짧게 유지하거나, 비관적 잠금 대신 버전 컬럼을 이용한 낙관적 동시성 제어로 구조를 바꾸는 쪽이 더 근본적인 해결책이 됩니다.
타임아웃과 데드락 감지, 둘 중 뭐가 먼저 작동하나요?
innodb_lock_wait_timeout과 데드락 감지는 서로 다른 메커니즘입니다. 데드락 감지는 순환 대기가 확인되는 즉시 둘 중 하나를 롤백시키고, 타임아웃은 순환이 없어도 한쪽이 일정 시간 넘게 기다리면 강제로 끝내버리는 안전장치입니다. 그래서 로그에 deadlock detected가 아니라 Lock wait timeout exceeded만 찍혀 있다면, 진짜 순환 교착이 아니라 한쪽 트랜잭션이 너무 오래 잠금을 쥐고 있었다는 뜻이므로 접근 방법이 달라져야 합니다. 트랜잭션 안에서 외부 API를 호출하거나 긴 로직을 넣어두지 않았는지부터 확인하는 게 우선입니다.
deadlock detected 로그를 받았을 때 핵심은 로그에 적힌 두 트랜잭션의 잠금 순서를 표로 옮겨 순환을 눈으로 확인하고, 코드에서 같은 리소스를 항상 같은 순서로 잠그도록 정렬 기준을 통일하는 것입니다. 당장은 innodb_print_all_deadlocks나 log_lock_waits를 켜서 누적 로그부터 모으고, 반복되는 테이블·인덱스 조합이 보이면 그 쿼리들의 잠금 순서를 맞추는 작업부터 시작해 보시기 바랍니다.
