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

2F000
2026년 06월 27일 | DBMS Error 가이드

이 글에서 다루는 내용

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

2F000 sql routine exception 는?

PostgreSQL 에러 코드 2F000SQL 루틴 예외(SQL Routine Exception) 를 의미하며, SQL 함수(Function) 또는 프로시저(Procedure) 내부에서 예기치 않은 오류가 발생했을 때 나타납니다. 이 에러는 주로 PL/pgSQL, PL/Python, PL/Perl 등 프로시저 언어로 작성된 루틴이 실행 도중 SQL 레벨의 예외를 처리하지 못할 때 발생합니다. 2F000은 에러 클래스 2F(SQL Routine Exception)에 속하는 부모 코드이며, 실제 현장에서는 더 구체적인 하위 코드(2F002, 2F003, 2F004, 2F005)와 함께 등장하는 경우가 많습니다.


주요 발생 원인

1. 함수 내부에서 허용되지 않는 SQL 구문 실행 (2F003 – prohibited_sql_statement_attempted)

SQL 함수(LANGUAGE SQL)로 선언된 루틴 내부에서 트랜잭션 제어 구문(COMMIT, ROLLBACK, SAVEPOINT 등)을 직접 호출하면 발생합니다. SQL 함수는 단순 쿼리 실행만을 위한 컨텍스트이므로, 트랜잭션 관리 명령어는 해당 레이어에서 허용되지 않습니다. 이 오류는 레거시 코드를 PostgreSQL로 마이그레이션할 때 자주 발견되는 패턴입니다.

2. 함수 내부에서 데이터 수정 불가 컨텍스트 위반 (2F002 – modifying_sql_data_not_permitted)

READS SQL DATA 또는 읽기 전용으로 선언된 함수 내에서 INSERT, UPDATE, DELETE 등 데이터 변경 쿼리를 실행하려 할 때 발생합니다. PostgreSQL은 함수의 VOLATILITY 속성(IMMUTABLE, STABLE, VOLATILE)과 함수 본문의 실제 동작이 일치하지 않을 경우 이 예외를 발생시킵니다. 특히 IMMUTABLE로 선언된 함수 내에서 테이블을 조회하거나 수정하려 할 때 예상치 못한 오류로 이어질 수 있습니다.

3. 중첩 함수 호출 시 예외 전파 미처리 (2F005 – function_executed_no_return_statement)

PL/pgSQL 함수가 모든 실행 경로에서 RETURN 문을 보장하지 않으면 발생합니다. 특히 조건 분기(IF-ELSIF-ELSE)가 복잡한 경우, 일부 분기에서 반환값 없이 함수가 종료될 수 있습니다. 이 오류는 개발 환경에서는 재현이 안 되다가 운영 환경의 특정 데이터 조건에서만 나타나는 경우가 있어 디버깅이 까다롭습니다.


해결 방법

원인 1 해결: 트랜잭션 제어 구문 제거 또는 프로시저로 전환

SQL 함수 내에서 트랜잭션 제어가 필요하다면, 해당 루틴을 PROCEDURE로 변환하세요. PostgreSQL 11 이상에서는 프로시저 내에서 COMMIT/ROLLBACK이 허용됩니다.

-- ❌ 잘못된 방법: SQL FUNCTION 내에서 COMMIT 시도
CREATE OR REPLACE FUNCTION bad_function()
RETURNS void
LANGUAGE plpgsql AS $$
BEGIN
    INSERT INTO orders(product, qty) VALUES ('apple', 10);
    COMMIT; -- 2F003 에러 발생!
END;
$$;

-- ✅ 올바른 방법: PROCEDURE로 변환하여 트랜잭션 제어
CREATE OR REPLACE PROCEDURE good_procedure()
LANGUAGE plpgsql AS $$
BEGIN
    INSERT INTO orders(product, qty) VALUES ('apple', 10);
    COMMIT; -- 프로시저에서는 허용됨
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE NOTICE 'Error occurred: %', SQLERRM;
END;
$$;

