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

39001
2026년 06월 30일 | DBMS Error 가이드

이 글에서 다루는 내용

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

39001 invalid sqlstate returned 는?

PostgreSQL 에러 코드 39001 invalid sqlstate returned는 PL/pgSQL 또는 외부 프로시저 언어(PL/Python, PL/Perl 등)에서 함수나 프로시저가 올바르지 않은 SQLSTATE 코드를 반환했을 때 발생하는 에러입니다. PostgreSQL은 표준 SQL 상태 코드(SQLSTATE)를 5자리 영숫자 문자열로 엄격하게 관리하며, 이 형식을 벗어나거나 정의되지 않은 코드를 반환하려 할 때 이 에러가 트리거됩니다. 주로 커스텀 예외 처리 로직을 작성할 때 개발자가 잘못된 상태 코드를 직접 지정하거나, 외부 언어 핸들러가 비표준 코드를 PostgreSQL 엔진으로 전달하는 경우에 나타납니다.

주요 발생 원인

  • PL/pgSQL에서 잘못된 SQLSTATE 코드를 직접 RAISE할 때

가장 흔한 원인입니다. 개발자가 RAISE EXCEPTION 구문에서 SQLSTATE 옵션을 사용할 때, 5자리 영숫자 형식이 아닌 값을 입력하거나 PostgreSQL이 허용하지 않는 예약 코드를 사용할 경우 이 에러가 발생합니다. 예를 들어 '390'처럼 3자리이거나, 'ZZZZZ'처럼 허용되지 않는 범위의 코드를 지정하면 엔진이 즉시 거부합니다. 반드시 5자리 영숫자 문자열이어야 하며, 첫 두 자리는 클래스 코드를 나타냅니다.

  • 외부 프로시저 언어(PL/Python, PL/Perl 등) 핸들러의 버그

PL/Python이나 PL/Perl과 같은 외부 언어로 작성된 함수에서 예외를 발생시킬 때, 해당 언어의 런타임이 PostgreSQL로 전달하는 SQLSTATE 값이 올바르지 않게 구성되는 경우가 있습니다. 이는 언어 확장 라이브러리의 버전 불일치, 잘못 패치된 언어 핸들러, 또는 직접 작성한 C 확장에서 ereport()elog() 호출 시 상태 코드를 잘못 설정했을 때 발생할 수 있습니다. 이 경우는 애플리케이션 코드 수준뿐 아니라 서버 확장 수준에서도 점검이 필요합니다.

  • 트리거 또는 이벤트 트리거 내부의 잘못된 예외 전파

트리거 함수 내에서 예외를 다시 발생시키거나(re-raise) 커스텀 SQLSTATE를 반환할 때, 예외 블록이 중첩된 구조에서 상태 코드가 잘못 전달되는 경우입니다. 특히 복잡한 트리거 체인에서 내부 함수가 반환한 SQLSTATE를 외부 트리거가 그대로 재사용하거나 변형하는 과정에서 오염이 발생할 수 있습니다. 이 문제는 디버깅이 어려운 편이며, 트리거 함수를 단독으로 테스트하는 절차가 필요합니다.

해결 방법

원인 1 해결: 올바른 SQLSTATE 코드 형식 사용

잘못된 예시와 올바른 예시를 비교합니다. SQLSTATE는 반드시 5자리 영숫자여야 하며, 사용자 정의 코드는 클래스 'P0'(PL/pgSQL 에러) 또는 'XX'(내부 에러) 범위를 활용하거나 'U'로 시작하는 사용자 정의 클래스를 사용하는 것이 권장됩니다.

-- ❌ 잘못된 예: 3자리 SQLSTATE 코드 사용 -> 39001 에러 유발
CREATE OR REPLACE FUNCTION bad_raise_example()
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
  RAISE EXCEPTION 'Something went wrong'
    USING SQLSTATE = '390'; -- 3자리라서 오류 발생
END;
$$;

-- ✅ 올바른 예: 5자리 SQLSTATE 코드 사용
CREATE OR REPLACE FUNCTION good_raise_example()
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
  RAISE EXCEPTION 'Something went wrong'
    USING SQLSTATE = 'P0001'; -- 사용자 정의 예외 (5자리)
END;
$$;

-- ✅ 사용자 정의 클래스(U로 시작) 사용 예
CREATE OR REPLACE FUNCTION custom_error_example(p_value INT)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
  IF p_value < 0 THEN
    RAISE EXCEPTION '음수 값은 허용되지 않습니다: %', p_value
      USING SQLSTATE = 'U0001',
            ERRCODE  = 'U0001',
            HINT     = '양수 값을 입력해 주세요.';
  END IF;
END;
$$;

원인 2 해결: 외부 언어 함수의 SQLSTATE 검증

PL/Python 함수에서 예외를 발생시킬 때 올바른 형식을 유지하는 방법입니다.

-- ✅ PL/Python에서 올바른 SQLSTATE 전달 예시
CREATE OR REPLACE FUNCTION python_error_example(val integer)
RETURNS integer
LANGUAGE plpython3u
AS $$
import re

VALID_SQLSTATE = re.compile(r'^[A-Z0-9]{5}$')

if val < 0:
    # 올바른 5자리 SQLSTATE 사용
    plpy.error("Negative value not allowed",
               sqlstate="P0001")
return val * 2
$$;

-- 함수 테스트
SELECT python_error_example(5);   -- 정상: 10 반환
SELECT python_error_example(-1);  -- P0001 에러 발생

원인 3 해결: 트리거 함수에서 예외 재발생(re-raise) 시 검증

-- ✅ 트리거 내 예외 처리 모범 사례
CREATE OR REPLACE FUNCTION trg_validate_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
  v_sqlstate TEXT;
  v_message  TEXT;
