주문 A의 재고를 줄이는 작업과 주문 B의 상태를 바꾸는 작업이 동시에 실행됐습니다. 한쪽은 재고 행을 먼저 잠그고 주문 행을 기다립니다. 다른 쪽은 주문 행을 먼저 잠그고 재고 행을 기다립니다. 서로가 가진 잠금이 풀리기만 기다리는 순환이 생기면 데이터베이스는 한 트랜잭션을 취소해야 합니다. 이것이 데드락입니다. 로그에 데드락이 한 번 보였다고 데이터가 망가진 것은 아닙니다. 하지만 이를 무시하거나 무조건 같은 SQL 한 줄만 다시 실행하면 실제 업무 실패가 남을 수 있습니다.
데드락은 왜 정상 코드에서도 생기나
동시 요청이 많아지면 잠금을 얻는 순서가 엇갈릴 수 있습니다. 여러 테이블을 수정하지 않아도 인덱스 레코드와 범위 잠금의 조합으로 발생할 수 있습니다. MySQL InnoDB 문서는 데드락 발생 시 한 트랜잭션을 희생자로 선택해 롤백한다고 설명합니다. 애플리케이션 입장에서는 그 업무가 완료된 것이 아니라 실패 응답을 받은 것입니다. 따라서 실패를 사용자에게 바로 던질지, 안전하게 다시 시도할지 결정해야 합니다.
우선 잠금을 짧게 잡습니다. 사용자 입력을 기다리거나 외부 API를 호출하는 동안 DB 트랜잭션을 열어 두지 않습니다. 필요한 행을 정확한 조건과 인덱스로 찾고, 여러 테이블을 수정한다면 코드 경로마다 같은 순서로 잠그도록 설계합니다. 예컨대 항상 고객 → 주문 → 재고 순서로 접근하면 반대 순서의 순환을 줄일 수 있습니다. 인덱스가 부실해 예상보다 넓은 범위를 잠그는지도 확인해야 합니다.

재시도는 SQL 한 줄이 아니라 트랜잭션 전체
희생자로 선택된 트랜잭션은 전체가 롤백됩니다. 마지막 UPDATE만 재실행하면 앞에서 읽고 계산한 상태가 오래된 값일 수 있습니다. 새 트랜잭션을 열어 현재 데이터를 다시 읽고 업무 조건을 다시 판단한 뒤 전체 작업을 수행해야 합니다. 무한 반복은 금물입니다. 제한된 횟수와 짧은 무작위 지연을 두고, 재시도 실패율을 알림으로 보냅니다. MySQL의 데드락 처리 지침도 트랜잭션 재시도를 준비하고 짧은 트랜잭션·일관된 접근 순서를 권합니다.
트랜잭션 안에서 이메일이나 결제 요청을 먼저 보내는 설계는 특히 조심해야 합니다. DB는 롤백됐는데 외부 부작용은 이미 발생했을 수 있기 때문입니다. 외부 호출을 DB 작업과 분리하고 업무 이벤트를 안전하게 전달하는 경로를 마련해야 합니다. 재시도 대상도 구분합니다. 데드락은 재시도 가능한 일시 오류일 수 있지만 유일 제약 위반이나 권한 거부는 같은 입력으로 반복해도 해결되지 않습니다. 오류 코드를 분석해 재시도 정책을 세워야 합니다.
재시도 중에는 사용자에게 두 번 저장된 것처럼 보이지 않게 요청 단위의 결과를 관리합니다. 예를 들어 결재 승인 명령에 업무 요청 ID를 붙여 이미 승인된 같은 요청인지 확인합니다. 다만 데드락이 발생한 트랜잭션은 롤백됐으므로 성공으로 기록해서는 안 됩니다. 성공 응답은 실제 커밋 이후에만 반환해야 합니다. 잠금 대기 시간 초과와 데드락은 서로 다른 오류이므로 로그와 메트릭에서도 분리해 집계하면 어떤 설계 문제가 있는지 더 빨리 찾을 수 있습니다.
빈도와 영향을 함께 측정한다
데드락 로그가 한 건 있었다는 사실보다 어떤 화면·배치에서 얼마나 자주 발생했고, 몇 번의 재시도로 성공했는지가 중요합니다. 실패한 트랜잭션의 유형, 걸린 시간, 관련 테이블과 인덱스를 기록합니다. 마지막 데드락 상황은 SHOW ENGINE INNODB STATUS 등으로 조사할 수 있습니다. 같은 잠금 그래프가 반복된다면 재시도를 늘리기 전에 쿼리 순서와 범위를 바꿔야 합니다. 재시도가 많아질수록 DB 부하가 늘고 다른 요청의 지연까지 악화시킬 수 있습니다.

테스트 환경에서는 두 트랜잭션이 서로 다른 순서로 같은 자원을 잡게 해 데드락을 재현합니다. 롤백 이후 부분 결과가 남지 않는지, 재시도 성공 시 결과가 한 번만 반영되는지 확인하세요. 데드락은 발생 가능성을 0으로 만들 수 있는 설정값이 아니라, 충돌을 줄이는 트랜잭션 설계와 실패를 되돌리는 복구 코드로 관리하는 운영 현상입니다.