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

23000
2026년 08월 24일 | DBMS Error 가이드

이 글에서 다루는 내용

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

23000 integrity constraint violation 는?

PostgreSQL 에러 코드 23000은 무결성 제약 조건 위반(Integrity Constraint Violation)을 의미하며, 데이터베이스에 정의된 제약 조건을 위반하는 데이터를 삽입하거나 수정하려 할 때 발생합니다. 이 에러는 데이터의 일관성과 정확성을 보호하기 위해 PostgreSQL이 강제하는 규칙을 어겼을 때 나타나는 상위 레벨 에러 클래스로, 실제 현장에서는 더 구체적인 하위 에러 코드(23001, 23502, 23503, 23505, 23514 등)와 함께 등장하는 경우가 많습니다. 특히 대용량 데이터 마이그레이션, 배치 처리, 또는 여러 테이블 간의 복잡한 트랜잭션 처리 시 빈번하게 발생하므로 DBA와 개발자 모두 반드시 이해하고 있어야 합니다.


주요 발생 원인

1. 외래 키 제약 조건 위반 (Foreign Key Violation)

가장 흔하게 발생하는 원인으로, 부모 테이블에 존재하지 않는 값을 자식 테이블에 삽입하거나, 자식 테이블에서 참조 중인 부모 레코드를 삭제하려 할 때 발생합니다. 예를 들어 orders 테이블에서 존재하지 않는 customer_id를 참조하거나, 아직 주문이 남아 있는 고객 정보를 customers 테이블에서 삭제하려는 경우가 대표적입니다. 대용량 데이터 적재 시 삽입 순서가 잘못되어 발생하는 경우도 매우 많습니다.

2. UNIQUE 제약 조건 위반 (Unique Constraint Violation)

테이블에 UNIQUE 또는 PRIMARY KEY 제약이 걸려 있는 컬럼에 중복된 값을 삽입하거나 수정할 때 발생합니다. 특히 멀티 스레드 환경이나 동시 트랜잭션이 많은 고트래픽 서비스에서 레이스 컨디션(Race Condition)으로 인해 동시에 같은 값을 삽입하려 할 때 자주 나타납니다. 이 경우 단순 재시도 로직만으로는 해결되지 않으며 ON CONFLICT 처리나 분산 락(Distributed Lock) 같은 추가적인 설계가 필요합니다.

3. NOT NULL 및 CHECK 제약 조건 위반

NOT NULL 제약이 설정된 컬럼에 NULL 값을 삽입하거나, CHECK 제약 조건이 정의한 범위나 조건을 벗어나는 값을 넣으려 할 때 발생합니다. 예를 들어 나이(age) 컬럼에 음수 값을 입력하거나, 이메일 형식 CHECK 제약이 걸린 컬럼에 잘못된 형식의 문자열을 삽입하는 경우입니다. 애플리케이션 레벨의 유효성 검사가 누락되거나 데이터 마이그레이션 과정에서 원본 데이터 품질이 낮을 때 특히 자주 발생합니다.


해결 방법

외래 키 제약 조건 위반 해결

먼저 어떤 레코드가 문제인지 확인한 후, 존재하지 않는 참조 값을 가진 데이터를 정리합니다.

-- 부모 테이블에 없는 customer_id를 참조하는 주문 레코드 조회
SELECT o.*
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;

-- 문제 레코드 삭제 또는 수정
DELETE FROM orders
WHERE customer_id NOT IN (SELECT id FROM customers);

-- 데이터 삽입 시 부모 레코드 먼저 삽입
BEGIN;
  INSERT INTO customers (id, name, email)
  VALUES (101, '홍길동', 'hong@example.com')
  ON CONFLICT (id) DO NOTHING;

  INSERT INTO orders (id, customer_id, product, amount)
  VALUES (5001, 101, '노트북', 1500000);
COMMIT;

-- 대용량 마이그레이션 시 FK 비활성화 후 재활성화
ALTER TABLE orders DISABLE TRIGGER ALL;
-- 데이터 적재 후
ALTER TABLE orders ENABLE TRIGGER ALL;

UNIQUE 제약 조건 위반 해결

중복 데이터 삽입 시 INSERT ... ON CONFLICT 구문을 활용하면 우아하게 처리할 수 있습니다.

-- 중복 데이터 확인
SELECT email, COUNT(*) as cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

-- Upsert 처리: 중복이면 업데이트, 없으면 삽입
INSERT INTO users (id, email, username, created_at)
VALUES (1, 'user@example.com', 'newuser', NOW())
ON CONFLICT (email)
DO UPDATE SET
  username = EXCLUDED.username,
  updated_at = NOW();

