2026년 06월 30일 | DBMS Error 가이드
이 글에서 다루는 내용
39004 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
39004 null value not allowed 는?
PostgreSQL 에러 코드 39004는 PL/pgSQL 또는 PL/Perl, PL/Python 등의 프로시저 언어 함수에서 STRICT 옵션이 설정된 함수에 NULL 값이 인자로 전달될 때 발생하는 에러입니다. STRICT 키워드는 “입력 인자 중 하나라도 NULL이면 함수를 실행하지 않고 즉시 NULL을 반환하라”는 의미가 아니라, 특정 컨텍스트에서는 NULL 전달 자체를 허용하지 않는 제약으로 작동합니다. 주로 저장 프로시저(Stored Procedure), 사용자 정의 함수(UDF), 또는 트리거 함수 내부에서 NOT NULL 제약이 걸린 변수나 반환값에 NULL을 할당하려 할 때 이 에러가 표면으로 드러납니다.
주요 발생 원인
- PL/pgSQL 함수의 STRICT 선언과 NULL 인자 전달
CREATE FUNCTION 시 STRICT 또는 RETURNS NULL ON NULL INPUT이 아닌 CALLED ON NULL INPUT(기본값)과 혼동하여 잘못 선언한 경우, 함수 내부 로직에서 NULL 값을 명시적으로 금지하는 변수에 NULL을 넣으려 할 때 발생합니다. 특히 함수 내부에서 NOT NULL 제약을 가진 변수를 선언한 뒤 NULL이 들어오는 경우가 대표적입니다. 이 에러는 단순히 NULL이 들어왔다는 사실보다, 함수 설계 단계에서 NULL 처리 전략이 빠져 있었음을 의미합니다.
- 저장 프로시저 내부에서 NOT NULL 변수에 NULL 할당
PL/pgSQL 블록 안에서 변수를 variable_name data_type NOT NULL := default_value 형태로 선언했는데, 이후 로직 흐름에서 해당 변수에 NULL이 대입되는 경우입니다. 예를 들어 SELECT INTO 구문으로 값을 가져올 때 쿼리 결과가 NULL이고, 그 결과를 NOT NULL 변수에 담으려 하면 즉시 39004 에러가 발생합니다. 이는 개발 초기에는 테스트 데이터가 잘 갖춰져 있어 문제가 없다가 운영 환경에서 예외 데이터가 들어오면서 뒤늦게 발견되는 경우가 많습니다.
- 커서(Cursor) 또는 루프 처리 중 NULL 레코드 접근
PL/pgSQL에서 커서나 FOR 루프를 사용할 때, 레코드 변수의 특정 필드가 NULL인 상태에서 이를 NOT NULL 타입의 컬럼이나 변수에 할당하려 하면 이 에러가 발생합니다. 대량 데이터를 배치 처리하는 프로시저에서 특히 자주 등장하며, NULL 체크 없이 직접 할당 로직을 작성한 경우 운영 중 간헐적으로 에러가 터지는 원인이 됩니다.
해결 방법
원인 1 해결: STRICT 함수에서 NULL 방어 처리
함수를 STRICT로 선언했다면, NULL 입력 시 자동으로 NULL을 반환하므로 문제없습니다. 하지만 CALLED ON NULL INPUT(기본) 상태에서 내부 NULL 처리가 없다면 아래처럼 방어 코드를 추가하세요.
-- 문제가 되는 함수 예시
CREATE OR REPLACE FUNCTION calculate_bonus(emp_salary NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
v_bonus NUMERIC NOT NULL := 0; -- NOT NULL 변수 선언
BEGIN
-- emp_salary가 NULL이면 에러 발생!
v_bonus := emp_salary * 0.1;
RETURN v_bonus;
END;
$$ LANGUAGE plpgsql;
-- 해결책 1: STRICT 선언으로 NULL 입력 시 자동 NULL 반환
CREATE OR REPLACE FUNCTION calculate_bonus(emp_salary NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
v_bonus NUMERIC NOT NULL := 0;
BEGIN
v_bonus := emp_salary * 0.1;
RETURN v_bonus;
END;
$$ LANGUAGE plpgsql STRICT;
-- 해결책 2: 함수 내부에서 COALESCE로 NULL 방어
CREATE OR REPLACE FUNCTION calculate_bonus(emp_salary NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
v_bonus NUMERIC NOT NULL := 0;
BEGIN
v_bonus := COALESCE(emp_salary, 0) * 0.1;
RETURN v_bonus;
END;
$$ LANGUAGE plpgsql;
-- 테스트
SELECT calculate_bonus(NULL); -- STRICT 버전: NULL 반환
SELECT calculate_bonus(50000); -- 정상: 5000 반환
원인 2 해결: NOT NULL 변수 선언 시 NULL 체크 추가
-- 문제가 되는 프로시저
CREATE OR REPLACE PROCEDURE update_employee_grade(p_emp_id INT)
LANGUAGE plpgsql AS $$
DECLARE
v_salary NUMERIC NOT NULL := 0; -- NOT NULL 변수
v_grade VARCHAR NOT NULL := 'N'; -- NOT NULL 변수
BEGIN
-- 쿼리 결과가 NULL이면 에러 발생!
SELECT salary INTO v_salary
FROM employees
WHERE emp_id = p_emp_id;
-- salary가 NULL인 직원 레코드가 있으면 39004 에러
IF v_salary >= 5000 THEN
v_grade := 'A';
ELSE
v_grade := 'B';
END IF;
UPDATE employees SET grade = v_grade WHERE emp_id = p_emp_id;
END;
$$;
-- 해결책: SELECT INTO 후 NULL 체크 및 기본값 처리
CREATE OR REPLACE PROCEDURE update_employee_grade(p_emp_id INT)
LANGUAGE plpgsql AS $$
DECLARE
v_salary NUMERIC; -- NOT NULL 제약 제거
v_grade VARCHAR(1) NOT NULL := 'N';
BEGIN
SELECT salary INTO v_salary
FROM employees
WHERE emp_id = p_emp_id;
-- NULL 체크 후 안전하게 처리
IF v_salary IS NULL THEN
RAISE WARNING 'emp_id %의 salary가 NULL입니다. 기본 등급을 적용합니다.', p_emp_id;
v_grade := 'C';
ELSIF v_salary >= 5000 THEN
v_grade := 'A';
ELSE
v_grade := 'B';
END IF;
UPDATE employees SET grade = v_grade WHERE emp_id = p_emp_id;
RAISE NOTICE 'emp_id % 등급 업데이트 완료: %', p_emp_id, v_grade;
END;
$$;
-- 실행 테스트
CALL update_employee_grade(101);
원인 3 해결: 커서/루프에서 NULL 안전 처리
-- 문제가 되는 배치 처리 프로시저
CREATE OR REPLACE PROCEDURE batch_process_orders()
LANGUAGE plpgsql AS $$
DECLARE
v_rec RECORD;
v_total NUMERIC NOT NULL := 0;
v_item_price NUMERIC NOT NULL := 0;
BEGIN
FOR v_rec IN SELECT order_id, unit_price, quantity FROM orders LOOP
-- unit_price나 quantity가 NULL이면 에러!
v_item_price := v_rec.unit_price * v_rec.quantity;
v_total := v_total + v_item_price;
END LOOP;
RAISE NOTICE '총 주문 금액: %', v_total;
END;
$$;
-- 해결책: 루프 내부에서 COALESCE 또는 NULL 체크 적용
CREATE OR REPLACE PROCEDURE batch_process_orders()
LANGUAGE plpgsql AS $$
DECLARE
v_rec RECORD;
v_total NUMERIC NOT NULL := 0;
v_item_price NUMERIC NOT NULL := 0;
v_skip_count INT NOT NULL := 0;
BEGIN
FOR v_rec IN SELECT order_id, unit_price, quantity FROM orders LOOP
-- NULL 체크로 안전하게 처리
IF v_rec.unit_price IS NULL OR v_rec.quantity IS NULL THEN
v_skip_count := v_skip_count + 1;
RAISE WARNING 'order_id %: unit_price 또는 quantity가 NULL이어서 건너뜁니다.', v_rec.order_id;
CONTINUE;
END IF;
v_item_price := v_rec.unit_price * v_rec.quantity;
v_total := v_total + v_item_price;
END LOOP;
RAISE NOTICE '총 주문 금액: %, 건너뛴 레코드: %건', v_total, v_skip_count;
END;
$$;
-- COALESCE를 활용한 더 간결한 방법
CREATE OR REPLACE PROCEDURE batch_process_orders_v2()
LANGUAGE plpgsql AS $$
DECLARE
v_rec RECORD;
v_total NUMERIC NOT NULL := 0;
BEGIN
FOR v_rec IN
SELECT order_id,
COALESCE(unit_price, 0) AS unit_price,
COALESCE(quantity, 0) AS quantity
FROM orders
LOOP
v_total := v_total + (v_rec.unit_price * v_rec.quantity);
END LOOP;
RAISE NOTICE '총 주문 금액: %', v_total;
END;
$$;
예방 방법
- 함수 및 프로시저 작성 시 NULL 처리 전략을 설계 단계에서 명문화하라
모든 UDF와 저장 프로시저를 작성하기 전에 “이 함수는 NULL 입력을 어떻게 처리할 것인가”를 팀 내 코딩 컨벤션으로 정의하세요. 단순히 NULL을 통과시킬 것인지(CALLED ON NULL INPUT), 아니면 NULL 입력 시 즉시 NULL을 반환할 것인지(STRICT)를 명시적으로 선택하고 문서화합니다. 특히 NOT NULL 변수를 선언할 때는 반드시 COALESCE, NULLIF, IS NULL 체크 등의 방어 로직을 함께 작성하는 것을 팀 표준으로 삼으세요.
“`sql
— 팀 표준 함수 템플릿 예시
CREATE OR REPLACE FUNCTION safe_divide(numerator NUMERIC, denominator NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
— NULL 입력 방어 및 0 나누기 방어
IF numerator IS NULL OR denominator IS NULL OR denominator = 0 THEN
RETURN NULL;
END IF;
RETURN numerator / denominator;
END;
$$ LANGUAGE plpgsql STRICT;
“`
- CI/CD 파이프라인에 NULL 경계값 테스트를 필수로 포함하라
운영 배포 전 모든 함수와 프로시저에 대해 NULL 경계값 테스트를 자동화된 테스트 스위트에 포함시키세요. pgTAP 같은 PostgreSQL 전용 테스트 프레임워크를 활용하면 NULL 입력, 빈 결과셋, 최솟값/최댓값 등의 경계 조건을 체계적으로 검증할 수 있습니다. 이를 통해 개발 단계에서 39004 에러를 조기에 발견하고, 운영 환경에서의 예기치 않은 장애를 예방할 수 있습니다.
“`sql
— pgTAP을 활용한 NULL 경계값 테스트 예시
SELECT plan(3);
SELECT is(calculate_bonus(NULL), NULL, ‘NULL 입력 시 NULL 반환 확인’);
SELECT is(calculate_bonus(0), 0::NUMERIC, ‘0 입력 시 0 반환 확인’);
SELECT is(calculate_bonus(50000), 5000::NUMERIC, ‘정상 입력 테스트’);
SELECT * FROM finish();
“`
관련 에러
- 22004 (
null_value_not_allowed): SQL 표준 레벨에서 NULL이 허용되지 않는 컨텍스트에 NULL이 사용될 때 발생하며, 39004와 유사하지만 발생 레이어가 다릅니다. - 23502 (
not_null_violation): 테이블의 NOT NULL 제약 조건을 가진 컬럼에 NULL을 INSERT 또는 UPDATE하려 할 때 발생하는 에러로, 39004보다 더 자주 접하는 에러입니다. - 25004 (
inappropriate_isolation_level_for_branch_transaction): 프로시저 트랜잭션 컨텍스트 관련 에러로, 복잡한 저장 프로시저 작성 시 함께 검토가 필요합니다. - 39001 (
external_routine_exception): PL/pgSQL 외부 루틴 실행 중 발생하는 상위 카테고리 에러로, 39004는 이 카테고리의 하위 에러 코드입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.