2026년 06월 27일 | DBMS Error 가이드
이 글에서 다루는 내용
2F000 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
2F000 sql routine exception 는?
PostgreSQL 에러 코드 2F000은 SQL 루틴 예외(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 블록을 포함하고, SQLSTATE와 SQLERRM을 로그 테이블 또는 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 error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.