-- 중복 데이터 정리 (가장 최근 레코드만 남기기)
DELETE FROM users
WHERE id NOT IN (
  SELECT DISTINCT ON (email) id
  FROM users
  ORDER BY email, created_at DESC
);

-- 동시성 문제 방지: Advisory Lock 사용
SELECT pg_advisory_xact_lock(hashtext('user_insert_lock'));
INSERT INTO users (email, username) VALUES ('user@example.com', 'testuser');

NOT NULL 및 CHECK 제약 조건 위반 해결

-- NOT NULL 위반 컬럼 확인 및 기본값 처리
UPDATE employees
SET department_id = 0  -- 기본 부서 ID로 설정
WHERE department_id IS NULL;

-- CHECK 제약 조건 내용 확인
SELECT conname, consrc
FROM pg_constraint
WHERE conrelid = 'products'::regclass
  AND contype = 'c';

-- CHECK 제약 위반 데이터 조회 및 수정
UPDATE products
SET price = 0
WHERE price < 0;

-- 데이터 삽입 시 COALESCE로 NULL 방어
INSERT INTO employees (id, name, department_id, salary)
VALUES (
  200,
  '김철수',
  COALESCE(NULL, 1),       -- NULL이면 기본값 1 사용
  GREATEST(COALESCE(NULL, 0), 0)  -- 음수 방지
);

-- CHECK 제약 조건 임시 비활성화 (마이그레이션용, 이후 반드시 재활성화)
ALTER TABLE products DISABLE TRIGGER ALL;
-- 데이터 정리 후
ALTER TABLE products VALIDATE CONSTRAINT chk_price_positive;

예방 방법

1. 애플리케이션과 DB 양쪽에서 이중 유효성 검사 적용

데이터베이스 제약 조건만 믿지 말고 애플리케이션 레벨에서도 반드시 입력값을 검증해야 합니다. 아래처럼 트랜잭션 처리 전에 데이터 존재 여부를 사전 확인하는 습관을 들이면 에러 발생 빈도를 크게 줄일 수 있습니다.

-- 삽입 전 부모 레코드 존재 여부 확인 함수 예시
CREATE OR REPLACE FUNCTION safe_insert_order(
  p_customer_id INT,
  p_product TEXT,
  p_amount NUMERIC
) RETURNS VOID AS $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM customers WHERE id = p_customer_id) THEN
    RAISE EXCEPTION '고객 ID %가 존재하지 않습니다.', p_customer_id
      USING ERRCODE = '23503';
  END IF;

  INSERT INTO orders (customer_id, product, amount)
  VALUES (p_customer_id, p_product, p_amount);
END;
$$ LANGUAGE plpgsql;

2. 제약 조건 위반 로깅 및 모니터링 체계 구축

운영 환경에서 23000 계열 에러가 발생했을 때 즉시 감지하고 원인을 추적할 수 있도록 로깅 체계를 갖추어야 합니다. pg_stat_activitylog_min_error_statement 설정을 활용해 에러 발생 시점의 쿼리를 캡처하고, 모니터링 도구(Prometheus, Grafana, pgBadger 등)와 연동하면 반복적인 제약 위반 패턴을 사전에 발견할 수 있습니다.

-- 제약 조건 위반 이력 관리 테이블 예시
CREATE TABLE constraint_violation_log (
  id SERIAL PRIMARY KEY,
  table_name TEXT NOT NULL,
  constraint_name TEXT NOT NULL,
  violating_data JSONB,
  occurred_at TIMESTAMPTZ DEFAULT NOW(),
  resolved BOOLEAN DEFAULT FALSE
);

-- postgresql.conf 설정 권장사항
-- log_min_error_statement = 'error'
-- log_error_verbosity = 'verbose'

관련 에러

| 에러 코드 | 이름 | 설명 |

|———–|——|——|

| 23001 | restrict_violation | ON DELETE RESTRICT/ON UPDATE RESTRICT 위반 |

| 23502 | not_null_violation | NOT NULL 제약 조건 위반 |

| 23503 | foreign_key_violation | 외래 키 제약 조건 위반 |

| 23505 | unique_violation | UNIQUE 또는 PRIMARY KEY 중복 위반 |

| 23514 | check_violation | CHECK 제약 조건 위반 |

| 23P01 | exclusion_violation | EXCLUDE 제약 조건 위반 (범위 중복 등) |

이 에러들은 모두 23000의 하위 에러 코드로, 애플리케이션에서 예외 처리 시 23000으로 묶어서 처리하거나 개별 코드별로 세분화하여 처리할 수 있습니다. 실무에서는 23503(FK 위반)과 23505(UNIQUE 위반)가 가장 빈번하게 발생하므로 우선적으로 대비책을 마련하는 것을 강력히 권장합니다.


DBMS 에러 코드 시리즈

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

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

댓글 남기기