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

39P01
2026년 07월 01일 | DBMS Error 가이드

이 글에서 다루는 내용

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

39P01 trigger protocol violated 는?

PostgreSQL 에러 코드 39P01 trigger protocol violated는 PL/pgSQL 또는 기타 절차적 언어로 작성된 트리거 함수가 PostgreSQL의 트리거 호출 규약(protocol)을 위반했을 때 발생하는 에러입니다. 트리거 함수는 일반 함수와 달리 반드시 trigger 타입을 반환해야 하며, 내부적으로 NEW, OLD, NULL 중 하나를 적절히 반환해야 합니다. 이 규약을 지키지 않거나, 트리거 컨텍스트 밖에서 트리거 전용 변수나 동작을 수행하려 할 때 이 에러가 발생합니다.


주요 발생 원인

1. 트리거 함수가 올바른 값을 반환하지 않는 경우

트리거 함수는 BEFORE 트리거의 경우 반드시 NEW 또는 NULL을 반환해야 하며, AFTER 트리거는 반환값이 무시되지만 여전히 NEW 또는 NULL을 반환하는 것이 표준입니다. PL/pgSQL 트리거 함수에서 RETURN 문이 누락되었거나, 잘못된 타입의 값을 반환하는 경우 PostgreSQL 내부의 트리거 디스패처가 프로토콜 위반으로 판단하여 이 에러를 발생시킵니다. 특히 C 언어로 작성된 커스텀 트리거 함수에서 HeapTuple을 반환하지 않거나 잘못 구성된 경우 자주 발생합니다.

2. 트리거로 선언되지 않은 함수를 트리거로 등록한 경우

트리거에 연결되는 함수는 반드시 RETURNS trigger로 선언되어 있어야 합니다. 일반 함수(예: RETURNS void 또는 RETURNS integer)를 트리거 이벤트에 연결하면, 함수 자체는 실행될 수 있으나 PostgreSQL이 트리거 실행 결과를 처리하는 과정에서 프로토콜 위반 에러가 발생합니다. 이는 초보 개발자가 기존 함수를 재활용하려 할 때 매우 흔하게 발생하는 패턴입니다.

3. PL/Python, PL/Perl 등 외부 언어 트리거에서 반환 형식 오류

PL/Python이나 PL/Perl 같은 외부 언어로 트리거를 작성할 때, 각 언어의 트리거 반환 규약을 따르지 않으면 이 에러가 발생합니다. 예를 들어 PL/Python 트리거에서는 BEFORE 트리거의 경우 딕셔너리 또는 None을 반환해야 하는데, 다른 타입의 객체를 반환하거나 return 문 자체를 누락하면 프로토콜 위반이 됩니다. 이 경우 에러 메시지가 다소 불명확하게 표시될 수 있어 디버깅이 어렵습니다.


해결 방법

원인 1 해결: 트리거 함수의 올바른 RETURN 작성

BEFORE 트리거에서는 반드시 RETURN NEW 또는 RETURN NULL을 명시해야 합니다. RETURN NEW는 행을 정상적으로 처리하고, RETURN NULL은 해당 행에 대한 작업(INSERT/UPDATE/DELETE)을 취소합니다.

-- 잘못된 예: RETURN 문 누락
CREATE OR REPLACE FUNCTION trg_check_salary()
RETURNS trigger AS $$
BEGIN
    IF NEW.salary < 0 THEN
        RAISE EXCEPTION 'Salary cannot be negative';
    END IF;
    -- RETURN 문이 없어서 39P01 발생 가능
END;
$$ LANGUAGE plpgsql;

-- 올바른 예: RETURN NEW 명시
CREATE OR REPLACE FUNCTION trg_check_salary()
RETURNS trigger AS $$
BEGIN
    IF NEW.salary < 0 THEN
        RAISE EXCEPTION 'Salary cannot be negative';
    END IF;
    RETURN NEW;  -- 반드시 명시
END;
$$ LANGUAGE plpgsql;

-- AFTER 트리거에서도 RETURN NULL 또는 RETURN NEW 명시
CREATE OR REPLACE FUNCTION trg_audit_log()
RETURNS trigger AS $$
BEGIN
    INSERT INTO audit_log(table_name, action, changed_at)
    VALUES (TG_TABLE_NAME, TG_OP, NOW());
    RETURN NULL;  -- AFTER 트리거에서는 반환값이 무시되지만 명시 필요
END;
$$ LANGUAGE plpgsql;

원인 2 해결: 함수 반환 타입을 trigger로 선언

기존에 RETURNS void로 선언된 함수를 트리거에 연결하려 했다면, 반드시 함수를 새로 작성하거나 RETURNS trigger로 재선언해야 합니다.

-- 잘못된 예: RETURNS void 함수를 트리거로 등록
CREATE OR REPLACE FUNCTION wrong_trigger_func()
RETURNS void AS $$
BEGIN
    RAISE NOTICE 'trigger fired';
END;
$$ LANGUAGE plpgsql;

-- 위 함수로 트리거 생성 시 나중에 39P01 발생
CREATE TRIGGER wrong_trg
BEFORE INSERT ON employees
FOR EACH ROW EXECUTE FUNCTION wrong_trigger_func();

-- 올바른 예: RETURNS trigger로 선언
CREATE OR REPLACE FUNCTION correct_trigger_func()
RETURNS trigger AS $$
BEGIN
    RAISE NOTICE 'trigger fired on table: %', TG_TABLE_NAME;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER correct_trg
