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

23001
2026년 08월 25일 | DBMS Error 가이드

이 글에서 다루는 내용

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

23001 restrict violation 는?

PostgreSQL 에러 코드 23001, 즉 restrict_violation은 외래 키(Foreign Key) 제약 조건이 RESTRICT 옵션으로 설정된 상황에서, 참조되고 있는 부모 테이블의 행을 삭제하거나 업데이트하려 할 때 발생합니다. 쉽게 말해, 자식 테이블에서 여전히 참조 중인 데이터를 부모 테이블에서 지우려고 시도할 때 PostgreSQL이 이를 차단하며 던지는 에러입니다. 이 에러는 데이터 무결성을 보호하기 위한 PostgreSQL의 정상적인 동작이지만, 예상치 못한 상황에서 발생하면 애플리케이션 장애로 이어질 수 있으므로 정확한 원인 파악과 대처가 필요합니다.


주요 발생 원인

1. RESTRICT 옵션이 설정된 외래 키 제약 조건에서 부모 행 삭제 시도

가장 흔한 원인으로, 외래 키를 생성할 때 ON DELETE RESTRICT 옵션이 명시되어 있거나, 아무 옵션도 지정하지 않았을 때(기본값이 RESTRICT) 자식 테이블에 참조 데이터가 존재하는 상태에서 부모 행을 삭제하려 하면 발생합니다. 예를 들어 orders 테이블이 customers 테이블을 참조하고 있는데, 특정 고객에 대한 주문이 남아 있는 상태에서 해당 고객 레코드를 삭제하려 할 경우 이 에러가 즉각적으로 발생합니다.

2. 부모 테이블의 참조 키(PK 또는 UK) 값 업데이트 시도

ON UPDATE RESTRICT 옵션이 설정된 경우, 자식 테이블에서 참조 중인 부모 테이블의 기본 키나 고유 키 값을 변경하려 할 때도 동일한 에러가 발생합니다. 이는 데이터 마이그레이션이나 레거시 시스템 연동 작업 중에 자주 발생하는 패턴으로, 대량의 UPDATE 쿼리를 실행하다가 중간에 에러가 발생해 전체 트랜잭션이 롤백되는 상황을 초래할 수 있습니다.

3. 트리거나 CASCADE 설정 미흡으로 인한 연쇄 삭제 처리 실패

복잡한 스키마 구조에서 여러 테이블이 계층적으로 참조 관계를 맺고 있을 때, 중간 테이블의 외래 키에 RESTRICT가 걸려 있으면 최상위 부모 레코드를 삭제하는 과정에서 예기치 않게 에러가 발생할 수 있습니다. 개발자가 CASCADE를 설정했다고 생각했지만 실제로는 RESTRICT가 적용된 경우, 또는 나중에 추가된 테이블에 외래 키 옵션을 잘못 설정한 경우에 자주 발생합니다.


해결 방법

원인 1 해결: 자식 데이터를 먼저 삭제하거나 외래 키 옵션 변경

자식 테이블의 관련 데이터를 먼저 삭제한 뒤 부모 행을 삭제하는 방식으로 해결할 수 있습니다.

-- 1단계: 자식 테이블 데이터 먼저 삭제
DELETE FROM orders
WHERE customer_id = 101;

-- 2단계: 이후 부모 테이블 행 삭제
DELETE FROM customers
WHERE customer_id = 101;

또는 외래 키 제약 조건을 ON DELETE CASCADE로 변경하여 부모 삭제 시 자식도 자동으로 삭제되도록 설정할 수 있습니다.

-- 기존 제약 조건 삭제
ALTER TABLE orders
DROP CONSTRAINT orders_customer_id_fkey;

-- CASCADE 옵션으로 재생성
ALTER TABLE orders
ADD CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id)
REFERENCES customers(customer_id)
ON DELETE CASCADE;

자식 데이터를 삭제하지 않고 NULL로 남기려면 ON DELETE SET NULL을 사용할 수 있습니다.

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 SET NULL;

