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

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

이 글에서 다루는 내용

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

09000 triggered action exception 는?

PostgreSQL 에러 코드 09000 triggered action exception은 트리거(Trigger) 내부에서 실행된 로직이 예외를 발생시킬 때 나타나는 에러입니다. 트리거 함수 내에서 RAISE EXCEPTION이 명시적으로 호출되거나, 트리거가 호출한 함수 혹은 쿼리가 내부적으로 오류를 유발할 경우 이 에러가 발생합니다. 일반적으로 데이터 무결성 검증, 비즈니스 로직 강제 적용, 또는 감사(Audit) 로그 처리 과정에서 자주 목격됩니다.


주요 발생 원인

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

가장 흔한 원인입니다. 개발자가 비즈니스 규칙을 강제하기 위해 트리거 내부에서 RAISE EXCEPTION을 의도적으로 사용하는 경우입니다. 예를 들어, 특정 컬럼 값이 음수이거나 허용 범위를 초과할 때 트랜잭션 자체를 롤백시키기 위해 사용합니다. 이 경우 에러 메시지를 명확히 설계하지 않으면 애플리케이션 레벨에서 에러 원인을 파악하기 어려워집니다.

2. 트리거 내부에서 호출된 함수 또는 쿼리의 런타임 오류

트리거 함수가 외부 함수를 호출하거나 복잡한 서브쿼리를 실행할 때, 해당 로직이 런타임 오류(예: division by zero, null value 참조, 외래 키 위반 등)를 유발할 수 있습니다. 트리거는 DML(INSERT/UPDATE/DELETE) 작업의 일부로 실행되므로, 트리거 내에서 발생한 모든 예외는 상위 트랜잭션 전체를 롤백시킵니다. 이로 인해 원래 DML 작업도 실패하게 되어, 에러 원인이 트리거에 있다는 사실을 놓치기 쉽습니다.

3. 트리거 연쇄 호출(Cascading Triggers)로 인한 예외

하나의 트리거가 다른 테이블에 DML을 수행하고, 그 테이블에도 트리거가 연결되어 있는 경우 연쇄적으로 트리거가 실행됩니다. 이 연쇄 과정에서 어느 한 트리거라도 예외를 던지면 최초의 트랜잭션까지 롤백됩니다. 특히 복잡한 스키마에서는 어느 트리거가 예외를 발생시켰는지 추적하기 매우 어렵습니다.


해결 방법

원인 1 해결: 트리거 함수의 예외 메시지 개선 및 조건 점검

먼저 해당 트리거 함수의 정의를 확인합니다.

-- 트리거 함수 정의 확인
SELECT 
    p.proname AS function_name,
    pg_get_functiondef(p.oid) AS function_definition
FROM pg_proc p
JOIN pg_trigger t ON t.tgfoid = p.oid
JOIN pg_class c ON c.oid = t.tgrelid
WHERE c.relname = 'your_table_name';  -- 테이블명 입력

트리거 함수 내 RAISE EXCEPTION에 명확한 에러 코드와 힌트를 추가합니다.

CREATE OR REPLACE FUNCTION check_salary_trigger()
RETURNS TRIGGER AS $$
BEGIN
    -- 급여가 음수인 경우 명시적 예외 발생
    IF NEW.salary < 0 THEN
        RAISE EXCEPTION 
            'Invalid salary value: %. Salary must be non-negative. Table: employees, Column: salary',
            NEW.salary
            USING ERRCODE = 'P0001',  -- raise_exception 코드
                  HINT = 'Please provide a salary value greater than or equal to 0.';
    END IF;

    -- 급여 상한선 검사
    IF NEW.salary > 1000000 THEN
        RAISE EXCEPTION 
            'Salary % exceeds the maximum allowed value of 1,000,000.',
            NEW.salary
            USING ERRCODE = 'P0001',
                  HINT = 'Contact HR department for executive-level salary configuration.';
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 트리거 연결
CREATE TRIGGER trg_check_salary
BEFORE INSERT OR UPDATE ON employees
FOR EACH ROW EXECUTE FUNCTION check_salary_trigger();

원인 2 해결: 트리거 내부 예외를 EXCEPTION 블록으로 안전하게 처리

트리거 내부에서 런타임 오류가 발생할 수 있는 로직은 반드시 예외 처리 블록으로 감쌉니다.

CREATE OR REPLACE FUNCTION audit_log_trigger()
RETURNS TRIGGER AS $$
BEGIN
    BEGIN
        -- 감사 로그 테이블에 기록 시도
        INSERT INTO audit_log (table_name, operation, old_data, new_data, changed_at)
        VALUES (
            TG_TABLE_NAME,
            TG_OP,
            row_to_json(OLD),
            row_to_json(NEW),
            NOW()
        );
    EXCEPTION
        WHEN OTHERS THEN
            -- 감사 로그 실패 시 경고만 발생시키고 원본 트랜잭션은 유지
            RAISE WARNING 
                'Audit log insert failed for table % operation %. SQLSTATE: %, SQLERRM: %',
                TG_TABLE_NAME, TG_OP, SQLSTATE, SQLERRM;
    END;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

