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

09000
2026년 10월 08일 | DBMS Error 가이드

이 글에서 다루는 내용

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

09000 triggered action exception 는?

09000 triggered_action_exception은 PostgreSQL에서 트리거(Trigger) 또는 규칙(Rule)이 실행되는 도중 예외가 발생했을 때 반환되는 에러 코드입니다. 주로 트리거 함수 내부에서 명시적으로 RAISE EXCEPTION을 호출하거나, 트리거가 수행하는 DML 작업이 제약 조건(Constraint)이나 비즈니스 로직을 위반할 때 발생합니다. 이 에러는 원래 실행하려던 INSERT, UPDATE, DELETE 문이 롤백되며, 트랜잭션 전체가 영향을 받을 수 있어 운영 환경에서 각별히 주의해야 합니다.


주요 발생 원인

1. 트리거 함수 내부에서 명시적 RAISE EXCEPTION 호출

가장 빈번하게 발생하는 원인입니다. 개발자가 비즈니스 로직 검증을 위해 트리거 내부에 RAISE EXCEPTION을 작성해 두었을 때, 해당 조건에 부합하는 데이터가 삽입·수정·삭제되면 이 에러가 발생합니다. 예를 들어 재고가 0 이하로 떨어지는 것을 막거나, 특정 상태 전환을 금지하는 로직이 여기에 해당합니다. 운영 데이터베이스에서 트리거 로직이 변경되었는지 모르는 상태로 애플리케이션을 배포했을 때 특히 예상치 못한 장애를 유발합니다.

2. 트리거 내부 DML이 제약 조건(Constraint)을 위반

트리거가 실행하는 INSERT, UPDATE 등의 DML이 외래 키(Foreign Key), 유니크(Unique), NOT NULL 등의 제약 조건을 위반하면 triggered_action_exception으로 포장되어 반환됩니다. 트리거 함수 자체는 정상적으로 작성되었어도, 그 내부에서 참조하는 테이블의 구조나 데이터 상태가 달라졌을 때 이 문제가 발생할 수 있습니다. 특히 CASCADE 관계가 복잡하게 얽힌 대형 스키마에서는 원인 추적이 어렵습니다.

3. 트리거 함수 내부 PL/pgSQL 런타임 오류

트리거 함수가 PL/pgSQL로 작성된 경우, 내부 로직에서 0으로 나누기, NULL 참조, 잘못된 타입 캐스팅 등 런타임 오류가 발생하면 09000 에러로 표면화됩니다. 이 경우는 특히 디버깅이 어려운데, 에러 메시지만 보아서는 어느 트리거의 어느 줄에서 문제가 생겼는지 파악하기 쉽지 않습니다. 적절한 예외 처리(EXCEPTION 블록)가 없는 트리거 함수에서 자주 발생합니다.


해결 방법

원인 1 해결: 트리거 정의 및 로직 확인

먼저 해당 테이블에 걸린 트리거 목록을 조회합니다.

-- 특정 테이블의 트리거 목록 조회
SELECT
    tgname AS trigger_name,
    tgtype,
    proname AS function_name,
    tgenabled AS enabled
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 = 'your_table_name'
  AND NOT tgisinternal;

트리거 함수의 소스 코드를 직접 확인합니다.

-- 트리거 함수 소스 조회
SELECT prosrc
FROM pg_proc
WHERE proname = 'your_trigger_function_name';

문제가 되는 RAISE EXCEPTION 조건을 파악한 후, 임시로 트리거를 비활성화하고 데이터를 수정할 수 있습니다.

-- 트리거 임시 비활성화 (슈퍼유저 또는 테이블 오너만 가능)
ALTER TABLE your_table_name DISABLE TRIGGER your_trigger_name;

-- 데이터 수정 작업 수행
UPDATE your_table_name SET status = 'valid' WHERE id = 123;

-- 트리거 다시 활성화
ALTER TABLE your_table_name ENABLE TRIGGER your_trigger_name;

원인 2 해결: 트리거 내부 DML 제약 조건 위반 수정

트리거 함수 내부에서 실행되는 DML을 점검하고, 제약 조건 위반 가능성이 있는 경우 방어 로직을 추가합니다.

-- 기존 문제 있는 트리거 함수 예시
CREATE OR REPLACE FUNCTION check_inventory()
RETURNS TRIGGER AS $$
BEGIN
    -- 재고 로그 테이블에 INSERT 시 제약 조건 위반 가능성 있음
    INSERT INTO inventory_log (product_id, changed_at, quantity)
    VALUES (NEW.product_id, NOW(), NEW.quantity);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 개선된 트리거 함수: 예외 처리 및 방어 로직 추가
CREATE OR REPLACE FUNCTION check_inventory()
RETURNS TRIGGER AS $$
BEGIN
    -- NULL 체크 및 유효성 검사
    IF NEW.quantity IS NULL OR NEW.quantity < 0 THEN
        RAISE EXCEPTION '재고 수량은 0 이상이어야 합니다. 입력값: %', NEW.quantity
            USING ERRCODE = '23514';
    END IF;

    BEGIN
        INSERT INTO inventory_log (product_id, changed_at, quantity)
        VALUES (NEW.product_id, NOW(), NEW.quantity);
    EXCEPTION
        WHEN foreign_key_violation THEN
            RAISE WARNING '존재하지 않는 product_id: %', NEW.product_id;
        WHEN unique_violation THEN
            -- 중복 로그는 무시
            NULL;
    END;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

