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

2F000
2026년 08월 31일 | DBMS Error 가이드

이 글에서 다루는 내용

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

2F000 sql routine exception 는?

PostgreSQL 에러 코드 2F000 (sql_routine_exception) 은 SQL 함수(FUNCTION) 또는 프로시저(PROCEDURE) 내부에서 예외가 발생했을 때 나타나는 에러입니다. 이 에러는 PL/pgSQL이 아닌 SQL 언어로 작성된 루틴에서 발생하며, 함수 본문의 SQL 구문이 실행 중 런타임 오류를 일으킬 때 트리거됩니다. 주로 함수 내부의 잘못된 데이터 처리, 제약 조건 위반, 또는 반환값 불일치 등의 상황에서 발생하며, 상위 에러 클래스 2F(SQL Routine Exception)의 기본 에러 코드로 분류됩니다.


주요 발생 원인

1. SQL 함수 내 반환 타입 불일치 (Return Type Mismatch)

SQL 함수에서 선언된 반환 타입과 실제 반환되는 값의 데이터 타입이 일치하지 않을 때 발생합니다. 예를 들어 RETURNS INTEGER로 선언된 함수가 내부적으로 TEXT 값을 반환하려 할 경우, PostgreSQL은 암묵적 형변환이 불가능하면 이 에러를 발생시킵니다. 특히 함수를 수정하면서 반환 타입을 변경하지 않거나, 복잡한 쿼리에서 컬럼 순서가 바뀌는 경우 자주 발생합니다.

2. SQL 함수 내부에서 NULL 처리 오류 또는 제약 조건 위반

함수 내부에서 실행되는 DML(INSERT, UPDATE, DELETE) 구문이 테이블의 NOT NULL 제약, UNIQUE 제약, FOREIGN KEY 제약 등을 위반할 때 이 에러가 발생할 수 있습니다. SQL 언어 루틴은 PL/pgSQL과 달리 EXCEPTION 블록을 통한 내부 예외 처리가 불가능하기 때문에, 에러가 그대로 상위 호출자에게 전파됩니다. 이는 배치 처리나 대량 데이터 삽입 함수에서 특히 위험한 패턴입니다.

3. 재귀 SQL 함수에서의 무한 루프 또는 스택 오버플로우

SQL 언어로 작성된 재귀 함수가 종료 조건을 잘못 설정하거나 누락한 경우, 스택 오버플로우가 발생하면서 2F000 계열의 에러가 트리거될 수 있습니다. PostgreSQL의 SQL 함수는 인라인 확장(inlining)이 가능하지만, 재귀 호출 시에는 이 최적화가 비활성화되며 호출 깊이에 따라 메모리 문제로 이어집니다. max_stack_depth 파라미터 설정과 무관하게 잘못된 재귀 로직 자체가 근본 원인이 됩니다.


해결 방법

원인 1: 반환 타입 불일치 해결

먼저 함수의 반환 타입을 확인하고, 실제 반환값과 일치하도록 수정합니다.

-- 문제가 있는 함수 예시
CREATE OR REPLACE FUNCTION get_user_age(p_user_id INT)
RETURNS INTEGER
LANGUAGE SQL
AS $$
    SELECT name FROM users WHERE user_id = p_user_id; -- TEXT를 반환하지만 INTEGER로 선언됨
$$;

-- 해결 방법 1: 반환 타입을 실제 값에 맞게 수정
CREATE OR REPLACE FUNCTION get_user_name(p_user_id INT)
RETURNS TEXT
LANGUAGE SQL
AS $$
    SELECT name FROM users WHERE user_id = p_user_id;
$$;

-- 해결 방법 2: 명시적 형변환(CAST) 사용
CREATE OR REPLACE FUNCTION get_user_age(p_user_id INT)
RETURNS INTEGER
LANGUAGE SQL
AS $$
    SELECT CAST(age AS INTEGER) FROM users WHERE user_id = p_user_id;
$$;

-- 함수 반환 타입 확인 쿼리
SELECT routine_name, data_type
FROM information_schema.routines
WHERE routine_schema = 'public'
  AND routine_type = 'FUNCTION';

원인 2: 제약 조건 위반 해결

SQL 함수 내 DML 실행 시 제약 조건을 사전에 검증하거나, PL/pgSQL로 변환하여 예외 처리를 추가합니다.

-- 문제가 있는 SQL 함수 (제약 조건 위반 가능)
CREATE OR REPLACE FUNCTION insert_user(p_email TEXT, p_name TEXT)
RETURNS VOID
LANGUAGE SQL
AS $$
    INSERT INTO users (email, name) VALUES (p_email, p_name);
$$;

-- 해결 방법: PL/pgSQL로 변환하여 예외 처리 추가
CREATE OR REPLACE FUNCTION insert_user_safe(p_email TEXT, p_name TEXT)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
BEGIN
    -- 중복 이메일 사전 확인
    IF EXISTS (SELECT 1 FROM users WHERE email = p_email) THEN
        RETURN '이미 존재하는 이메일입니다: ' || p_email;
    END IF;

    INSERT INTO users (email, name) VALUES (p_email, p_name);
    RETURN '사용자가 성공적으로 등록되었습니다.';

