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

38000
2026년 09월 02일 | DBMS Error 가이드

이 글에서 다루는 내용

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

38000 external routine exception 는?

PostgreSQL 에러 코드 38000은 external routine exception으로, 외부 루틴(External Routine), 즉 데이터베이스 외부에서 작성된 프로시저나 함수가 실행되는 도중 예외가 발생했을 때 나타나는 에러입니다. 주로 PL/Python, PL/Perl, PL/Tcl, PL/Java, PL/R과 같은 외부 언어(procedural language)로 작성된 함수 내부에서 처리되지 않은 예외나 런타임 오류가 발생할 때 트리거됩니다. SQL 표준의 외부 루틴 예외 클래스(Class 38)에 속하며, 세부적인 하위 에러 코드(38001, 38002, 38003, 38004)로 분류되기도 하지만 원인을 특정하기 어려운 경우 38000으로 묶여 보고됩니다.


주요 발생 원인

  • 외부 언어 함수(PL/Python, PL/Perl 등) 내 처리되지 않은 예외

PostgreSQL에서 PL/Python이나 PL/Perl로 작성된 함수가 실행 중 내부적으로 예외를 발생시키고, 이를 함수 내에서 적절하게 catch하거나 처리하지 않으면 38000 에러가 발생합니다. 예를 들어, PL/Python 함수 안에서 None을 반환해야 하는 상황에 정수형 연산 오류가 발생하거나, 파일 I/O 접근 실패, 외부 라이브러리 오류 등이 catch되지 않고 상위로 전파될 때 이 에러가 PostgreSQL 레벨로 올라오게 됩니다. 실무에서는 PL/Python 함수 내에서 외부 API를 호출하거나 복잡한 연산을 수행하는 경우 이런 상황이 빈번하게 발생합니다.

  • 외부 공유 라이브러리(C 언어 확장) 내부의 런타임 오류

PostgreSQL의 C 언어 확장 함수(Shared Library)에서 잘못된 메모리 접근, NULL 포인터 역참조, 혹은 시그널 처리 오류가 발생할 때도 38000 에러가 발생할 수 있습니다. C로 작성된 사용자 정의 함수(UDF)는 PostgreSQL의 일반적인 예외 처리 메커니즘 밖에 존재하기 때문에, 내부에서 에러가 발생하면 PostgreSQL이 이를 external routine exception으로 분류합니다. 특히 서드파티 확장 모듈(예: PostGIS, TimescaleDB 등)을 사용하거나, 직접 작성한 C 확장 함수를 배포한 환경에서 자주 발생할 수 있습니다.

  • PL/pgSQL 함수에서 외부 함수 호출 시 발생하는 연쇄 예외

PL/pgSQL 함수 내에서 외부 루틴(PL/Python, PL/Perl, C 확장 등)을 호출했을 때, 외부 루틴에서 발생한 에러가 PL/pgSQL 레이어로 올라오면서 38000으로 래핑되는 경우가 있습니다. 이 경우 에러 메시지만으로는 정확한 원인 파악이 어렵기 때문에 로그 레벨을 높이거나 외부 함수 자체에서 로깅을 추가하는 것이 중요합니다. 연쇄 예외(chained exception)의 특성상 실제 원인이 되는 에러가 스택 트레이스 하단에 숨어 있을 수 있으므로 주의가 필요합니다.


해결 방법

원인 1: PL/Python 함수 내 예외 처리 강화

PL/Python 함수 내부에서 발생할 수 있는 모든 예외를 명시적으로 처리하고, 에러 발생 시 PostgreSQL이 이해할 수 있는 방식으로 에러를 반환하거나 로깅하는 구조를 추가합니다.

-- 문제가 발생하는 PL/Python 함수 예시
CREATE OR REPLACE FUNCTION calculate_ratio(numerator FLOAT, denominator FLOAT)
RETURNS FLOAT
LANGUAGE plpython3u
AS $$
    # denominator가 0이면 ZeroDivisionError 발생 → 38000 에러로 전파됨
    return numerator / denominator
$$;