원인 2 해결: UPDATE 시 자식 테이블 먼저 처리

부모의 키 값을 변경해야 하는 경우, 트랜잭션 내에서 자식 테이블을 먼저 업데이트한 뒤 부모를 업데이트하거나, DEFERRABLE 제약 조건을 활용합니다.

-- 트랜잭션 내에서 순서 보장
BEGIN;

-- 자식 테이블 먼저 업데이트
UPDATE orders
SET customer_id = 202
WHERE customer_id = 101;

-- 이후 부모 테이블 업데이트
UPDATE customers
SET customer_id = 202
WHERE customer_id = 101;

COMMIT;

또는 외래 키를 DEFERRABLE INITIALLY DEFERRED로 설정하여 트랜잭션 커밋 시점에 제약을 검사하도록 할 수 있습니다.

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 UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED;

원인 3 해결: 외래 키 제약 조건 전체 점검 및 수정

현재 데이터베이스에 존재하는 모든 외래 키와 그 옵션을 조회하여 잘못 설정된 제약을 파악합니다.

-- 현재 스키마의 모든 외래 키 및 옵션 조회
SELECT
    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
        AND tc.table_schema = kcu.table_schema
    JOIN information_schema.constraint_column_usage AS ccu
        ON ccu.constraint_name = tc.constraint_name
        AND ccu.table_schema = tc.table_schema
    JOIN information_schema.referential_constraints AS rc
        ON rc.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
ORDER BY tc.table_name;

이 쿼리를 통해 delete_rule이나 update_ruleNO ACTION 또는 RESTRICT로 설정된 외래 키를 찾아, 비즈니스 요구사항에 맞게 수정합니다.


예방 방법

1. 외래 키 설계 시 비즈니스 로직에 맞는 참조 무결성 옵션을 명시적으로 지정하라

테이블을 설계할 때부터 ON DELETEON UPDATE 옵션을 명확히 정의하는 습관을 들여야 합니다. 기본값인 NO ACTION(사실상 RESTRICT와 동일)에 의존하지 말고, 데이터 삭제 시 자식 데이터를 함께 지워야 하는지(CASCADE), NULL로 남겨야 하는지(SET NULL), 아니면 삭제 자체를 막아야 하는지(RESTRICT)를 팀 내에서 명확히 협의하고 DDL에 반영해야 합니다. 이를 코드 리뷰 체크리스트에 포함시키면 실수를 크게 줄일 수 있습니다.

2. 삭제/업데이트 로직을 처리하는 공통 함수 또는 프로시저를 만들어 순서를 보장하라

애플리케이션 코드나 SQL 스크립트에서 직접 DELETE/UPDATE를 날리는 대신, PostgreSQL 함수나 프로시저로 삭제 로직을 캡슐화하면 참조 무결성 위반을 사전에 방지할 수 있습니다. 아래처럼 자식 데이터 정리 후 부모 삭제를 보장하는 함수를 만들어 두면 실수를 예방할 수 있습니다.

CREATE OR REPLACE FUNCTION delete_customer_safely(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 'Customer % and all related data deleted successfully.', p_customer_id;
END;
$$ LANGUAGE plpgsql;

-- 사용 예시
SELECT delete_customer_safely(101);

관련 에러

  • 23000 integrity_constraint_violation: 23001의 상위 에러 클래스로, 모든 무결성 제약 위반을 포괄합니다.
  • 23503 foreign_key_violation: RESTRICT가 아닌 일반적인 외래 키 위반 상황(자식 테이블에 존재하지 않는 부모 키를 참조하는 INSERT/UPDATE 시)에 발생합니다. 23001과 가장 혼동하기 쉬운 에러입니다.
  • 23502 not_null_violation: NOT NULL 제약 조건 위반 시 발생합니다.
  • 23505 unique_violation: UNIQUE 제약 조건 위반 시 발생하며, 중복 데이터 삽입 시 자주 마주치는 에러입니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기