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

2F002
2026년 09월 01일 | DBMS Error 가이드

이 글에서 다루는 내용

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

2F002 modifying sql data not permitted 는?

PostgreSQL 에러 코드 2F002 (modifying sql data not permitted)는 SQL 함수(Function) 또는 프로시저(Procedure) 내부에서 데이터를 수정하는 DML(INSERT, UPDATE, DELETE) 작업이 허용되지 않은 상황에서 시도될 때 발생합니다. 이 에러는 주로 함수의 변동성(Volatility) 속성이 IMMUTABLE 또는 STABLE로 정의되어 있는데, 내부에서 데이터를 변경하려 할 때 발생합니다. 특히 읽기 전용 트랜잭션 컨텍스트에서 쓰기 작업을 시도하거나, 제약 조건(Constraint) 트리거처럼 특수한 실행 컨텍스트 내에서 데이터 변경을 시도할 때도 동일한 에러가 나타납니다.


주요 발생 원인

1. 함수 변동성(Volatility)이 IMMUTABLE 또는 STABLE로 잘못 선언된 경우

가장 빈번하게 발생하는 원인입니다. PostgreSQL에서 함수를 IMMUTABLE 또는 STABLE로 선언하면, 해당 함수는 데이터베이스의 데이터를 변경하지 않는다는 것을 엔진에 보장하는 것입니다. 그러나 개발자가 실수로 이러한 함수 내부에 INSERT, UPDATE, DELETE 구문을 작성하면 PostgreSQL은 즉시 2F002 에러를 발생시킵니다. 예를 들어, 캐싱 목적으로 로그를 남기는 함수에 STABLE 속성을 부여하면서 동시에 로그 테이블에 INSERT를 시도하는 경우가 대표적입니다.

2. 읽기 전용(Read-Only) 트랜잭션 내에서 함수를 통한 데이터 수정 시도

SET TRANSACTION READ ONLY 또는 BEGIN READ ONLY로 시작된 트랜잭션 블록 안에서는 어떠한 데이터 변경도 허용되지 않습니다. 이 상황에서 내부적으로 DML을 수행하는 함수를 호출하면 2F002 에러가 발생합니다. 읽기 전용 복제본(Read Replica) 또는 Hot Standby 서버에 연결된 상태에서 쓰기 함수를 호출하는 경우도 동일한 문제가 발생하므로, 연결 대상 서버의 역할을 반드시 확인해야 합니다.

3. 트리거(Trigger) 또는 제약 조건 내에서 허용되지 않는 DML 수행

PostgreSQL의 특정 실행 컨텍스트, 예를 들어 CONSTRAINT TRIGGER나 일부 시스템 트리거 내에서는 데이터 변경이 제한될 수 있습니다. 개발자가 복잡한 비즈니스 로직을 트리거에 넣으면서 다른 테이블의 데이터를 수정하려 할 때, 해당 실행 컨텍스트의 제약으로 인해 2F002 에러가 발생할 수 있습니다. 이는 트리거의 WHEN 조건과 함수 내부 로직이 복잡하게 얽힌 경우에 특히 디버깅이 어렵습니다.


해결 방법

원인 1 해결: 함수 변동성 속성 수정

함수의 변동성을 올바르게 변경합니다. 데이터를 수정하는 함수는 반드시 VOLATILE(기본값)으로 선언되어야 합니다.

-- 문제가 되는 함수 (STABLE로 잘못 선언됨)
CREATE OR REPLACE FUNCTION log_user_action(user_id INT, action TEXT)
RETURNS VOID
LANGUAGE plpgsql
STABLE  -- ❌ 잘못된 선언: DML을 수행하는데 STABLE로 설정됨
AS $$
BEGIN
    INSERT INTO audit_log (user_id, action, logged_at)
    VALUES (user_id, action, NOW());
END;
$$;

-- 올바른 수정: VOLATILE로 변경 (또는 속성 생략, 기본값이 VOLATILE)
CREATE OR REPLACE FUNCTION log_user_action(user_id INT, action TEXT)
RETURNS VOID
LANGUAGE plpgsql
VOLATILE  -- ✅ 올바른 선언: 데이터를 변경하므로 VOLATILE
AS $$
BEGIN
    INSERT INTO audit_log (user_id, action, logged_at)
    VALUES (user_id, action, NOW());
END;
$$;

-- 기존 함수의 변동성만 변경할 경우
ALTER FUNCTION log_user_action(INT, TEXT) VOLATILE;

원인 2 해결: 트랜잭션 모드 확인 및 수정

읽기 전용 트랜잭션에서 쓰기 작업이 필요한 경우, 트랜잭션 모드를 변경하거나 별도의 쓰기 가능 세션을 사용해야 합니다.

-- 문제 상황: 읽기 전용 트랜잭션에서 DML 함수 호출
BEGIN READ ONLY;
    SELECT log_user_action(1, 'LOGIN');  -- ❌ 2F002 에러 발생
