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 error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.