2026년 06월 28일 | DBMS Error 가이드
이 글에서 다루는 내용
2F002 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
2F002 modifying sql data not permitted 는?
PostgreSQL 에러 코드 2F002는 SQL 함수 또는 PL/pgSQL 함수 내부에서 데이터 수정(INSERT, UPDATE, DELETE, TRUNCATE 등)이 허용되지 않는 컨텍스트에서 실행될 때 발생합니다. 주로 READS SQL DATA 또는 CONTAINS SQL 등으로 선언된 함수, 혹은 읽기 전용으로 호출된 함수 안에서 DML 문을 실행하려 할 때 나타납니다. 쉽게 말해, “이 함수는 데이터를 읽기만 해야 하는데, 왜 수정하려 하느냐”는 PostgreSQL의 경고입니다.
주요 발생 원인
1. 함수의 volatility 또는 SQL 데이터 접근 속성 불일치
PostgreSQL 함수는 READS SQL DATA, MODIFIES SQL DATA, CONTAINS SQL, NO SQL 등의 속성을 가질 수 있으며, 함수 선언 시 이 속성과 실제 내부 동작이 불일치할 때 에러가 발생합니다. 예를 들어 함수를 READS SQL DATA로 선언했지만 내부에서 INSERT나 UPDATE를 수행하려 하면 PostgreSQL은 이를 즉시 거부합니다. 특히 PostgreSQL 14 이상부터 이 속성에 대한 검사가 더욱 엄격해졌습니다.
2. 트리거 함수 또는 뷰 내부에서 잘못된 데이터 조작 시도
INSTEAD OF 트리거나 특정 보안 제약이 걸린 뷰(View) 위에서 함수를 호출할 때, 해당 컨텍스트가 읽기 전용으로 제한되어 있으면 내부에서 DML을 수행할 수 없습니다. 특히 SECURITY DEFINER 함수가 읽기 전용 트랜잭션 내에서 호출되거나, 복잡한 뷰 체인 안에서 데이터 수정을 시도할 때 이 에러가 빈번하게 발생합니다. 실무에서 뷰 위에 트리거를 쌓는 복잡한 구조에서 많이 보이는 패턴입니다.
3. 읽기 전용 트랜잭션(READ ONLY Transaction) 내에서 데이터 변경 시도
SET TRANSACTION READ ONLY 혹은 BEGIN READ ONLY로 시작된 트랜잭션 내부에서 데이터 변경을 시도하면 이 에러가 발생할 수 있습니다. 특히 읽기 전용 복제본(Standby 서버)에 연결된 상태에서 함수를 실행하거나, 애플리케이션 레벨에서 트랜잭션 모드를 잘못 설정한 경우에 해당합니다. ORM 프레임워크가 자동으로 READ ONLY 트랜잭션을 설정하는 경우에도 이 문제가 숨어있을 수 있습니다.
해결 방법
원인 1 해결: 함수 속성 수정
함수 선언에서 데이터 수정을 허용하는 속성으로 변경합니다.
-- 잘못된 선언 예시 (에러 발생)
CREATE OR REPLACE FUNCTION update_user_status(p_user_id INT)
RETURNS VOID
LANGUAGE plpgsql
READS SQL DATA -- 이 속성이 문제
AS $$
BEGIN
UPDATE users SET status = 'active' WHERE user_id = p_user_id;
END;
$$;
-- 올바른 선언 예시 (수정 허용)
CREATE OR REPLACE FUNCTION update_user_status(p_user_id INT)
RETURNS VOID
LANGUAGE plpgsql
-- MODIFIES SQL DATA 또는 속성 생략 (기본값은 수정 허용)
AS $$
BEGIN
UPDATE users SET status = 'active' WHERE user_id = p_user_id;
END;
$$;
만약 SQL 함수(LANGUAGE sql)를 사용하는 경우도 동일하게 적용됩니다.
-- SQL 함수에서의 올바른 선언
CREATE OR REPLACE FUNCTION insert_log(p_message TEXT)
RETURNS VOID
LANGUAGE sql
AS $$
INSERT INTO app_logs (message, created_at)
VALUES (p_message, NOW());
$$;
원인 2 해결: 트리거 및 뷰 구조 점검
뷰 위에서 데이터를 수정해야 한다면 INSTEAD OF 트리거를 올바르게 구성해야 합니다.
-- 뷰 생성
CREATE VIEW active_users_view AS
SELECT user_id, username, email
FROM users
WHERE status = 'active';
-- INSTEAD OF 트리거 함수 생성 (데이터 수정 허용)
CREATE OR REPLACE FUNCTION update_active_user()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE users
SET username = NEW.username,
email = NEW.email
WHERE user_id = OLD.user_id
AND status = 'active';
RETURN NEW;
END;
$$;
-- 트리거 연결
CREATE TRIGGER trg_update_active_user
INSTEAD OF UPDATE ON active_users_view
FOR EACH ROW EXECUTE FUNCTION update_active_user();
트리거 함수 자체에는 별도의 READS SQL DATA 속성을 부여하지 말고, 기본 상태로 두어야 합니다.
원인 3 해결: 트랜잭션 읽기 전용 설정 확인
현재 트랜잭션의 읽기/쓰기 여부를 확인하고 수정합니다.
-- 현재 트랜잭션 모드 확인
SHOW transaction_read_only;
-- 읽기 전용 해제 후 데이터 수정 (트랜잭션 새로 시작)
BEGIN;
SET TRANSACTION READ WRITE;
UPDATE orders SET status = 'completed' WHERE order_id = 12345;
COMMIT;
스탠바이 서버에 연결된 경우 반드시 Primary 서버로의 연결을 사용해야 합니다.
-- 현재 서버가 스탠바이인지 확인
SELECT pg_is_in_recovery();
-- 결과가 true이면 스탠바이 → Primary 서버로 연결 전환 필요
예방 방법
1. 함수 생성 시 명시적 데이터 접근 속성 지정 및 코드 리뷰 프로세스 도입
모든 함수를 생성하거나 수정할 때 READS SQL DATA, MODIFIES SQL DATA 등의 속성을 명확히 문서화하고, 함수 내부 로직과 속성이 일치하는지 코드 리뷰 단계에서 반드시 검토해야 합니다. CI/CD 파이프라인에 정적 분석 도구를 연동하여 함수 속성 불일치를 사전에 감지하는 것이 좋습니다.
2. 애플리케이션 연결 풀 및 트랜잭션 모드 설정의 표준화
PgBouncer, HikariCP 등 커넥션 풀 도구에서 연결별 트랜잭션 모드(default_transaction_read_only)를 명확히 설정하고, 읽기 전용 복제본과 쓰기 가능한 Primary를 분리하는 라우팅 전략을 표준화해야 합니다. 특히 ORM 레벨에서 자동으로 READ ONLY를 설정하는 경우, DML이 필요한 함수 호출 전에 트랜잭션 모드를 명시적으로 선언하는 습관을 들이는 것이 중요합니다.
관련 에러
2F000(sql_routine_exception): SQL 루틴 실행 중 발생하는 일반적인 예외의 부모 에러 코드로,2F002를 포함한 SQL 함수 관련 에러들의 상위 카테고리입니다.25006(read_only_sql_transaction): 읽기 전용 트랜잭션에서 데이터 변경을 시도할 때 발생하는 에러로,2F002와 발생 맥락이 매우 유사합니다. 트랜잭션 레벨에서의 읽기 전용 위반은25006, 함수 속성 레벨에서의 위반은2F002로 구분됩니다.2F003(prohibited_sql_statement_attempted): SQL 함수 내에서 허용되지 않는 SQL 구문을 실행할 때 발생하며,2F002와 함께 SQL 함수 제약 위반 에러군에 속합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.