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

27000
2026년 06월 26일 | DBMS Error 가이드

이 글에서 다루는 내용

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

27000 triggered data change violation 는?

PostgreSQL 에러 코드 27000 triggered data change violation은 트리거(Trigger) 내부에서 허용되지 않는 데이터 변경 작업이 시도될 때 발생하는 오류입니다. 대표적으로 AFTER 트리거 또는 STATEMENT 레벨 트리거에서 해당 트리거가 걸린 테이블을 직접 수정하려 할 때 이 에러가 발생합니다. 즉, 트리거의 실행 컨텍스트와 데이터 변경 규칙이 충돌하는 상황에서 PostgreSQL이 데이터 무결성을 보호하기 위해 이 에러를 발생시킵니다.


주요 발생 원인

  • AFTER 트리거에서 트리거 대상 테이블을 직접 수정하는 경우

AFTER INSERT, AFTER UPDATE, AFTER DELETE 트리거 내부에서 트리거가 걸려 있는 동일한 테이블에 대해 UPDATE, INSERT, DELETE를 직접 수행하려고 할 때 이 에러가 발생합니다. PostgreSQL은 AFTER 트리거 실행 중에는 트리거가 부착된 테이블의 데이터를 변경하는 행위를 허용하지 않으며, 이는 무한 루프와 데이터 불일치를 방지하기 위한 안전 장치입니다.

  • STATEMENT 레벨 트리거에서 트리거 대상 테이블을 수정하는 경우

FOR EACH STATEMENT 옵션으로 정의된 트리거에서 트리거 대상 테이블을 수정하려고 할 때도 이 에러가 발생합니다. STATEMENT 레벨 트리거는 문장 전체에 대해 한 번만 실행되는 특성상, 내부에서 동일 테이블을 수정하게 되면 PostgreSQL이 이를 허용되지 않는 데이터 변경으로 간주합니다. 특히 뷰(View)에 대한 INSTEAD OF 트리거가 아닌 일반 테이블에 Statement 트리거를 사용할 때 자주 발생합니다.

  • 트리거 함수 내부에서 트랜잭션 경계를 잘못 처리하는 경우

트리거 함수 내부에서 복잡한 로직을 처리하면서 PERFORM, EXECUTE 등의 동적 SQL이나 함수 호출을 통해 간접적으로 트리거 대상 테이블을 수정하는 경우에도 이 에러가 발생할 수 있습니다. 개발자가 직접 수정하는 코드가 없더라도, 호출된 함수나 프로시저가 내부적으로 해당 테이블에 변경을 가할 경우 PostgreSQL은 동일하게 27000 에러를 반환합니다.


해결 방법

원인 1 해결: AFTER 트리거에서 동일 테이블 수정 문제

AFTER 트리거에서 동일 테이블을 수정해야 하는 경우, BEFORE 트리거로 변경하거나 수정 대상을 다른 테이블로 분리하는 방법을 사용합니다.

잘못된 예시 (에러 발생):

-- 에러를 유발하는 트리거 예시
CREATE OR REPLACE FUNCTION update_salary_after()
RETURNS TRIGGER AS $$
BEGIN
    -- AFTER 트리거에서 동일 테이블(employees)을 수정 → 27000 에러 발생
    UPDATE employees
    SET updated_at = NOW()
    WHERE id = NEW.id;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_after_update_salary
AFTER UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION update_salary_after();

올바른 예시 (BEFORE 트리거로 변경):

-- BEFORE 트리거로 변경하여 동일 테이블 수정 허용
CREATE OR REPLACE FUNCTION update_salary_before()
RETURNS TRIGGER AS $$
BEGIN
    -- BEFORE 트리거에서 NEW 레코드를 직접 수정
    NEW.updated_at := NOW();
    RETURN NEW;  -- 수정된 NEW를 반환하면 자동으로 반영됨
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_before_update_salary
BEFORE UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION update_salary_before();

원인 2 해결: STATEMENT 트리거에서 동일 테이블 수정 문제

STATEMENT 레벨 트리거에서 동일 테이블을 수정해야 하는 경우, 별도의 로그 테이블이나 히스토리 테이블을 분리하여 사용합니다.

잘못된 예시 (에러 발생):

-- STATEMENT 레벨 트리거에서 동일 테이블 수정 → 27000 에러
CREATE OR REPLACE FUNCTION stmt_trigger_func()
RETURNS TRIGGER AS $$
BEGIN
    -- 동일한 employees 테이블을 수정하려고 시도 → 에러 발생
    UPDATE employees SET last_bulk_update = NOW();
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_stmt_update
AFTER UPDATE ON employees
FOR EACH STATEMENT
EXECUTE FUNCTION stmt_trigger_func();

올바른 예시 (별도 테이블에 기록):

