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

38002
2026년 09월 02일 | DBMS Error 가이드

이 글에서 다루는 내용

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

38002 modifying sql data not permitted 는?

PostgreSQL 에러 코드 38002(modifying sql data not permitted)는 SQL 함수 또는 프로시저가 데이터 수정(INSERT, UPDATE, DELETE 등)을 허용하지 않는 컨텍스트에서 실행될 때 발생합니다. 이 에러는 주로 READS SQL DATA 또는 CONTAINS SQL로 선언된 함수 내부에서 데이터를 변경하려 할 때, 혹은 읽기 전용 트랜잭션 내에서 쓰기 작업을 시도할 때 나타납니다. 특히 PL/pgSQL 함수나 저장 프로시저의 권한(volatility) 설정이 잘못되어 있을 경우 실무에서 자주 마주치게 되는 에러입니다.


주요 발생 원인

  • 함수의 SQL 데이터 접근 수준이 잘못 선언된 경우

PostgreSQL 함수를 생성할 때 READS SQL DATA 또는 NO SQL로 선언해 놓고, 함수 본문 내에서 데이터를 수정하는 DML 구문(INSERT, UPDATE, DELETE)을 실행하려 하면 이 에러가 발생합니다. 이는 함수 선언부의 데이터 접근 속성과 실제 구현 내용이 일치하지 않아서 발생하는 전형적인 설정 오류입니다. PostgreSQL은 함수 실행 전 선언된 속성을 검사하여 데이터 수정 가능 여부를 판단하기 때문에, 선언과 구현이 다를 경우 즉시 에러를 반환합니다.

  • 읽기 전용(Read-Only) 트랜잭션 내에서 데이터 수정 시도

SET TRANSACTION READ ONLY 또는 BEGIN READ ONLY로 시작된 트랜잭션 내에서 데이터를 변경하려 하면 이 에러가 발생할 수 있습니다. 읽기 전용 트랜잭션은 데이터의 일관성을 보장하기 위해 쓰기 작업 자체를 원천 차단합니다. 특히 복제(Replication) 환경에서 스탠바이(Standby) 서버에 연결된 세션이 실수로 DML을 실행하려 할 때 이와 유사한 상황이 자주 발생합니다.

  • 트리거 또는 중첩 함수 호출에서 권한 범위 충돌

읽기 전용으로 선언된 함수 내부에서 다른 함수를 호출하거나, 트리거가 실행되는 과정에서 데이터 수정이 제한된 컨텍스트에 진입하는 경우에도 38002 에러가 발생합니다. 예를 들어, STABLE 또는 IMMUTABLE 함수 내에서 데이터를 변경하는 함수를 호출하거나, 트리거 함수가 잘못된 접근 수준으로 설정된 경우입니다. 중첩 호출 구조에서는 에러의 근본 원인을 추적하기 어렵기 때문에 반드시 함수 호출 스택을 면밀히 살펴봐야 합니다.


해결 방법

원인 1 해결: 함수 선언 속성 수정

함수가 데이터를 수정해야 한다면 MODIFIES SQL DATA (또는 PostgreSQL 표준에서는 VOLATILE)으로 선언해야 합니다. 기존 함수의 선언을 확인하고 수정하는 방법은 다음과 같습니다.

-- 잘못된 함수 선언 예시 (데이터 수정 불가 선언인데 INSERT 포함)
CREATE OR REPLACE FUNCTION update_user_status(p_user_id INT)
RETURNS VOID
LANGUAGE plpgsql
STABLE  -- 이 선언이 문제! 데이터 수정 불가를 의미함
AS $$
BEGIN
    UPDATE users SET status = 'active' WHERE user_id = p_user_id;
END;
$$;

-- 올바른 함수 선언 예시 (VOLATILE로 수정)
CREATE OR REPLACE FUNCTION update_user_status(p_user_id INT)
RETURNS VOID
LANGUAGE plpgsql
VOLATILE  -- 데이터 수정이 가능하도록 변경
AS $$
BEGIN
    UPDATE users SET status = 'active' WHERE user_id = p_user_id;
END;
$$;

-- 현재 함수의 volatility 확인 방법
SELECT proname, provolatile
FROM pg_proc
WHERE proname = 'update_user_status';
-- provolatile: 'v'=VOLATILE, 's'=STABLE, 'i'=IMMUTABLE

원인 2 해결: 트랜잭션 모드 확인 및 변경

읽기 전용 트랜잭션을 읽기-쓰기 모드로 전환하거나, 트랜잭션 시작 전 세션 레벨의 설정을 확인해야 합니다.

-- 현재 트랜잭션 읽기/쓰기 모드 확인
SHOW transaction_read_only;

