MySQL로 웹 서비스를 운영하다가 갑자기 "ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction" 메시지를 마주치며 트랜잭션이 튕겨 나간 경험이 있을 것이다.
특정 데이터에 대한 변경 요청이 폭주하거나 트랜잭션이 종료되지 않을 때 자주 발생하는 이 에러는, 다만 정확한 원인이나 락을 쥐고 있는 원인 쿼리를 찾는 방법을 몰라 우왕좌왕하는 경우가 많다.
이번에는 MySQL ERROR 1205 발생 원인이 정확히 뭔지, 점유 중인 트랜잭션을 어떻게 추적하는지, 그리고 실무에서 안전하게 해결하는 대처법까지 완벽하게 정리해서 소개하겠다.

 

1. MySQL ERROR 1205 (Lock Wait Timeout) 이해하기

MySQL의 InnoDB 스토리지 엔진은 데이터 일관성을 유지하기 위해 행 단위 락(Row-level Lock)을 사용한다.
하나의 트랜잭션이 특정 행(Row)을 수정(UPDATE/DELETE)하거나 Exclusive Lock(X-Lock)을 획득했을 때, 다른 트랜잭션이 동일한 행을 수정하려고 하면 기존 트랜잭션이 끝나기를 기다리며 대기 상태(Lock Wait)에 들어간다.

이때 대기 시간이 MySQL 시스템 변수인 innodb_lock_wait_timeout(기본값: 50초)을 초과하게 되면, MySQL은 대기 중이던 쿼리를 강제로 취소하고 ERROR 1205를 반환한다.

데드락(Deadlock)과 Lock Wait Timeout은 자주 혼동되지만 명확한 차이가 있다.

구분Lock Wait Timeout (ERROR 1205)Deadlock (ERROR 1213)
발생 원인선행 트랜잭션이 락을 오래 쥐고 있어 후행 트랜잭션이 지정된 시간 동안 대기함두 개 이상의 트랜잭션이 서로 상대방이 쥐고 있는 락을 교차로 대기함
엔진의 동작대기 시간(기본 50초) 초과 시 대기 중인 쿼리만 타임아웃 에러 발생InnoDB 데드락 감지기(Detector)가 즉시 감지하여 한 트랜잭션을 강제 롤백함
주요 해결책롱 트랜잭션(Long Transaction) 제거, 인덱스 최적화, 락 점유 시간 단축트랜잭션 내 테이블 접근 순서 동일화, 쿼리 범위 축소

 

2. 락을 점유 중인 트랜잭션 및 쿼리 추적 방법

ERROR 1205를 해결하려면 '누가 락을 대기하고 있는지'가 아니라 '누가 락을 쥐고 안 놓아주는지'를 찾아내야 한다.
다만 대부분의 개발자들은 SHOW PROCESSLIST만 확인하다가 'Sleep' 상태로 표시된 락 점유 프로세스를 놓치곤 한다.

MySQL 5.7 이상에서는 information_schemaperformance_schema를 통해 락 대기 관계를 명확히 조회할 수 있다.

 

현재 대기 중인 락과 점유 중인 트랜잭션 조회
-- MySQL 8.0 기준: 락을 쥐고 있는 트랜잭션과 대기 중인 트랜잭션 조회
SELECT 
    r.trx_id AS waiting_trx_id,
    r.trx_mysql_thread_id AS waiting_thread,
    r.trx_query AS waiting_query,
    b.trx_id AS blocking_trx_id,
    b.trx_mysql_thread_id AS blocking_thread,
    b.trx_query AS blocking_query
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_engine_transaction_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_engine_transaction_id = b.trx_id;

만약 선행 트랜잭션이 이미 쿼리 실행을 마치고 애플리케이션 외부 연동 등으로 로직 내부에서 오랜 시간 머물러 있다면 blocking_query가 NULL로 표시될 수 있다.
이 경우 blocking_thread ID를 이용해 해당 세션에서 마지막으로 실행한 쿼리를 추적해야 한다.

-- 블로킹 쓰레드(THREAD_ID)의 마지막 실행 쿼리 확인
SELECT 
    p.PROCESSLIST_ID,
    h.SQL_TEXT 
FROM performance_schema.events_statements_history h
JOIN performance_schema.threads p ON h.THREAD_ID = p.THREAD_ID
WHERE p.PROCESSLIST_ID = [blocking_thread_id]
ORDER BY h.TIMER_START DESC LIMIT 5;

 

3. 실전 예제: 흔한 실수 코드와 올바른 트랜잭션 격리

실무에서 ERROR 1205가 발생하는 대표적인 원인은 트랜잭션 안에서 외부 API 통신, 파일 업로드, 대량의 SELECT 작업 등 시간이 오래 걸리는 로직을 함께 수행하는 '롱 트랜잭션' 때문이다.