EXCEPTION
    WHEN unique_violation THEN
        RETURN 'UNIQUE 제약 조건 위반: ' || SQLERRM;
    WHEN not_null_violation THEN
        RETURN 'NOT NULL 제약 조건 위반: ' || SQLERRM;
    WHEN OTHERS THEN
        RETURN '알 수 없는 오류: ' || SQLERRM;
END;
$$;

-- 테스트 실행
SELECT insert_user_safe('test@example.com', 'Hong Gildong');
SELECT insert_user_safe(NULL, 'Hong Gildong'); -- NOT NULL 위반 테스트

원인 3: 재귀 함수 무한 루프 해결

재귀 종료 조건을 명확히 하고, WITH RECURSIVE를 사용하는 방식으로 전환합니다.

-- 문제가 있는 재귀 SQL 함수 (종료 조건 누락)
CREATE OR REPLACE FUNCTION bad_factorial(n INT)
RETURNS BIGINT
LANGUAGE SQL
AS $$
    SELECT n * bad_factorial(n - 1); -- 종료 조건 없음! 무한 재귀 발생
$$;

-- 해결 방법 1: PL/pgSQL로 전환하여 종료 조건 추가
CREATE OR REPLACE FUNCTION safe_factorial(n INT)
RETURNS BIGINT
LANGUAGE plpgsql
AS $$
BEGIN
    IF n < 0 THEN
        RAISE EXCEPTION '음수는 팩토리얼 계산이 불가합니다: %', n;
    END IF;
    IF n = 0 OR n = 1 THEN
        RETURN 1;
    END IF;
    RETURN n * safe_factorial(n - 1);
END;
$$;

-- 해결 방법 2: WITH RECURSIVE를 활용한 비재귀적 접근 (계층 구조 탐색)
WITH RECURSIVE category_tree AS (
    -- 기저 케이스 (Base Case)
    SELECT id, name, parent_id, 0 AS depth
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    -- 재귀 케이스 (Recursive Case) - 종료 조건이 자동으로 관리됨
    SELECT c.id, c.name, c.parent_id, ct.depth + 1
    FROM categories c
    INNER JOIN category_tree ct ON c.parent_id = ct.id
    WHERE ct.depth < 10  -- 최대 깊이 제한으로 안전장치 추가
)
SELECT * FROM category_tree ORDER BY depth, name;

-- 현재 스택 깊이 설정 확인
SHOW max_stack_depth;

예방 방법

1. 복잡한 비즈니스 로직은 PL/pgSQL로 작성하라

SQL 언어 함수는 단순한 단일 쿼리 래퍼에 적합하며, 조건 분기, 예외 처리, 트랜잭션 제어가 필요한 복잡한 로직에는 반드시 PL/pgSQL 또는 PL/Python 등의 절차형 언어를 사용해야 합니다. 함수 생성 시 STRICT 옵션을 활용하면 입력값이 NULL일 경우 자동으로 NULL을 반환하여 불필요한 내부 실행을 방지할 수 있습니다.

-- STRICT 옵션 활용 예시 (NULL 입력 시 자동으로 NULL 반환)
CREATE OR REPLACE FUNCTION divide(a NUMERIC, b NUMERIC)
RETURNS NUMERIC
LANGUAGE SQL
STRICT  -- a 또는 b가 NULL이면 함수 본문 실행 없이 NULL 반환
AS $$
    SELECT a / b;
$$;

2. 함수 배포 전 단위 테스트와 pgTAP 활용

운영 환경에 함수를 배포하기 전, pgTAP 프레임워크를 사용하여 반환 타입, 경계값, 예외 케이스를 자동화된 테스트로 검증하는 체계를 갖추어야 합니다. 특히 함수의 시그니처(파라미터 타입, 반환 타입)가 변경될 경우, 해당 함수를 호출하는 모든 상위 함수와 뷰를 pg_depend 카탈로그를 통해 사전에 파악하고 영향도를 분석해야 합니다.

-- 함수 의존성 확인 쿼리 (배포 전 영향도 분석)
SELECT
    dependent_obj.relname AS dependent_object,
    pg_describe_object(d.classid, d.objid, d.objsubid) AS object_description,
    pg_describe_object(d.refclassid, d.refobjid, 0) AS referenced_object
FROM pg_depend d
JOIN pg_class dependent_obj ON d.objid = dependent_obj.oid
WHERE d.refobjid = (
    SELECT oid FROM pg_proc WHERE proname = 'your_function_name'
);

관련 에러

  • 2F002 (modifying_sql_data_not_permitted): READ ONLY로 선언된 SQL 함수 내에서 데이터 변경(INSERT/UPDATE/DELETE)을 시도할 때 발생합니다.
  • 2F003 (prohibited_sql_statement_attempted): SQL 함수 내에서 허용되지 않는 SQL 구문(예: 트랜잭션 제어 명령)을 실행할 때 발생합니다.
  • 2F004 (reading_sql_data_not_permitted): CALLED ON NULL INPUT 또는 특정 보안 설정에서 데이터 읽기가 제한된 컨텍스트에서 발생합니다.
  • P0001 (raise_exception): PL/pgSQL의 RAISE EXCEPTION 구문으로 명시적으로 발생시킨 예외이며, 2F000과 함께 함수 내 오류 처리 패턴을 설계할 때 함께 고려해야 합니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기