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

40000
2026년 09월 05일 | DBMS Error 가이드

이 글에서 다루는 내용

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

40000 transaction rollback 는?

PostgreSQL 에러 코드 40000 (transaction_rollback) 은 트랜잭션이 정상적으로 완료되지 못하고 강제로 롤백되었을 때 발생하는 에러입니다. 이 에러는 데이터 무결성을 보호하기 위해 PostgreSQL이 진행 중인 트랜잭션을 취소해야 하는 상황에서 발생하며, 주로 동시성 충돌, 데드락, 직렬화 실패 등의 상황에서 나타납니다. 애플리케이션 레벨에서 이 에러를 적절히 처리하지 않으면 데이터 손실이나 비정상적인 서비스 동작으로 이어질 수 있어 반드시 주의가 필요합니다.


주요 발생 원인

1. 직렬화 실패 (Serialization Failure – 에러 코드 40001)

SERIALIZABLE 또는 REPEATABLE READ 격리 수준을 사용하는 환경에서 두 개 이상의 트랜잭션이 동일한 데이터를 동시에 읽고 수정하려 할 때 발생합니다. PostgreSQL은 데이터 일관성을 위해 나중에 커밋을 시도하는 트랜잭션을 강제로 롤백시키며, 이 경우 반드시 애플리케이션에서 해당 트랜잭션을 재시도(retry)하는 로직을 구현해야 합니다.

2. 데드락 감지 (Deadlock Detected – 에러 코드 40P01)

두 개 이상의 트랜잭션이 서로 상대방이 보유한 락을 기다리는 순환 대기 상태에 빠질 때 발생합니다. PostgreSQL의 데드락 감지 프로세스(deadlock_timeout 기본값 1초)가 이를 감지하면 그 중 하나의 트랜잭션을 희생자(victim)로 선택하여 롤백시킵니다. 데드락은 주로 애플리케이션에서 여러 테이블이나 로우를 일관되지 않은 순서로 접근할 때 빈번하게 발생합니다.

3. 명시적 트랜잭션 오류 및 잘못된 트랜잭션 상태

트랜잭션 블록 내부에서 SQL 문법 오류, 제약 조건 위반(UNIQUE, FK 등), 또는 함수 내 예외가 발생하면 PostgreSQL은 해당 트랜잭션을 aborted 상태로 전환합니다. 이 상태에서 ROLLBACK 없이 추가적인 SQL을 실행하려 하면 ERROR: current transaction is aborted, commands ignored until end of transaction block 에러가 발생하며, 트랜잭션 전체가 롤백 처리됩니다.


해결 방법

원인 1: 직렬화 실패 처리 (Retry 로직 구현)

직렬화 실패는 애플리케이션 레벨에서 재시도 로직으로 해결하는 것이 권장되는 방식입니다.

-- 직렬화 격리 수준 트랜잭션 예시
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

UPDATE accounts
SET balance = balance - 1000
WHERE account_id = 1;

UPDATE accounts
SET balance = balance + 1000
WHERE account_id = 2;

COMMIT;

애플리케이션에서는 다음과 같은 방식으로 재시도 로직을 구현해야 합니다:

-- PL/pgSQL을 이용한 재시도 로직 예시
DO $$
DECLARE
  retry_count INT := 0;
  max_retries INT := 5;
  success BOOLEAN := FALSE;
BEGIN
  WHILE retry_count < max_retries AND NOT success LOOP
    BEGIN
      -- 트랜잭션 시작
      BEGIN;
        UPDATE accounts SET balance = balance - 1000 WHERE account_id = 1;
        UPDATE accounts SET balance = balance + 1000 WHERE account_id = 2;
      COMMIT;
      success := TRUE;

    EXCEPTION
      WHEN serialization_failure THEN
        retry_count := retry_count + 1;
        RAISE NOTICE '직렬화 실패, 재시도 중... (시도 횟수: %)', retry_count;
        PERFORM pg_sleep(0.1 * retry_count); -- 지수 백오프
      WHEN OTHERS THEN
        RAISE; -- 다른 에러는 즉시 전파
    END;
  END LOOP;

  IF NOT success THEN
    RAISE EXCEPTION '최대 재시도 횟수 초과. 트랜잭션 실패.';
  END IF;
END;
$$;

원인 2: 데드락 예방 및 해결

데드락을 예방하려면 항상 동일한 순서로 테이블과 로우에 접근하도록 쿼리를 설계해야 합니다.

-- 데드락을 유발하는 잘못된 패턴 (절대 사용 금지)
-- 트랜잭션 A: 테이블 A 먼저 락 → 테이블 B 락 시도
-- 트랜잭션 B: 테이블 B 먼저 락 → 테이블 A 락 시도

-- ❌ 잘못된 방식
-- 세션 1
BEGIN;
UPDATE orders SET status = 'processing' WHERE order_id = 100;
UPDATE inventory SET quantity = quantity - 1 WHERE item_id = 50;
COMMIT;

