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로 락을 건 뒤 INSERT나 UPDATE를 실행하는 패턴에서 자주 발생한다.

 

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