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

23001
2026년 06월 21일 | DBMS Error 가이드

이 글에서 다루는 내용

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

23001 restrict violation 는?

PostgreSQL 에러 코드 23001은 restrict violation으로, 외래 키 제약 조건(Foreign Key Constraint)이 RESTRICT 옵션으로 설정된 상황에서 참조되고 있는 부모 테이블의 레코드를 삭제하거나 업데이트하려 할 때 발생합니다. 즉, 자식 테이블에서 해당 레코드를 아직 참조하고 있기 때문에 부모 레코드의 변경이 차단되는 상황입니다. 이 에러는 데이터 무결성을 보호하기 위한 PostgreSQL의 핵심 메커니즘 중 하나로, 잘못된 데이터 삭제나 변경으로 인한 고아(orphan) 레코드 생성을 방지합니다.


주요 발생 원인

1. RESTRICT 옵션이 설정된 외래 키 관계에서 부모 레코드 삭제 시도

가장 흔한 원인은 외래 키가 ON DELETE RESTRICT로 설정된 상태에서 자식 테이블에 연관 데이터가 존재하는 부모 레코드를 삭제하려는 경우입니다. PostgreSQL에서 RESTRICT는 기본값에 가까운 동작으로, 자식 행이 존재하는 한 부모 행의 삭제를 즉시 차단합니다. 이는 NO ACTION과 비슷하지만, RESTRICT는 트랜잭션 내에서도 지연 없이 즉각적으로 제약을 검사합니다.

2. RESTRICT 옵션이 설정된 외래 키 관계에서 부모 레코드의 참조 키 값 업데이트 시도

ON UPDATE RESTRICT 설정이 되어 있을 때 자식 테이블에서 참조 중인 부모 테이블의 기본 키(PK) 또는 고유 키(UK) 값을 변경하려 하면 에러가 발생합니다. 예를 들어, orders 테이블에서 참조하고 있는 customers 테이블의 customer_id를 수정하려는 경우가 해당됩니다. 이 경우 자식 데이터가 부모의 변경된 키를 추적하지 못하기 때문에 PostgreSQL이 해당 작업을 거부합니다.

3. 애플리케이션 레벨에서 삭제 순서 미준수

다중 테이블 삭제 시 자식 테이블의 데이터를 먼저 삭제하지 않고 부모 테이블의 데이터를 먼저 삭제하려는 애플리케이션 로직의 실수로 발생합니다. 특히 마이그레이션 스크립트, 데이터 정리(cleanup) 배치 작업, 혹은 ORM 프레임워크가 삭제 순서를 자동으로 처리하지 못하는 경우에 자주 나타납니다. 복잡한 참조 관계를 가진 스키마일수록 이 문제가 발생할 가능성이 높습니다.


해결 방법

원인 1 해결: 자식 데이터를 먼저 삭제한 후 부모 데이터 삭제

-- 문제 상황 재현
CREATE TABLE customers (
    customer_id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_id INT,
    CONSTRAINT fk_customer
        FOREIGN KEY (customer_id)
        REFERENCES customers(customer_id)
        ON DELETE RESTRICT
);

INSERT INTO customers (name) VALUES ('홍길동');
INSERT INTO orders (customer_id) VALUES (1);

-- 아래 실행 시 ERROR 23001 발생
-- DELETE FROM customers WHERE customer_id = 1;

-- 올바른 해결 방법: 트랜잭션 내에서 자식 먼저 삭제
BEGIN;
    DELETE FROM orders WHERE customer_id = 1;
    DELETE FROM customers WHERE customer_id = 1;
COMMIT;

원인 2 해결: 부모 키 업데이트 전 자식 데이터 처리

-- ON UPDATE RESTRICT 상황에서 부모 키 변경이 필요한 경우
-- 트랜잭션 내에서 자식 레코드의 참조값을 먼저 업데이트

BEGIN;
    -- 자식 테이블의 참조 값을 먼저 NULL 또는 임시값으로 변경 (nullable인 경우)
    UPDATE orders SET customer_id = NULL WHERE customer_id = 1;
    
    -- 부모 테이블 키 업데이트
    UPDATE customers SET customer_id = 999 WHERE customer_id = 1;
    
    -- 자식 테이블 참조 값 재설정
    UPDATE orders SET customer_id = 999 WHERE customer_id IS NULL;
COMMIT;

원인 3 해결: CASCADE 옵션으로 외래 키 재정의 (운영 환경 주의)

-- 기존 제약 조건 확인
SELECT conname, confdeltype, confupdtype
FROM pg_constraint
WHERE conrelid = 'orders'::regclass AND contype = 'f';

-- 기존 RESTRICT 제약 조건 삭제 후 CASCADE로 재정의
ALTER TABLE orders DROP CONSTRAINT fk_customer;

ALTER TABLE orders
    ADD CONSTRAINT fk_customer
    FOREIGN KEY (customer_id)
    REFERENCES customers(customer_id)
    ON DELETE CASCADE
    ON UPDATE CASCADE;

-- 이제 부모 삭제 시 자식도 자동 삭제됨
DELETE FROM customers WHERE customer_id = 1;

현재 외래 키 제약 조건 현황 파악 쿼리

