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

02001
2026년 10월 06일 | DBMS Error 가이드

이 글에서 다루는 내용

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

02001 no additional dynamic result sets returned 는?

PostgreSQL 에러 코드 02001은 SQLSTATE 클래스 02(No Data)에 속하는 에러로, 저장 프로시저(Stored Procedure)가 반환할 것으로 예상된 동적 결과 집합(Dynamic Result Set)이 더 이상 존재하지 않을 때 발생합니다. 이 에러는 주로 SQL/PSM(Persistent Stored Modules) 표준을 따르는 프로시저 호출 환경에서, 호출자(Caller)가 추가 결과 집합을 요청했지만 프로시저가 이미 모든 결과 집합을 소진했거나 애초에 충분한 결과 집합을 생성하지 않았을 때 트리거됩니다. 실무에서는 PostgreSQL의 CALL 문을 통해 프로시저를 호출하거나, JDBC/ODBC 드라이버와 같은 외부 클라이언트가 getMoreResults() 등의 메서드로 추가 결과 집합을 순회할 때 자주 마주치게 됩니다.


주요 발생 원인

1. 프로시저가 선언한 것보다 적은 수의 결과 집합을 반환하는 경우

저장 프로시저를 정의할 때 DYNAMIC RESULT SETS N 절로 N개의 결과 집합을 반환하겠다고 선언했지만, 실제 실행 로직에서 조건 분기(IF/ELSE) 또는 예외 처리로 인해 일부 결과 집합이 열리지 않은 채 프로시저가 종료되는 경우에 발생합니다. 호출자는 N개의 결과 집합이 올 것을 기대하고 계속 FETCH를 시도하지만, 실제로는 그보다 적은 결과 집합만 존재하여 02001이 발생합니다.

2. 클라이언트 드라이버가 결과 집합을 이미 모두 소진한 후 추가 요청하는 경우

JDBC, ODBC, psycopg2 등의 드라이버를 사용할 때, 애플리케이션 코드가 ResultSet을 전부 소비한 뒤에도 getMoreResults()나 유사한 메서드를 반복 호출하면 데이터베이스 서버로부터 02001 상태 코드를 수신하게 됩니다. 이는 드라이버 레벨의 루프 종료 조건이 잘못 구현되었거나, 결과 집합 개수를 하드코딩하여 관리하는 레거시 코드에서 빈번하게 나타납니다.

3. 커서(Cursor) 기반 동적 결과 집합의 조기 종료 또는 미개방

프로시저 내부에서 커서를 열어 결과 집합으로 넘겨주는 패턴을 사용할 때, 특정 비즈니스 로직 조건에서 커서가 OPEN되지 않거나 CLOSE가 예상보다 일찍 실행되면 호출자 측에서 해당 결과 집합을 읽으려 할 때 02001이 발생합니다. 특히 PL/pgSQL의 EXCEPTION 블록 내에서 커서 자원이 정리되는 경우, 이후 결과 집합 접근이 불가능해지는 상황이 자주 발생합니다.


해결 방법

원인 1 해결: 프로시저의 결과 집합 반환 일관성 확보

프로시저 내 모든 코드 경로에서 동일한 수의 결과 집합이 반환되도록 보장해야 합니다. 아래 예제처럼 조건 분기 시에도 빈 결과 집합이라도 반환하도록 설계하세요.