BEGIN
  -- 비즈니스 로직 수행
  IF NEW.amount <= 0 THEN
    RAISE EXCEPTION '주문 금액은 0보다 커야 합니다.'
      USING SQLSTATE = 'P0002',
            DETAIL   = format('입력된 금액: %s', NEW.amount),
            HINT     = '양수 금액을 입력하세요.';
  END IF;

  RETURN NEW;

EXCEPTION
  WHEN OTHERS THEN
    -- SQLSTATE를 그대로 재사용하지 말고, 검증 후 재발생
    GET STACKED DIAGNOSTICS
      v_sqlstate = RETURNED_SQLSTATE,
      v_message  = MESSAGE_TEXT;

    -- 로그 테이블에 기록
    INSERT INTO error_log(sqlstate, message, occurred_at)
    VALUES (v_sqlstate, v_message, now());

    -- 원본 에러를 그대로 다시 발생 (안전한 방식)
    RAISE;
END;
$$;

-- 트리거 적용
CREATE TRIGGER trg_order_validate
  BEFORE INSERT OR UPDATE ON orders
  FOR EACH ROW EXECUTE FUNCTION trg_validate_order();

SQLSTATE 유효성 사전 검증 유틸리티 함수

-- 재사용 가능한 SQLSTATE 유효성 검사 함수
CREATE OR REPLACE FUNCTION validate_sqlstate(p_code TEXT)
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
BEGIN
  -- 5자리 영숫자인지 확인
  IF p_code !~ '^[A-Z0-9]{5}$' THEN
    RAISE WARNING 'Invalid SQLSTATE format: %. Must be 5 alphanumeric characters.', p_code;
    RETURN FALSE;
  END IF;
  RETURN TRUE;
END;
$$;

-- 사용 예
SELECT validate_sqlstate('P0001'); -- true
SELECT validate_sqlstate('390');   -- false + WARNING
SELECT validate_sqlstate('ZZZZZ'); -- true (형식은 맞지만 사용 권장 안 함)

예방 방법

  • 커스텀 SQLSTATE 코드 중앙 관리 테이블 운영

프로젝트에서 사용하는 모든 커스텀 SQLSTATE 코드를 별도의 레퍼런스 테이블로 관리하고, 개발자가 임의로 코드를 만들어 사용하지 않도록 코드 리뷰 프로세스에 포함시키십시오. 아래와 같이 중앙 관리 테이블을 만들어 팀 전체가 공유하면 중복 및 오류를 방지할 수 있습니다.

“`sql

— 커스텀 SQLSTATE 코드 관리 테이블

CREATE TABLE IF NOT EXISTS app_error_codes (

sqlstate CHAR(5) NOT NULL PRIMARY KEY

CHECK (sqlstate ~ ‘^[A-Z0-9]{5}$’),

error_name VARCHAR(100) NOT NULL,

description TEXT,

created_at TIMESTAMPTZ DEFAULT now()

);

— 사용 예시 등록

INSERT INTO app_error_codes (sqlstate, error_name, description) VALUES

(‘U0001’, ‘INVALID_AMOUNT’, ‘금액이 0 이하인 경우’),

(‘U0002’, ‘DUPLICATE_ORDER’, ‘중복 주문 시도’),

(‘U0003’, ‘USER_NOT_FOUND’, ‘사용자를 찾을 수 없음’);

“`

  • CI/CD 파이프라인에 PL/pgSQL 정적 분석 및 테스트 통합

pgTAP이나 pg_prove와 같은 PostgreSQL 전용 테스트 프레임워크를 CI/CD 파이프라인에 통합하여, 배포 전에 모든 함수와 프로시저의 예외 처리 로직을 자동으로 검증하십시오. 특히 RAISE 구문이 포함된 함수는 정상 경로와 예외 경로 모두를 테스트하도록 테스트 케이스를 작성해야 합니다.

“`sql

— pgTAP을 이용한 SQLSTATE 테스트 예시

SELECT plan(2);

— 정상 케이스 테스트

SELECT lives_ok(

$$ SELECT good_raise_example() $$,

‘정상 함수 호출은 에러 없이 통과해야 함’

);

— 예외 케이스 테스트: 올바른 SQLSTATE 반환 여부 확인

SELECT throws_ok(

$$ SELECT custom_error_example(-1) $$,

‘U0001’,

‘음수 값은 허용되지 않습니다: -1’,

‘음수 입력 시 U0001 SQLSTATE가 반환되어야 함’

);

SELECT * FROM finish();

“`

관련 에러

  • P0001 (raise_exception): PL/pgSQL에서 RAISE EXCEPTION을 통해 발생하는 일반적인 사용자 정의 예외입니다. 39001과 달리 유효한 SQLSTATE를 가진 정상적인 예외 처리 흐름입니다.
  • P0000 (plpgsql_error): PL/pgSQL 엔진 자체의 일반 오류로, 내부 처리 중 예상치 못한 문제가 발생할 때 나타납니다.
  • XX000 (internal_error): PostgreSQL 내부 에러로, C 확장이나 언어 핸들러에서 예기치 않은 상태가 전달될 때 39001과 함께 나타날 수 있습니다.
  • 38001 (containing_sql_not_permitted): 외부 루틴에서 SQL 포함이 허용되지 않을 때 발생하며, 외부 언어 함수 관련 에러라는 점에서 39001과 유사한 맥락에서 다뤄집니다.
  • 39P01 (trigger_protocol_violated): 트리거 함수가 프로토콜을 위반했을 때 발생하는 에러로, 트리거 내 잘못된 예외 처리와 함께 39001이 연쇄적으로 나타날 수 있습니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기