-- 프로시저 호출
CALL good_procedure();

원인 2 해결: 함수 VOLATILITY 속성 올바르게 설정

데이터를 수정하는 함수는 반드시 VOLATILE로 선언해야 합니다. 읽기만 하는 함수는 STABLE, 입력값에만 의존하는 순수 함수는 IMMUTABLE을 사용하세요.

-- ❌ 잘못된 방법: IMMUTABLE 함수 내에서 테이블 수정 시도
CREATE OR REPLACE FUNCTION bad_immutable_func(p_id INT)
RETURNS TEXT
LANGUAGE plpgsql
IMMUTABLE AS $$  -- IMMUTABLE인데 DML 실행 → 2F002 에러!
DECLARE
    v_name TEXT;
BEGIN
    UPDATE products SET last_accessed = NOW() WHERE id = p_id
    RETURNING name INTO v_name;
    RETURN v_name;
END;
$$;

-- ✅ 올바른 방법: VOLATILE로 선언
CREATE OR REPLACE FUNCTION good_volatile_func(p_id INT)
RETURNS TEXT
LANGUAGE plpgsql
VOLATILE AS $$
DECLARE
    v_name TEXT;
BEGIN
    UPDATE products SET last_accessed = NOW() WHERE id = p_id
    RETURNING name INTO v_name;
    RETURN v_name;
END;
$$;

-- 순수 계산 함수의 올바른 IMMUTABLE 사용 예
CREATE OR REPLACE FUNCTION calc_tax(price NUMERIC, rate NUMERIC)
RETURNS NUMERIC
LANGUAGE plpgsql
IMMUTABLE AS $$
BEGIN
    RETURN price * rate;  -- 외부 상태에 의존하지 않음
END;
$$;

원인 3 해결: 모든 분기에 RETURN 구문 보장

PL/pgSQL 함수에서 모든 코드 경로가 반드시 값을 반환하도록 작성하고, 마지막에 기본 반환값을 추가하세요.

-- ❌ 잘못된 방법: ELSE 절 누락으로 일부 경로에서 반환값 없음
CREATE OR REPLACE FUNCTION get_grade(score INT)
RETURNS TEXT
LANGUAGE plpgsql AS $$
BEGIN
    IF score >= 90 THEN
        RETURN 'A';
    ELSIF score >= 80 THEN
        RETURN 'B';
    ELSIF score >= 70 THEN
        RETURN 'C';
    -- score < 70 인 경우 RETURN 없음 → 2F005 에러!
END;
$$;

-- ✅ 올바른 방법: 모든 분기에 RETURN 보장
CREATE OR REPLACE FUNCTION get_grade(score INT)
RETURNS TEXT
LANGUAGE plpgsql AS $$
BEGIN
    IF score >= 90 THEN
        RETURN 'A';
    ELSIF score >= 80 THEN
        RETURN 'B';
    ELSIF score >= 70 THEN
        RETURN 'C';
    ELSE
        RETURN 'F';  -- 누락된 분기 처리
    END IF;
END;
$$;

-- 예외 처리를 포함한 실무 패턴
CREATE OR REPLACE FUNCTION safe_get_grade(score INT)
RETURNS TEXT
LANGUAGE plpgsql AS $$
BEGIN
    IF score IS NULL THEN
        RAISE EXCEPTION 'Score cannot be NULL'
            USING ERRCODE = 'invalid_parameter_value';
    END IF;

    RETURN CASE
        WHEN score >= 90 THEN 'A'
        WHEN score >= 80 THEN 'B'
        WHEN score >= 70 THEN 'C'
        ELSE 'F'
    END;
EXCEPTION
    WHEN OTHERS THEN
        RAISE LOG '함수 실행 중 오류 발생: SQLSTATE=%, SQLERRM=%',
            SQLSTATE, SQLERRM;
        RETURN NULL;
END;
$$;

예방 방법