-- 문제가 있는 프로시저 예시
CREATE OR REPLACE PROCEDURE get_user_data(p_user_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
    cur_main REFCURSOR;
    cur_detail REFCURSOR;
BEGIN
    -- 메인 결과 집합은 항상 반환
    OPEN cur_main FOR
        SELECT id, name FROM users WHERE id = p_user_id;
    RETURN NEXT cur_main;

    -- 문제: 조건에 따라 두 번째 결과 집합이 반환되지 않을 수 있음
    IF p_user_id > 0 THEN
        OPEN cur_detail FOR
            SELECT * FROM user_details WHERE user_id = p_user_id;
        RETURN NEXT cur_detail;
    END IF;
    -- p_user_id <= 0 이면 cur_detail이 반환되지 않아 02001 발생 가능
END;
$$;

-- 개선된 프로시저: 모든 경로에서 결과 집합 반환 보장
CREATE OR REPLACE PROCEDURE get_user_data_fixed(p_user_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
    cur_main   REFCURSOR;
    cur_detail REFCURSOR;
BEGIN
    -- 메인 결과 집합: 항상 반환
    OPEN cur_main FOR
        SELECT id, name FROM users WHERE id = p_user_id;
    RETURN NEXT cur_main;

    -- 개선: 조건에 관계없이 항상 두 번째 커서를 열어 반환
    -- 데이터가 없으면 빈 결과 집합 반환
    IF p_user_id > 0 THEN
        OPEN cur_detail FOR
            SELECT * FROM user_details WHERE user_id = p_user_id;
    ELSE
        -- 빈 결과 집합을 명시적으로 반환하여 02001 방지
        OPEN cur_detail FOR
            SELECT * FROM user_details WHERE FALSE;
    END IF;
    RETURN NEXT cur_detail;
END;
$$;

원인 2 해결: 클라이언트 드라이버 루프 종료 조건 수정

JDBC를 사용하는 Java 애플리케이션 예시를 기반으로, 결과 집합 소진 여부를 명확하게 확인하는 방식으로 코드를 수정해야 합니다. PostgreSQL 측에서는 프로시저 호출 후 예상 결과 집합 수를 사전에 파악하고 관리하는 것이 안전합니다.

-- 프로시저 메타정보 확인 쿼리 (반환할 결과 집합 수 파악)
SELECT  p.proname          AS procedure_name,
        p.pronargs         AS num_args,
        p.prorettype::regtype AS return_type
FROM    pg_proc p
JOIN    pg_namespace n ON n.oid = p.pronamespace
WHERE   p.proname = 'get_user_data_fixed'
  AND   n.nspname = 'public';

-- 결과 집합 수를 명시적으로 문서화하는 주석 패턴 권장
CREATE OR REPLACE PROCEDURE get_orders_summary(p_date DATE)
LANGUAGE plpgsql
AS $$
/*
 * 반환 결과 집합: 2개
 *   1번: 일별 주문 요약 (orders_summary)
 *   2번: 상품별 집계   (product_aggregation)
 */
DECLARE
    cur_summary REFCURSOR := 'orders_summary';
    cur_product REFCURSOR := 'product_aggregation';
BEGIN
    OPEN cur_summary FOR
        SELECT  order_date,
                COUNT(*)       AS total_orders,
                SUM(amount)    AS total_amount
        FROM    orders
        WHERE   order_date = p_date
        GROUP BY order_date;
    RETURN NEXT cur_summary;

    OPEN cur_product FOR
        SELECT  product_id,
                SUM(quantity)  AS total_qty,
                SUM(amount)    AS total_revenue
        FROM    order_items oi
        JOIN    orders o ON o.id = oi.order_id
        WHERE   o.order_date = p_date
        GROUP BY product_id
        ORDER BY total_revenue DESC;
    RETURN NEXT cur_product;
END;
$$;

원인 3 해결: 커서 생명주기 명확히 관리

EXCEPTION 블록에서 커서가 강제 종료되는 상황을 방지하고, 커서의 OPEN/CLOSE 상태를 추적하는 로직을 추가합니다.

CREATE OR REPLACE PROCEDURE safe_cursor_procedure(p_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
    cur_result  REFCURSOR;
    cur_opened  BOOLEAN := FALSE;
BEGIN
    BEGIN
        OPEN cur_result FOR
            SELECT * FROM some_table WHERE id = p_id;
        cur_opened := TRUE;

    EXCEPTION WHEN OTHERS THEN
        -- 예외 발생 시 빈 커서로 대체하여 02001 방지
        RAISE WARNING '커서 열기 실패: %, 빈 결과 집합으로 대체합니다.', SQLERRM;
        OPEN cur_result FOR
            SELECT * FROM some_table WHERE FALSE;
        cur_opened := TRUE;
    END;

    -- 커서가 반드시 열렸을 때만 반환
    IF cur_opened THEN
        RETURN NEXT cur_result;
    END IF;
END;
$$;

-- 커서 상태 확인 쿼리 (pg_cursors 뷰 활용)
SELECT  name,
        statement,
        is_holdable,
        is_binary,
        creation_time
FROM    pg_cursors
WHERE   name LIKE '%result%';

예방 방법

1. 프로시저 내 결과 집합 반환 수를 항상 고정하고 문서화하라

저장 프로시저를 설계할 때 반환하는 결과 집합의 수를 상수로 고정하고, 코드 주석 및 내부 문서(Wiki, README)에 명시하세요. 조건 분기에 따라 결과 집합 수가 달라지는 설계는 02001의 온상이 됩니다. 아래와 같이 단위 테스트 쿼리를 작성하여 배포 전에 검증하는 습관을 들이세요.

-- 프로시저 반환 결과 집합 수 검증용 테스트 시나리오
DO $$
DECLARE
    cur1 REFCURSOR;
    cur2 REFCURSOR;
    rec  RECORD;
BEGIN
    -- 정상 케이스 테스트
    CALL get_orders_summary(CURRENT_DATE);
    FETCH ALL FROM orders_summary INTO rec;
    FETCH ALL FROM product_aggregation INTO rec;
    RAISE NOTICE '모든 결과 집합 정상 반환 확인 완료';

    -- 경계값 테스트 (데이터 없는 날짜)
    CALL get_orders_summary('1900-01-01'::DATE);
    FETCH ALL FROM orders_summary INTO rec;
    FETCH ALL FROM product_aggregation INTO rec;
    RAISE NOTICE '빈 데이터 케이스 정상 처리 확인 완료';
END;
$$;

2. 애플리케이션 레벨에서 결과 집합 수를 방어적으로 처리하라

클라이언트 애플리케이션(Java, Python, Node.js 등)에서 결과 집합을 순회할 때, hasMoreResults 또는 getMoreResults의 반환값을 반드시 확인하고, 02001 SQLSTATE를 명시적으로 캐치하여 정상 종료 신호로 처리하는 방어적 코딩 패턴을 적용하세요. 또한 PostgreSQL의 pg_stat_activity 뷰를 주기적으로 모니터링하여 프로시저 실행 중 비정상적인 커서 누수가 발생하지 않는지 확인하세요.

-- 커서 누수 모니터링 쿼리
SELECT  pid,
        usename,
        application_name,
        state,
        query,
        NOW() - query_start AS elapsed
FROM    pg_stat_activity
WHERE   state != 'idle'
  AND   query ILIKE '%CALL%'
ORDER BY elapsed DESC;

관련 에러

  • 02000 (no_data): 동일한 클래스 02의 기본 에러로, SELECT INTO 또는 FETCH 시 행이 없을 때 발생합니다. 02001과 달리 단일 쿼리 결과가 없는 상황을 나타냅니다.
  • 34000 (invalid_cursor_name): 존재하지 않는 커서 이름을 참조할 때 발생하며, 02001과 함께 커서 기반 프로시저 디버깅 시 자주 같이 나타납니다.
  • 24000 (invalid_cursor_state): 커서가 올바르지 않은 상태(예: 이미 닫힌 커서를 FETCH하려는 경우)에서 작업을 시도할 때 발생합니다. 02001이 발생하는 맥락에서 연쇄적으로 나타날 수 있습니다.
  • P0002 (no_data_found): PL/pgSQL 전용 에러로, SELECT INTO로 데이터를 가져올 때 결과가 없을 경우 발생합니다. 동적 결과 집합 처리 중 내부 쿼리 실패 시 02001로 이어지는 원인이 되기도 합니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기