2026년 09월 06일 | DBMS Error 가이드
이 글에서 다루는 내용
40002 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
40002 transaction integrity constraint violation 는?
PostgreSQL 에러 코드 40002는 transaction_integrity_constraint_violation으로, 트랜잭션 내에서 무결성 제약 조건을 위반하는 상황이 발생했을 때 반환되는 에러입니다. 이 에러는 주로 직렬화 격리 수준(Serializable Isolation Level) 또는 반복 읽기(Repeatable Read) 격리 수준에서 여러 트랜잭션이 동시에 실행될 때 데이터 일관성을 보장하기 위해 PostgreSQL이 강제로 트랜잭션을 중단시키는 과정에서 발생합니다. 40001(serialization_failure)과 밀접하게 연관되어 있으며, 애플리케이션 레벨에서 반드시 재시도(retry) 로직을 구현해야 하는 에러 클래스 40번대에 속합니다.
주요 발생 원인
1. 직렬화 격리 수준에서의 쓰기-쓰기 충돌 (Write-Write Conflict)
가장 흔한 원인으로, 두 개 이상의 트랜잭션이 동일한 행(Row)에 대해 동시에 수정을 시도할 때 발생합니다. PostgreSQL의 직렬화 격리 수준(SERIALIZABLE)은 트랜잭션의 실행 순서가 직렬(Serial) 실행과 동일한 결과를 보장해야 하므로, 충돌이 감지되면 나중에 커밋을 시도하는 트랜잭션을 강제로 롤백시킵니다. 이로 인해 40002 에러가 발생하며, 애플리케이션은 반드시 이를 처리해야 합니다.
2. 지연 제약 조건(Deferrable Constraint)과 트랜잭션 종료 시점 충돌
PostgreSQL은 DEFERRABLE INITIALLY DEFERRED 옵션을 통해 제약 조건 검사를 트랜잭션 커밋 시점까지 지연시킬 수 있습니다. 이 경우, 트랜잭션 중간에는 문제가 없어 보이지만 커밋 직전에 외래 키(Foreign Key), 유니크(Unique), 체크(Check) 등의 제약 조건 검사가 일괄 실행되면서 위반이 발견되어 40002 에러가 반환될 수 있습니다. 이는 개발 중에는 잘 드러나지 않다가 운영 환경에서 데이터 볼륨이 증가하면 나타나는 경우가 많습니다.
3. SSI(Serializable Snapshot Isolation)에서의 읽기-쓰기 충돌 (Read-Write Anti-dependency)
PostgreSQL 9.1부터 도입된 SSI는 SERIALIZABLE 격리 수준에서 팬텀 읽기(Phantom Read)까지 방지하는 정교한 메커니즘입니다. 트랜잭션 A가 특정 범위의 데이터를 읽고, 트랜잭션 B가 그 범위에 새로운 데이터를 삽입한 뒤, 트랜잭션 A가 그 결과를 기반으로 또 다른 쓰기를 수행하는 패턴(읽기-쓰기 사이클)이 감지되면 PostgreSQL은 이를 직렬화 이상(Serialization Anomaly)으로 판단하고 트랜잭션을 중단시킵니다. 이 패턴은 회계 시스템, 재고 관리, 금융 거래 등 복잡한 비즈니스 로직에서 자주 나타납니다.
해결 방법
원인 1 해결: 트랜잭션 재시도 로직 구현
40002 에러는 근본적으로 재시도(Retry)를 통해 해결해야 합니다. 아래는 PL/pgSQL을 이용한 재시도 함수 예제입니다.
-- 재시도 가능한 트랜잭션 처리 함수 예제
CREATE OR REPLACE FUNCTION transfer_funds(
p_from_account INT,
p_to_account INT,
p_amount NUMERIC
) RETURNS VOID AS $$
DECLARE
v_retry_count INT := 0;
v_max_retries INT := 5;
BEGIN
LOOP
BEGIN
-- SERIALIZABLE 격리 수준으로 트랜잭션 시작
PERFORM set_config('transaction_isolation', 'serializable', true);
UPDATE accounts
SET balance = balance - p_amount
WHERE account_id = p_from_account
AND balance >= p_amount;
IF NOT FOUND THEN
RAISE EXCEPTION '잔액 부족 또는 계좌 없음';
END IF;
UPDATE accounts
SET balance = balance + p_amount
WHERE account_id = p_to_account;
-- 성공 시 루프 종료
RETURN;
EXCEPTION
WHEN transaction_integrity_constraint_violation OR serialization_failure THEN
v_retry_count := v_retry_count + 1;
IF v_retry_count >= v_max_retries THEN
RAISE EXCEPTION '최대 재시도 횟수 초과: %', SQLERRM;
END IF;
-- 지수 백오프 (exponential backoff) 적용
PERFORM pg_sleep(0.1 * (2 ^ v_retry_count));
END;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- 함수 실행
SELECT transfer_funds(1001, 1002, 5000.00);
원인 2 해결: 지연 제약 조건 점검 및 조기 검증
-- 지연 제약 조건 확인
SELECT conname, condeferrable, condeferred
FROM pg_constraint
WHERE conrelid = 'orders'::regclass;
-- 문제가 되는 지연 제약 조건을 즉시 검사로 변경
ALTER TABLE orders
ALTER CONSTRAINT orders_customer_id_fkey
NOT DEFERRABLE;
-- 만약 지연 제약 조건을 유지해야 한다면,
-- 트랜잭션 내에서 수동으로 조기 검증 수행
BEGIN;
SET CONSTRAINTS ALL IMMEDIATE; -- 트랜잭션 내에서 즉시 검사로 전환
INSERT INTO orders (order_id, customer_id, amount)
VALUES (9001, 12345, 99900);
-- 이 시점에 바로 제약 위반이 발생하므로 커밋 전에 처리 가능
COMMIT;
-- 트랜잭션 수준에서 특정 제약만 지연 처리하는 예제
BEGIN;
SET CONSTRAINTS orders_customer_id_fkey DEFERRED;
-- ... 복잡한 데이터 조작 ...
SET CONSTRAINTS orders_customer_id_fkey IMMEDIATE; -- 커밋 전 명시적 검사
COMMIT;
원인 3 해결: 격리 수준 조정 및 쿼리 최적화
-- 현재 격리 수준 확인
SHOW transaction_isolation;
-- 세션 수준에서 격리 수준 조정 (필요에 따라 낮춤)
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 또는 트랜잭션 단위로 격리 수준 지정
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE account_id = 1001 FOR UPDATE;
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 1001;
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 1002;
COMMIT;
-- SSI 충돌 모니터링 쿼리
SELECT pid, usename, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE state = 'active'
AND query NOT LIKE '%pg_stat_activity%';
-- pg_locks를 통해 충돌 트랜잭션 확인
SELECT
l1.pid AS waiting_pid,
l2.pid AS blocking_pid,
a1.query AS waiting_query,
a2.query AS blocking_query
FROM pg_locks l1
JOIN pg_locks l2
ON l1.transactionid = l2.transactionid
AND l1.pid != l2.pid
JOIN pg_stat_activity a1 ON l1.pid = a1.pid
JOIN pg_stat_activity a2 ON l2.pid = a2.pid
WHERE NOT l1.granted;
예방 방법
1. 애플리케이션 레벨에서 반드시 Retry 로직과 지수 백오프(Exponential Backoff)를 구현하세요
40002를 포함한 에러 클래스 40번대는 PostgreSQL 공식 문서에서 “재시도 가능한 에러”로 분류됩니다. 따라서 모든 데이터베이스 연동 코드에서는 SQLSTATE 40002 또는 40001을 감지하면 자동으로 재시도하는 로직을 구현해야 합니다. 재시도 시에는 즉시 재실행하지 않고 지수 백오프(100ms → 200ms → 400ms → …)를 적용하여 동시성 충돌 가능성을 줄이고, 최대 재시도 횟수(5~10회)를 설정하여 무한 루프를 방지하세요. 또한 재시도 발생 빈도를 모니터링하여 빈번하게 발생한다면 데이터 접근 패턴이나 격리 수준을 재검토하는 신호로 삼아야 합니다.
2. 격리 수준을 업무 요건에 맞게 적절히 선택하고, 트랜잭션을 최대한 짧게 유지하세요
SERIALIZABLE 격리 수준은 강력한 일관성을 보장하지만 충돌 가능성도 높아집니다. 실제 업무에서 팬텀 읽기가 문제가 되지 않는 경우라면 REPEATABLE READ 또는 READ COMMITTED를 사용하는 것이 좋습니다. 또한 트랜잭션 내에서 외부 API 호출, 파일 I/O, 긴 연산 등을 수행하지 않도록 하여 트랜잭션 수명을 최소화하세요. 트랜잭션이 짧을수록 다른 트랜잭션과 충돌할 확률이 낮아지며, lock_timeout과 statement_timeout을 적절히 설정하여 장기 실행 트랜잭션을 조기에 차단하는 것도 효과적입니다.
관련 에러
- 40001 (serialization_failure): 40002와 가장 밀접한 에러로, 직렬화 실패 시 발생합니다. 두 에러 모두 재시도가 필요하며, 실무에서는 함께 처리하는 것이 일반적입니다.
- 23000 (integrity_constraint_violation): 즉시 제약 조건 위반 시 발생하는 에러로, 40002와 달리 재시도로 해결되지 않으며 데이터 자체를 수정해야 합니다.
- 23505 (unique_violation): 유니크 제약 조건 위반으로, 지연 제약 조건 설정 시 40002 형태로 나타날 수 있습니다.
- 55P03 (lock_not_available):
NOWAIT옵션 사용 시 락 획득 실패로 발생하며, 동시성 문제 디버깅 시 함께 확인해야 합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.