MySQL을 사용해 트래픽이 많은 웹 서비스를 운영하다 보면 어느 날 갑자기 Deadlock found when trying to get lock; try restarting transaction 에러를 경험해봤을 것이다.
다만 이 에러가 어떤 상황에서 왜 발생하는지, 혹은 무조건 쿼리 성능이 떨어져서 발생하는 문제인지 정확한 원인과 해결책을 모르는 경우가 많다.
이번에는 MySQL 데드락(Deadlock, 교착 상태)이 정확히 뭔지, 왜 발생하는지, 그리고 실무 트랜잭션 최적화를 통해 어떻게 예방하고 처리하는지 완벽하게 정리해서 소개하겠다.

 

1단계. MySQL 데드락(Deadlock)의 기본 개념 이해하기

데드락(교착 상태)은 두 개 이상의 트랜잭션이 서로가 소유하고 있는 락(Lock)을 기다리며 무한 대기 상태에 빠지는 현상을 말한다.
InnoDB 스토리지 엔진은 데드락을 감지하면 자동으로 락 소유량이 적은 트랜잭션 하나를 강제로 롤백(Rollback)시키고 위와 같은 에러를 반환한다.

쉽게 비유하자면, 사용자 A가 1번 상품을 구매 후 2번 상품을 수정하려 하고, 동시에 사용자 B가 2번 상품을 수정 후 1번 상품을 업데이트하려고 할 때 발생한다.
각자 하나의 자원을 선점한 채 상대방의 자원이 해제되기만을 기다리므로 시스템은 교착 상태에 빠지게 된다.

구분트랜잭션 A트랜잭션 B
1단계1번 레코드 X-Lock 획득2번 레코드 X-Lock 획득
2단계2번 레코드 X-Lock 요청 (대기)1번 레코드 X-Lock 요청 (대기)
결과Deadlock 발생! (InnoDB가 트랜잭션 하나를 자동 롤백)

 

2단계. 데드락이 흔히 발생하는 실무 원인과 대책

실무 환경에서 데드락은 단지 테이블 접근 순서가 달라서만 발생하는 것이 아니다. InnoDB의 락 동작 방식을 제대로 이해하지 못했을 때 주로 일어난다.

  • 인덱스가 없는 컬럼 조건 검색: 인덱스가 없으면 InnoDB는 테이블 전체 레코드 및 갭(Gap)에 락을 걸어 데드락 확률이 급격히 증가한다.
  • 트랜잭션 범위의 과도한 확장: 트랜잭션 내부에 외부 API 호출이나 긴 비즈니스 로직이 들어있으면 락 유지 시간이 길어진다.
  • 동시성 높은 조건부 UPDATE: SELECT ... FOR UPDATE로 락을 건 뒤 INSERTUPDATE를 실행하는 패턴에서 자주 발생한다.

 

3단계. 실전 코드로 보는 문제 상황과 해결책

실제 개발 현장에서 자주 접하는 쿼리 패턴을 통해 데드락을 유발하는 잘못된 코드와 이를 최적화한 올바른 코드를 살펴보자.

 

예제 1: 접근 순서 불일치로 인한 데드락

✗ 잘못된 코드 (트랜잭션별로 테이블/레코드 업데이트 순서가 다름)

-- 트랜잭션 A (주문 처리)
START TRANSACTION;
UPDATE point_wallet SET balance = balance - 1000 WHERE user_id = 10;
UPDATE order_status SET status = 'PAID' WHERE order_id = 500;
COMMIT;

-- 트랜잭션 B (주문 취소/관리자 변경)
START TRANSACTION;
UPDATE order_status SET status = 'CANCELLED' WHERE order_id = 500;
UPDATE point_wallet SET balance = balance + 1000 WHERE user_id = 10;
COMMIT;

✓ 올바른 코드 (모든 트랜잭션에서 테이블 접근 순서를 동일하게 통일)

