2026년 09월 01일 | DBMS Error 가이드
이 글에서 다루는 내용
2F004 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
2F004 reading sql data not permitted 는?
PostgreSQL 에러 코드 2F004 (reading sql data not permitted)는 함수 또는 프로시저 내부에서 SQL 데이터를 읽는 작업이 해당 함수의 선언된 특성(volatility 또는 data access level)과 충돌할 때 발생하는 에러입니다. 쉽게 말해, NO SQL 또는 CONTAINS SQL 속성으로 정의된 함수 내부에서 SELECT와 같이 데이터를 읽는 SQL 문을 실행하려 할 때 PostgreSQL이 이를 차단하면서 발생합니다. 주로 PL/pgSQL, PL/Python, PL/Java 등 Procedural Language로 작성된 함수에서 발생하며, 함수의 데이터 접근 제약 조건을 잘못 설정했을 때 실무에서 자주 마주치게 됩니다.
주요 발생 원인
1. 함수 선언 시 잘못된 데이터 접근 레벨 지정
PostgreSQL 함수는 NO SQL, CONTAINS SQL, READS SQL DATA, MODIFIES SQL DATA 중 하나의 데이터 접근 레벨을 가질 수 있습니다. 함수 내부에서 테이블을 조회하는 SELECT 문을 사용하면서 함수 선언부에 NO SQL 또는 CONTAINS SQL을 명시한 경우, PostgreSQL은 이 불일치를 감지하고 2F004 에러를 발생시킵니다. 이는 특히 타 DBMS(Oracle, MySQL 등)에서 PostgreSQL로 마이그레이션할 때 함수 정의 구문 차이를 이해하지 못하고 그대로 변환할 때 빈번하게 발생합니다.
2. PL/pgSQL 함수 내 외부 함수 호출 시 접근 레벨 충돌
어떤 함수가 NO SQL 속성으로 선언되었는데, 그 함수의 내부에서 SQL 데이터를 읽는 다른 함수를 호출하는 경우에도 이 에러가 발생할 수 있습니다. PostgreSQL은 함수 호출 스택 전체를 고려하여 데이터 접근 레벨을 검사하기 때문에, 겉으로는 단순해 보이는 함수 호출이 내부적으로는 데이터 읽기를 유발하는 경우를 잡아냅니다. 복잡한 함수 체인이 있는 레거시 코드베이스에서 특히 발견하기 어려운 원인입니다.
3. Trigger 함수 또는 특수 목적 함수의 잘못된 속성 설정
트리거 함수나 시스템 레벨에서 호출되는 함수를 정의할 때, 성능 최적화를 목적으로 불필요하게 NO SQL 속성을 부여하는 경우가 있습니다. 이런 경우 트리거 로직 내부에서 참조 테이블(lookup table)을 조회하거나 다른 테이블의 데이터를 읽어야 하는 상황이 오면 즉시 2F004 에러가 발생합니다. 개발 초기에는 문제가 없다가 요구사항이 추가되면서 함수 내부에 SELECT 문이 추가될 때 뒤늦게 발견되는 패턴입니다.
해결 방법
원인 1 해결: 함수의 데이터 접근 레벨을 올바르게 수정
SQL 데이터를 읽는 함수라면 READS SQL DATA 또는 PL/pgSQL에서는 기본값인 속성을 사용해야 합니다. 기존에 잘못 선언된 함수를 ALTER FUNCTION 또는 CREATE OR REPLACE FUNCTION으로 수정하세요.
-- 문제가 있는 함수 정의 (NO SQL 로 잘못 선언)
CREATE OR REPLACE FUNCTION get_user_name(p_user_id INT)
RETURNS TEXT
LANGUAGE plpgsql
NO SQL -- ❌ 잘못된 선언: 내부에서 SELECT를 사용하는데 NO SQL로 지정
AS $$
DECLARE
v_name TEXT;
BEGIN
SELECT name INTO v_name
FROM users
WHERE user_id = p_user_id;
RETURN v_name;
END;
$$;
-- 올바르게 수정된 함수 정의
CREATE OR REPLACE FUNCTION get_user_name(p_user_id INT)
RETURNS TEXT
LANGUAGE plpgsql
READS SQL DATA -- ✅ 올바른 선언: SQL 데이터 읽기를 허용
STABLE -- 동일 트랜잭션 내 동일 입력에 대해 동일 결과 반환
AS $$
DECLARE
v_name TEXT;
BEGIN
SELECT name INTO v_name
FROM users
WHERE user_id = p_user_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'User with id % not found', p_user_id;
END IF;
RETURN v_name;
END;
$$;
-- 현재 함수의 속성 확인 방법
SELECT
p.proname AS function_name,
p.provolatile AS volatility,
p.proparallel AS parallel_safety,
CASE p.provolatile
WHEN 'i' THEN 'IMMUTABLE'
WHEN 's' THEN 'STABLE'
WHEN 'v' THEN 'VOLATILE'
END AS volatility_desc
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.proname = 'get_user_name'
AND n.nspname = 'public';
원인 2 해결: 함수 호출 체인의 접근 레벨 통일
중첩 호출 구조에서 각 함수의 데이터 접근 레벨이 일관되도록 수정합니다.
-- 문제 상황: 외부 함수가 NO SQL인데 내부에서 SQL 읽기 함수 호출
CREATE OR REPLACE FUNCTION inner_lookup(p_code TEXT)
RETURNS INT
LANGUAGE plpgsql
READS SQL DATA
AS $$
DECLARE
v_id INT;
BEGIN
SELECT id INTO v_id
FROM code_master
WHERE code = p_code;
RETURN v_id;
END;
$$;
-- ❌ 이 함수는 inner_lookup을 호출하므로 NO SQL이 될 수 없음
CREATE OR REPLACE FUNCTION outer_process(p_code TEXT)
RETURNS TEXT
LANGUAGE plpgsql
NO SQL -- ❌ 에러 원인
AS $$
BEGIN
RETURN 'ID: ' || inner_lookup(p_code)::TEXT;
END;
$$;
-- ✅ 올바른 수정: outer_process도 READS SQL DATA로 변경
CREATE OR REPLACE FUNCTION outer_process(p_code TEXT)
RETURNS TEXT
LANGUAGE plpgsql
READS SQL DATA -- ✅ 내부 호출 함수와 레벨 일치
AS $$
BEGIN
RETURN 'ID: ' || inner_lookup(p_code)::TEXT;
END;
$$;
원인 3 해결: 트리거 함수의 속성 재검토
-- ❌ 잘못된 트리거 함수 (NO SQL인데 내부에서 참조 테이블 조회)
CREATE OR REPLACE FUNCTION trg_validate_order()
RETURNS TRIGGER
LANGUAGE plpgsql
NO SQL -- ❌ 트리거 함수에서 참조 테이블을 읽으므로 잘못된 설정
AS $$
BEGIN
-- 참조 테이블에서 유효성 검사 (NO SQL이므로 2F004 에러 발생)
IF NOT EXISTS (
SELECT 1 FROM product_catalog WHERE product_id = NEW.product_id
) THEN
RAISE EXCEPTION 'Invalid product_id: %', NEW.product_id;
END IF;
RETURN NEW;
END;
$$;
-- ✅ 올바른 트리거 함수
CREATE OR REPLACE FUNCTION trg_validate_order()
RETURNS TRIGGER
LANGUAGE plpgsql
-- 트리거 함수는 기본적으로 VOLATILE이며 SQL 접근 제한을 별도 명시하지 않는 것이 안전
AS $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM product_catalog WHERE product_id = NEW.product_id
) THEN
RAISE EXCEPTION 'Invalid product_id: %', NEW.product_id;
END IF;
RETURN NEW;
END;
$$;
-- 트리거 재생성
DROP TRIGGER IF EXISTS validate_order_trigger ON orders;
CREATE TRIGGER validate_order_trigger
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION trg_validate_order();
예방 방법
1. 함수 생성 표준 템플릿 및 코드 리뷰 체크리스트 운영
팀 내에서 PostgreSQL 함수를 작성할 때 사용할 표준 템플릿을 미리 정의하고, 함수 내부에서 SQL을 사용하는 경우 반드시 READS SQL DATA 또는 MODIFIES SQL DATA를 명시하도록 코드 리뷰 체크리스트에 포함시켜야 합니다. 또한 CI/CD 파이프라인에 pg_dump를 활용한 함수 정의 검사 스크립트를 추가하거나, pg_catalog.pg_proc 뷰를 주기적으로 조회하여 SQL 접근이 필요한 함수가 올바른 속성을 가지고 있는지 자동으로 검증하는 루틴을 구축하는 것이 좋습니다.
-- 잠재적으로 잘못 설정된 함수를 사전에 탐지하는 쿼리
-- (prosrc에 SELECT가 포함되어 있으나 volatile이 IMMUTABLE인 경수 등 검사)
SELECT
n.nspname AS schema_name,
p.proname AS function_name,
CASE p.provolatile
WHEN 'i' THEN 'IMMUTABLE'
WHEN 's' THEN 'STABLE'
WHEN 'v' THEN 'VOLATILE'
END AS volatility,
p.prosrc AS function_body
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
AND p.provolatile = 'i' -- IMMUTABLE로 선언된 함수
AND p.prosrc ILIKE '%SELECT%' -- 내부에 SELECT 포함
ORDER BY n.nspname, p.proname;
2. 개발/스테이징 환경에서 충분한 통합 테스트 수행
프로덕션 배포 전 개발 또는 스테이징 환경에서 함수를 실제 데이터와 함께 실행하는 통합 테스트를 의무화해야 합니다. 특히 pgTAP 같은 PostgreSQL 전용 테스트 프레임워크를 활용하면 함수의 동작을 선언적으로 검증할 수 있으며, 에러 코드 2F004를 포함한 다양한 런타임 에러를 배포 전 단계에서 조기에 발견할 수 있습니다.
관련 에러
- 2F000 (sql_routine_exception): 2F 클래스의 부모 에러로, SQL 루틴 실행 중 발생하는 일반적인 예외입니다. 2F004는 이 에러 클래스의 하위 항목입니다.
- 2F002 (modifying_sql_data_not_permitted):
READS SQL DATA로 선언된 함수에서 INSERT/UPDATE/DELETE 등 데이터 변경 작업을 시도할 때 발생합니다. 2F004와 반대 방향의 접근 레벨 위반입니다. - 2F003 (prohibited_sql_statement_attempted): 함수 내에서 허용되지 않는 SQL 구문(예: 트랜잭션 제어 문)을 실행할 때 발생하며, 2F004와 함께 SQL 루틴 제약 위반의 대표적인 에러 코드입니다.
- 42P13 (invalid_function_definition): 함수 정의 자체가 문법적으로 잘못되었을 때 발생하는 에러로, 데이터 접근 레벨을 잘못 조합한 경우 함수 생성 단계에서 이 에러가 먼저 나타날 수 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.