2026년 09월 04일 | DBMS Error 가이드
이 글에서 다루는 내용
39P01 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
39P01 trigger protocol violated 는?
PostgreSQL 에러 코드 39P01 trigger protocol violated는 PL/pgSQL 또는 C 언어로 작성된 트리거 함수가 PostgreSQL 트리거 프로토콜을 위반했을 때 발생하는 에러입니다. 트리거 함수는 반드시 특정 규칙(반환값, 호출 방식, 컨텍스트 등)을 준수해야 하는데, 이를 어기면 데이터베이스 엔진이 이 에러를 발생시킵니다. 주로 트리거 함수가 잘못된 반환값을 반환하거나, 트리거가 아닌 일반 함수로 호출될 때, 혹은 트리거 함수 내부 로직이 프로토콜에 맞지 않게 구현되었을 때 나타납니다.
주요 발생 원인
1. 트리거 함수가 잘못된 반환 타입을 사용한 경우
트리거 함수는 반드시 RETURNS trigger로 선언되어야 하며, 함수 본문에서 RETURN NEW, RETURN OLD, 또는 RETURN NULL을 명시적으로 반환해야 합니다. 일반 테이블 타입이나 void, integer 등 다른 타입을 반환하도록 선언하면 PostgreSQL은 이를 트리거 프로토콜 위반으로 간주하고 즉시 에러를 발생시킵니다.
2. 트리거 함수를 일반 SQL 함수처럼 직접 호출한 경우
트리거 함수는 트리거 이벤트(INSERT, UPDATE, DELETE 등)가 발생했을 때만 PostgreSQL 내부적으로 호출되어야 합니다. 개발자가 실수로 SELECT my_trigger_function() 처럼 직접 함수를 호출하면, 트리거 컨텍스트(TG_OP, NEW, OLD 등의 특수 변수)가 존재하지 않아 프로토콜 위반 에러가 발생합니다.
3. 행 수준(ROW-LEVEL) 트리거 함수에서 RETURN을 누락하거나 잘못 처리한 경우
행 수준 트리거(FOR EACH ROW)에서 BEFORE 트리거는 반드시 RETURN NEW 또는 RETURN OLD를 반환해야 합니다. NULL을 반환하면 해당 행의 DML 작업이 취소되는데, 이를 의도치 않게 사용하거나 RETURN 구문 자체를 누락하면 트리거 프로토콜 위반 또는 예기치 않은 동작이 발생할 수 있습니다.
해결 방법
원인 1 해결: 올바른 반환 타입 선언
트리거 함수는 반드시 RETURNS trigger로 선언해야 합니다. 아래는 잘못된 예시와 올바른 예시입니다.
-- ❌ 잘못된 예: RETURNS void로 선언된 트리거 함수
CREATE OR REPLACE FUNCTION bad_trigger_func()
RETURNS void AS $$
BEGIN
RAISE NOTICE 'Trigger fired';
-- RETURN NEW 누락 및 잘못된 반환 타입
END;
$$ LANGUAGE plpgsql;
-- ✅ 올바른 예: RETURNS trigger로 선언 및 RETURN NEW 명시
CREATE OR REPLACE FUNCTION good_trigger_func()
RETURNS trigger AS $$
BEGIN
RAISE NOTICE 'Trigger fired on table: %', TG_TABLE_NAME;
-- BEFORE INSERT/UPDATE 트리거에서는 반드시 RETURN NEW
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 트리거 생성
CREATE TRIGGER my_trigger
BEFORE INSERT OR UPDATE ON my_table
FOR EACH ROW EXECUTE FUNCTION good_trigger_func();
원인 2 해결: 트리거 함수를 직접 호출하지 않기
트리거 함수는 직접 호출해서는 안 됩니다. 트리거 함수 로직을 일반 함수로 분리하여 호출하거나, 트리거를 통해서만 실행되도록 구성하세요.
-- ❌ 잘못된 예: 트리거 함수를 직접 SELECT로 호출
SELECT good_trigger_func(); -- 에러 발생: 39P01
-- ✅ 올바른 예: 로직을 별도의 일반 함수로 분리
CREATE OR REPLACE FUNCTION log_change(table_name TEXT, operation TEXT)
RETURNS void AS $$
BEGIN
INSERT INTO audit_log(table_name, operation, changed_at)
VALUES (table_name, operation, NOW());
END;
$$ LANGUAGE plpgsql;
-- 트리거 함수에서 일반 함수를 호출하는 방식
CREATE OR REPLACE FUNCTION audit_trigger_func()
RETURNS trigger AS $$
BEGIN
PERFORM log_change(TG_TABLE_NAME, TG_OP);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON my_table
FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
-- 일반 함수는 직접 호출 가능
SELECT log_change('my_table', 'MANUAL_CHECK');
원인 3 해결: BEFORE/AFTER 트리거 반환값 올바르게 처리
-- ✅ BEFORE 행 수준 트리거: 항상 NEW 또는 OLD를 반환
CREATE OR REPLACE FUNCTION before_row_trigger_func()
RETURNS trigger AS $$
BEGIN
-- 데이터 유효성 검사
IF NEW.salary < 0 THEN
RAISE EXCEPTION '급여는 0 이상이어야 합니다. 입력값: %', NEW.salary;
END IF;
-- 자동 타임스탬프 업데이트
NEW.updated_at := NOW();
-- 반드시 NEW를 반환 (NULL 반환 시 해당 행의 DML이 취소됨)
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- ✅ AFTER 행 수준 트리거: 반환값이 무시되지만 명시적으로 NULL 반환
CREATE OR REPLACE FUNCTION after_row_trigger_func()
RETURNS trigger AS $$
BEGIN
-- AFTER 트리거에서는 반환값이 실제로 사용되지 않음
INSERT INTO change_log(table_name, operation, row_id, changed_at)
VALUES (TG_TABLE_NAME, TG_OP, NEW.id, NOW());
RETURN NULL; -- AFTER 트리거에서는 NULL 반환 권장
END;
$$ LANGUAGE plpgsql;
-- ✅ STATEMENT 레벨 트리거: 반드시 NULL 반환
CREATE OR REPLACE FUNCTION statement_trigger_func()
RETURNS trigger AS $$
BEGIN
RAISE NOTICE '% 작업이 % 테이블에서 실행됨', TG_OP, TG_TABLE_NAME;
RETURN NULL; -- FOR EACH STATEMENT 트리거는 반드시 NULL 반환
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER stmt_trigger
AFTER INSERT ON my_table
FOR EACH STATEMENT EXECUTE FUNCTION statement_trigger_func();
예방 방법
1. 트리거 함수 작성 시 표준 템플릿 활용 및 코드 리뷰 강화
팀 내에서 트리거 함수를 작성할 때 반드시 검증된 표준 템플릿을 사용하도록 규정하고, 모든 트리거 관련 코드는 배포 전 DBA가 반드시 코드 리뷰를 수행하도록 프로세스를 수립하세요. 특히 RETURNS trigger 선언 여부, 적절한 RETURN 구문 존재 여부, 트리거 특수 변수(TG_OP, TG_TABLE_NAME, NEW, OLD) 사용의 적절성을 체크리스트로 만들어 활용하는 것이 좋습니다.
-- 트리거 함수 표준 템플릿
CREATE OR REPLACE FUNCTION template_trigger_func()
RETURNS trigger AS $$
DECLARE
v_operation TEXT := TG_OP;
BEGIN
-- 1. 입력값 유효성 검증
-- 2. 비즈니스 로직 처리
-- 3. 감사 로그 기록 (필요 시)
-- BEFORE 트리거: RETURN NEW (또는 변경된 NEW)
-- AFTER 트리거: RETURN NULL
-- INSTEAD OF 트리거: RETURN NEW 또는 RETURN NULL
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
2. pg_trigger 카탈로그를 통한 주기적인 트리거 감사(Audit)
운영 환경에서 주기적으로 pg_trigger와 pg_proc 시스템 카탈로그를 조회하여 등록된 모든 트리거 함수의 반환 타입과 정의를 검증하는 스크립트를 CI/CD 파이프라인 또는 정기 배치 작업에 포함시키세요. 이를 통해 잘못된 트리거 함수가 운영 환경에 배포되기 전에 사전 차단할 수 있습니다.
-- 트리거 함수 감사 쿼리: 반환 타입이 trigger가 아닌 함수를 사용하는 트리거 탐지
SELECT
t.tgname AS trigger_name,
c.relname AS table_name,
p.proname AS function_name,
pg_catalog.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_class c ON t.tgrelid = c.oid
JOIN pg_proc p ON t.tgfoid = p.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
AND NOT t.tgisinternal
ORDER BY c.relname, t.tgname;
관련 에러
39004null value not allowed: 트리거 또는 PL/pgSQL 함수에서 NOT NULL 컬럼에 NULL을 반환하려 할 때 발생하며,39P01과 함께 트리거 함수 프로토콜 위반 맥락에서 자주 등장합니다.- **
42809wrong object type`: 트리거를 생성할 때 지정한 함수가 트리거 함수가 아닌 일반 함수일 경우 발생합니다. P0001raise_exception: 트리거 함수 내에서RAISE EXCEPTION을 사용하여 의도적으로 트랜잭션을 중단시킬 때 발생하며, 트리거 로직의 유효성 검증 실패 시 자주 활용됩니다.55P02cant_change_runtime_param: 트리거 함수 내에서 세션 레벨 파라미터를 변경하려 할 때 발생하는 에러로, 트리거의 실행 컨텍스트 제한과 관련이 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.