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

38004
2026년 09월 03일 | DBMS Error 가이드

이 글에서 다루는 내용

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

38004 reading sql data not permitted 는?

PostgreSQL 에러 코드 38004는 reading sql data not permitted 메시지와 함께 발생하며, 함수(Function) 또는 프로시저(Procedure) 내부에서 SQL 데이터를 읽는 작업이 허용되지 않을 때 나타납니다. 이 에러는 주로 함수의 READS SQL DATA 또는 NO SQL 속성이 잘못 설정된 경우, 혹은 특정 보안 컨텍스트에서 SQL 조회가 제한될 때 발생합니다. 특히 PL/pgSQL, PL/Python, PL/Perl 등 다양한 절차적 언어 함수에서 데이터 접근 권한이 함수 정의 수준에서 통제될 때 자주 마주치게 되는 에러입니다.


주요 발생 원인

1. 함수의 데이터 접근 속성(Data Access Privilege)이 잘못 설정된 경우

PostgreSQL 함수를 생성할 때 NO SQL, CONTAINS SQL, READS SQL DATA, MODIFIES SQL DATA 등의 속성을 명시할 수 있습니다. 만약 함수가 NO SQL로 선언되었음에도 불구하고 내부에서 SELECT 또는 다른 SQL 읽기 작업을 수행하려 하면 38004 에러가 발생합니다. 이는 함수 선언과 실제 동작이 불일치하는 상황에서 PostgreSQL이 보안 정책에 따라 실행을 차단하는 것입니다.

2. SECURITY DEFINER 함수에서의 권한 충돌

SECURITY DEFINER 속성으로 생성된 함수는 함수를 호출한 사용자가 아닌 함수 소유자의 권한으로 실행됩니다. 함수 소유자에게 특정 테이블이나 뷰에 대한 읽기(SELECT) 권한이 없거나, 접근이 Row-Level Security(RLS) 정책에 의해 차단된 경우 이 에러가 발생할 수 있습니다. 소유자 권한과 실행 컨텍스트 간의 불일치는 실무에서 자주 놓치기 쉬운 원인 중 하나입니다.

3. Trusted/Untrusted 언어 함수에서의 보안 제한

PostgreSQL에서 PL/Perl, PL/Python 등 외부 언어로 작성된 함수는 trusteduntrusted 버전이 존재합니다. Untrusted 언어(예: plpythonu, plperlu)로 작성된 함수는 슈퍼유저만 생성할 수 있으며, 이러한 함수 내에서 SQL 데이터를 읽으려 할 때 데이터베이스 보안 정책 또는 설정에 따라 읽기 작업이 거부될 수 있습니다. 특히 클라우드 환경(AWS RDS, Azure Database for PostgreSQL 등)에서는 슈퍼유저 권한이 제한되어 있어 이 문제가 더 자주 발생합니다.


해결 방법

원인 1 해결: 함수의 데이터 접근 속성 수정

함수 정의 시 데이터 읽기가 필요하다면 READS SQL DATA 또는 기본 설정으로 변경하세요. PostgreSQL에서는 명시적으로 이 속성을 지정하지 않아도 되지만, 다른 DBMS에서 마이그레이션한 경우 속성 정의가 충돌할 수 있습니다.

-- 잘못된 함수 정의 예시 (NO SQL로 선언했지만 SELECT 포함)
CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
BEGIN
    -- 아래 SELECT는 NO SQL 속성과 충돌하여 에러 발생
    RETURN (SELECT COUNT(*) FROM users);
END;
$$;

-- 올바른 함수 정의 예시
-- PostgreSQL에서는 함수 속성을 아래와 같이 명시할 수 있음
CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER
LANGUAGE plpgsql
-- READS SQL DATA를 명시하거나 기본값(CONTAINS SQL) 사용
AS $$
DECLARE
    v_count INTEGER;
BEGIN
    SELECT COUNT(*) INTO v_count FROM users;
    RETURN v_count;
END;
$$;

-- 함수 속성 확인 방법
SELECT proname, prosrc, provolatile, prosecdef
FROM pg_proc
WHERE proname = 'get_user_count';

원인 2 해결: SECURITY DEFINER 함수 권한 재설정

SECURITY DEFINER 함수의 소유자에게 필요한 권한을 부여하고, RLS 정책도 함께 검토해야 합니다.

-- SECURITY DEFINER 함수 예시
CREATE OR REPLACE FUNCTION get_sensitive_data(p_user_id INT)
RETURNS TABLE(id INT, name TEXT, email TEXT)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public  -- 보안을 위해 search_path 명시 권장
AS $$
BEGIN
    RETURN QUERY
    SELECT u.id, u.name, u.email
    FROM users u
    WHERE u.id = p_user_id;
END;
$$;

-- 함수 소유자에게 테이블 읽기 권한 부여
GRANT SELECT ON TABLE users TO function_owner_role;

-- 함수 소유자 확인
SELECT proname, pg_get_userbyid(proowner) AS owner
FROM pg_proc
WHERE proname = 'get_sensitive_data';

