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 error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.