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

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

이 글에서 다루는 내용

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

38001 containing sql not permitted 는?

PostgreSQL 에러 코드 38001 (containing_sql_not_permitted)은 SQL 구문을 포함할 수 없는 컨텍스트에서 SQL 명령어를 실행하려 할 때 발생하는 에러입니다. 주로 PL/pgSQL 또는 다른 절차형 언어(PL/Python, PL/Perl 등)로 작성된 함수나 프로시저에서, 해당 함수가 NO SQL 또는 CONTAINS SQL이 허용되지 않는 특정 속성으로 정의되었음에도 불구하고 SQL 문을 실행하려 할 때 나타납니다. 이 에러는 특히 함수의 LANGUAGE 속성과 SQL 접근 수준(sql-data-access) 설정이 실제 함수 내부 코드와 충돌할 때 발생하며, 데이터베이스 보안 정책 및 함수 실행 컨텍스트와 밀접하게 연관되어 있습니다.


주요 발생 원인

1. 함수 정의 시 NO SQL 속성 지정 후 내부에서 SQL 실행

가장 흔한 원인으로, 함수를 생성할 때 NO SQL 옵션을 명시적으로 선언했지만 함수 본문에 실제로 SQL 쿼리(SELECT, INSERT, UPDATE 등)가 포함된 경우입니다. NO SQL은 PostgreSQL에게 이 함수가 어떠한 SQL 명령도 실행하지 않겠다고 약속하는 것인데, 이 약속이 위반될 경우 런타임에 에러가 발생합니다. 특히 외부 라이브러리나 팀원이 작성한 함수를 수정하면서 이 속성을 간과하는 경우가 실무에서 자주 발생합니다.

2. PL/Python, PL/Perl 등 외부 언어 함수에서의 SQL 실행 제한

PL/Python(plpython3u), PL/Perl(plperlu)과 같은 외부 절차형 언어(Untrusted Language)로 정의된 함수는 기본적으로 SQL 실행 권한에 대한 엄격한 제어가 필요합니다. 이러한 환경에서 함수의 데이터 접근 속성이 잘못 설정되어 있거나, 내부적으로 plpy.execute() 또는 유사 메서드를 통해 SQL을 실행하려 할 때 컨텍스트 불일치로 인해 에러가 발생합니다. 보안 강화 목적으로 설정된 pg_hba.conf 정책이나 역할(Role) 기반 제한과 결합될 때 더욱 복잡한 양상을 보입니다.

3. 트리거 함수 또는 특수 컨텍스트에서의 SQL 접근 속성 충돌

트리거(Trigger) 함수나 이벤트 트리거(Event Trigger)로 등록된 함수, 또는 특정 시스템 카탈로그 레벨에서 호출되는 함수에서 SQL 실행 속성이 해당 컨텍스트의 요구사항과 맞지 않는 경우에도 이 에러가 발생합니다. 예를 들어, SECURITY DEFINER 함수 내부에서 호출되는 중첩 함수가 잘못된 SQL 접근 레벨을 갖고 있을 때, 또는 pg_catalog 스키마의 시스템 함수와 사용자 정의 함수가 충돌할 때 이 에러가 나타납니다.


해결 방법

원인 1 해결: 함수 속성 수정 (NO SQLREADS SQL DATA 또는 MODIFIES SQL DATA)

함수 내부에 SQL이 포함되어 있다면, 함수 정의에서 SQL 접근 속성을 올바르게 변경해야 합니다.

문제가 되는 코드 예시:

-- 잘못된 예: NO SQL로 선언했지만 내부에 SELECT가 존재
CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER
LANGUAGE plpgsql
NO SQL  -- 이 선언이 문제
AS $$
DECLARE
    v_count INTEGER;
BEGIN
    SELECT COUNT(*) INTO v_count FROM users;  -- SQL 실행 시도 → 에러 발생
    RETURN v_count;
END;
$$;

올바른 수정 방법:

-- 수정된 예: SQL을 읽는 함수이므로 READS SQL DATA 사용
CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER
LANGUAGE plpgsql
READS SQL DATA  -- 올바른 속성으로 변경
AS $$
DECLARE
    v_count INTEGER;
BEGIN
    SELECT COUNT(*) INTO v_count FROM users;
    RETURN v_count;
END;
$$;

-- 데이터를 수정하는 경우에는 MODIFIES SQL DATA 사용
CREATE OR REPLACE FUNCTION insert_log(p_message TEXT)
RETURNS VOID
LANGUAGE plpgsql
MODIFIES SQL DATA
AS $$
BEGIN
    INSERT INTO audit_log(message, created_at)
    VALUES (p_message, NOW());
END;
$$;

현재 함수의 SQL 접근 속성을 확인하는 쿼리:

-- 함수의 현재 속성 확인
SELECT 
    proname AS function_name,
    prolang AS language_oid,
    CASE provolatile
        WHEN 'i' THEN 'IMMUTABLE'
        WHEN 's' THEN 'STABLE'
        WHEN 'v' THEN 'VOLATILE'
    END AS volatility,
    CASE prokind
        WHEN 'f' THEN 'FUNCTION'
        WHEN 'p' THEN 'PROCEDURE'
        WHEN 'a' THEN 'AGGREGATE'
    END AS kind,
    prosrc AS source_code