-- 특정 테이블을 참조하는 모든 외래 키 확인
SELECT
    tc.table_schema,
    tc.table_name AS child_table,
    kcu.column_name AS child_column,
    ccu.table_name AS parent_table,
    ccu.column_name AS parent_column,
    rc.delete_rule,
    rc.update_rule
FROM
    information_schema.table_constraints AS tc
    JOIN information_schema.key_column_usage AS kcu
        ON tc.constraint_name = kcu.constraint_name
    JOIN information_schema.constraint_column_usage AS ccu
        ON ccu.constraint_name = tc.constraint_name
    JOIN information_schema.referential_constraints AS rc
        ON rc.constraint_name = tc.constraint_name
WHERE
    tc.constraint_type = 'FOREIGN KEY'
    AND ccu.table_name = 'customers'  -- 조회하고 싶은 부모 테이블명
ORDER BY
    tc.table_name;

안전한 삭제를 위한 함수 예시

-- 참조 관계를 고려한 안전한 고객 삭제 프로시저
CREATE OR REPLACE FUNCTION safe_delete_customer(p_customer_id INT)
RETURNS VOID AS $$
BEGIN
    -- 자식 테이블 데이터 먼저 삭제
    DELETE FROM order_items
    WHERE order_id IN (
        SELECT order_id FROM orders WHERE customer_id = p_customer_id
    );

    DELETE FROM orders WHERE customer_id = p_customer_id;

    -- 부모 테이블 삭제
    DELETE FROM customers WHERE customer_id = p_customer_id;

    RAISE NOTICE '고객 ID % 및 관련 데이터가 성공적으로 삭제되었습니다.', p_customer_id;
END;
$$ LANGUAGE plpgsql;

-- 사용 예
SELECT safe_delete_customer(1);

예방 방법

1. 외래 키 전략을 비즈니스 요구사항에 맞게 명확히 정의하기

테이블 설계 단계에서 각 참조 관계의 삭제/업데이트 정책을 명확히 결정해야 합니다. 부모 삭제 시 자식도 함께 삭제되어야 하는 경우 ON DELETE CASCADE, 자식을 NULL로 처리해야 하는 경우 ON DELETE SET NULL, 삭제를 원천 차단해야 하는 경우 ON DELETE RESTRICT를 사용하는 기준을 팀 내 문서로 정리하세요. 이러한 정책 문서는 신규 개발자의 실수를 예방하고 일관된 데이터 무결성 유지에 큰 도움이 됩니다.

-- 권장: 설계 시점에 명확한 정책 선택
-- 1. 부모 삭제 시 자식도 삭제 (연쇄 삭제)
ALTER TABLE orders ADD CONSTRAINT fk_customer_cascade
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
    ON DELETE CASCADE ON UPDATE CASCADE;

-- 2. 부모 삭제 시 자식의 참조값을 NULL로 설정
ALTER TABLE orders ADD CONSTRAINT fk_customer_null
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
    ON DELETE SET NULL ON UPDATE SET NULL;

-- 3. 자식이 있는 한 부모 삭제 차단 (엄격한 무결성 유지)
ALTER TABLE orders ADD CONSTRAINT fk_customer_restrict
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
    ON DELETE RESTRICT ON UPDATE RESTRICT;

2. 마이그레이션 및 배치 작업에 참조 무결성 체크 로직 포함

데이터 마이그레이션이나 대량 삭제 배치 작업 시 실행 전에 참조 무결성을 미리 검증하는 쿼리를 포함하는 것이 좋습니다. 또한 CI/CD 파이프라인에 외래 키 제약 조건 위반 테스트를 포함시켜, 코드 배포 전에 잠재적인 restrict violation을 사전에 탐지할 수 있도록 하는 것이 실무에서 매우 효과적입니다.

-- 삭제 전 참조 여부를 사전에 확인하는 안전 체크 쿼리
DO $$
DECLARE
    v_ref_count INT;
BEGIN
    SELECT COUNT(*) INTO v_ref_count
    FROM orders
    WHERE customer_id = 1;  -- 삭제 예정인 customer_id

    IF v_ref_count > 0 THEN
        RAISE EXCEPTION '삭제 불가: customer_id=1을 참조하는 orders 레코드가 % 건 존재합니다.', v_ref_count;
    ELSE
        DELETE FROM customers WHERE customer_id = 1;
        RAISE NOTICE '고객이 안전하게 삭제되었습니다.';
    END IF;
END;
$$;

관련 에러

  • 23000 (integrity_constraint_violation): 모든 무결성 제약 위반의 상위 에러 클래스로, 23001은 이 클래스의 하위 에러입니다.
  • 23503 (foreign_key_violation): 자식 테이블에 존재하지 않는 부모 키를 참조하려 할 때 발생하며, 23001과 반대 방향의 외래 키 위반입니다. 부모 레코드 없이 자식 레코드를 INSERT 하거나 UPDATE 할 때 주로 발생합니다.
  • 23505 (unique_violation): UNIQUE 또는 PRIMARY KEY 제약 조건 위반으로, 중복 값 삽입 시 발생합니다.
  • 23514 (check_violation): CHECK 제약 조건을 위반하는 값을 삽입하거나 업데이트할 때 발생합니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기