✗ 잘못된 코드 (트랜잭션 내부에서 외부 API 호출 및 긴 작업 수행)

<?php
// ✗ 원인: 트랜잭션 시작 후 외부 API 응답을 기다리는 동안 DB 행 락이 계속 유지됨
$db->beginTransaction();

// 1. 주문 상태 업데이트 (X-Lock 발생)
$stmt = $db->prepare("UPDATE orders SET status = 'PROCESSING' WHERE id = :id");
$stmt->execute([':id' => $orderId]);

// 2. 외부 PG사 결제 승인 API 호출 (3~10초 소요 가능성)
$paymentResult = $paymentApi->approvePayment($orderData);

if ($paymentResult->isSuccess()) {
    $db->commit();
} else {
    $db->rollBack();
}
?>

위 코드는 PG사 결제 승인 응답이 지연되는 동안 orders 테이블의 특정 행에 락을 계속 쥐고 있다.
이 순간 동일한 주문 건을 관리자 페이지에서 조회/수정하거나, 재요청이 들어오면 대기 세션들은 모조리 ERROR 1205를 뱉게 된다.

✓ 올바른 코드 (트랜잭션 범위 최소화)

<?php
// ✓ 해결: 외부 API 요청을 먼저 완료한 후 DB 트랜잭션을 최단 시간으로 수행함

// 1. 외부 PG사 결제 승인 API를 트랜잭션 외부에서 먼저 진행
$paymentResult = $paymentApi->approvePayment($orderData);

if (!$paymentResult->isSuccess()) {
    throw new Exception("결제 승인 실패");
}

// 2. DB 업데이트만 빠르게 처리하기 위해 최소 범위의 트랜잭션 열기
$db->beginTransaction();
try {
    $stmt = $db->prepare("UPDATE orders SET status = 'PAID', paid_at = NOW() WHERE id = :id");
    $stmt->execute([':id' => $orderId]);
    
    $db->commit();
} catch (Exception $e) {
    $db->rollBack();
    // 결제 취소 API 연동 등 보상 트랜잭션 수행
    $paymentApi->cancelPayment($paymentResult->transactionId);
    throw $e;
}
?>

결과 / 출력값 :
DB 트랜잭션 점유 시간이 수 초 대에서 불과 몇 밀리초(ms) 단위로 줄어들어, 다른 요청들이 락 대기 시간 초과(ERROR 1205)에 걸리지 않고 즉시 처리된다.

 

4. 주의사항 및 흔한 실수

 

인덱스 부재로 인한 테이블 락(Table Lock) 수준의 범위 확장

UPDATE나 DELETE 문 수행 시 조건절(WHERE) 컬럼에 인덱스가 지정되어 있지 않으면, InnoDB는 조건에 해당하는 행만 락을 거는 것이 아니라 테이블 전체의 모든 행에 Gap Lock 및 Record Lock을 설정하게 된다.
이 경우 아주 가벼운 UPDATE 쿼리라 할지라도 테이블 전체의 다른 접근을 막아 수많은 대기 프로세스를 양산하고 ERROR 1205를 유발한다.

  • ✗ 잘못된 예: 인덱스가 없는 user_email 컬럼 조건으로 UPDATE 수행 -> 테이블 풀 스캔 및 전체 행 락 발생
  • ✓ 올바른 예: Primary Key 또는 인덱스가 걸린 컬럼을 필수 조건으로 사용하여 Exact Row Lock 유도

 

innodb_lock_wait_timeout을 무작정 높이는 실수

에러가 자주 난다고 해서 innodb_lock_wait_timeout 속성을 50초에서 300초 등으로 늘리는 것은 근본적인 해결책이 아니다.
오히려 대기 세션이 DB 커넥션을 오랫동안 잡아먹게 되어 Too many connections 에러로 이어지는 2차 장애를 유발하게 된다.

 

5. 마무리 및 정답 요약

MySQL ERROR 1205는 데이터베이스가 정상적으로 데이터 일관성을 지켜내는 과정에서 타임아웃이 발생하는 자연스러운 현상이다.
문제를 해결하기 위해서는 대기 시간을 늘릴 것이 아니라, 락을 오래 쥐고 있는 근본 원인인 롱 트랜잭션을 제거하고 인덱스를 최적화해야 한다.

MySQL ERROR 1205 해결은 DB 성능 향상의 핵심이다.
트랜잭션 범위를 단 몇 줄의 UPDATE문 수준으로 축소하는 작은 습관이 모여서 고성능 트래픽을 견디는 안정적인 백엔드 시스템을 만든다는 점을 잊지 말자.
이 글의 락 추적 쿼리를 활용해 현재 시스템에서 무겁게 돌아가는 트랜잭션을 즉시 점검해보면, 락 경합 없는 쾌적한 DB 환경을 구축할 수 있을 것이다.