1. 함수 작성 시 명시적 예외 처리 블록과 로깅 표준화

모든 중요한 PL/pgSQL 함수에는 EXCEPTION 블록을 포함하고, SQLSTATESQLERRM을 로그 테이블 또는 PostgreSQL 로그에 기록하는 표준 템플릿을 팀 내에서 공유하세요. 예외가 발생했을 때 어느 함수의 어느 라인에서 문제가 생겼는지 즉시 파악할 수 있어야 운영 장애를 빠르게 복구할 수 있습니다.

-- 표준 함수 템플릿 예시
CREATE OR REPLACE FUNCTION template_function(p_input TEXT)
RETURNS JSONB
LANGUAGE plpgsql
VOLATILE
SECURITY INVOKER AS $$
DECLARE
    v_result JSONB;
BEGIN
    -- 입력값 유효성 검사
    IF p_input IS NULL OR p_input = '' THEN
        RAISE EXCEPTION 'Invalid input: parameter cannot be null or empty'
            USING ERRCODE = '22023';
    END IF;

    -- 핵심 로직
    SELECT jsonb_build_object('status', 'ok', 'value', p_input)
    INTO v_result;

    RETURN v_result;

EXCEPTION
    WHEN OTHERS THEN
        -- 반드시 에러 정보를 남길 것
        RAISE LOG '[template_function] SQLSTATE: %, MESSAGE: %',
            SQLSTATE, SQLERRM;
        RAISE;  -- 예외 재전파
END;
$$;

2. CI/CD 파이프라인에 함수 유닛 테스트 통합 (pgTAP 활용)

pgTAP 확장을 활용하여 모든 데이터베이스 함수에 대한 유닛 테스트를 작성하고, 배포 전 자동으로 실행되도록 CI/CD 파이프라인에 통합하세요. 특히 경계값(NULL, 0, 최대값 등) 입력에 대한 테스트를 반드시 포함해야 운영 환경에서의 2F000 계열 에러를 사전에 차단할 수 있습니다.

-- pgTAP을 활용한 함수 테스트 예시
BEGIN;
SELECT plan(4);

-- 정상 입력 테스트
SELECT is(get_grade(95), 'A', '95점은 A학점이어야 함');
SELECT is(get_grade(85), 'B', '85점은 B학점이어야 함');
SELECT is(get_grade(65), 'F', '65점은 F학점이어야 함');

-- NULL 입력 예외 테스트
SELECT throws_ok(
    $$ SELECT safe_get_grade(NULL) $$,
    '22023',
    NULL,
    'NULL 입력 시 예외 발생해야 함'
);

SELECT * FROM finish();
ROLLBACK;

관련 에러

| 에러 코드 | 이름 | 설명 |

|———–|——|——|

| 2F002 | modifying_sql_data_not_permitted | 읽기 전용 함수에서 데이터 수정 시도 |

| 2F003 | prohibited_sql_statement_attempted | 함수 내에서 허용되지 않는 SQL 구문 실행 |

| 2F004 | reading_sql_data_not_permitted | 데이터 읽기가 금지된 컨텍스트에서 SELECT 실행 |

| 2F005 | function_executed_no_return_statement | 함수가 RETURN 없이 종료됨 |

| P0001 | raise_exception | PL/pgSQL에서 RAISE EXCEPTION으로 발생하는 사용자 정의 예외 |

| 39000 | external_routine_exception | 외부 루틴(C 함수, 외부 언어) 실행 중 예외 |

2F000 계열 에러는 대부분 함수 설계 단계에서의 규칙 위반에서 비롯됩니다. PostgreSQL 공식 문서의 [Appendix A: PostgreSQL Error Codes](https://www.postgresql.org/docs/current/errcodes-appendix.html)를 참고하여 유사 에러 코드와의 차이를 숙지해 두면 장애 대응 시간을 크게 줄일 수 있습니다.


DBMS 에러 코드 시리즈

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

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

댓글 남기기