MySQL InnoDB 데드락 발생 원리와 SHOW ENGINE INNODB STATUS 분석 방법

MySQL을 운영하다 보면 정상적으로 처리되던 INSERT나 UPDATE 작업이 갑자기 실패하면서 Deadlock found when trying to get lock 오류가 발생할 수 있다. 데드락은 단순히 쿼리 실행 시간이 길어지는 현상이 아니라, 두 개 이상의 트랜잭션이 서로 상대방이 보유한 잠금이 해제되기를 기다리면서 더 이상 진행하지 못하는 상태를 의미한다.
InnoDB는 이러한 교착 상태를 감지하면 트랜잭션 하나를 강제로 롤백해 전체 시스템이 무한정 정지하는 것을 방지한다. 따라서 데드락 오류가 발생했다는 사실만으로 데이터베이스 장애라고 판단할 필요는 없다. 다만 동일한 데드락이 반복된다면 애플리케이션의 트랜잭션 처리 순서, 인덱스 구성, 쿼리 조건 등을 점검해야 한다.
운영 환경에서는 오류 메시지만 확인하고 문제 쿼리를 추측하기보다 SHOW ENGINE INNODB STATUS 결과에 기록된 트랜잭션과 잠금 정보를 정확하게 읽는 것이 중요하다. 이 글에서는 InnoDB 데드락이 발생하는 원리부터 실제 분석 순서와 해결 방법까지 자세히 살펴본다.
InnoDB 데드락이 발생하는 원리
InnoDB는 여러 사용자가 동시에 데이터를 변경하더라도 일관성을 유지하기 위해 레코드 잠금, 갭 잠금, 넥스트 키 잠금 등의 잠금 방식을 사용한다. 트랜잭션이 특정 행을 변경하면 해당 작업이 커밋되거나 롤백될 때까지 필요한 잠금을 보유한다.
대표적인 데드락은 두 트랜잭션이 서로 다른 순서로 동일한 데이터를 변경할 때 발생한다. 예를 들어 트랜잭션 A가 1번 주문을 먼저 수정한 뒤 2번 주문을 수정하려 하고, 트랜잭션 B는 2번 주문을 먼저 수정한 뒤 1번 주문을 수정하려는 상황을 생각할 수 있다.
트랜잭션 A는 1번 주문의 잠금을 보유한 상태에서 2번 주문을 기다린다. 반대로 트랜잭션 B는 2번 주문의 잠금을 보유하면서 1번 주문을 기다린다. 어느 쪽도 먼저 작업을 완료할 수 없으므로 순환 대기 관계가 형성된다.
InnoDB의 데드락 감지 기능은 이러한 관계를 확인한 후 일반적으로 롤백 비용이 더 적은 트랜잭션을 희생 대상으로 선택한다. 변경한 행이 적은 트랜잭션이 롤백될 가능성이 높지만, 항상 단순한 실행 시간이나 시작 순서만으로 결정되는 것은 아니다.
데드락은 행 단위 UPDATE에서만 발생하지 않는다. 외래 키 검증, 중복 키 검사, 범위 조건을 사용한 UPDATE와 DELETE, 적절한 인덱스가 없는 쿼리에서도 발생할 수 있다. 특히 REPEATABLE READ 격리 수준에서는 팬텀 읽기를 방지하기 위한 갭 잠금과 넥스트 키 잠금이 적용될 수 있어 실제 데이터가 없는 인덱스 구간에서도 잠금 충돌이 생길 수 있다.
SHOW ENGINE INNODB STATUS에서 확인할 핵심 정보
가장 최근에 발생한 InnoDB 데드락은 다음 명령으로 확인할 수 있다.
SHOW ENGINE INNODB STATUS;
결과가 길게 출력되므로 LATEST DETECTED DEADLOCK 영역을 우선 찾아야 한다. 이 영역에는 데드락에 참여한 트랜잭션, 실행 중이던 SQL, 보유한 잠금, 기다리는 잠금, 최종적으로 롤백된 트랜잭션이 기록된다.
먼저 (1) TRANSACTION과 (2) TRANSACTION 영역에서 각 트랜잭션의 상태를 확인한다. ACTIVE 뒤에 표시된 시간은 해당 트랜잭션이 활성 상태로 유지된 시간을 나타낸다. LOCK WAIT는 현재 잠금 대기 중이라는 의미이며, undo log entries는 해당 트랜잭션이 변경한 작업량을 판단하는 참고 자료가 된다.
다음으로 각 트랜잭션 아래에 출력되는 SQL을 확인한다. 여기서 중요한 것은 두 SQL을 개별적으로 보는 것이 아니라 어떤 테이블과 인덱스의 어떤 레코드를 서로 반대 순서로 접근했는지 연결해서 해석하는 것이다.
HOLDS THE LOCK(S)는 해당 트랜잭션이 이미 보유한 잠금을 의미하고, WAITING FOR THIS LOCK TO BE GRANTED는 추가로 획득하려다 대기하게 된 잠금을 의미한다. 두 트랜잭션이 보유한 잠금과 기다리는 잠금을 교차해서 비교하면 데드락의 순환 구조를 확인할 수 있다.
잠금 정보에 표시되는 index 항목도 반드시 살펴봐야 한다. PRIMARY가 표시되면 기본 키 인덱스 레코드에서 충돌한 것이고, 보조 인덱스 이름이 표시되면 해당 보조 인덱스를 검색하거나 변경하는 과정에서 충돌한 것이다. lock_mode X는 배타적 잠금을 의미하며, locks rec but not gap은 갭을 제외한 레코드 자체에 대한 잠금이라는 뜻이다.
마지막의 WE ROLL BACK TRANSACTION 문구는 InnoDB가 어느 트랜잭션을 롤백 대상으로 선택했는지 보여준다. 이 번호는 오류가 발생한 애플리케이션 세션과 연결해 확인할 때 유용하다.
실제 운영 환경에서 적용한 분석 순서
개인적으로 데드락을 분석할 때는 가장 먼저 애플리케이션 오류 로그의 발생 시간, SQL, 바인드 값, 트랜잭션 처리 단위를 확보한다. SHOW ENGINE INNODB STATUS에는 가장 최근 데드락 한 건만 남기 때문에 새로운 데드락이 발생하면 이전 내용이 덮어쓰일 수 있다. 따라서 장애가 발생한 직후 결과를 별도의 파일이나 모니터링 시스템에 보관하는 것이 좋다.
이후 두 트랜잭션이 실행한 SQL을 비교하고 WHERE 조건에 사용된 인덱스를 확인한다. 실행 계획은 다음과 같이 점검할 수 있다.
EXPLAIN
UPDATE orders
SET order_status = 'COMPLETED'
WHERE customer_id = 1001
AND order_date >= '2026-08-01';
WHERE 조건에 적절한 복합 인덱스가 없으면 InnoDB가 예상보다 넓은 범위의 인덱스 레코드를 탐색하고 잠글 수 있다. 이 경우 단순히 처리 속도만 느려지는 것이 아니라 다른 트랜잭션과 잠금 범위가 겹치면서 데드락 가능성도 커진다.
실제 트러블슈팅 과정에서는 단일 SQL만 수정해서 해결하려 하면 원인을 놓치는 경우가 많았다. 서비스 코드 전체의 트랜잭션 흐름을 확인해 보니 한 기능은 회원 테이블을 수정한 후 주문 테이블을 변경하고, 다른 기능은 주문 테이블을 수정한 뒤 회원 테이블에 접근하는 식으로 잠금 순서가 반대인 사례가 있었다. 두 기능 모두 개별적으로는 정상적인 쿼리였지만 동시에 실행되면서 데드락이 발생한 것이다.
이 문제는 모든 업무 로직에서 테이블과 행을 동일한 순서로 잠그도록 변경해 해결할 수 있었다. 여러 주문을 한꺼번에 처리할 때도 입력받은 순서대로 수정하기보다 기본 키를 기준으로 정렬한 후 처리하면 트랜잭션별 잠금 순서가 일정해져 데드락 가능성을 낮출 수 있다.
분석 시에는 performance_schema.data_locks와 performance_schema.data_lock_waits도 유용하다. SHOW ENGINE INNODB STATUS가 최근 데드락의 결과를 보여준다면 Performance Schema는 현재 보유 중인 잠금과 대기 관계를 확인하는 데 적합하다. MySQL 버전에 따라 제공되는 컬럼이 다를 수 있으므로 운영 버전의 공식 문서를 기준으로 조회문을 구성해야 한다.
반복되는 데드락 전체를 기록해야 한다면 다음 설정도 검토할 수 있다.
SET GLOBAL innodb_print_all_deadlocks = ON;
이 설정을 활성화하면 모든 데드락 정보가 MySQL 오류 로그에 기록된다. 다만 데드락이 매우 빈번한 환경에서는 로그 발생량이 증가할 수 있으므로 분석 기간에 한시적으로 사용하거나 로그 수집 용량을 함께 점검하는 것이 안전하다.
데드락을 줄이는 실질적인 해결 방법
가장 효과적인 해결책은 트랜잭션이 데이터에 접근하는 순서를 통일하는 것이다. 여러 테이블을 수정한다면 모든 기능에서 동일한 테이블 순서를 따르고, 동일 테이블의 여러 행을 변경한다면 기본 키와 같은 일정한 기준으로 정렬해 처리해야 한다.
트랜잭션의 범위도 가능한 한 짧게 유지해야 한다. 트랜잭션을 시작한 뒤 외부 API를 호출하거나 사용자 입력을 기다리면 잠금 보유 시간이 불필요하게 길어진다. 데이터베이스 변경에 필요한 로직만 트랜잭션 안에 포함하고, 네트워크 호출이나 복잡한 계산은 가능하면 트랜잭션 밖에서 처리하는 것이 좋다.
인덱스 추가 역시 중요한 해결책이지만 무조건 인덱스를 많이 만드는 방식은 바람직하지 않다. UPDATE와 DELETE 조건이 필요한 행을 정확하게 찾을 수 있도록 선택도가 높은 인덱스를 설계해야 한다. 인덱스가 부족하면 잠금 대상 범위가 넓어지고, 지나치게 많은 인덱스는 데이터 변경 비용과 잠금 작업을 증가시킬 수 있다.
SELECT ... FOR UPDATE를 사용할 때는 검색 범위를 특히 주의해야 한다. 인덱스를 타지 않는 넓은 조건으로 실행하면 예상보다 많은 레코드나 인덱스 구간이 잠길 수 있다. 먼저 실행 계획을 확인하고, 업무적으로 필요한 행만 명확하게 잠그도록 조건을 제한해야 한다.
데드락을 완전히 없애기 어려운 시스템이라면 애플리케이션의 재시도 처리도 필요하다. InnoDB가 데드락으로 트랜잭션을 롤백하면 해당 트랜잭션은 처음부터 다시 실행해야 한다. 짧은 대기 시간을 둔 뒤 제한된 횟수만큼 재시도하되, 무제한 재시도는 부하를 증폭시킬 수 있으므로 피해야 한다. 결제나 주문처럼 중복 처리 위험이 있는 작업은 요청 식별자와 멱등성 검증을 함께 적용해야 한다.
격리 수준을 READ COMMITTED로 변경하면 일부 갭 잠금의 영향을 줄일 수 있지만, 이를 단순한 데드락 해결 수단으로 적용해서는 안 된다. 격리 수준 변경은 데이터 일관성, 복제 방식, 애플리케이션 조회 결과에 영향을 줄 수 있으므로 충분한 테스트를 거쳐야 한다.
마치며
MySQL InnoDB 데드락은 데이터베이스가 비정상적으로 동작해서 발생하는 오류라기보다 동시성 제어 과정에서 나타날 수 있는 정상적인 보호 메커니즘에 가깝다. InnoDB는 순환 대기를 감지하면 트랜잭션 하나를 롤백해 시스템 전체가 멈추는 상황을 방지한다.
하지만 동일한 SQL과 업무에서 데드락이 반복된다면 단순 재시도만으로 덮어서는 안 된다. SHOW ENGINE INNODB STATUS의 트랜잭션별 SQL, 보유 잠금, 대기 잠금, 사용 인덱스를 순서대로 비교하고 애플리케이션의 전체 트랜잭션 흐름까지 확인해야 한다.
운영 경험상 가장 확실한 개선 방법은 데이터 접근 순서를 통일하고, 트랜잭션 범위를 줄이며, 조건에 맞는 인덱스를 구성하는 것이었다. 여기에 데드락 로그 수집과 제한적인 재시도 정책을 적용하면 일시적인 충돌은 안정적으로 처리하면서 반복되는 구조적 문제도 빠르게 찾아낼 수 있다.




댓글 0
첫 댓글을 남겨보세요.