COMMIT;

-- 해결책 1: 트랜잭션을 읽기/쓰기 모드로 변경
BEGIN;  -- 기본값은 READ WRITE
    SELECT log_user_action(1, 'LOGIN');  -- ✅ 정상 동작
COMMIT;

-- 해결책 2: 현재 트랜잭션 특성 확인
SHOW transaction_read_only;

-- 해결책 3: 세션 레벨에서 기본 트랜잭션 모드 확인
SHOW default_transaction_read_only;

-- 해결책 4: 세션 기본값을 읽기/쓰기로 변경 (필요한 경우)
SET default_transaction_read_only = OFF;

원인 3 해결: 트리거 내 DML 로직 분리

트리거 내에서 데이터 수정이 필요한 경우, AFTER 트리거를 활용하거나 로직을 애플리케이션 레이어로 이동하는 것을 고려합니다.

-- 문제가 되는 CONSTRAINT TRIGGER 패턴
CREATE OR REPLACE FUNCTION validate_and_update()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    -- 다른 테이블의 데이터를 수정하려는 시도 (특정 컨텍스트에서 2F002 발생)
    UPDATE inventory SET quantity = quantity - NEW.amount
    WHERE product_id = NEW.product_id;
    RETURN NEW;
END;
$$;

-- 해결책: AFTER 트리거로 변경하여 DML 허용
CREATE OR REPLACE FUNCTION update_inventory_after_order()
RETURNS TRIGGER
LANGUAGE plpgsql
VOLATILE  -- ✅ 명시적으로 VOLATILE 선언
AS $$
BEGIN
    UPDATE inventory 
    SET quantity = quantity - NEW.amount,
        updated_at = NOW()
    WHERE product_id = NEW.product_id;
    
    -- 재고 부족 시 알림 로그 기록
    IF NOT FOUND THEN
        INSERT INTO inventory_alerts (product_id, alert_type, created_at)
        VALUES (NEW.product_id, 'OUT_OF_STOCK', NOW());
    END IF;
    
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_update_inventory
AFTER INSERT ON orders  -- AFTER 트리거 사용
FOR EACH ROW
EXECUTE FUNCTION update_inventory_after_order();

예방 방법

1. 함수 생성 시 변동성(Volatility) 속성을 항상 명시적으로 선언하고 검토하는 습관

함수를 생성할 때 VOLATILE, STABLE, IMMUTABLE 중 어떤 속성이 올바른지 반드시 의도적으로 결정하고 코드 리뷰 시 이를 확인하는 프로세스를 도입하는 것이 중요합니다. 아래 쿼리를 활용하여 현재 운영 중인 함수들의 변동성 속성을 정기적으로 감사(Audit)하면 잘못된 선언을 조기에 발견할 수 있습니다.

-- 운영 중인 모든 함수의 변동성 속성 감사 쿼리
SELECT 
    n.nspname AS schema_name,
    p.proname AS function_name,
    CASE p.provolatile
        WHEN 'i' THEN 'IMMUTABLE'
        WHEN 's' THEN 'STABLE'
        WHEN 'v' THEN 'VOLATILE'
    END AS volatility,
    p.prosrc AS function_body
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
  AND p.prokind = 'f'  -- 일반 함수만 조회
ORDER BY n.nspname, p.proname;

2. 개발 환경에서 함수 테스트 시 다양한 트랜잭션 컨텍스트로 검증

CI/CD 파이프라인 또는 개발 단계에서 함수를 테스트할 때, 일반 트랜잭션뿐 아니라 읽기 전용 트랜잭션, 중첩 트랜잭션(Savepoint) 등 다양한 컨텍스트에서도 함수가 올바르게 동작하는지 검증해야 합니다. 이를 통해 운영 환경의 Read Replica 또는 특수한 트랜잭션 설정에서 발생할 수 있는 2F002 에러를 사전에 차단할 수 있습니다.


관련 에러

  • 2F000 (sql routine exception): SQL 루틴 실행 중 발생하는 일반적인 예외로, 2F002의 상위 에러 클래스입니다.
  • 2F003 (prohibited sql statement attempted): 함수 내에서 허용되지 않는 SQL 구문(예: IMMUTABLE 함수 내 SELECT 외의 구문)을 시도할 때 발생합니다.
  • 2F004 (reading sql data not permitted): NO SQL 또는 CONTAINS SQL로 선언된 루틴에서 데이터 읽기를 시도할 때 발생하며, 2F002의 읽기 버전에 해당합니다.
  • 25006 (read only sql transaction): 읽기 전용 트랜잭션에서 직접 DML을 실행할 때 발생하는 에러로, 2F002와 함께 자주 나타납니다.
  • 42809 (wrong object type): 함수나 프로시저에 잘못된 객체 유형을 사용할 때 발생하며, 루틴 관련 에러와 연관될 수 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기