PostgreSQL 40P01 오류 원인과 해결 방법 완벽 가이드

40P01
2026년 07월 03일 | DBMS Error 가이드

이 글에서 다루는 내용

40P01 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.

40P01 deadlock detected 는?

PostgreSQL 에러 코드 40P01은 교착 상태(Deadlock)가 감지되었을 때 발생합니다. 교착 상태란 두 개 이상의 트랜잭션이 서로 상대방이 보유한 잠금(Lock)을 기다리며 무한히 대기하는 상황을 말합니다. PostgreSQL은 이러한 상황을 주기적으로 감지하여 트랜잭션 중 하나를 강제로 롤백시키고, 나머지 트랜잭션이 진행될 수 있도록 해결합니다.


주요 발생 원인

1. 서로 다른 순서로 테이블 또는 행을 잠금 획득

가장 흔한 교착 상태 원인은 두 트랜잭션이 동일한 리소스를 반대 순서로 잠금을 시도할 때입니다. 예를 들어 트랜잭션 A가 accounts 테이블의 행 1을 잠근 후 행 2를 잠그려 하고, 트랜잭션 B는 행 2를 잠근 후 행 1을 잠그려 한다면 두 트랜잭션은 서로를 기다리며 교착 상태에 빠집니다. 이는 배치 처리나 대량 업데이트 작업에서 빈번하게 발생합니다.

2. 여러 테이블에 대한 비일관적인 잠금 순서

애플리케이션에서 여러 테이블을 동시에 수정하는 경우, 각 코드 경로마다 테이블 접근 순서가 다를 수 있습니다. 트랜잭션 A가 ordersinventory 순서로 잠금을 획득하고, 트랜잭션 B가 inventoryorders 순서로 잠금을 획득하려 할 때 교착 상태가 발생합니다. 마이크로서비스 환경에서 여러 서비스가 동일한 DB를 공유할 경우 특히 주의가 필요합니다.

3. 불필요하게 긴 트랜잭션과 과도한 잠금 범위

트랜잭션이 필요 이상으로 많은 행을 잠그거나, 오랜 시간 동안 잠금을 보유하면 교착 상태 가능성이 급격히 높아집니다. SELECT ... FOR UPDATE 없이 대량 업데이트를 수행하거나, 인덱스가 없는 컬럼으로 조건을 걸어 테이블 전체에 잠금이 걸리는 상황이 대표적입니다. 이런 경우 의도치 않게 다른 트랜잭션과 잠금이 충돌하게 됩니다.


해결 방법

원인 1 해결: 일관된 순서로 행 잠금

교착 상태를 유발하는 비일관적인 잠금 순서를 수정하려면, 항상 기본 키(PK) 오름차순 등 일관된 순서로 행을 처리해야 합니다.

-- ❌ 교착 상태를 유발하는 패턴
-- 트랜잭션 A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- 트랜잭션 B (동시에 실행)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;

-- ✅ 해결책: 항상 id 오름차순으로 잠금 획득
-- 트랜잭션 A
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- 트랜잭션 B
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;

원인 2 해결: 여러 테이블 잠금 순서 통일

-- ❌ 위험한 패턴: 서비스마다 다른 테이블 잠금 순서
-- 서비스 A의 트랜잭션
BEGIN;
UPDATE orders SET status = 'processing' WHERE order_id = 101;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 55;
COMMIT;

-- 서비스 B의 트랜잭션 (반대 순서 - 교착 상태 위험)
BEGIN;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 55;
UPDATE orders SET status = 'reserved' WHERE order_id = 101;
COMMIT;

-- ✅ 해결책: 애플리케이션 전체에서 테이블 잠금 순서를 통일
-- 규칙: 항상 orders → inventory 순서로 처리
BEGIN;
SELECT * FROM orders WHERE order_id = 101 FOR UPDATE;
SELECT * FROM inventory WHERE product_id = 55 FOR UPDATE;
UPDATE orders SET status = 'processing' WHERE order_id = 101;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 55;
COMMIT;

원인 3 해결: 잠금 범위 최소화 및 인덱스 활용

-- ❌ 인덱스 없는 컬럼으로 대량 잠금 유발
BEGIN;
UPDATE orders SET status = 'completed'
WHERE customer_email = 'user@example.com'; -- 인덱스 없음 → 전체 스캔
COMMIT;

-- ✅ 해결책: 인덱스 생성 및 잠금 범위 최소화
-- 1. 인덱스 추가
CREATE INDEX CONCURRENTLY idx_orders_customer_email
    ON orders(customer_email);

-- 2. 배치 처리로 잠금 범위 분산
DO $$
DECLARE
    batch_size INT := 1000;
    offset_val INT := 0;
    rows_updated INT;