FROM pg_proc
WHERE proname = 'get_user_count'
  AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public');

원인 2 해결: PL/Python 함수에서의 올바른 SQL 실행 방법

PL/Python 함수에서 SQL을 실행할 때는 반드시 plpy 모듈을 사용하고, 함수 정의도 이에 맞게 설정해야 합니다.

-- PL/Python 함수에서 SQL 실행 시 올바른 방법
CREATE OR REPLACE FUNCTION py_get_active_users()
RETURNS TABLE(user_id INT, username TEXT)
LANGUAGE plpython3u
AS $$
    # plpy.execute를 사용하여 SQL 실행
    result = plpy.execute(
        "SELECT user_id, username FROM users WHERE is_active = TRUE"
    )
    return [(row['user_id'], row['username']) for row in result]
$$;

-- 함수의 SQL 데이터 접근 수준을 명시적으로 지정
CREATE OR REPLACE FUNCTION py_update_user_status(p_user_id INT, p_status BOOLEAN)
RETURNS VOID
LANGUAGE plpython3u
AS $$
    plan = plpy.prepare(
        "UPDATE users SET is_active = $1 WHERE user_id = $2",
        ["boolean", "integer"]
    )
    plpy.execute(plan, [p_status, p_user_id])
$$;

원인 3 해결: 트리거 함수 컨텍스트 수정

-- 올바른 트리거 함수 정의 예시
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER
LANGUAGE plpgsql
-- 트리거 함수는 명시적으로 SQL 접근 수준을 지정하는 것이 좋음
AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        INSERT INTO audit_table(
            table_name, 
            operation, 
            new_data, 
            changed_at,
            changed_by
        )
        VALUES (
            TG_TABLE_NAME,
            TG_OP,
            row_to_json(NEW),
            NOW(),
            current_user
        );
        RETURN NEW;
    ELSIF TG_OP = 'DELETE' THEN
        INSERT INTO audit_table(
            table_name, 
            operation, 
            old_data, 
            changed_at,
            changed_by
        )
        VALUES (
            TG_TABLE_NAME,
            TG_OP,
            row_to_json(OLD),
            NOW(),
            current_user
        );
        RETURN OLD;
    END IF;
    RETURN NULL;
END;
$$;

-- 트리거 생성
CREATE TRIGGER users_audit_trigger
AFTER INSERT OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_trigger_function();

예방 방법

1. 함수 생성 시 SQL 접근 속성을 명시적으로 선언하는 코딩 컨벤션 수립

모든 함수와 프로시저를 생성할 때 SQL 접근 수준(NO SQL, CONTAINS SQL, READS SQL DATA, MODIFIES SQL DATA)을 명시적으로 선언하는 팀 내 코딩 가이드라인을 수립하세요. 이 습관은 런타임 에러를 사전에 방지할 뿐만 아니라, PostgreSQL 옵티마이저가 함수 호출 계획을 더 효율적으로 수립할 수 있도록 도와줍니다. 또한 CI/CD 파이프라인에 함수 정의 검증 스크립트를 포함시켜 배포 전 자동으로 속성 일관성을 점검하는 것을 권장합니다.

-- 함수 속성 검증 쿼리 (CI/CD 파이프라인에 활용)
SELECT 
    n.nspname AS schema_name,
    p.proname AS function_name,
    l.lanname AS language,
    p.prosrc AS source
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
JOIN pg_language l ON p.prolang = l.oid
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
  AND l.lanname IN ('plpython3u', 'plperlu', 'plpgsql')
ORDER BY n.nspname, p.proname;

2. 개발 환경에서의 충분한 테스트 및 pg_proc 카탈로그 정기 감사

운영 환경 배포 전 반드시 개발/스테이징 환경에서 모든 함수를 실행 테스트하고, 정기적으로 pg_proc 시스템 카탈로그를 감사하여 잘못된 SQL 접근 속성을 가진 함수를 사전에 탐지하세요. 특히 ORM(Object-Relational Mapping)이나 마이그레이션 툴(Flyway, Liquibase 등)을 사용하는 경우, 자동 생성된 함수의 속성이 의도에 맞는지 반드시 수동으로 검토해야 합니다.


관련 에러

  • 38000 (external_routine_exception): 외부 루틴 실행 중 발생하는 일반적인 예외로, 38001의 상위 에러 클래스에 해당합니다.
  • 38002 (modifying_sql_data_not_permitted): READS SQL DATA로 선언된 함수에서 데이터 수정(INSERT, UPDATE, DELETE)을 시도할 때 발생합니다.
  • 38003 (prohibited_sql_statement_attempted): 허용되지 않은 SQL 구문(예: COMMIT, ROLLBACK 등 트랜잭션 제어 구문)을 부적절한 컨텍스트에서 실행하려 할 때 발생합니다.
  • 38004 (reading_sql_data_not_permitted): SQL 데이터 읽기조차 허용되지 않는 컨텍스트에서 SELECT를 실행하려 할 때 발생합니다.
  • 39P01 (invalid_sqlstate_returned): 외부 루틴에서 잘못된 SQLSTATE를 반환할 때 발생하며, 외부 언어 함수 디버깅 시 함께 확인해야 할 에러입니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기