-- RLS가 활성화된 경우 확인
SELECT tablename, rowsecurity
FROM pg_tables
WHERE tablename = 'users';

-- 함수 소유자에 대한 RLS 정책 예외 설정 (필요한 경우)
ALTER TABLE users FORCE ROW LEVEL SECURITY;

CREATE POLICY allow_function_owner
ON users
TO function_owner_role
USING (true);  -- 소유자에게 모든 행 접근 허용

원인 3 해결: 언어 권한 및 함수 재작성

Trusted 언어를 사용하거나, 필요한 권한을 적절히 부여하는 방식으로 해결합니다.

-- Untrusted Python 대신 Trusted PL/pgSQL 사용 예시
-- 기존 (문제가 될 수 있는 untrusted 언어 사용)
-- CREATE FUNCTION read_data() RETURNS TEXT LANGUAGE plpythonu AS $$
-- import plpy
-- result = plpy.execute("SELECT name FROM users LIMIT 1")
-- return result[0]['name']
-- $$;

-- 개선된 버전: plpgsql 사용
CREATE OR REPLACE FUNCTION read_data()
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
    v_name TEXT;
BEGIN
    SELECT name INTO v_name FROM users LIMIT 1;
    RETURN v_name;
END;
$$;

-- 특정 역할에 언어 사용 권한 부여 (슈퍼유저 필요)
GRANT USAGE ON LANGUAGE plpgsql TO app_user;

-- 현재 설치된 언어 및 권한 확인
SELECT lanname, lanpltrusted, lanacl
FROM pg_language
WHERE lanname IN ('plpgsql', 'plpython3u', 'plperlu');

예방 방법

1. 함수 생성 시 명시적 속성 정의 및 코드 리뷰 프로세스 도입

모든 함수를 생성하거나 수정할 때는 데이터 접근 속성, 보안 컨텍스트(SECURITY DEFINER vs SECURITY INVOKER), 실행 언어를 팀 내 코드 리뷰를 통해 검증하는 프로세스를 도입하세요. 특히 마이그레이션 작업 시에는 원본 DBMS의 속성이 PostgreSQL과 다를 수 있으므로 반드시 변환 검증을 수행해야 합니다.

-- 함수 생성 템플릿 (Best Practice)
CREATE OR REPLACE FUNCTION schema_name.function_name(
    p_param1 TYPE,
    p_param2 TYPE
)
RETURNS RETURN_TYPE
LANGUAGE plpgsql
SECURITY INVOKER           -- 기본적으로 호출자 권한 사용 권장
SET search_path = schema_name  -- search_path 명시로 보안 강화
STABLE                     -- 또는 VOLATILE, IMMUTABLE 적절히 선택
AS $$
DECLARE
    -- 변수 선언
BEGIN
    -- 로직 구현
END;
$$;

2. 정기적인 함수 권한 감사(Audit) 자동화

데이터베이스 내 모든 함수의 권한, 소유자, 보안 속성을 정기적으로 점검하는 쿼리를 스케줄링하여 실행하고, 이상 징후를 조기에 발견하세요.

-- 함수 권한 전체 감사 쿼리
SELECT
    n.nspname AS schema_name,
    p.proname AS function_name,
    pg_get_userbyid(p.proowner) AS owner,
    p.prosecdef AS is_security_definer,
    p.provolatile AS volatility,
    l.lanname AS language,
    p.proacl AS access_privileges
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')
ORDER BY n.nspname, p.proname;

-- SECURITY DEFINER 함수만 필터링하여 집중 감사
SELECT
    n.nspname AS schema_name,
    p.proname AS function_name,
    pg_get_userbyid(p.proowner) AS owner
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.prosecdef = true
  AND n.nspname NOT IN ('pg_catalog', 'information_schema');

관련 에러

  • 38000 (external_routine_exception): 외부 루틴 실행 중 일반적인 예외가 발생했을 때 나타나는 상위 에러 코드입니다. 38004는 이 카테고리의 하위 에러입니다.
  • 38001 (containing_sql_not_permitted): SQL을 포함할 수 없는 컨텍스트에서 SQL 구문을 사용하려 할 때 발생하며, 38004와 유사한 맥락에서 나타납니다.
  • 38002 (modifying_sql_data_not_permitted): 읽기가 아닌 데이터 수정(INSERT, UPDATE, DELETE)이 허용되지 않는 컨텍스트에서 발생하는 에러로, 38004의 쓰기 버전에 해당합니다.
  • 42501 (insufficient_privilege): 권한 부족으로 인해 특정 객체에 접근할 수 없을 때 발생하며, SECURITY DEFINER 함수 관련 문제 해결 시 함께 확인해야 할 에러입니다.
  • 39P01 (trigger_protocol_violated): 트리거 내에서 잘못된 SQL 접근이 발생할 때 나타날 수 있으며, 함수 기반 트리거에서 38004와 연관될 수 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기