-- 세션 레벨에서 읽기 전용 해제
SET SESSION CHARACTERISTICS AS TRANSACTION READ WRITE;

-- 트랜잭션 시작 시 명시적으로 읽기-쓰기 모드 지정
BEGIN READ WRITE;
    UPDATE orders SET status = 'shipped' WHERE order_id = 1001;
COMMIT;

-- 읽기 전용 트랜잭션 올바른 사용 예 (SELECT만 허용)
BEGIN READ ONLY;
    SELECT * FROM orders WHERE order_id = 1001;
COMMIT;

-- 기본 트랜잭션 격리 수준과 접근 모드 동시 설정
BEGIN ISOLATION LEVEL REPEATABLE READ READ WRITE;
    INSERT INTO audit_log (event, created_at) VALUES ('data_check', NOW());
COMMIT;

원인 3 해결: 트리거 및 중첩 함수 권한 정리

트리거 함수와 중첩 호출 구조를 점검하고, 데이터 수정이 필요한 모든 함수를 VOLATILE로 선언해야 합니다.

-- 문제가 되는 트리거 함수 예시 (STABLE로 잘못 선언)
CREATE OR REPLACE FUNCTION trg_update_modified_at()
RETURNS TRIGGER
LANGUAGE plpgsql
STABLE  -- 트리거 함수에서 STABLE은 데이터 수정 시 문제 발생
AS $$
BEGIN
    NEW.modified_at := NOW();
    RETURN NEW;
END;
$$;

-- 올바른 트리거 함수 선언 (VOLATILE 사용)
CREATE OR REPLACE FUNCTION trg_update_modified_at()
RETURNS TRIGGER
LANGUAGE plpgsql
VOLATILE  -- 트리거 함수는 반드시 VOLATILE로 선언
AS $$
BEGIN
    NEW.modified_at := NOW();
    RETURN NEW;
END;
$$;

-- 트리거 생성 예시
CREATE TRIGGER set_modified_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION trg_update_modified_at();

-- 중첩 함수 호출 시 각 함수의 volatility 점검
SELECT p.proname, p.provolatile,
       CASE p.provolatile
           WHEN 'v' THEN 'VOLATILE'
           WHEN 's' THEN 'STABLE'
           WHEN 'i' THEN 'IMMUTABLE'
       END AS volatility_label
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname = 'public'
ORDER BY p.proname;

예방 방법

  • 함수 생성 시 Volatility 속성을 명확히 문서화하고 코드 리뷰를 철저히 시행하세요.

함수를 생성하거나 수정할 때 VOLATILE, STABLE, IMMUTABLE의 차이를 팀 전체가 이해하고 있어야 합니다. 데이터를 수정하는 함수는 반드시 VOLATILE로 선언하고, 단순 조회만 하는 함수는 STABLE, 항상 동일한 결과를 반환하는 순수 계산 함수는 IMMUTABLE로 구분하여 선언하는 규칙을 팀 코딩 컨벤션에 포함시키세요. CI/CD 파이프라인에서 pg_proc 뷰를 활용한 자동 점검 스크립트를 추가하면 배포 전에 잘못된 설정을 사전에 차단할 수 있습니다.

  • 운영 환경에서 함수 및 트랜잭션 설정을 주기적으로 감사(Audit)하세요.

다음 쿼리를 정기적으로 실행하여 STABLE 또는 IMMUTABLE로 잘못 선언된 함수 중 실제로는 데이터를 수정하는 함수가 없는지 모니터링하세요. 또한 애플리케이션에서 세션 레벨의 transaction_read_only 설정이 의도치 않게 활성화되지 않도록 커넥션 풀 설정을 주기적으로 점검하는 습관을 들이세요. 정기 감사는 장애가 발생하기 전에 잠재적 문제를 발견하는 가장 효과적인 예방 수단입니다.


관련 에러

  • 38000 (external routine exception): 외부 루틴 실행 중 발생하는 일반적인 에러로, 38002의 상위 카테고리입니다.
  • 38001 (containing sql not permitted): SQL 구문 자체를 허용하지 않는 컨텍스트에서 SQL을 실행하려 할 때 발생합니다.
  • 38003 (prohibited sql statement attempted): 특정 컨텍스트에서 금지된 SQL 구문을 실행하려 할 때 발생하며, 38002와 혼동하기 쉽습니다.
  • 25006 (read_only_sql_transaction): 읽기 전용 트랜잭션에서 데이터 수정을 시도할 때 발생하는 에러로, 38002와 함께 자주 나타납니다.
  • 0A000 (feature_not_supported): 특정 기능이 현재 컨텍스트에서 지원되지 않을 때 발생하며, 함수 권한 설정 오류와 관련될 수 있습니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기