-- 개선된 버전: 예외 처리 추가
CREATE OR REPLACE FUNCTION calculate_ratio_safe(numerator FLOAT, denominator FLOAT)
RETURNS FLOAT
LANGUAGE plpython3u
AS $$
    try:
        if denominator == 0:
            plpy.warning("denominator is zero, returning NULL")
            return None
        return numerator / denominator
    except ZeroDivisionError as e:
        plpy.error(f"ZeroDivisionError in calculate_ratio_safe: {str(e)}")
    except Exception as e:
        plpy.error(f"Unexpected error in calculate_ratio_safe: {str(e)}")
$$;

-- 호출 테스트
SELECT calculate_ratio_safe(10.0, 0.0);
-- WARNING:  denominator is zero, returning NULL
-- 결과: NULL 반환 (에러 없이 처리됨)

SELECT calculate_ratio_safe(10.0, 2.0);
-- 결과: 5.0

원인 2: PL/Perl 함수 내 예외 처리

PL/Perl로 작성된 함수에서도 동일한 패턴으로 예외를 처리해야 합니다.

-- PL/Perl 함수에서 예외 처리
CREATE OR REPLACE FUNCTION parse_json_perl(json_text TEXT)
RETURNS TEXT
LANGUAGE plperl
AS $$
    use strict;
    use warnings;
    eval {
        # JSON 파싱 로직 (예시)
        my $input = $_[0];
        if (!defined $input || $input eq '') {
            elog(WARNING, "Input is empty or undefined");
            return undef;
        }
        # 간단한 처리 예시
        $input =~ s/^\s+|\s+$//g;  # trim
        return $input;
    };
    if ($@) {
        elog(ERROR, "Error in parse_json_perl: $@");
    }
$$;

-- 테스트 실행
SELECT parse_json_perl('  hello world  ');
-- 결과: 'hello world'

SELECT parse_json_perl(NULL);
-- 결과: NULL (WARNING 로그 출력)

원인 3: PL/pgSQL에서 외부 함수 호출 시 예외 캐치

PL/pgSQL 레이어에서 외부 함수를 호출할 때 EXCEPTION 블록을 활용하여 38000 에러를 명시적으로 처리합니다.

-- 외부 함수를 안전하게 호출하는 래퍼 함수
CREATE OR REPLACE FUNCTION safe_external_call(input_value FLOAT)
RETURNS FLOAT
LANGUAGE plpgsql
AS $$
DECLARE
    result FLOAT;
BEGIN
    -- 외부 루틴 호출 시도
    BEGIN
        result := calculate_ratio(input_value, 0.0);  -- 의도적으로 에러 유발
    EXCEPTION
        WHEN external_routine_exception THEN
            -- 38000 에러 클래스 캐치
            RAISE WARNING 'External routine exception caught: %', SQLERRM;
            result := NULL;
        WHEN OTHERS THEN
            RAISE WARNING 'Unexpected error: % (SQLSTATE: %)', SQLERRM, SQLSTATE;
            result := NULL;
    END;
    RETURN result;
END;
$$;

-- 에러 상세 정보 확인을 위한 확장 쿼리
DO $$
DECLARE
    v_result FLOAT;
    v_sqlstate TEXT;
    v_message TEXT;
    v_detail TEXT;
    v_hint TEXT;
    v_context TEXT;
BEGIN
    BEGIN
        v_result := calculate_ratio(10.0, 0.0);
    EXCEPTION
        WHEN external_routine_exception THEN
            GET STACKED DIAGNOSTICS
                v_sqlstate = RETURNED_SQLSTATE,
                v_message  = MESSAGE_TEXT,
                v_detail   = PG_EXCEPTION_DETAIL,
                v_hint     = PG_EXCEPTION_HINT,
                v_context  = PG_EXCEPTION_CONTEXT;

            RAISE NOTICE 'SQLSTATE: %', v_sqlstate;
            RAISE NOTICE 'Message: %', v_message;
            RAISE NOTICE 'Detail: %', v_detail;
            RAISE NOTICE 'Hint: %', v_hint;
            RAISE NOTICE 'Context: %', v_context;
    END;
END;
$$;

