2026년 09월 03일 | DBMS Error 가이드
이 글에서 다루는 내용
39004 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
39004 null value not allowed 는?
PostgreSQL 에러 코드 39004는 PL/pgSQL 함수 또는 프로시저 내에서 NULL 값이 허용되지 않는 변수나 파라미터에 NULL을 할당하려 할 때 발생합니다. 이 에러는 주로 NOT NULL 제약이 걸린 변수에 NULL 값이 반환되거나, STRICT 옵션이 설정된 쿼리에서 결과가 없을 때 나타납니다. 실무에서는 데이터 파이프라인, 복잡한 비즈니스 로직을 처리하는 함수, 또는 외부 시스템과 연동되는 프로시저에서 특히 자주 마주치는 에러입니다.
주요 발생 원인
1. NOT NULL 선언된 PL/pgSQL 변수에 NULL 할당
PL/pgSQL에서 변수를 선언할 때 NOT NULL 키워드를 붙이면 해당 변수는 NULL 값을 가질 수 없습니다. 그런데 이 변수에 값을 할당하는 SELECT 쿼리나 함수 호출이 NULL 또는 결과 없음을 반환할 경우, PostgreSQL은 즉시 39004 에러를 발생시킵니다. 특히 초기값을 지정하지 않거나 외부 테이블에서 집계 함수 결과를 받아오는 경우 이 문제가 빈번하게 나타납니다.
-- 에러 발생 예시
CREATE OR REPLACE FUNCTION get_user_score(p_user_id INT)
RETURNS INT AS $$
DECLARE
v_score INT NOT NULL := 0; -- NOT NULL 선언
BEGIN
-- users 테이블에 해당 user_id가 없으면 NULL 반환 → 에러 발생
SELECT score INTO v_score
FROM users
WHERE user_id = p_user_id;
RETURN v_score;
END;
$$ LANGUAGE plpgsql;
-- 존재하지 않는 user_id로 호출 시 39004 에러 발생
SELECT get_user_score(9999);
2. STRICT 모드 쿼리에서 결과 없음 또는 다중 행 반환
SELECT INTO STRICT 구문은 정확히 한 행만 반환해야 합니다. 결과가 0건이면 NO_DATA_FOUND, 2건 이상이면 TOO_MANY_ROWS 에러가 발생하며, NULL 처리와 결합될 경우 39004 에러로 이어질 수 있습니다. STRICT 모드는 데이터 정합성을 강제하는 유용한 옵션이지만, 입력값 검증 없이 사용하면 예기치 않은 에러를 유발합니다.
-- STRICT 모드에서 에러 발생 예시
CREATE OR REPLACE FUNCTION fetch_order(p_order_id INT)
RETURNS TEXT AS $$
DECLARE
v_status TEXT NOT NULL := 'UNKNOWN';
BEGIN
-- 해당 order_id가 없으면 NO_DATA_FOUND → v_status에 NULL 할당 시도 → 39004
SELECT status INTO STRICT v_status
FROM orders
WHERE order_id = p_order_id;
RETURN v_status;
END;
$$ LANGUAGE plpgsql;
3. 함수 반환값이 NULL인 경우 (RETURNS NOT NULL 계약 위반)
PostgreSQL에서 함수 정의 시 반환 타입에 묵시적 또는 명시적으로 NOT NULL 계약이 있을 때, 함수가 NULL을 반환하면 39004 에러가 발생할 수 있습니다. 특히 도메인 타입(Domain Type)을 반환 타입으로 사용하는 경우, 해당 도메인에 NOT NULL 제약이 있다면 NULL 반환 시 반드시 에러가 발생합니다. 이 경우는 디버깅이 까다로운데, 함수 내부 로직보다 반환 타입 정의를 먼저 확인해야 합니다.
-- NOT NULL 도메인 정의
CREATE DOMAIN positive_int AS INT NOT NULL CHECK (VALUE > 0);
-- 도메인을 반환 타입으로 사용하는 함수
CREATE OR REPLACE FUNCTION calculate_bonus(p_emp_id INT)
RETURNS positive_int AS $$
DECLARE
v_bonus positive_int;
BEGIN
SELECT bonus INTO v_bonus
FROM employees
WHERE emp_id = p_emp_id;
-- bonus 컬럼이 NULL이면 39004 에러 발생
RETURN v_bonus;
END;
$$ LANGUAGE plpgsql;
해결 방법
원인 1 해결: COALESCE 또는 기본값으로 NULL 방어
NOT NULL 변수에 값을 할당할 때는 반드시 COALESCE를 사용하여 NULL이 들어오는 상황을 방어해야 합니다.
-- 수정된 버전: COALESCE로 NULL 방어
CREATE OR REPLACE FUNCTION get_user_score(p_user_id INT)
RETURNS INT AS $$
DECLARE
v_score INT NOT NULL := 0;
BEGIN
SELECT COALESCE(score, 0) INTO v_score
FROM users
WHERE user_id = p_user_id;
-- SELECT INTO가 아무 행도 반환하지 않을 경우도 대비
IF NOT FOUND THEN
v_score := 0;
END IF;
RETURN v_score;
END;
$$ LANGUAGE plpgsql;
원인 2 해결: STRICT 모드에서 예외 처리 추가
STRICT 모드를 사용할 때는 반드시 EXCEPTION 블록으로 NO_DATA_FOUND와 TOO_MANY_ROWS를 처리해야 합니다.
CREATE OR REPLACE FUNCTION fetch_order(p_order_id INT)
RETURNS TEXT AS $$
DECLARE
v_status TEXT NOT NULL := 'UNKNOWN';
BEGIN
BEGIN
SELECT status INTO STRICT v_status
FROM orders
WHERE order_id = p_order_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- 데이터 없을 때 기본값 유지
RAISE NOTICE 'Order % not found, returning default status.', p_order_id;
v_status := 'NOT_FOUND';
WHEN TOO_MANY_ROWS THEN
RAISE EXCEPTION 'Multiple orders found for id: %', p_order_id;
END;
RETURN v_status;
END;
$$ LANGUAGE plpgsql;
원인 3 해결: 도메인 타입 사용 시 명시적 NULL 체크
도메인 타입을 사용하는 경우, 반환 전에 명시적으로 NULL 여부를 확인하고 적절한 기본값이나 예외 처리를 추가하세요.
CREATE OR REPLACE FUNCTION calculate_bonus(p_emp_id INT)
RETURNS positive_int AS $$
DECLARE
v_raw_bonus INT;
v_bonus positive_int;
BEGIN
SELECT bonus INTO v_raw_bonus
FROM employees
WHERE emp_id = p_emp_id;
-- NULL 또는 비양수 값에 대한 명시적 처리
IF v_raw_bonus IS NULL OR v_raw_bonus <= 0 THEN
RAISE EXCEPTION 'Invalid or null bonus for employee %: %', p_emp_id, v_raw_bonus
USING ERRCODE = '39004';
END IF;
v_bonus := v_raw_bonus;
RETURN v_bonus;
END;
$$ LANGUAGE plpgsql;
예방 방법
1. 변수 선언 시 NOT NULL 사용 최소화 및 방어적 초기화
PL/pgSQL 함수에서 변수를 선언할 때 NOT NULL을 무분별하게 사용하기보다는, 꼭 필요한 경우에만 사용하고 반드시 의미 있는 기본값을 함께 선언하는 습관을 들이세요. 또한, 외부 테이블이나 뷰로부터 값을 가져올 때는 항상 COALESCE, NULLIF, CASE WHEN 등을 활용한 방어적 쿼리를 작성하여 NULL이 변수에 직접 할당되는 상황을 원천 차단하는 것이 좋습니다.
-- 권장 패턴: 방어적 초기화와 COALESCE 조합
DECLARE
v_count INT NOT NULL := 0;
v_name TEXT NOT NULL := 'UNKNOWN';
v_amount NUMERIC NOT NULL := 0.0;
BEGIN
SELECT
COALESCE(COUNT(*), 0),
COALESCE(MAX(name), 'UNNAMED'),
COALESCE(SUM(amount), 0.0)
INTO v_count, v_name, v_amount
FROM transactions
WHERE created_at >= NOW() - INTERVAL '1 day';
END;
2. 함수 개발 시 단위 테스트로 NULL 경계값 검증
운영 배포 전에 NULL, 빈 문자열, 존재하지 않는 ID 등의 경계값(Edge Case)을 포함한 단위 테스트를 작성하세요. PostgreSQL의 pgTAP 확장이나 간단한 DO 블록을 활용하여 함수가 다양한 입력값에서 올바르게 동작하는지 사전에 검증하면 운영 환경에서의 39004 에러를 크게 줄일 수 있습니다.
-- pgTAP을 활용한 NULL 경계값 테스트 예시
SELECT plan(3);
SELECT lives_ok(
$$ SELECT get_user_score(NULL) $$,
'NULL user_id should not raise an error'
);
SELECT lives_ok(
$$ SELECT get_user_score(9999) $$,
'Non-existent user_id should return default score'
);
SELECT is(
get_user_score(9999),
0,
'Default score for missing user should be 0'
);
SELECT * FROM finish();
관련 에러
- 23502 (
not_null_violation): 테이블의 NOT NULL 컬럼에 NULL 값을 INSERT/UPDATE 시 발생하는 에러로, 39004와 유사하지만 PL/pgSQL 변수가 아닌 테이블 레벨에서 발생합니다. - 02000 (
no_data_found): STRICT 모드 쿼리에서 결과 행이 없을 때 발생하며, NOT NULL 변수와 결합 시 39004로 이어질 수 있습니다. - 21000 (
cardinality_violation): STRICT 쿼리에서 2건 이상의 행이 반환될 때 발생하는 에러로, STRICT 사용 시 39004와 함께 자주 발생합니다. - 42804 (
datatype_mismatch): 반환 타입 불일치로 발생하며, 도메인 타입 사용 시 39004와 함께 발생할 수 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.