2026년 06월 20일 | DBMS Error 가이드
이 글에서 다루는 내용
23000 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
23000 integrity constraint violation 는?
PostgreSQL 에러 코드 23000 (integrity_constraint_violation) 은 데이터베이스에 정의된 무결성 제약 조건(Integrity Constraint)을 위반하는 작업이 시도될 때 발생하는 에러입니다. 이 에러는 테이블에 설정된 규칙, 예를 들어 외래 키(Foreign Key), 고유 키(Unique Key), NOT NULL, CHECK 제약 조건 등을 어기는 INSERT, UPDATE, DELETE 작업 시 PostgreSQL이 해당 트랜잭션을 중단시키며 발생합니다. 실무에서는 애플리케이션 레벨의 유효성 검사가 미흡하거나, 여러 테이블 간 관계를 고려하지 않은 데이터 조작 시 빈번하게 마주치는 에러입니다.
주요 발생 원인
1. 외래 키 제약 조건 위반 (Foreign Key Violation)
가장 흔한 원인으로, 참조 대상 테이블에 존재하지 않는 값을 자식 테이블에 삽입하거나, 자식 테이블에서 참조 중인 부모 레코드를 삭제할 때 발생합니다. 예를 들어 orders 테이블이 customers 테이블의 customer_id를 참조하는데, 존재하지 않는 customer_id로 주문을 생성하려 하면 이 에러가 발생합니다. 또한 자식 테이블에 데이터가 남아 있는 부모 레코드를 삭제하려고 해도 동일하게 발생합니다.
2. 고유 제약 조건 위반 (Unique Constraint Violation)
테이블에 UNIQUE 또는 PRIMARY KEY 제약이 설정된 컬럼에 이미 존재하는 값을 중복 삽입하거나 업데이트할 때 발생합니다. 이 경우 세부 에러 코드는 23505 (unique_violation) 로 구분되지만, 23000의 하위 분류에 속합니다. 대량 데이터 마이그레이션이나 배치 작업 중 중복 레코드가 포함된 경우 자주 발생하며, 애플리케이션에서 멱등성(Idempotency) 처리를 하지 않을 경우 재시도 로직에서도 자주 발생합니다.
3. NOT NULL 및 CHECK 제약 조건 위반
NOT NULL로 정의된 컬럼에 NULL 값을 삽입하거나, CHECK 제약 조건에서 정의한 규칙(예: age > 0, status IN ('active', 'inactive'))을 만족하지 못하는 데이터를 입력할 때 발생합니다. 애플리케이션 코드에서 입력 유효성 검증이 제대로 이루어지지 않거나, 외부 시스템에서 데이터를 직접 적재할 때 데이터 품질 문제로 인해 자주 발생합니다. CHECK 제약 조건은 비즈니스 규칙을 DB 레벨에서 강제하는 강력한 도구이지만, 변경된 비즈니스 규칙이 DB에 반영되지 않으면 운영 중에 갑작스러운 에러를 유발할 수 있습니다.
해결 방법
원인 1: 외래 키 제약 조건 위반 해결
먼저 참조 관계를 확인하고, 삽입 전에 부모 레코드가 존재하는지 검증합니다.
-- 문제 상황: 존재하지 않는 customer_id로 주문 삽입 시도
INSERT INTO orders (order_id, customer_id, amount)
VALUES (1001, 9999, 50000);
-- ERROR: insert or update on table "orders" violates foreign key constraint
-- 해결책 1: 삽입 전 부모 레코드 존재 여부 확인
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM customers WHERE customer_id = 9999) THEN
INSERT INTO orders (order_id, customer_id, amount)
VALUES (1001, 9999, 50000);
ELSE
RAISE NOTICE 'customer_id 9999 does not exist. Skipping insert.';
END IF;
END $$;
-- 해결책 2: 자식 레코드가 있는 부모 삭제 시 CASCADE 옵션 활용
-- 기존 FK 제약 삭제 후 ON DELETE CASCADE로 재생성
ALTER TABLE orders
DROP CONSTRAINT orders_customer_id_fkey;
ALTER TABLE orders
ADD CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id)
REFERENCES customers(customer_id)
ON DELETE CASCADE;
-- 해결책 3: 부모 삭제 전 자식 레코드 먼저 삭제
BEGIN;
DELETE FROM orders WHERE customer_id = 9999;
DELETE FROM customers WHERE customer_id = 9999;
COMMIT;
원인 2: 고유 제약 조건 위반 해결
중복 삽입 시도 시 ON CONFLICT 구문을 활용하여 우아하게 처리합니다.
-- 문제 상황: 이미 존재하는 이메일로 사용자 등록 시도
INSERT INTO users (user_id, email, name)
VALUES (1, 'hong@example.com', '홍길동');
-- ERROR: duplicate key value violates unique constraint "users_email_key"
-- 해결책 1: ON CONFLICT DO NOTHING - 중복 시 무시
INSERT INTO users (user_id, email, name)
VALUES (1, 'hong@example.com', '홍길동')
ON CONFLICT (email) DO NOTHING;
-- 해결책 2: ON CONFLICT DO UPDATE - UPSERT 처리 (중복 시 업데이트)
INSERT INTO users (user_id, email, name)
VALUES (1, 'hong@example.com', '홍길동')
ON CONFLICT (email)
DO UPDATE SET
name = EXCLUDED.name,
updated_at = NOW();
-- 해결책 3: 대량 데이터 마이그레이션 시 중복 제거 후 삽입
INSERT INTO users (user_id, email, name)
SELECT DISTINCT ON (email) user_id, email, name
FROM users_staging
ORDER BY email, created_at DESC
ON CONFLICT (email) DO NOTHING;
원인 3: NOT NULL 및 CHECK 제약 조건 위반 해결
-- 문제 상황: NOT NULL 컬럼에 NULL 삽입
INSERT INTO products (product_id, name, price)
VALUES (101, NULL, 15000);
-- ERROR: null value in column "name" violates not-null constraint
-- 해결책 1: COALESCE로 기본값 제공
INSERT INTO products (product_id, name, price)
VALUES (101, COALESCE(NULL, '이름 없음'), 15000);
-- 문제 상황: CHECK 제약 조건 위반
-- price > 0 CHECK 조건이 있는 테이블에 음수 가격 삽입
INSERT INTO products (product_id, name, price)
VALUES (102, '테스트상품', -500);
-- ERROR: new row for relation "products" violates check constraint "products_price_check"
-- 해결책 2: 삽입 전 CHECK 조건 검증 후 처리
DO $$
DECLARE
v_price NUMERIC := -500;
BEGIN
IF v_price > 0 THEN
INSERT INTO products (product_id, name, price)
VALUES (102, '테스트상품', v_price);
ELSE
RAISE EXCEPTION '가격은 0보다 커야 합니다. 입력값: %', v_price;
END IF;
END $$;
-- 해결책 3: 현재 테이블의 CHECK 제약 조건 확인
SELECT
conname AS constraint_name,
pg_get_constraintdef(oid) AS constraint_definition
FROM pg_constraint
WHERE conrelid = 'products'::regclass
AND contype = 'c';
-- 해결책 4: 기존 CHECK 제약 조건 수정 (비즈니스 규칙 변경 시)
ALTER TABLE products
DROP CONSTRAINT products_price_check;
ALTER TABLE products
ADD CONSTRAINT products_price_check CHECK (price >= 0); -- 0 허용으로 변경
예방 방법
1. 애플리케이션과 DB 레이어의 이중 유효성 검사 구현
DB 제약 조건은 최후의 방어선으로 활용하되, 애플리케이션 레이어에서도 반드시 동일한 규칙으로 사전 유효성 검사를 수행해야 합니다. DB에만 의존하면 매번 에러 처리를 해야 하고 성능에도 영향을 줄 수 있으므로, 입력 데이터가 DB로 전달되기 전에 애플리케이션 코드에서 NULL 여부, 범위, 참조 무결성을 미리 검증하는 습관을 들이는 것이 중요합니다. 특히 대량 배치 작업 전에는 스테이징 테이블을 활용한 사전 검증 쿼리를 실행하여 문제 데이터를 미리 걸러내는 것을 권장합니다.
-- 배치 작업 전 사전 검증 쿼리 예시
SELECT
s.customer_id,
s.order_amount,
CASE
WHEN c.customer_id IS NULL THEN 'FK 위반: customer_id 없음'
WHEN s.order_amount <= 0 THEN 'CHECK 위반: 금액이 0 이하'
WHEN s.order_amount IS NULL THEN 'NOT NULL 위반: 금액이 NULL'
END AS violation_reason
FROM orders_staging s
LEFT JOIN customers c ON s.customer_id = c.customer_id
WHERE c.customer_id IS NULL
OR s.order_amount <= 0
OR s.order_amount IS NULL;
2. 제약 조건 문서화 및 DDL 변경 프로세스 관리
테이블의 모든 제약 조건을 팀 전체가 공유할 수 있는 문서로 관리하고, 비즈니스 규칙이 변경될 때마다 DB 스키마도 함께 업데이트하는 프로세스를 수립해야 합니다. 제약 조건 변경은 반드시 마이그레이션 스크립트로 관리하고 코드 리뷰 프로세스를 거쳐야 하며, 운영 환경 적용 전 개발/스테이징 환경에서 충분히 검증해야 합니다.
-- 현재 테이블의 모든 제약 조건 확인 쿼리 (정기 감사용)
SELECT
tc.table_name,
tc.constraint_name,
tc.constraint_type,
pg_get_constraintdef(pgc.oid) AS constraint_definition
FROM information_schema.table_constraints tc
JOIN pg_constraint pgc
ON pgc.conname = tc.constraint_name
WHERE tc.table_schema = 'public'
ORDER BY tc.table_name, tc.constraint_type;
관련 에러
- 23001 (restrict_violation): ON DELETE RESTRICT 또는 ON UPDATE RESTRICT 옵션이 설정된 외래 키에서 참조된 레코드를 삭제/수정 시도할 때 발생합니다.
- 23502 (not_null_violation): NOT NULL 제약이 설정된 컬럼에 NULL 값 삽입 시 발생하는 23000의 하위 에러입니다.
- 23503 (foreign_key_violation): 외래 키 참조 무결성 위반 시 발생하며, 23000 중 가장 자주 접하는 세부 에러 코드입니다.
- 23505 (unique_violation): PRIMARY KEY 또는 UNIQUE 제약 조건 위반 시 발생합니다.
- 23514 (check_violation): CHECK 제약 조건을 만족하지 못하는 데이터 입력 시 발생합니다.
- 23P01 (exclusion_violation): EXCLUDE 제약 조건(범위 중복 방지 등) 위반 시 발생하며, PostgreSQL 고유의 에러 코드입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.