BEGIN
    LOOP
        UPDATE orders SET status = 'completed'
        WHERE id IN (
            SELECT id FROM orders
            WHERE customer_email = 'user@example.com'
              AND status != 'completed'
            ORDER BY id
            LIMIT batch_size
        );
        GET DIAGNOSTICS rows_updated = ROW_COUNT;
        EXIT WHEN rows_updated = 0;
        PERFORM pg_sleep(0.1); -- 잠깐 대기하여 다른 트랜잭션 기회 부여
    END LOOP;
END $$;

교착 상태 발생 시 재시도 로직 구현

교착 상태는 완전히 막기 어려운 경우가 있으므로, 애플리케이션 레벨에서 재시도(Retry) 로직을 반드시 구현해야 합니다.

-- PL/pgSQL을 활용한 재시도 로직 예제
CREATE OR REPLACE FUNCTION transfer_funds(
    p_from_id INT,
    p_to_id   INT,
    p_amount  NUMERIC
) RETURNS VOID AS $$
DECLARE
    v_retry     INT := 0;
    v_max_retry INT := 3;
BEGIN
    LOOP
        BEGIN
            -- 항상 id 오름차순으로 잠금 획득
            PERFORM *
            FROM accounts
            WHERE id IN (p_from_id, p_to_id)
            ORDER BY id
            FOR UPDATE;

            UPDATE accounts
               SET balance = balance - p_amount
             WHERE id = p_from_id;

            UPDATE accounts
               SET balance = balance + p_amount
             WHERE id = p_to_id;

            RETURN; -- 성공 시 종료

        EXCEPTION
            WHEN deadlock_detected THEN
                v_retry := v_retry + 1;
                IF v_retry >= v_max_retry THEN
                    RAISE EXCEPTION '최대 재시도 횟수 초과: 교착 상태 해결 실패';
                END IF;
                PERFORM pg_sleep(0.05 * v_retry); -- 지수 백오프
        END;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

교착 상태 모니터링 쿼리

-- 현재 대기 중인 잠금 확인
SELECT
    pid,
    usename,
    application_name,
    state,
    wait_event_type,
    wait_event,
    query_start,
    now() - query_start AS elapsed,
    LEFT(query, 100) AS query_preview
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY query_start;

-- 잠금 충돌 관계 확인
SELECT
    blocked.pid          AS blocked_pid,
    blocked.query        AS blocked_query,
    blocking.pid         AS blocking_pid,
    blocking.query       AS blocking_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocking
    ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';

예방 방법

1. 잠금 순서 표준화 및 코드 리뷰 정책 수립

모든 개발팀이 공유하는 잠금 획득 순서 가이드라인을 문서화하고, 코드 리뷰 시 이를 반드시 점검해야 합니다. 특히 여러 테이블을 수정하는 트랜잭션은 항상 테이블 이름의 알파벳 순서 또는 사전에 정의된 우선순위 순서대로 잠금을 획득하도록 규칙을 정합니다. lock_timeout 파라미터를 설정하여 잠금 대기 시간에 상한을 두는 것도 좋은 방법입니다.

-- postgresql.conf 또는 세션 레벨에서 설정
SET lock_timeout = '5s';       -- 5초 이상 잠금 대기 시 에러
SET deadlock_timeout = '1s';   -- 교착 상태 감지 주기 (기본값 1s)

2. 트랜잭션을 짧고 간결하게 유지 (Short Transaction)

트랜잭션 내부에서 외부 API 호출, 파일 I/O, 사용자 입력 대기 등 시간이 오래 걸리는 작업을 절대 수행하지 않아야 합니다. 트랜잭션은 가능한 한 짧게 유지하고, 필요한 데이터를 미리 조회한 후 트랜잭션을 시작하는 패턴을 사용하면 잠금 보유 시간을 크게 줄일 수 있습니다. 또한 pg_log에서 교착 상태 로그를 정기적으로 분석하여 반복적으로 발생하는 패턴을 사전에 제거해야 합니다.


관련 에러

  • 40001 (serialization_failure): 직렬화 격리 수준(SERIALIZABLE)에서 트랜잭션 충돌 시 발생하며, 40P01과 마찬가지로 재시도 로직이 필요합니다.
  • 55P03 (lock_not_available): NOWAIT 옵션 사용 시 즉시 잠금을 획득하지 못하면 발생합니다.
  • 57014 (query_canceled): lock_timeout 초과로 쿼리가 취소될 때 발생하며, 교착 상태 예방 설정과 밀접하게 연관됩니다.

DBMS 에러 코드 시리즈

주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.

본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.

댓글 남기기