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

39000
2026년 09월 03일 | DBMS Error 가이드

이 글에서 다루는 내용

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

39000 external routine invocation exception 는?

PostgreSQL 에러 코드 39000 (external routine invocation exception) 은 외부 루틴(External Routine), 즉 PL/Python, PL/Perl, PL/Java, PL/R 등과 같은 외부 언어로 작성된 함수나 프로시저를 호출하는 과정에서 예외가 발생했을 때 나타나는 에러입니다. 이 에러는 외부 런타임 환경과 PostgreSQL 사이의 인터페이스 계층에서 처리되지 않은 예외가 발생할 때 트리거됩니다. 주로 PL/Python이나 PL/Perl 함수 내부에서 잘못된 로직, 잘못된 데이터 타입 처리, 또는 외부 라이브러리 호출 실패 등으로 인해 발생하며, 데이터베이스 운영 중 매우 까다롭게 디버깅해야 하는 에러 중 하나입니다.


주요 발생 원인

1. PL/Python 또는 PL/Perl 함수 내부에서 처리되지 않은 예외 발생

외부 언어로 작성된 함수 내부에서 Python의 ZeroDivisionError, KeyError, TypeError 등 처리되지 않은 예외가 발생하면 PostgreSQL은 이를 external routine invocation exception으로 감싸서 반환합니다. 이는 외부 언어 런타임의 예외가 PostgreSQL 에러 체계로 변환되는 과정에서 발생하며, 함수 작성자가 예외 처리를 명시적으로 구현하지 않았을 때 특히 자주 나타납니다.

2. 잘못된 데이터 타입 또는 NULL 값 처리 오류

외부 루틴에 전달된 인자가 예상한 타입과 다르거나, NULL 값을 제대로 처리하지 못하는 경우에도 이 에러가 발생합니다. 예를 들어 PL/Python 함수에서 NULL 값에 대해 문자열 메서드를 직접 호출하거나, 정수형 인자로 기대했지만 실제로는 문자열이 전달되는 경우가 대표적입니다. 이러한 문제는 함수의 입력값 유효성 검사 로직이 부재할 때 실제 운영 환경에서 간헐적으로 나타나 디버깅을 어렵게 만듭니다.

3. 외부 라이브러리 또는 모듈 임포트 실패

PL/Python 함수가 numpy, pandas, requests 등 외부 Python 패키지를 임포트하는 경우, 해당 패키지가 PostgreSQL 서버 프로세스의 Python 환경에 설치되어 있지 않거나 버전이 맞지 않으면 ImportError가 발생하여 이 에러로 이어집니다. PostgreSQL 서버가 사용하는 Python 인터프리터와 개발자가 테스트한 Python 환경이 다를 때 특히 자주 발생하며, 운영 서버와 개발 서버 간의 환경 불일치가 원인이 되는 경우가 많습니다.


해결 방법

원인 1 해결: 함수 내부에 명시적 예외 처리 추가

PL/Python 함수에 try-except 블록을 추가하여 예외를 명시적으로 처리하고, 의미 있는 에러 메시지를 반환하거나 PostgreSQL 에러로 변환합니다.

-- 문제가 있는 PL/Python 함수 예시
CREATE OR REPLACE FUNCTION divide_values(a NUMERIC, b NUMERIC)
RETURNS NUMERIC
LANGUAGE plpython3u
AS $$
    return a / b  -- b가 0이면 ZeroDivisionError 발생 -> 39000 에러
$$;

-- 해결: 예외 처리 추가
CREATE OR REPLACE FUNCTION divide_values_safe(a NUMERIC, b NUMERIC)
RETURNS NUMERIC
LANGUAGE plpython3u
AS $$
    try:
        if b == 0:
            plpy.error("Division by zero is not allowed. b must be non-zero.")
        return a / b
    except ZeroDivisionError as e:
        plpy.error(f"ZeroDivisionError caught: {str(e)}")
    except Exception as e:
        plpy.error(f"Unexpected error in divide_values_safe: {str(e)}")
$$;

-- 테스트
SELECT divide_values_safe(10, 2);  -- 정상: 5
SELECT divide_values_safe(10, 0);  -- 명확한 에러 메시지 반환

원인 2 해결: NULL 및 데이터 타입 유효성 검사 추가

-- NULL 처리가 누락된 함수 예시
CREATE OR REPLACE FUNCTION process_text(input_val TEXT)
RETURNS TEXT
LANGUAGE plpython3u
AS $$
    return input_val.upper()  -- NULL이면 AttributeError 발생 -> 39000 에러
$$;

-- 해결: NULL 체크 및 타입 검증 추가
CREATE OR REPLACE FUNCTION process_text_safe(input_val TEXT)
RETURNS TEXT
LANGUAGE plpython3u
AS $$
    # NULL 체크
    if input_val is None:
        return None  # 또는 plpy.error("input_val cannot be NULL")

    # 타입 검증
    if not isinstance(input_val, str):
        plpy.error(f"Expected TEXT, got {type(input_val).__name__}")

    try:
        return input_val.strip().upper()
    except Exception as e:
        plpy.error(f"Error processing text: {str(e)}")
$$;

-- 테스트
SELECT process_text_safe('hello world');  -- 'HELLO WORLD'
SELECT process_text_safe(NULL);           -- NULL 반환 (안전하게 처리)