BEFORE INSERT ON employees
FOR EACH ROW EXECUTE FUNCTION correct_trigger_func();

-- 기존 잘못된 트리거 교체
DROP TRIGGER IF EXISTS wrong_trg ON employees;
CREATE TRIGGER fixed_trg
BEFORE INSERT ON employees
FOR EACH ROW EXECUTE FUNCTION correct_trigger_func();

원인 3 해결: PL/Python 트리거 반환값 수정

-- PL/Python BEFORE 트리거 올바른 작성 예
CREATE OR REPLACE FUNCTION plpython_trg_func()
RETURNS trigger AS $$
    # BEFORE 트리거에서는 TD["new"] 딕셔너리 또는 None 반환
    if TD["new"]["salary"] < 0:
        raise plpy.Error("Salary cannot be negative")
    
    # 행 수정이 필요한 경우 딕셔너리 수정 후 "MODIFY" 반환
    TD["new"]["updated_at"] = "NOW()"
    return "MODIFY"  # 또는 None (행 취소), 생략 시 그대로 진행
$$ LANGUAGE plpython3u;

에러 확인 및 디버깅 쿼리

-- 현재 트리거 함수의 반환 타입 확인
SELECT p.proname AS function_name,
       pg_get_function_result(p.oid) AS return_type,
       l.lanname AS language
FROM pg_proc p
JOIN pg_language l ON p.prolang = l.oid
WHERE pg_get_function_result(p.oid) != 'trigger'
  AND p.oid IN (
      SELECT tgfoid FROM pg_trigger
  );

-- 특정 테이블의 트리거 및 연결된 함수 확인
SELECT t.tgname AS trigger_name,
       p.proname AS function_name,
       pg_get_function_result(p.oid) AS return_type,
       CASE t.tgtype & 2 WHEN 2 THEN 'BEFORE' ELSE 'AFTER' END AS timing,
       CASE t.tgtype & 1 WHEN 1 THEN 'ROW' ELSE 'STATEMENT' END AS level
FROM pg_trigger t
JOIN pg_proc p ON t.tgfoid = p.oid
JOIN pg_class c ON t.tgrelid = c.oid
WHERE c.relname = 'employees'
  AND NOT t.tgisinternal;

예방 방법

1. 트리거 함수 작성 템플릿 표준화

팀 내에서 트리거 함수를 작성할 때 반드시 아래와 같은 표준 템플릿을 사용하도록 코딩 가이드라인을 수립하세요. 템플릿에 RETURNS triggerRETURN NEW를 기본으로 포함시키면 누락으로 인한 실수를 원천 차단할 수 있습니다. 또한 CI/CD 파이프라인에서 트리거 함수 배포 전 pg_get_function_result()를 이용해 반환 타입을 자동으로 검증하는 스크립트를 추가하면 프로덕션 환경으로의 잘못된 코드 배포를 방지할 수 있습니다.

-- 팀 표준 트리거 함수 템플릿
CREATE OR REPLACE FUNCTION schema_name.trg_<table>_<action>()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
    -- BEFORE ROW 트리거 기본 구조
    -- 로직 작성

    -- INSERT/UPDATE 시
    RETURN NEW;
    -- DELETE 시에는 RETURN OLD;
    -- 작업 취소 시에는 RETURN NULL;
END;
$$;

2. 정기적인 트리거 유효성 감사(Audit) 쿼리 실행

데이터베이스의 트리거 함수가 올바른 반환 타입을 가지고 있는지 주기적으로 검사하는 감사 쿼리를 cron 또는 pgAgent로 스케줄링하세요. 이를 통해 배포 실수나 마이그레이션 오류로 인해 잘못된 트리거 함수가 등록되는 상황을 사전에 감지할 수 있습니다.

-- 트리거 유효성 감사 쿼리 (주기적 실행 권장)
SELECT
    n.nspname AS schema_name,
    c.relname AS table_name,
    t.tgname AS trigger_name,
    p.proname AS function_name,
    pg_get_function_result(p.oid) AS return_type,
    CASE
        WHEN pg_get_function_result(p.oid) = 'trigger' THEN 'OK'
        ELSE 'WARNING: Not a trigger function!'
    END AS status
FROM pg_trigger t
JOIN pg_proc p ON t.tgfoid = p.oid
JOIN pg_class c ON t.tgrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE NOT t.tgisinternal
ORDER BY status DESC, schema_name, table_name;

관련 에러

  • 39004 null value not allowed: 트리거 함수에서 NOT NULL 제약이 있는 컬럼에 NULL 값을 반환하려 할 때 발생하며, 39P01과 함께 트리거 실행 오류 계열에 속합니다.
  • 42P13 invalid_function_definition: 트리거 함수 자체의 정의가 잘못된 경우 발생하며, CREATE FUNCTION 시점에 감지됩니다. 39P01은 런타임 시점에 발생한다는 점에서 차이가 있습니다.
  • 55P02 cant_change_runtime_param: 트리거 내에서 변경할 수 없는 런타임 파라미터를 수정하려 할 때 발생할 수 있으며, 트리거 컨텍스트 제약과 연관됩니다.
  • P0001 raise_exception: 트리거 함수 내에서 RAISE EXCEPTION으로 명시적으로 발생시킨 에러로, 39P01과 구분하여 트리거 로직 오류를 추적할 때 참고해야 합니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기