-- 38000 에러를 발생시키는 상황 재현 및 로그 확인
-- postgresql.conf 설정 권장:
-- log_min_error_statement = error
-- log_error_verbosity = verbose
-- log_min_messages = warning

에러 발생 원인 진단 쿼리

-- 현재 DB에 등록된 외부 언어 함수 목록 조회
SELECT
    n.nspname AS schema_name,
    p.proname AS function_name,
    l.lanname AS language,
    p.prosrc AS function_body
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
JOIN pg_language l ON p.prolang = l.oid
WHERE l.lanname IN ('plpython3u', 'plpythonu', 'plperlu', 'plperl', 'plr', 'pltcl')
ORDER BY l.lanname, n.nspname, p.proname;

-- 최근 에러 로그에서 38000 관련 항목 확인 (pg_log 접근 가능한 경우)
-- 또는 pg_stat_activity를 통한 현재 실행 중인 쿼리 모니터링
SELECT
    pid,
    usename,
    application_name,
    state,
    query,
    now() - pg_stat_activity.query_start AS duration
FROM pg_stat_activity
WHERE state != 'idle'
  AND query_start < now() - interval '30 seconds'
ORDER BY duration DESC;

예방 방법

  • 외부 언어 함수 내 철저한 예외 처리 및 로깅 표준화

모든 외부 언어(PL/Python, PL/Perl 등)로 작성된 함수에는 반드시 전역 예외 처리 블록을 포함시키고, 에러 발생 시 plpy.error() 또는 elog(ERROR, ...) 등을 통해 PostgreSQL이 추적 가능한 에러 메시지를 남기는 것이 중요합니다. 또한 함수 배포 전에 단위 테스트를 통해 다양한 엣지 케이스(NULL 입력, 빈 문자열, 경계값 등)에 대한 동작을 사전에 검증하는 프로세스를 팀 내 표준으로 정착시키는 것이 좋습니다.

“`sql

— 함수 배포 전 테스트 예시

DO $$

BEGIN

— NULL 입력 테스트

PERFORM calculate_ratio_safe(NULL, 5.0);

PERFORM calculate_ratio_safe(5.0, NULL);

— 경계값 테스트

PERFORM calculate_ratio_safe(0.0, 0.0);

PERFORM calculate_ratio_safe(1e308, 1e-308);

RAISE NOTICE ‘All edge case tests passed’;

EXCEPTION

WHEN OTHERS THEN

RAISE EXCEPTION ‘Test failed: % (SQLSTATE: %)’, SQLERRM, SQLSTATE;

END;

$$;

“`

  • PostgreSQL 로그 레벨 설정 최적화 및 모니터링 체계 구축

postgresql.conf에서 log_min_error_statement = error, log_error_verbosity = verbose로 설정하여 38000 에러 발생 시 관련 SQL 문과 상세 컨텍스트가 로그에 기록되도록 설정해야 합니다. 또한 Prometheus + pg_stat_statements, pgBadger와 같은 모니터링 도구를 활용하여 외부 함수 호출 빈도와 에러율을 지속적으로 추적하면, 문제가 심각해지기 전에 사전 감지하고 대응할 수 있습니다.


관련 에러

  • 38001 containing_sql_not_permitted: 외부 루틴 내에서 SQL을 포함하는 것이 허용되지 않을 때 발생합니다.
  • 38002 modifying_sql_data_not_permitted: 외부 루틴 내에서 SQL 데이터 변경이 허용되지 않을 때 발생합니다.
  • 38003 prohibited_sql_statement_attempted: 외부 루틴 내에서 금지된 SQL 문을 시도할 때 발생합니다.
  • 38004 reading_sql_data_not_permitted: 외부 루틴 내에서 SQL 데이터 읽기가 허용되지 않을 때 발생합니다.
  • 39000 external_routine_invocation_exception: 외부 루틴 호출 자체에 관련된 예외로, 38000과 유사하지만 호출 시점의 문제에 초점을 맞춥니다.
  • XX000 internal_error: C 확장이나 외부 함수에서 심각한 내부 오류가 발생할 경우 함께 나타날 수 있는 에러입니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기