원인 3 해결: PL/pgSQL 런타임 오류 디버깅

client_min_messages를 조정하거나 RAISE DEBUG 구문을 활용해 트리거 내부 흐름을 추적합니다.

-- 세션 레벨에서 디버그 메시지 활성화
SET client_min_messages = DEBUG;

-- 트리거 함수에 디버그 로그 추가 예시
CREATE OR REPLACE FUNCTION safe_trigger_function()
RETURNS TRIGGER AS $$
DECLARE
    v_result NUMERIC;
BEGIN
    RAISE DEBUG '트리거 시작: TG_OP=%, 테이블=%', TG_OP, TG_TABLE_NAME;

    -- 0 나누기 방지
    IF NEW.denominator = 0 THEN
        RAISE EXCEPTION 'denominator 값이 0입니다. 0 나누기 오류 방지.'
            USING ERRCODE = '09000',
                  HINT = 'denominator 컬럼에 CHECK 제약을 추가하는 것을 권장합니다.';
    END IF;

    v_result := NEW.numerator / NEW.denominator;

    RAISE DEBUG '계산 결과: %', v_result;

    NEW.ratio := v_result;
    RETURN NEW;

EXCEPTION
    WHEN OTHERS THEN
        RAISE WARNING '트리거 오류 발생: SQLSTATE=%, MESSAGE=%', SQLSTATE, SQLERRM;
        RETURN NULL; -- 또는 RETURN NEW; 상황에 따라 선택
END;
$$ LANGUAGE plpgsql;

에러 발생 시 PostgreSQL 로그에서 상세 정보를 확인하려면 다음 설정을 활용하세요.

-- postgresql.conf 또는 세션 설정으로 상세 에러 로깅
SET log_min_messages = DEBUG1;
SET log_error_verbosity = VERBOSE;

-- pg_stat_activity로 현재 실행 중인 쿼리 및 에러 원인 추적
SELECT pid, usename, application_name, state, query, wait_event_type, wait_event
FROM pg_stat_activity
WHERE state != 'idle';

예방 방법

1. 트리거 함수에 반드시 EXCEPTION 블록과 명확한 에러 메시지 작성

모든 트리거 함수는 예상 가능한 예외 상황을 EXCEPTION 블록으로 처리하고, RAISE EXCEPTION 사용 시 ERRCODE, DETAIL, HINT를 명확하게 기술해야 합니다. 이렇게 하면 애플리케이션 레벨에서 에러를 파싱해 사용자에게 의미 있는 메시지를 전달할 수 있고, DBA가 로그를 분석할 때도 원인 파악이 훨씬 수월해집니다.

-- 권장 RAISE EXCEPTION 형식
RAISE EXCEPTION '비즈니스 규칙 위반: 주문 상태를 [%]에서 [%]로 변경할 수 없습니다.',
    OLD.status, NEW.status
    USING ERRCODE = '09000',
          DETAIL = format('order_id=%s, user_id=%s', NEW.order_id, NEW.user_id),
          HINT = '허용된 상태 전환 규칙을 확인하세요: https://wiki.internal/order-state-machine';

2. 트리거 변경 이력 관리 및 배포 전 테스트 자동화

트리거 함수는 DDL이므로 반드시 버전 관리 시스템(Git 등)으로 이력을 관리하고, CI/CD 파이프라인에 트리거 관련 통합 테스트를 포함시켜야 합니다. pgTAP 같은 PostgreSQL 전용 테스트 프레임워크를 활용하면 트리거가 기대한 대로 동작하는지 자동으로 검증할 수 있습니다.

-- pgTAP을 활용한 트리거 테스트 예시
BEGIN;
SELECT plan(2);

-- 정상 케이스: 재고가 충분할 때 UPDATE 성공해야 함
UPDATE products SET quantity = 10 WHERE id = 1;
SELECT ok(FOUND, '정상 재고 수량 업데이트 성공');

-- 비정상 케이스: 재고가 음수가 되는 UPDATE는 실패해야 함
SELECT throws_ok(
    $$ UPDATE products SET quantity = -1 WHERE id = 1 $$,
    '09000',
    '재고 수량은 0 이상이어야 합니다.',
    '음수 재고 수량 업데이트 차단 확인'
);

SELECT * FROM finish();
ROLLBACK;

관련 에러

  • P0001 (raise_exception): PL/pgSQL에서 RAISE EXCEPTION이 호출될 때 발생하는 기본 에러 코드로, 09000과 함께 자주 등장합니다. 트리거가 아닌 일반 함수 또는 저장 프로시저에서 발생하는 경우 이 코드가 반환됩니다.
  • 23000 (integrity_constraint_violation): 트리거 내부 DML이 제약 조건을 위반할 때 09000의 하위 원인으로 발생할 수 있으며, 외래 키(23503), 유니크(23505), NOT NULL(23502) 위반을 포함합니다.
  • 42883 (undefined_function): 트리거가 참조하는 함수가 삭제되었거나 시그니처가 변경되었을 때 발생하며, 결과적으로 트리거 실행 실패로 이어집니다.
  • 55006 (object_in_use): 트리거가 실행 중인 상태에서 해당 객체에 DDL을 시도할 때 발생할 수 있으며, 트리거 관련 유지보수 작업 시 주의가 필요합니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기