2026년 10월 03일 | DBMS Error 가이드
이 글에서 다루는 내용
P0004 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
P0004 assert failure 란?
PostgreSQL 에러 코드 P0004 (assert_failure)는 PL/pgSQL 함수나 프로시저 내부에서 ASSERT 구문을 사용할 때, 지정한 조건이 거짓(FALSE)으로 평가되거나 NULL이 반환될 경우 발생하는 런타임 에러입니다. 이 에러는 개발자가 코드의 불변 조건(invariant)이나 전제 조건(precondition)을 명시적으로 검증하기 위해 삽입한 ASSERT 구문이 실패했음을 의미하며, PostgreSQL 9.5 버전 이후부터 PL/pgSQL에 공식적으로 도입되었습니다. 주로 개발 및 테스트 환경에서 로직 오류를 조기에 탐지하기 위한 목적으로 사용되며, 운영 환경에서는 plpgsql.check_asserts 파라미터로 비활성화할 수도 있습니다.
주요 발생 원인
1. ASSERT 조건이 실제로 거짓(FALSE)인 경우
가장 일반적인 원인으로, 개발자가 “반드시 참이어야 한다”고 가정한 조건이 실제 데이터나 로직 흐름에서 거짓으로 판명되는 경우입니다. 예를 들어 잔액이 항상 0 이상이어야 한다는 가정 아래 작성된 함수에서, 외부 요인(버그, 잘못된 마이그레이션, 직접 UPDATE 등)으로 인해 음수 잔액이 존재할 때 ASSERT가 실패합니다. 이 경우는 단순한 에러가 아니라 실제 데이터 무결성 문제가 숨어 있다는 강력한 신호이므로, 원인을 철저히 조사해야 합니다.
2. ASSERT 조건이 NULL을 반환하는 경우
PostgreSQL의 ASSERT는 조건이 FALSE일 때뿐만 아니라 NULL로 평가될 때도 동일하게 실패합니다. 이는 SQL의 3값 논리(TRUE / FALSE / NULL) 특성 때문인데, 개발자가 NULL 가능성을 고려하지 않고 ASSERT 조건을 작성할 경우 예상치 못한 에러가 발생합니다. 예를 들어 함수 파라미터나 서브쿼리 결과가 NULL인 상태에서 비교 연산을 수행하면 조건 자체가 NULL이 되어 ASSERT가 실패하게 됩니다.
3. 잘못 설계된 ASSERT 로직 (비즈니스 규칙 불일치)
비즈니스 규칙이 변경되었음에도 불구하고 기존의 ASSERT 조건이 그대로 유지되어 충돌이 발생하는 경우입니다. 서비스가 성장함에 따라 초기에 정의한 불변 조건이 더 이상 유효하지 않게 되는 상황이 자주 발생합니다. 예컨대 “사용자는 반드시 하나의 역할만 가져야 한다”는 초기 가정이 멀티 롤 기능 도입 이후 위반되는 경우가 대표적입니다.
해결 방법
원인 1 해결: 데이터 및 로직 검증
먼저 ASSERT가 실패한 조건을 별도의 SELECT 쿼리로 재현하여 실제 데이터 상태를 확인합니다.
-- ASSERT 실패를 일으킨 함수 예시
CREATE OR REPLACE FUNCTION process_withdrawal(p_account_id INT, p_amount NUMERIC)
RETURNS VOID AS $$
DECLARE
v_balance NUMERIC;
BEGIN
SELECT balance INTO v_balance
FROM accounts
WHERE account_id = p_account_id;
-- 이 ASSERT가 실패하면 P0004 발생
ASSERT v_balance >= 0, '잔액이 음수입니다: account_id=' || p_account_id;
UPDATE accounts
SET balance = balance - p_amount
WHERE account_id = p_account_id;
END;
$$ LANGUAGE plpgsql;
-- ASSERT 실패 원인 조사 쿼리
SELECT account_id, balance
FROM accounts
WHERE balance < 0;
-- 데이터 수정 (원인 파악 후 적용)
UPDATE accounts
SET balance = 0
WHERE balance < 0
AND account_id = 12345; -- 해당 계정만 선택적으로 수정
원인 2 해결: NULL 처리를 포함한 ASSERT 조건 재작성
NULL이 반환될 가능성이 있는 경우 COALESCE나 IS NOT NULL 조건을 함께 사용합니다.
CREATE OR REPLACE FUNCTION validate_order(p_order_id INT)
RETURNS VOID AS $$
DECLARE
v_order_count INT;
v_customer_id INT;
BEGIN
-- 잘못된 ASSERT (customer_id가 NULL이면 비교 결과가 NULL → 실패)
-- ASSERT (SELECT customer_id FROM orders WHERE order_id = p_order_id) > 0;
-- 올바른 방법 1: COALESCE로 NULL 방어
SELECT COALESCE(customer_id, 0) INTO v_customer_id
FROM orders
WHERE order_id = p_order_id;
ASSERT v_customer_id > 0,
'유효하지 않은 customer_id: order_id=' || p_order_id;
-- 올바른 방법 2: IS NOT NULL 명시적 체크
SELECT COUNT(*) INTO v_order_count
FROM orders
WHERE order_id = p_order_id
AND customer_id IS NOT NULL;
ASSERT v_order_count = 1,
'주문이 존재하지 않거나 customer_id가 NULL입니다.';
END;
$$ LANGUAGE plpgsql;
원인 3 해결: ASSERT 조건 업데이트 및 plpgsql.check_asserts 활용
비즈니스 규칙이 변경된 경우 ASSERT 조건을 최신 규칙에 맞게 수정하고, 운영 환경에서는 ASSERT를 일시적으로 비활성화할 수 있습니다.
-- 세션 단위로 ASSERT 비활성화 (긴급 상황 시 임시 조치)
SET plpgsql.check_asserts = OFF;
-- 특정 함수 실행
SELECT process_withdrawal(12345, 100.00);
-- 다시 활성화
SET plpgsql.check_asserts = ON;
-- postgresql.conf 또는 ALTER SYSTEM으로 전역 설정 변경
-- (재시작 없이 적용: SELECT pg_reload_conf(); 필요)
ALTER SYSTEM SET plpgsql.check_asserts = OFF;
SELECT pg_reload_conf();
-- 수정된 ASSERT 조건 예시 (멀티 롤 지원 반영)
CREATE OR REPLACE FUNCTION assign_role(p_user_id INT, p_role TEXT)
RETURNS VOID AS $$
DECLARE
v_role_count INT;
BEGIN
SELECT COUNT(*) INTO v_role_count
FROM user_roles
WHERE user_id = p_user_id;
-- 기존: ASSERT v_role_count = 0 (단일 역할만 허용)
-- 변경 후: 최대 5개 역할까지 허용
ASSERT v_role_count < 5,
'사용자당 최대 5개의 역할만 허용됩니다: user_id=' || p_user_id;
INSERT INTO user_roles (user_id, role)
VALUES (p_user_id, p_role);
END;
$$ LANGUAGE plpgsql;
예방 방법
1. ASSERT 조건 작성 시 NULL 방어 코드를 습관화하라
ASSERT 구문을 작성할 때는 항상 조건이 NULL을 반환할 가능성을 고려해야 합니다. 변수를 선언할 때 기본값을 지정하거나, 서브쿼리를 사용할 때 COALESCE를 활용하여 NULL이 조건 평가에 영향을 미치지 않도록 방어 코드를 작성하는 습관을 들여야 합니다. 또한, 단위 테스트(pgTAP 등)를 통해 경계값 및 NULL 케이스를 포함한 다양한 시나리오를 자동화된 방식으로 검증하는 것이 중요합니다.
-- pgTAP을 활용한 ASSERT 검증 예시
SELECT plan(2);
SELECT ok(
(SELECT balance >= 0 FROM accounts WHERE account_id = 1),
'계정 1의 잔액은 0 이상이어야 합니다'
);
SELECT ok(
(SELECT customer_id IS NOT NULL FROM orders WHERE order_id = 100),
'주문 100의 customer_id는 NULL이 아니어야 합니다'
);
SELECT * FROM finish();
2. 운영 환경과 개발 환경의 ASSERT 전략을 분리하라
개발 및 QA 환경에서는 plpgsql.check_asserts = ON으로 설정하여 모든 ASSERT가 적극적으로 동작하게 하고, 운영 환경에서는 필요에 따라 OFF로 설정하는 전략을 수립해야 합니다. 단, 운영 환경에서 ASSERT를 OFF로 설정하더라도, ASSERT가 검증하던 조건들은 별도의 CHECK 제약 조건이나 트리거로 대체하여 데이터 무결성을 보장해야 합니다.
-- CHECK 제약 조건으로 ASSERT 역할 대체 (운영 환경)
ALTER TABLE accounts
ADD CONSTRAINT chk_balance_non_negative
CHECK (balance >= 0);
-- 트리거로 복잡한 비즈니스 규칙 검증
CREATE OR REPLACE FUNCTION trg_validate_order()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.customer_id IS NULL THEN
RAISE EXCEPTION '주문의 customer_id는 NULL일 수 없습니다.';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_order_customer_not_null
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION trg_validate_order();
관련 에러
- P0001 (raise_exception):
RAISE EXCEPTION으로 명시적으로 발생시키는 에러로, P0004와 달리 조건 기반이 아닌 개발자가 직접 발생시키는 방식입니다. - P0002 (no_data_found):
SELECT INTO구문에서 결과 행이 없을 때 발생하며, ASSERT 조건 내 서브쿼리 실패와 함께 나타날 수 있습니다. - P0003 (too_many_rows):
SELECT INTO에서 여러 행이 반환될 때 발생하며, ASSERT 이전 단계의 데이터 조회 로직에서 함께 나타나는 경우가 있습니다. - 23514 (check_violation): ASSERT 대신 CHECK 제약 조건을 위반할 때 발생하며, 운영 환경에서 ASSERT를 대체하는 수단으로 활용됩니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.