-- PL/pgSQL 래퍼로 추가 보호
CREATE OR REPLACE FUNCTION safe_wrapper(input_val TEXT)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
BEGIN
    IF input_val IS NULL THEN
        RETURN NULL;
    END IF;
    RETURN process_text_safe(input_val);
EXCEPTION
    WHEN OTHERS THEN
        RAISE WARNING 'safe_wrapper caught error: %', SQLERRM;
        RETURN NULL;
END;
$$;

원인 3 해결: 외부 라이브러리 임포트 오류 처리 및 환경 확인

-- 라이브러리 임포트 실패 예시
CREATE OR REPLACE FUNCTION calculate_with_numpy(arr_val FLOAT[])
RETURNS FLOAT
LANGUAGE plpython3u
AS $$
    import numpy as np  -- numpy 미설치 시 ImportError -> 39000 에러
    arr = np.array(arr_val)
    return float(np.mean(arr))
$$;

-- 해결: 임포트 실패 처리 및 대체 로직 구현
CREATE OR REPLACE FUNCTION calculate_mean_safe(arr_val FLOAT[])
RETURNS FLOAT
LANGUAGE plpython3u
AS $$
    try:
        import numpy as np
        arr = np.array(arr_val)
        return float(np.mean(arr))
    except ImportError:
        # numpy 없을 때 순수 Python으로 대체
        plpy.warning("numpy not available, using pure Python fallback")
        if not arr_val:
            return None
        return sum(arr_val) / len(arr_val)
    except Exception as e:
        plpy.error(f"Error in calculate_mean_safe: {str(e)}")
$$;

-- 현재 PostgreSQL이 사용하는 Python 환경 확인
CREATE OR REPLACE FUNCTION check_python_env()
RETURNS TABLE(package_name TEXT, version TEXT)
LANGUAGE plpython3u
AS $$
    import sys
    import pkg_resources
    results = []
    try:
        for pkg in pkg_resources.working_set:
            results.append((pkg.project_name, pkg.version))
    except Exception as e:
        results.append(('ERROR', str(e)))
    return results
$$;

SELECT * FROM check_python_env() ORDER BY package_name;

-- 실제 에러 발생 시 상세 로그 확인 쿼리
SELECT 
    pid,
    query_start,
    state,
    query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;

예방 방법

1. 외부 함수에 반드시 표준 예외 처리 템플릿 적용

모든 PL/Python, PL/Perl 함수를 작성할 때 표준 예외 처리 템플릿을 팀 내 코딩 컨벤션으로 강제화하고, 코드 리뷰 과정에서 예외 처리 누락 여부를 반드시 확인해야 합니다. 특히 NULL 체크, 타입 검증, 외부 라이브러리 임포트 방어 코드를 항상 포함하는 함수 작성 가이드라인을 문서화하여 팀 전체가 공유해야 합니다.

-- 팀 표준 PL/Python 함수 템플릿
CREATE OR REPLACE FUNCTION template_function(param1 TEXT, param2 INTEGER)
RETURNS TEXT
LANGUAGE plpython3u
AS $$
    # 1. NULL 체크
    if param1 is None or param2 is None:
        plpy.error("Parameters cannot be NULL")

    # 2. 타입 검증
    if not isinstance(param2, int):
        plpy.error(f"param2 must be INTEGER, got {type(param2).__name__}")

    # 3. 핵심 로직
    try:
        result = param1 * param2
        return str(result)
    except Exception as e:
        plpy.error(f"[template_function] Unexpected error: {str(e)}")
$$;

2. CI/CD 파이프라인에 외부 함수 단위 테스트 통합

외부 루틴 함수에 대한 단위 테스트를 pgTAP 또는 별도의 테스트 프레임워크를 사용해 작성하고, CI/CD 파이프라인에 통합하여 배포 전에 자동으로 검증되도록 합니다. 특히 NULL 입력, 경계값, 잘못된 타입 등 엣지 케이스에 대한 테스트를 반드시 포함시켜 운영 환경에서의 39000 에러 발생을 사전에 차단해야 합니다.

-- pgTAP을 이용한 테스트 예시
BEGIN;
SELECT plan(4);

SELECT ok(divide_values_safe(10, 2) = 5, 'Normal division works');
SELECT ok(process_text_safe('hello') = 'HELLO', 'Text processing works');
SELECT ok(process_text_safe(NULL) IS NULL, 'NULL input returns NULL safely');
SELECT throws_ok(
    'SELECT divide_values_safe(10, 0)',
    'P0001',
    'Division by zero is not allowed. b must be non-zero.',
    'Zero division throws expected error'
);

SELECT * FROM finish();
ROLLBACK;

관련 에러

  • 39001 (sql_routine_exception): SQL 루틴 내부에서 허용되지 않는 SQL 구문을 실행하려 할 때 발생하는 에러로, 외부 루틴이 아닌 SQL 언어 함수에서 유사한 상황에 나타납니다.
  • 39004 (null_value_not_allowed): 외부 루틴에서 NULL을 허용하지 않는 파라미터에 NULL이 전달될 때 발생하며, 39000의 하위 분류 에러입니다.
  • 2F005 (function_executed_no_return_statement): PL/pgSQL 함수에서 반환문 없이 함수가 종료될 때 발생하며, 외부 루틴 관련 에러와 함께 디버깅해야 하는 경우가 많습니다.
  • 58030 (io_error): 외부 루틴에서 파일 I/O 작업 중 오류가 발생할 때 나타나며, PL/Python에서 파일을 읽거나 쓰는 과정에서 39000과 함께 발생할 수 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기