런타임 오류 중 zero division 방지 예시:

CREATE OR REPLACE FUNCTION calculate_ratio_trigger()
RETURNS TRIGGER AS $$
DECLARE
    v_ratio NUMERIC;
BEGIN
    -- 0으로 나누기 방지
    IF NEW.total_count IS NULL OR NEW.total_count = 0 THEN
        v_ratio := 0;
    ELSE
        v_ratio := NEW.success_count::NUMERIC / NEW.total_count;
    END IF;

    NEW.success_ratio := v_ratio;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

원인 3 해결: 연쇄 트리거 디버깅 및 제어

연쇄 트리거 구조를 파악하려면 아래 쿼리를 사용합니다.

-- 특정 테이블에 연결된 모든 트리거 조회
SELECT
    t.tgname AS trigger_name,
    c.relname AS table_name,
    p.proname AS function_name,
    CASE t.tgtype & 2 WHEN 2 THEN 'BEFORE' ELSE 'AFTER' END AS timing,
    CASE t.tgtype & 28
        WHEN 4  THEN 'INSERT'
        WHEN 8  THEN 'DELETE'
        WHEN 16 THEN 'UPDATE'
        WHEN 20 THEN 'INSERT OR DELETE'
        WHEN 28 THEN 'INSERT OR UPDATE OR DELETE'
    END AS event,
    t.tgenabled AS is_enabled
FROM pg_trigger t
JOIN pg_class c ON c.oid = t.tgrelid
JOIN pg_proc p ON p.oid = t.tgfoid
WHERE NOT t.tgisinternal
ORDER BY c.relname, t.tgname;

특정 트리거를 일시적으로 비활성화하여 원인을 격리합니다.

-- 트리거 일시 비활성화 (슈퍼유저 또는 테이블 소유자 권한 필요)
ALTER TABLE your_table DISABLE TRIGGER trigger_name;

-- 문제 DML 재실행 후 원인 파악
INSERT INTO your_table (col1, col2) VALUES ('test', 100);

-- 디버깅 완료 후 트리거 재활성화
ALTER TABLE your_table ENABLE TRIGGER trigger_name;

트리거 함수 내에서 RAISE NOTICE를 활용한 단계별 디버깅:

CREATE OR REPLACE FUNCTION debug_trigger()
RETURNS TRIGGER AS $$
BEGIN
    RAISE NOTICE '[DEBUG] Trigger: %, Table: %, Operation: %, Row: %',
        TG_NAME, TG_TABLE_NAME, TG_OP, row_to_json(NEW);

    -- 비즈니스 로직 수행
    -- ...

    RAISE NOTICE '[DEBUG] Trigger % completed successfully.', TG_NAME;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

예방 방법

1. 트리거 함수는 단순하고 명확하게 유지하며 반드시 예외 처리를 포함할 것

트리거 함수 내부 로직은 가능한 한 단순하게 유지해야 합니다. 복잡한 비즈니스 로직은 애플리케이션 레이어나 별도의 저장 프로시저로 분리하고, 트리거는 단순 검증과 로깅 용도로만 사용하는 것이 Best Practice입니다. 모든 트리거 함수에는 반드시 EXCEPTION 블록을 작성하여 예상치 못한 런타임 오류가 전체 트랜잭션을 중단시키지 않도록 설계해야 합니다. 또한 RAISE EXCEPTION을 사용할 때는 ERRCODEHINT를 명시하여 애플리케이션 레벨에서 에러를 명확히 구분하고 처리할 수 있도록 해야 합니다.

2. 트리거 변경 전 반드시 테스트 환경에서 검증하고, 운영 환경에는 모니터링을 강화할 것

트리거는 DML 작업에 자동으로 연동되므로, 트리거 함수를 변경하기 전에 반드시 개발/스테이징 환경에서 충분한 테스트를 수행해야 합니다. 운영 환경에서는 pg_stat_user_tablespg_log를 활용하여 트리거 관련 예외 발생 빈도를 주기적으로 모니터링하고, log_min_messages = WARNING 이상으로 로그 레벨을 설정하여 트리거 내 RAISE WARNING 메시지도 캡처되도록 구성하는 것을 권장합니다.


관련 에러

  • P0001 (raise_exception): 트리거 함수 내 RAISE EXCEPTION으로 직접 발생하는 사용자 정의 예외로, 09000과 함께 자주 등장합니다.
  • 23000 (integrity_constraint_violation): 트리거 내부에서 외래 키 제약이나 유니크 제약을 위반할 경우 발생하며, 09000으로 감싸져 보고될 수 있습니다.
  • 42883 (undefined_function): 트리거 함수가 존재하지 않는 함수를 호출할 때 발생하며, 트리거 실행 시 09000으로 노출될 수 있습니다.
  • 55P02 (cant_change_runtime_param): 트리거 내에서 허용되지 않는 세션 파라미터 변경을 시도할 경우 나타납니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기