2026년 06월 30일 | 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/pgSQL과 달리, 외부 프로세스나 런타임 환경과의 상호작용 중 발생하는 오류를 포괄하는 상위 에러 클래스(Class 39)에 속합니다. 주로 외부 언어 함수가 잘못된 입력값을 처리하거나, 외부 런타임 환경이 초기화되지 않았거나, 외부 함수 내부에서 처리되지 않은 예외가 PostgreSQL 엔진으로 전파될 때 발생합니다.
주요 발생 원인
1. 외부 언어 함수(PL/Python, PL/Perl 등) 내부의 처리되지 않은 예외
외부 언어로 작성된 함수에서 Python의 ZeroDivisionError, Perl의 die, Java의 RuntimeException 등 처리되지 않은 예외가 발생하면, 해당 예외가 PostgreSQL 엔진으로 그대로 전파되어 39000 에러를 유발합니다. 특히 PL/Python 함수에서 예외 처리 블록(try/except)이 누락된 경우, 런타임 에러가 곧바로 데이터베이스 세션을 중단시킬 수 있어 주의가 필요합니다.
2. 외부 런타임 환경의 초기화 실패 또는 의존성 문제
PL/Java나 PL/Python 같은 외부 언어 확장은 별도의 런타임 환경(JVM, Python 인터프리터 등)에 의존합니다. 해당 런타임이 제대로 설치되지 않았거나, 필요한 라이브러리 또는 모듈이 누락되었거나, 환경 변수(PYTHONPATH, CLASSPATH 등)가 잘못 설정된 경우 함수 호출 시점에 39000 에러가 발생할 수 있습니다. 이는 서버 재기동 이후나 OS 업그레이드 후 특히 자주 나타나는 패턴입니다.
3. NULL 또는 유효하지 않은 입력값에 대한 방어 로직 부재
외부 함수가 NULL 값이나 예상치 못한 데이터 타입을 입력으로 받았을 때, 내부적으로 이를 처리하는 방어 코드가 없으면 예외가 발생합니다. 예를 들어, PL/Python 함수에서 None(NULL) 입력에 대해 문자열 연산을 수행하려 하면 AttributeError가 발생하고, 이것이 PostgreSQL 레벨에서 39000 에러로 표출됩니다.
해결 방법
원인 1 해결: 외부 언어 함수 내 예외 처리 강화
PL/Python 함수 내부에서 반드시 try/except 블록을 사용하여 모든 예외를 명시적으로 처리하십시오.
-- 문제가 있는 PL/Python 함수 (예외 처리 없음)
CREATE OR REPLACE FUNCTION divide_numbers(a FLOAT, b FLOAT)
RETURNS FLOAT
LANGUAGE plpython3u AS $$
return a / b -- b가 0이면 ZeroDivisionError 발생 -> 39000 에러
$$;
-- 수정된 PL/Python 함수 (예외 처리 포함)
CREATE OR REPLACE FUNCTION divide_numbers_safe(a FLOAT, b FLOAT)
RETURNS FLOAT
LANGUAGE plpython3u AS $$
try:
if b == 0:
raise ValueError("Division by zero is not allowed.")
return a / b
except ZeroDivisionError as e:
plpy.error(f"Math error: {e}")
except Exception as e:
plpy.error(f"Unexpected error in divide_numbers_safe: {e}")
$$;
-- 테스트
SELECT divide_numbers_safe(10.0, 2.0); -- 정상: 5.0
SELECT divide_numbers_safe(10.0, 0.0); -- 명확한 에러 메시지 반환
원인 2 해결: 런타임 환경 및 의존성 확인
외부 언어 확장의 설치 상태와 의존성을 점검하십시오.
-- 현재 설치된 언어 확장 확인
SELECT lanname, lanpltrusted, lanvalidator::regproc
FROM pg_language
WHERE lanname IN ('plpython3u', 'plperlu', 'pljavau');
-- PL/Python에서 의존 모듈 임포트 테스트 함수
CREATE OR REPLACE FUNCTION test_python_import(module_name TEXT)
RETURNS TEXT
LANGUAGE plpython3u AS $$
try:
import importlib
mod = importlib.import_module(module_name)
return f"Module '{module_name}' loaded successfully. Version: {getattr(mod, '__version__', 'N/A')}"
except ImportError as e:
plpy.error(f"Import failed for module '{module_name}': {e}")
$$;
-- numpy 모듈 로드 테스트
SELECT test_python_import('numpy');
SELECT test_python_import('pandas');
-- PL/Java 설치 확인 (postgresql.conf 설정 확인용)
SHOW pljava.libjvm_location;
원인 3 해결: NULL 및 입력값 방어 로직 추가
-- NULL 입력에 취약한 함수 (문제 있음)
CREATE OR REPLACE FUNCTION process_text_bad(input_val TEXT)
RETURNS TEXT
LANGUAGE plpython3u AS $$
return input_val.upper() -- input_val이 None이면 AttributeError 발생
$$;
-- NULL 안전 함수 (수정된 버전)
CREATE OR REPLACE FUNCTION process_text_safe(input_val TEXT)
RETURNS TEXT
LANGUAGE plpython3u AS $$
if input_val is None:
return None # NULL 입력은 NULL 반환
try:
return input_val.strip().upper()
except AttributeError as e:
plpy.error(f"Invalid input type: {e}")
except Exception as e:
plpy.error(f"Unexpected error in process_text_safe: {e}")
$$;
-- STRICT 옵션 사용: NULL 입력 시 자동으로 NULL 반환 (함수 실행 자체를 건너뜀)
CREATE OR REPLACE FUNCTION process_text_strict(input_val TEXT)
RETURNS TEXT
STRICT
LANGUAGE plpython3u AS $$
try:
return input_val.strip().upper()
except Exception as e:
plpy.error(f"Error processing text: {e}")
$$;
-- 테스트
SELECT process_text_safe(' hello world '); -- 'HELLO WORLD'
SELECT process_text_safe(NULL); -- NULL 반환 (에러 없음)
SELECT process_text_strict(NULL); -- NULL 반환 (함수 자체 실행 안 함)
-- 에러 로그 확인용 쿼리 (pg_log 기반)
SELECT pid, usename, application_name, state, query, wait_event_type, wait_event
FROM pg_stat_activity
WHERE state = 'active';
예방 방법
1. 외부 언어 함수 전용 테스트 스위트 구축 및 CI/CD 통합
외부 언어로 작성된 모든 함수에 대해 단위 테스트와 경계값 테스트(NULL, 빈 문자열, 최대/최솟값, 잘못된 타입 등)를 포함하는 테스트 스위트를 작성하고, 배포 파이프라인에 통합하십시오. pgTAP 또는 별도의 Python 테스트 프레임워크(pytest)를 활용하면 함수 배포 전에 잠재적인 39000 에러를 사전에 탐지할 수 있습니다. 또한, 스테이징 환경에서 실제 프로덕션 데이터 샘플을 활용해 함수의 안정성을 반드시 검증하는 프로세스를 정착시키는 것이 중요합니다.
2. plpy.error 대신 구조화된 에러 핸들링과 로깅 체계 도입
외부 언어 함수 내에서 예외 발생 시 단순히 에러를 던지는 것이 아니라, plpy.warning() 또는 plpy.log()를 활용해 상세한 진단 정보를 PostgreSQL 로그에 기록하는 습관을 들이십시오. 함수 입력값, 스택 트레이스, 발생 시각 등을 로그에 남기면 운영 중 문제가 발생했을 때 원인을 신속하게 파악할 수 있습니다. 또한 postgresql.conf의 log_min_messages와 log_error_verbosity 설정을 적절히 조정하여 외부 루틴 관련 에러가 충분한 상세 정보와 함께 기록되도록 설정을 유지하십시오.
관련 에러
- 39001 (no_saved_data): 외부 루틴에서 저장된 데이터가 없을 때 발생하는 에러로, 39000의 하위 에러 클래스입니다.
- 39004 (null_value_not_allowed): STRICT로 선언되지 않은 외부 함수에서 NULL 처리 오류 발생 시 나타납니다.
- 39P01 (trigger_protocol_violated): 트리거로 사용되는 외부 루틴이 트리거 프로토콜을 위반할 때 발생합니다.
- 39P02 (srf_protocol_violated): Set-Returning Function(SRF)으로 사용되는 외부 루틴이 프로토콜을 위반할 때 나타납니다.
- 58000 (system_error): 외부 런타임 프로세스 자체의 충돌이나 OS 레벨 오류로 이어질 경우 함께 발생할 수 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.