-- 별도의 audit 테이블을 만들어 데이터 변경 이력을 기록
CREATE TABLE IF NOT EXISTS employees_audit_log (
    id          SERIAL PRIMARY KEY,
    action      TEXT NOT NULL,
    action_time TIMESTAMPTZ DEFAULT NOW(),
    row_count   INTEGER
);

CREATE OR REPLACE FUNCTION stmt_trigger_safe_func()
RETURNS TRIGGER AS $$
BEGIN
    -- 원본 테이블 대신 별도 audit 테이블에 기록
    INSERT INTO employees_audit_log (action, action_time)
    VALUES (TG_OP, NOW());
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_stmt_update_safe
AFTER UPDATE ON employees
FOR EACH STATEMENT
EXECUTE FUNCTION stmt_trigger_safe_func();

원인 3 해결: 간접적인 테이블 수정 문제

트리거 함수 내에서 호출하는 함수가 트리거 대상 테이블을 수정하는 경우, 해당 로직을 분리하거나 SECURITY DEFINER 및 별도 스키마를 활용하여 우회합니다.

-- 트리거 함수에서 호출하는 외부 함수가 동일 테이블을 건드리지 않도록
-- 별도의 중간 테이블(staging table)을 활용하는 패턴
CREATE TABLE employees_pending_updates (
    employee_id  INTEGER,
    new_status   TEXT,
    processed    BOOLEAN DEFAULT FALSE,
    created_at   TIMESTAMPTZ DEFAULT NOW()
);

CREATE OR REPLACE FUNCTION safe_trigger_with_staging()
RETURNS TRIGGER AS $$
BEGIN
    -- 직접 employees를 수정하지 않고 staging 테이블에 변경사항 기록
    INSERT INTO employees_pending_updates (employee_id, new_status)
    VALUES (NEW.id, NEW.status);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_safe_staging
AFTER INSERT ON employees
FOR EACH ROW
EXECUTE FUNCTION safe_trigger_with_staging();

-- 별도 배치 작업이나 스케줄러에서 pending_updates를 처리
-- (트리거 실행 컨텍스트 밖에서 실행)
UPDATE employees e
SET status = pu.new_status
FROM employees_pending_updates pu
WHERE e.id = pu.employee_id
  AND pu.processed = FALSE;

UPDATE employees_pending_updates
SET processed = TRUE
WHERE processed = FALSE;

예방 방법

  • 트리거 설계 시 ROW vs STATEMENT, BEFORE vs AFTER를 명확히 구분하라

트리거를 설계할 때는 반드시 실행 시점(BEFORE / AFTER)과 레벨(FOR EACH ROW / FOR EACH STATEMENT)을 신중하게 결정해야 합니다. 동일 테이블의 컬럼 값을 수정해야 하는 경우는 무조건 BEFORE FOR EACH ROW 트리거를 사용하고, NEW 레코드를 직접 수정하여 반환하는 패턴을 표준화하세요. 로깅, 감사(Audit) 목적의 트리거는 반드시 별도 테이블을 대상으로 데이터를 기록하도록 설계하여 트리거 대상 테이블과 명확히 분리하십시오.

  • 트리거 함수 개발 및 배포 전 테스트 환경에서 반드시 검증하라

트리거는 DML 작업이 발생할 때마다 자동으로 실행되므로, 운영 환경 적용 전에 반드시 개발/테스트 환경에서 다양한 시나리오를 검증해야 합니다. 특히 트리거 함수 내에서 다른 함수나 프로시저를 호출하는 경우, 해당 함수들이 트리거 대상 테이블을 간접적으로 수정하는지 pg_trigger, information_schema.triggers 뷰를 활용하여 의존성을 사전에 파악하고 검토해야 합니다.

-- 현재 데이터베이스의 모든 트리거 목록 및 실행 시점 확인
SELECT
    trigger_name,
    event_object_table AS table_name,
    action_timing,        -- BEFORE / AFTER / INSTEAD OF
    event_manipulation,   -- INSERT / UPDATE / DELETE
    action_orientation,   -- ROW / STATEMENT
    action_statement
FROM information_schema.triggers
WHERE trigger_schema = 'public'
ORDER BY event_object_table, action_timing, event_manipulation;

관련 에러

  • 25006 read_only_sql_transaction: 읽기 전용 트랜잭션 내에서 데이터를 변경하려 할 때 발생하며, 27000과 유사하게 허용되지 않는 데이터 변경 시도 시 나타나는 에러입니다.
  • 09000 triggered_action_exception: 트리거 실행 중 일반적인 예외가 발생할 때 나타나는 에러로, 27000보다 포괄적인 트리거 관련 에러입니다.
  • 0A000 feature_not_supported: 특정 PostgreSQL 버전이나 컨텍스트에서 지원되지 않는 기능을 트리거 내에서 사용하려 할 때 발생할 수 있으며, 트리거 설계 오류와 함께 나타나는 경우가 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기