-- 세션 2 (동시에 실행 - 데드락 위험)
BEGIN;
UPDATE inventory SET quantity = quantity - 1 WHERE item_id = 50;
UPDATE orders SET status = 'processing' WHERE order_id = 101;
COMMIT;

-- ✅ 올바른 방식: 항상 orders → inventory 순서로 접근
BEGIN;
-- 항상 낮은 ID부터 접근하거나 정해진 순서 유지
SELECT * FROM orders WHERE order_id = 100 FOR UPDATE;
SELECT * FROM inventory WHERE item_id = 50 FOR UPDATE;

UPDATE orders SET status = 'processing' WHERE order_id = 100;
UPDATE inventory SET quantity = quantity - 1 WHERE item_id = 50;
COMMIT;

데드락 발생 현황을 모니터링하는 쿼리:

-- 현재 대기 중인 락 정보 조회
SELECT
    pid,
    usename,
    application_name,
    state,
    wait_event_type,
    wait_event,
    query,
    now() - query_start AS query_duration
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY query_duration DESC;

-- 데드락 관련 로그 설정 확인
SHOW deadlock_timeout;
SHOW log_lock_waits;

-- 데드락 타임아웃 조정 (세션 레벨)
SET deadlock_timeout = '500ms';

원인 3: 트랜잭션 오류 상태 복구

-- ❌ 잘못된 패턴: 에러 후 계속 쿼리 실행
BEGIN;
INSERT INTO users (id, email) VALUES (1, 'test@example.com');
INSERT INTO users (id, email) VALUES (1, 'duplicate@example.com'); -- UNIQUE 위반 에러 발생!
-- 이 시점부터 트랜잭션은 aborted 상태
SELECT * FROM users; -- ERROR: current transaction is aborted
COMMIT; -- 실제로는 ROLLBACK 처리됨

-- ✅ 올바른 패턴: SAVEPOINT를 활용한 부분 롤백
BEGIN;
INSERT INTO users (id, email) VALUES (1, 'test@example.com');

SAVEPOINT before_second_insert;

BEGIN
  INSERT INTO users (id, email) VALUES (1, 'duplicate@example.com');
EXCEPTION
  WHEN unique_violation THEN
    ROLLBACK TO SAVEPOINT before_second_insert;
    RAISE NOTICE '중복 데이터 스킵: 계속 진행합니다.';
END;

-- 트랜잭션은 여전히 유효
SELECT * FROM users WHERE id = 1;
COMMIT;

예방 방법

1. 트랜잭션 범위를 최소화하고 명확한 에러 핸들링 구현

트랜잭션은 가능한 한 짧게 유지하고, 트랜잭션 내에서 불필요한 외부 호출(API, 파일 I/O 등)을 제거해야 합니다. 또한 애플리케이션 레벨에서 40001(serialization_failure)40P01(deadlock_detected) 에러 코드를 명시적으로 감지하고, 지수 백오프(exponential backoff) 전략을 포함한 재시도 로직을 반드시 구현해야 합니다.

-- 트랜잭션 타임아웃 설정으로 장기 실행 트랜잭션 방지
SET statement_timeout = '30s';
SET lock_timeout = '5s';
SET idle_in_transaction_session_timeout = '60s';

2. 정기적인 모니터링과 로그 분석

log_lock_waits = on 설정을 활성화하여 락 대기 이벤트를 로그에 기록하고, pg_stat_activity, pg_locks 뷰를 주기적으로 모니터링하여 잠재적인 데드락 패턴을 사전에 탐지해야 합니다.

-- 롤백 통계 모니터링 (비율이 높으면 문제 신호)
SELECT
    datname,
    xact_commit,
    xact_rollback,
    ROUND(xact_rollback::numeric / NULLIF(xact_commit + xact_rollback, 0) * 100, 2) AS rollback_ratio_pct
FROM pg_stat_database
WHERE datname = current_database();

관련 에러

| 에러 코드 | 에러명 | 설명 |

|———–|——–|——|

| 40001 | serialization_failure | SERIALIZABLE 격리 수준에서 직렬화 충돌 발생 |

| 40002 | transaction_integrity_constraint_violation | 트랜잭션 무결성 제약 조건 위반 |

| 40003 | statement_completion_unknown | 구문 완료 여부 불확실 (네트워크 장애 등) |

| 40P01 | deadlock_detected | 데드락 감지 및 트랜잭션 강제 롤백 |

| 25006 | read_only_sql_transaction | 읽기 전용 트랜잭션에서 쓰기 시도 |

이 에러들은 모두 Class 40 (Transaction Rollback) 계열로 묶이며, 공통적으로 트랜잭션 재시도 로직과 적절한 격리 수준 선택이 핵심 해결책입니다.


DBMS 에러 코드 시리즈

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

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

댓글 남기기