2026년 07월 03일 | DBMS Error 가이드
이 글에서 다루는 내용
40002 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
40002 transaction integrity constraint violation 는?
PostgreSQL 에러 코드 40002는 transaction_integrity_constraint_violation으로, 트랜잭션 내부에서 무결성 제약 조건이 위반되었을 때 발생하는 에러입니다. 이 에러는 주로 직렬화 가능(Serializable) 격리 수준이나 반복 읽기(Repeatable Read) 격리 수준에서 동시 트랜잭션들이 서로 충돌할 때 나타나며, 데이터베이스가 트랜잭션의 논리적 일관성을 유지하기 위해 강제로 트랜잭션을 중단시키는 경우에 발생합니다. 쉽게 말해, 여러 트랜잭션이 동시에 같은 데이터를 수정하려 할 때 PostgreSQL이 데이터 무결성을 지키기 위해 일부 트랜잭션을 실패시키는 상황입니다.
주요 발생 원인
1. Serializable 격리 수준에서의 직렬화 실패 (Serialization Failure)
Serializable 격리 수준을 사용할 때, PostgreSQL의 SSI(Serializable Snapshot Isolation) 엔진은 트랜잭션들이 논리적으로 직렬 실행 가능한지 검사합니다. 두 개 이상의 트랜잭션이 서로의 읽기/쓰기 결과에 의존하는 사이클(cycle)이 형성되면, PostgreSQL은 해당 트랜잭션 중 하나를 강제 종료시키고 40002 에러를 반환합니다. 예를 들어 트랜잭션 A가 테이블의 합계를 읽고 새 행을 삽입하는 동시에, 트랜잭션 B도 같은 합계를 읽고 다른 행을 삽입하는 경우 이 충돌이 발생할 수 있습니다.
2. 동시 트랜잭션 간의 쓰기-쓰기 충돌 (Write-Write Conflict)
동일한 행 또는 범위를 두 트랜잭션이 동시에 수정하려 할 때, 격리 수준에 따라 충돌이 감지됩니다. Repeatable Read 이상의 격리 수준에서는 먼저 커밋된 트랜잭션의 변경 사항이 나중 트랜잭션의 스냅샷에 반영되지 않아, 데이터 불일치가 발생할 수 있으며 PostgreSQL은 이를 방지하기 위해 트랜잭션을 중단합니다. 이는 특히 배치 업데이트나 재고 관리 시스템처럼 다수의 클라이언트가 동시에 같은 레코드를 업데이트하는 환경에서 빈번하게 발생합니다.
3. 외래 키 및 유니크 제약 조건과의 충돌 (Constraint Conflict in Concurrent Inserts)
동시에 여러 트랜잭션이 동일한 유니크 키 값을 삽입하거나, 상위 테이블에서 삭제가 일어나는 동시에 하위 테이블에서 해당 키를 참조하는 삽입이 발생하는 경우, 트랜잭션 무결성이 깨질 수 있습니다. PostgreSQL은 이런 상황에서 제약 조건을 지키기 위해 후발 트랜잭션을 실패시킵니다. 특히 ON CONFLICT DO NOTHING이나 INSERT ... ON CONFLICT DO UPDATE 구문에서도 동시성 문제가 겹치면 이 에러가 발생할 수 있습니다.
해결 방법
원인 1: Serializable 격리 수준 충돌 해결
가장 권장되는 해결책은 애플리케이션 레벨에서 재시도 로직(retry logic)을 구현하는 것입니다.
-- 재시도 로직 예시 (PL/pgSQL 함수 내에서)
CREATE OR REPLACE FUNCTION safe_transfer(
from_account INT,
to_account INT,
amount NUMERIC
) RETURNS VOID AS $$
DECLARE
max_retries INT := 5;
retry_count INT := 0;
success BOOLEAN := FALSE;
BEGIN
WHILE retry_count < max_retries AND NOT success LOOP
BEGIN
-- Serializable 트랜잭션 시작
PERFORM set_config('transaction_isolation', 'serializable', TRUE);
UPDATE accounts
SET balance = balance - amount
WHERE account_id = from_account;
UPDATE accounts
SET balance = balance + amount
WHERE account_id = to_account;
success := TRUE;
EXCEPTION
WHEN transaction_integrity_constraint_violation OR serialization_failure THEN
retry_count := retry_count + 1;
PERFORM pg_sleep(0.1 * retry_count); -- 점진적 대기
IF retry_count >= max_retries THEN
RAISE EXCEPTION '최대 재시도 횟수 초과: %', SQLERRM;
END IF;
END;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- 격리 수준을 낮춰 충돌을 줄이는 방법 (상황에 따라 선택적으로)
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 2;
COMMIT;
원인 2: 쓰기-쓰기 충돌 해결
명시적 잠금(SELECT FOR UPDATE)을 사용해 충돌을 사전에 방지합니다.
-- SELECT FOR UPDATE를 사용해 행 잠금 선점
BEGIN;
SELECT *
FROM inventory
WHERE product_id = 101
FOR UPDATE; -- 해당 행을 잠금으로 다른 트랜잭션의 동시 수정 방지
UPDATE inventory
SET stock_quantity = stock_quantity - 5
WHERE product_id = 101
AND stock_quantity >= 5;
-- 재고 부족 여부 확인
DO $$
BEGIN
IF NOT FOUND THEN
RAISE EXCEPTION '재고가 부족합니다.';
END IF;
END $$;
COMMIT;
-- SKIP LOCKED를 활용한 대기 없는 처리 (큐 시스템에 유용)
BEGIN;
SELECT *
FROM job_queue
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
UPDATE job_queue
SET status = 'processing',
updated_at = NOW()
WHERE job_id = (SELECT job_id FROM job_queue WHERE status = 'pending' LIMIT 1 FOR UPDATE SKIP LOCKED);
COMMIT;
원인 3: 유니크 제약 조건 충돌 해결
-- INSERT ON CONFLICT를 활용해 중복 삽입 충돌 방지
INSERT INTO users (email, username, created_at)
VALUES ('user@example.com', 'newuser', NOW())
ON CONFLICT (email)
DO UPDATE SET
username = EXCLUDED.username,
updated_at = NOW()
WHERE users.updated_at < EXCLUDED.created_at;
-- 외래 키 충돌 예방: 트랜잭션 순서 보장
BEGIN;
-- 먼저 상위 테이블 처리
INSERT INTO departments (dept_id, dept_name)
VALUES (10, 'Engineering')
ON CONFLICT (dept_id) DO NOTHING;
-- 이후 하위 테이블 처리
INSERT INTO employees (emp_id, dept_id, emp_name)
VALUES (1001, 10, '홍길동');
COMMIT;
예방 방법
1. 재시도 로직과 지수 백오프(Exponential Backoff) 구현
애플리케이션 코드에서 40002 및 40001 에러를 명시적으로 캐치하고, 트랜잭션을 자동으로 재시도하는 로직을 반드시 구현해야 합니다. 단순 즉시 재시도보다는 지수 백오프(예: 100ms → 200ms → 400ms) 방식을 적용해 데이터베이스 서버의 부하를 줄이는 것이 실무에서 권장됩니다. Python의 psycopg2나 Java의 JDBC에서는 에러 코드를 확인해 재시도 여부를 결정하는 패턴이 널리 사용됩니다.
2. 격리 수준 최적화 및 트랜잭션 범위 최소화
모든 트랜잭션에 무조건 SERIALIZABLE 격리 수준을 적용하기보다는, 실제로 완전한 직렬화가 필요한 비즈니스 로직에만 한정적으로 사용하고 나머지는 READ COMMITTED(PostgreSQL 기본값)를 활용하는 것이 좋습니다. 또한 트랜잭션 내에서 처리하는 작업의 범위를 최소화하고, 트랜잭션이 오래 열려 있지 않도록 네트워크 I/O나 외부 API 호출을 트랜잭션 밖으로 이동시켜야 합니다.
관련 에러
- 40001 (serialization_failure): 40002와 가장 유사한 에러로, Serializable 격리 수준에서 트랜잭션 직렬화가 불가능할 때 발생합니다. 40002가 무결성 제약 위반에 초점을 맞춘다면, 40001은 읽기/쓰기 패턴의 사이클 감지에 초점을 맞춥니다.
- 23000 (integrity_constraint_violation): 트랜잭션 수준이 아닌 단일 SQL 문 수준에서 발생하는 무결성 제약 위반입니다. NOT NULL, UNIQUE, FOREIGN KEY 등의 일반적인 제약 조건 위반 시 발생합니다.
- 40P01 (deadlock_detected): 두 트랜잭션이 서로의 잠금을 기다리는 교착 상태 발생 시 나타나며, 40002와 마찬가지로 재시도 로직이 필요합니다.
- 55P03 (lock_not_available):
NOWAIT옵션을 사용한 잠금 시도에서 즉시 잠금을 획득하지 못할 때 발생합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.