2026년 06월 30일 | DBMS Error 가이드
이 글에서 다루는 내용
38004 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
38004 reading sql data not permitted 는?
PostgreSQL 에러 코드 38004(reading sql data not permitted)는 PL/pgSQL 또는 다른 프로시저 언어로 작성된 함수나 프로시저가 선언된 SQL 데이터 접근 레벨보다 더 높은 수준의 데이터 읽기를 시도할 때 발생합니다. 쉽게 말해, 함수가 NO SQL 또는 CONTAINS SQL로 선언되어 있는데 내부에서 SELECT 등의 SQL 데이터 읽기 구문을 실행하려 할 때 이 에러가 트리거됩니다. 이는 SQL 표준의 함수 데이터 접근 레벨 제한을 PostgreSQL이 엄격히 적용하는 과정에서 발생하는 에러입니다.
주요 발생 원인
- 함수 선언 시 잘못된 데이터 접근 레벨 지정
함수를 생성할 때 CONTAINS SQL 또는 NO SQL 옵션을 명시했지만, 함수 본문 내에서 테이블 데이터를 읽는 SELECT 문을 사용하는 경우 이 에러가 발생합니다. PostgreSQL은 함수 선언 시 명시된 데이터 접근 레벨과 실제 함수 본문의 동작이 일치하지 않을 때 이를 감지하고 실행을 차단합니다. 특히 외부 시스템(예: Oracle에서 마이그레이션)에서 가져온 함수 정의를 그대로 사용할 때 자주 발생합니다.
- PL/Python, PL/Perl 등 비표준 언어 함수에서의 SQL 실행 제약
PL/Python(plpython3u)이나 PL/Perl(plperlu) 같은 Untrusted 언어로 작성된 함수에서 내부적으로 SQL 데이터를 읽으려 할 때, 함수의 선언된 접근 레벨과 충돌하면 이 에러가 발생할 수 있습니다. 이러한 언어들은 PostgreSQL의 내부 함수 보안 메커니즘과 상호작용하는 방식이 다르기 때문에, 접근 레벨을 명확히 설정하지 않으면 예상치 못한 에러를 유발합니다.
- 저장 프로시저(Stored Procedure) 또는 트리거 함수의 잘못된 옵션 설정
트리거 함수나 저장 프로시저를 정의할 때 데이터 접근 레벨을 잘못 설정하면, 실제 실행 단계에서 38004 에러가 발생합니다. 특히 레거시 코드를 유지보수하거나 ORM(Object-Relational Mapping) 도구가 자동으로 생성한 함수에서 이 문제가 빈번히 관찰됩니다. 개발 환경에서는 정상 동작하다가 운영 환경에서 갑자기 에러가 발생하는 경우도 있어 디버깅이 어렵습니다.
해결 방법
원인 1 해결: 함수 선언 시 올바른 데이터 접근 레벨로 수정
가장 일반적인 해결책은 함수 정의에서 데이터 접근 레벨을 READS SQL DATA로 명시적으로 변경하는 것입니다.
문제가 되는 코드 예시:
-- 잘못된 선언: CONTAINS SQL은 데이터 읽기를 허용하지 않음
CREATE OR REPLACE FUNCTION get_employee_salary(emp_id INT)
RETURNS NUMERIC
LANGUAGE plpgsql
CONTAINS SQL -- 이 선언이 문제!
AS $$
DECLARE
v_salary NUMERIC;
BEGIN
SELECT salary INTO v_salary
FROM employees
WHERE id = emp_id;
RETURN v_salary;
END;
$$;
올바른 수정 코드:
-- 올바른 선언: READS SQL DATA로 변경
CREATE OR REPLACE FUNCTION get_employee_salary(emp_id INT)
RETURNS NUMERIC
LANGUAGE plpgsql
READS SQL DATA -- SQL 데이터 읽기를 명시적으로 허용
AS $$
DECLARE
v_salary NUMERIC;
BEGIN
SELECT salary INTO v_salary
FROM employees
WHERE id = emp_id;
RETURN v_salary;
END;
$$;
-- 함수 테스트
SELECT get_employee_salary(101);
원인 2 해결: PL/Python 함수의 접근 레벨 수정
-- 잘못된 PL/Python 함수 선언
CREATE OR REPLACE FUNCTION py_get_count()
RETURNS INT
LANGUAGE plpython3u
NO SQL -- 데이터를 읽으면서 NO SQL은 모순
AS $$
rv = plpy.execute("SELECT COUNT(*) FROM orders")
return rv[0]['count']
$$;
-- 올바른 PL/Python 함수 선언
CREATE OR REPLACE FUNCTION py_get_count()
RETURNS INT
LANGUAGE plpython3u
READS SQL DATA -- 데이터 읽기 허용으로 변경
AS $$
rv = plpy.execute("SELECT COUNT(*) FROM orders")
return rv[0]['count']
$$;
-- 실행 확인
SELECT py_get_count();
원인 3 해결: 트리거 함수 및 저장 프로시저 수정
-- 기존 함수 접근 레벨 확인 방법
SELECT
p.proname AS function_name,
p.prokind AS kind,
CASE p.provolatile
WHEN 'i' THEN 'IMMUTABLE'
WHEN 's' THEN 'STABLE'
WHEN 'v' THEN 'VOLATILE'
END AS volatility,
CASE p.proparallel
WHEN 's' THEN 'SAFE'
WHEN 'r' THEN 'RESTRICTED'
WHEN 'u' THEN 'UNSAFE'
END AS parallel
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname = 'public'
AND p.proname = 'your_function_name';
-- 저장 프로시저에서 데이터 읽기가 필요한 경우
CREATE OR REPLACE PROCEDURE audit_order_status(order_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
v_status TEXT;
v_customer TEXT;
BEGIN
-- 데이터 읽기
SELECT o.status, c.name
INTO v_status, v_customer
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.id = order_id;
-- 감사 로그 기록
INSERT INTO audit_log (event_time, event_desc)
VALUES (NOW(), format('Order %s for %s has status: %s',
order_id, v_customer, v_status));
COMMIT;
END;
$$;
-- 프로시저 호출
CALL audit_order_status(1001);
빠른 진단 쿼리
-- 현재 데이터베이스의 모든 함수와 데이터 접근 레벨 확인
SELECT
n.nspname AS schema_name,
p.proname AS function_name,
l.lanname AS language,
pg_get_function_arguments(p.oid) AS arguments,
pg_get_functiondef(p.oid) AS definition
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;
예방 방법
- 함수 생성 시 데이터 접근 레벨을 항상 명시적으로 선언하기
함수를 생성할 때 데이터 접근 패턴을 미리 분석하여 NO SQL, CONTAINS SQL, READS SQL DATA, MODIFIES SQL DATA 중 적절한 레벨을 항상 명시적으로 선언하는 습관을 들이세요. 기본값에 의존하지 말고, 코드 리뷰 체크리스트에 “데이터 접근 레벨 확인” 항목을 추가하면 실수를 방지할 수 있습니다. CI/CD 파이프라인에서 함수 정의 파일을 정적 분석하는 스크립트를 추가하는 것도 좋은 방법입니다.
- 마이그레이션 및 배포 전 함수 접근 레벨 일괄 검증 자동화
데이터베이스 마이그레이션이나 새로운 기능 배포 전에, 모든 함수의 선언된 접근 레벨과 실제 함수 본문의 SQL 사용 패턴이 일치하는지 자동으로 검증하는 스크립트를 운영하세요. 아래와 같은 검증 쿼리를 배포 파이프라인에 포함시키면 운영 환경에서의 에러를 사전에 방지할 수 있습니다.
-- 배포 전 검증: SELECT를 포함하지만 READS SQL DATA가 아닌 함수 탐지
SELECT
n.nspname AS schema_name,
p.proname AS function_name,
pg_get_functiondef(p.oid) AS definition
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname = 'public'
AND pg_get_functiondef(p.oid) ILIKE '%SELECT%'
AND pg_get_functiondef(p.oid) NOT ILIKE '%READS SQL DATA%'
ORDER BY p.proname;
관련 에러
- 38000 (
external routine exception): 외부 루틴 실행 중 발생하는 일반적인 에러로, 38004와 같은 카테고리(Class 38)에 속합니다. - 38001 (
containing sql not permitted): SQL 실행 자체가 허용되지 않는 컨텍스트에서 SQL을 실행하려 할 때 발생합니다. - 38002 (
modifying sql data not permitted): 38004와 유사하지만, 데이터 수정(INSERT/UPDATE/DELETE)이 허용되지 않는 함수에서 데이터를 변경하려 할 때 발생합니다. - 39004 (
null value not allowed): 프로시저나 함수의 반환값이나 파라미터에서 NULL이 허용되지 않을 때 발생하는 관련 에러입니다. - 42501 (
insufficient_privilege): 데이터 접근 레벨과 별개로, 실제 테이블에 대한 권한이 없을 때 발생하며 종종 혼동될 수 있는 에러입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.