2026년 09월 03일 | DBMS Error 가이드
이 글에서 다루는 내용
39001 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
39001 invalid sqlstate returned 는?
PostgreSQL 에러 코드 39001 (invalid_sqlstate_returned) 는 PL/pgSQL 또는 외부 프로시저 언어(PL/Python, PL/Perl, PL/Java 등)로 작성된 함수나 프로시저가 유효하지 않은 SQLSTATE 코드를 반환할 때 발생합니다. 즉, 사용자 정의 함수 내에서 예외를 발생시킬 때 표준 SQLSTATE 형식(영문자/숫자 5자리)을 따르지 않거나, 존재하지 않는 코드를 사용했을 경우 PostgreSQL 엔진이 이 에러를 던집니다. 이는 주로 외부 언어 프로시저(PL/Python, PL/Perl 등)가 내부적으로 잘못된 상태 코드를 PostgreSQL에 전달하는 과정에서 빈번하게 발생합니다.
주요 발생 원인
- 외부 프로시저 언어에서 잘못된 SQLSTATE 코드 반환
PL/Python, PL/Perl, PL/Java 등의 외부 언어 확장에서 예외를 발생시킬 때, 해당 언어의 예외 처리 메커니즘이 PostgreSQL이 인식할 수 없는 SQLSTATE 코드를 반환하는 경우입니다. 예를 들어 PL/Python에서 plpy.error를 호출할 때 잘못된 SQLSTATE를 명시하면 PostgreSQL 내부에서 39001 에러가 트리거됩니다. 특히 외부 라이브러리나 서드파티 확장 모듈이 오래되어 PostgreSQL 버전과 호환되지 않을 때 이 현상이 잦습니다.
- 사용자 정의 예외 코드가 PostgreSQL 표준 형식(5자리 영숫자)을 위반
PostgreSQL의 SQLSTATE는 반드시 5자리 영문 대문자 또는 숫자 조합이어야 합니다. 만약 개발자가 PL/pgSQL 함수 내에서 RAISE EXCEPTION USING ERRCODE = '12' 처럼 5자리가 아닌 코드를 사용하거나, 소문자를 포함한 코드를 지정할 경우 PostgreSQL은 이를 유효하지 않은 SQLSTATE로 판단합니다. 코드 리뷰 없이 빠르게 작성된 레거시 함수들에서 이 실수가 자주 발견됩니다.
- PostgreSQL 버전 업그레이드 후 기존 확장(Extension) 또는 함수와의 비호환성
PostgreSQL 메이저 버전 업그레이드(예: 12 → 15) 이후, 기존에 작성된 외부 언어 함수나 확장 플러그인이 새 버전의 SQLSTATE 처리 방식과 충돌할 수 있습니다. 구버전에서는 허용되던 SQLSTATE 코드가 신버전에서 더 엄격하게 검증되면서 39001 에러가 처음 나타나는 경우도 있습니다. 이런 경우는 업그레이드 직후 회귀 테스트(regression test)를 수행하지 않았을 때 운영 환경에서 뒤늦게 발견되기도 합니다.
해결 방법
원인 1 해결: PL/Python 함수에서 올바른 SQLSTATE 코드 사용
PL/Python 함수에서 예외를 발생시킬 때 반드시 유효한 5자리 SQLSTATE를 사용해야 합니다.
잘못된 예시 (에러 발생):
CREATE OR REPLACE FUNCTION bad_plpython_func()
RETURNS void
LANGUAGE plpython3u
AS $$
# 잘못된 SQLSTATE 코드 사용 (4자리, 비표준)
raise plpy.Error("Something went wrong", sqlstate="9999X_INVALID")
$$;
올바른 예시 (수정 후):
CREATE OR REPLACE FUNCTION good_plpython_func()
RETURNS void
LANGUAGE plpython3u
AS $$
# 유효한 5자리 SQLSTATE 코드 사용 (P0001: raise_exception)
raise plpy.Error("Something went wrong", sqlstate="P0001")
$$;
-- 함수 테스트
SELECT good_plpython_func();
원인 2 해결: PL/pgSQL에서 ERRCODE 형식 교정
PL/pgSQL에서 RAISE 구문을 사용할 때 올바른 5자리 ERRCODE를 지정해야 합니다.
잘못된 예시:
CREATE OR REPLACE FUNCTION bad_raise_func()
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
-- 잘못된 SQLSTATE: 2자리만 사용 (비표준)
RAISE EXCEPTION 'Custom error occurred'
USING ERRCODE = '99';
END;
$$;
올바른 예시:
CREATE OR REPLACE FUNCTION correct_raise_func(p_value INT)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
IF p_value < 0 THEN
-- 올바른 5자리 SQLSTATE (사용자 정의 에러: P0001)
RAISE EXCEPTION '입력값은 0 이상이어야 합니다. 입력값: %', p_value
USING ERRCODE = 'P0001',
HINT = '양수 값을 입력하세요.',
DETAIL = '전달된 값: ' || p_value::TEXT;
END IF;
RAISE NOTICE '정상 처리: %', p_value;
END;
$$;
-- 테스트
SELECT correct_raise_func(-5);
SELECT correct_raise_func(10);
원인 3 해결: 업그레이드 후 함수 재컴파일 및 검증
버전 업그레이드 이후에는 기존 함수들을 점검하고 필요시 재생성해야 합니다.
-- 현재 DB에서 외부 언어(plpython3u, plperlu 등)를 사용하는 함수 목록 조회
SELECT
n.nspname AS schema_name,
p.proname AS function_name,
l.lanname AS language_name,
pg_get_function_identity_arguments(p.oid) AS arguments
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
JOIN pg_language l ON l.oid = p.prolang
WHERE l.lanname NOT IN ('sql', 'plpgsql', 'internal', 'c')
ORDER BY n.nspname, p.proname;
-- 특정 함수의 소스코드 확인
SELECT
p.proname,
p.prosrc
FROM pg_proc p
JOIN pg_language l ON l.oid = p.prolang
WHERE p.proname = 'your_function_name'
AND l.lanname = 'plpython3u';
-- 함수 재생성 예시 (함수 정의를 덮어씀)
CREATE OR REPLACE FUNCTION your_function_name()
RETURNS void
LANGUAGE plpython3u
AS $$
# 수정된 코드
plpy.notice("Function executed successfully")
$$;
-- PostgreSQL 로그에서 39001 에러 발생 함수 추적
-- pg_stat_activity와 로그를 조합하여 확인
SELECT
pid,
usename,
application_name,
query,
state,
query_start
FROM pg_stat_activity
WHERE state != 'idle'
AND query ILIKE '%your_function%'
ORDER BY query_start;
예방 방법
- 함수 작성 시 SQLSTATE 코드 유효성 검증 절차 수립
모든 사용자 정의 함수(UDF)를 개발할 때, 코드 리뷰 체크리스트에 SQLSTATE 코드 형식 검증을 필수 항목으로 포함시켜야 합니다. 특히 외부 언어(PL/Python, PL/Perl)를 사용하는 함수는 CI/CD 파이프라인에서 자동 회귀 테스트를 실행하여 배포 전에 유효하지 않은 SQLSTATE가 반환되는지 검사하는 것이 좋습니다. PostgreSQL 공식 문서의 [Appendix A: Error Codes](https://www.postgresql.org/docs/current/errcodes-appendix.html)를 팀 내 개발 가이드에 링크로 첨부하고 공유하세요.
“`sql
— 유효한 PostgreSQL SQLSTATE 코드 범위 확인용 쿼리 예시
— (사용자 정의 에러는 P로 시작하는 코드 권장)
SELECT sqlstate, message
FROM (
VALUES
(‘P0001’, ‘사용자 정의 예외 – 일반’),
(‘P0002’, ‘데이터 없음 예외’),
(‘P0003’, ‘너무 많은 행 반환’),
(‘P0004’, ‘어설션 실패’)
) AS valid_states(sqlstate, message);
“`
- PostgreSQL 메이저 버전 업그레이드 전 전수 함수 테스트 자동화
메이저 버전 업그레이드 전에는 반드시 스테이징 환경에서 pg_upgrade 또는 논리적 복제(Logical Replication)를 활용한 업그레이드 시뮬레이션을 수행하고, 모든 사용자 정의 함수와 외부 언어 프로시저를 자동 테스트 스크립트로 검증해야 합니다. pgTAP 같은 PostgreSQL 테스트 프레임워크를 활용하면 함수 단위 테스트를 체계적으로 관리할 수 있습니다.
“`sql
— pgTAP을 활용한 함수 테스트 예시
— (pgTAP 확장이 설치된 경우)
SELECT plan(2);
SELECT lives_ok(
$$ SELECT correct_raise_func(10) $$,
‘양수 입력 시 정상 동작’
);
SELECT throws_ok(
$$ SELECT correct_raise_func(-1) $$,
‘P0001’,
‘입력값은 0 이상이어야 합니다. 입력값: -1’,
‘음수 입력 시 P0001 예외 발생 확인’
);
SELECT finish();
“`
관련 에러
- 39P01 (trigger_protocol_violated): 트리거 함수가 PostgreSQL 트리거 프로토콜을 위반했을 때 발생하며, 39001과 같은 외부 인터페이스 위반 계열에 속합니다.
- 39P02 (srf_protocol_violated): Set-Returning Function(SRF)이 프로토콜을 위반했을 때 발생합니다. 외부 언어로 작성된 집합 반환 함수에서 자주 나타납니다.
- 39P03 (event_trigger_protocol_violated): 이벤트 트리거 함수가 올바르지 않은 방식으로 구현된 경우 발생합니다.
- 42601 (syntax_error): ERRCODE에 잘못된 형식의 문자열을 전달했을 때 파싱 단계에서 먼저 이 에러가 나타날 수 있습니다.
- XX000 (internal_error): 외부 언어 런타임과 PostgreSQL 코어 간 심각한 인터페이스 불일치가 있을 때 39001 대신 이 에러가 나타나기도 합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.