-- 트랜잭션 A 및 B 모두 동일한 순서로 접근하도록 애플리케이션 로직 통일
START TRANSACTION;
UPDATE point_wallet SET balance = balance - 1000 WHERE user_id = 10;
UPDATE order_status SET status = 'PAID' WHERE order_id = 500;
COMMIT;

 

예제 2: PHP PDO 기반의 데드락 재시도(Retry) 패턴 구현

데드락은 아무리 방어 코드를 짜더라도 완전히 100% 안 생기게 할 수는 없다. 따라서 백엔드 애플리케이션에는 데드락 발생 시 쿼리를 자동으로 재시도하는 로직이 반드시 필요하다.

✗ 잘못된 코드 (데드락 발생 시 예외를 던지고 바로 실패 처리)

<?php
try {
    $pdo->beginTransaction();
    $pdo->exec("UPDATE points SET amount = amount + 100 WHERE user_id = 1");
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack();
    echo "처리 실패: " . $e->getMessage();
}
?>

✓ 올바른 코드 (MySQL 데드락 에러코드 1213 감지 시 지정 횟수만큼 재시도)

<?php
function executeTransactionWithRetry(PDO $pdo, callable $callback, int $maxRetries = 3) {
    $attempts = 0;
    while ($attempts < $maxRetries) {
        try {
            $pdo->beginTransaction();
            $result = $callback($pdo);
            $pdo->commit();
            return $result;
        } catch (PDOException $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            
            // MySQL error code 1213: Deadlock found when trying to get lock
            if ($e->getCode() == '40001' || (isset($e->errorInfo[1]) && $e->errorInfo[1] == 1213)) {
                $attempts++;
                usleep(50000 * $attempts); // 백오프(Backoff) 지연 시간 적용
                continue;
            }
            throw $e;
        }
    }
    throw new Exception("데드락으로 인한 최대 재시도 횟수 초과");
}

// 실무 적용 예시
executeTransactionWithRetry($pdo, function($pdo) {
    $stmt = $pdo->prepare("UPDATE points SET amount = amount + 100 WHERE user_id = :id");
    $stmt->execute([':id' => 1]);
});
?>

[결과/출력값]
데드락이 일어 순간 발생하더라도 사용자에게 에러 화면이 노출되지 않고, 50ms 후 자동 재시도되어 정상적으로 트랜잭션이 완료된다.

 

4단계. 주의사항 및 흔한 실수

실무에서 데드락을 다룰 때 개발자들이 자주 착각하는 부분들을 정리했다.

  • 실수: 트랜잭션 내부에 외부 API 호출(카카오페이, PG 결제 연동 등)을 포함시킨다.
    올바른 방법: 외부 통신은 트랜잭션 밖에서 완료하고, DB 트랜잭션은 순수 DB 작업만 짧게 묶어서 수행한다.
  • 실수: 조건절 컬럼에 인덱스가 없는 상태로 UPDATE/DELETE를 실행한다.
    올바른 방법: 조건절 컬럼에 적절한 인덱스를 생성하여 레코드 락(Record Lock)만 발생하도록 유도하고 테이블 전체/갭 락 확장을 방지한다.
  • 실수: 데드락을 오직 DB 튜닝만으로 완전 0건으로 없애려고 고집한다.
    올바른 방법: 데드락은 분산 환경이나 높은 동시성 상태에서 언제든 유기적으로 발생할 수 있으므로, 애플리케이션의 Retry(재시도) 로직을 필수적으로 구축해야 한다.

 

5단계. 마무리 및 핵심 요약

MySQL 데드락 방지는 데이터베이스 안정성과 직결되는 핵심 요소다. 작은 트랜잭션 최적화와 올바른 쿼리 순서 정립이라는 습관이 모여서 큰 효과를 만든다는 점을 잊지 말자. 이 글의 재시도 로직과 트랜잭션 분리 방법을 참고해 기존 쿼리를 점검해보면, 데드락 없는 안정적인 데이터베이스 환경을 얻을 수 있을 것이다.