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

42P03
2026년 07월 09일 | DBMS Error 가이드

이 글에서 다루는 내용

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

42P03 duplicate cursor 는?

PostgreSQL 에러 코드 42P03은 duplicate cursor 에러로, 이미 동일한 이름으로 선언된 커서가 현재 트랜잭션 또는 세션 내에 존재할 때 발생합니다. 커서(Cursor)는 대용량 데이터를 행 단위로 처리하기 위해 사용되는 데이터베이스 객체인데, 같은 커서 이름을 중복으로 열려고 시도할 경우 PostgreSQL은 이 에러를 반환합니다. 주로 PL/pgSQL 프로시저나 함수 내부, 혹은 명시적 트랜잭션 블록 안에서 커서를 반복적으로 선언하거나 열 때 자주 목격됩니다.


주요 발생 원인

  • 동일한 이름의 커서를 닫지 않고 재선언 (가장 흔한 원인)

커서를 OPEN 한 후 CLOSE를 하지 않은 상태에서 동일한 이름으로 다시 OPEN을 시도하면 이 에러가 발생합니다. 트랜잭션이 롤백되지 않는 한 커서는 명시적으로 닫아야 해제되며, 이를 간과하면 반복 호출 시 문제가 됩니다. 특히 반복문(LOOP) 안에서 커서를 매번 여는 코드를 작성할 때 자주 발생합니다.

  • PL/pgSQL 함수나 프로시저에서 예외 처리 없이 커서 재사용

PL/pgSQL 함수가 중간에 예외 없이 정상 종료되면 커서가 자동으로 닫히지만, 예외가 발생했을 때 EXCEPTION 블록에서 커서를 제대로 닫지 않으면 다음 호출 시 커서가 이미 열려 있는 상태가 됩니다. 같은 세션에서 함수가 재호출될 때 이전 커서가 남아 있는 경우 42P03 에러가 발생할 수 있습니다. 이는 커넥션 풀링 환경에서 특히 위험한 패턴입니다.

  • 동시 실행 혹은 중첩 호출에서 고정된 커서 이름 사용

여러 세션이나 중첩 함수 호출에서 하드코딩된 커서 이름(예: my_cursor)을 사용하면, 동일 세션 내 중첩 호출 시 충돌이 발생합니다. 커서 이름을 동적으로 생성하지 않고 고정값으로 사용하는 설계는 재진입(reentrant) 상황에서 치명적입니다. 동일 세션에서 재귀 함수나 중첩 프로시저를 사용할 때 반드시 고려해야 할 사항입니다.


해결 방법

원인 1 해결: 커서를 항상 명시적으로 닫기

커서를 OPEN 했다면 사용 후 반드시 CLOSE를 호출해야 합니다.

DO $$
DECLARE
    my_cursor CURSOR FOR SELECT id, name FROM employees;
    rec RECORD;
BEGIN
    -- 커서 열기
    OPEN my_cursor;

    LOOP
        FETCH my_cursor INTO rec;
        EXIT WHEN NOT FOUND;
        RAISE NOTICE 'ID: %, Name: %', rec.id, rec.name;
    END LOOP;

    -- 반드시 명시적으로 닫기
    CLOSE my_cursor;

    -- 이제 동일 이름으로 다시 열 수 있음
    OPEN my_cursor;
    CLOSE my_cursor;
END;
$$;

원인 2 해결: EXCEPTION 블록에서 커서 안전하게 닫기

예외가 발생하더라도 커서를 안전하게 닫을 수 있도록 처리합니다.

CREATE OR REPLACE FUNCTION process_employees()
RETURNS void AS $$
DECLARE
    emp_cursor CURSOR FOR SELECT id, name FROM employees;
    rec RECORD;
BEGIN
    OPEN emp_cursor;

    LOOP
        FETCH emp_cursor INTO rec;
        EXIT WHEN NOT FOUND;

        -- 비즈니스 로직 처리
        RAISE NOTICE 'Processing: %', rec.name;
    END LOOP;

    CLOSE emp_cursor;

EXCEPTION
    WHEN OTHERS THEN
        -- 예외 발생 시에도 커서를 닫아 리소스 정리
        BEGIN
            CLOSE emp_cursor;
        EXCEPTION
            WHEN invalid_cursor_name THEN
                -- 이미 닫혀있는 경우 무시
                NULL;
        END;
        RAISE; -- 원래 예외 재발생
END;
$$ LANGUAGE plpgsql;

원인 3 해결: 동적 커서 이름 사용

고정 커서 이름 대신 동적으로 생성된 고유한 이름을 사용합니다.

CREATE OR REPLACE FUNCTION process_data_dynamic(p_table_name TEXT)
RETURNS void AS $$
DECLARE
    -- 세션 ID와 타임스탬프를 조합하여 고유한 커서 이름 생성
    v_cursor_name TEXT := 'cursor_' || pg_backend_pid() || '_' || extract(epoch from clock_timestamp())::bigint;
    v_query TEXT;
    rec RECORD;
    ref_cursor REFCURSOR;
BEGIN
    v_query := 'SELECT id, name FROM ' || quote_ident(p_table_name);

    -- 동적 커서 이름으로 REFCURSOR 사용
    OPEN ref_cursor FOR EXECUTE v_query;

    LOOP
        FETCH ref_cursor INTO rec;
        EXIT WHEN NOT FOUND;
        RAISE NOTICE 'Row: %', rec;
    END LOOP;

    CLOSE ref_cursor;
END;
$$ LANGUAGE plpgsql;

커서 존재 여부 확인 후 처리

pg_cursors 시스템 뷰를 활용하여 현재 열린 커서를 확인할 수 있습니다.

-- 현재 세션에서 열린 커서 목록 확인
SELECT name, statement, is_holdable, is_binary, is_scrollable, creation_time
FROM pg_cursors;

-- 특정 커서가 열려있는지 확인하는 함수
CREATE OR REPLACE FUNCTION safe_close_cursor(p_cursor_name TEXT)
RETURNS void AS $$
BEGIN
    IF EXISTS (SELECT 1 FROM pg_cursors WHERE name = p_cursor_name) THEN
        EXECUTE 'CLOSE ' || quote_ident(p_cursor_name);
        RAISE NOTICE 'Cursor % closed successfully.', p_cursor_name;
    ELSE
        RAISE NOTICE 'Cursor % was not open, skipping.', p_cursor_name;
    END IF;
END;
$$ LANGUAGE plpgsql;

-- 사용 예시
SELECT safe_close_cursor('my_cursor');

예방 방법

  • 커서 수명 주기를 명확히 관리하는 코딩 컨벤션 수립

팀 내에서 커서를 사용하는 모든 PL/pgSQL 코드에 대해 OPEN → FETCH → CLOSE 패턴을 의무화하는 코딩 표준을 만들어야 합니다. 가능하다면 FOR rec IN SELECT ... 형태의 암묵적 커서(implicit cursor)를 사용하는 것이 훨씬 안전합니다. 암묵적 커서는 루프가 끝나면 자동으로 닫히므로 42P03 에러를 원천적으로 방지할 수 있습니다.

“`sql

— 권장: 암묵적 커서 사용 (자동으로 닫힘)

DO $$

DECLARE

rec RECORD;

BEGIN

FOR rec IN SELECT id, name FROM employees LOOP

RAISE NOTICE ‘ID: %, Name: %’, rec.id, rec.name;

END LOOP;

— 별도의 CLOSE 불필요

END;

$$;

“`

  • 커넥션 풀 환경에서 세션 상태 초기화 절차 구축

PgBouncer나 pgpool-II 같은 커넥션 풀러를 사용하는 환경에서는 커넥션이 재사용될 때 이전 세션의 상태(커서 포함)가 남아 있을 수 있습니다. 커넥션 반환 시 DISCARD ALL 또는 CLOSE ALL을 실행하는 훅을 설정하거나, 애플리케이션 레벨에서 커넥션 반환 전 정리 쿼리를 실행하도록 설계해야 합니다.

“`sql

— 커넥션 반환 전 모든 커서 닫기

CLOSE ALL;

— 또는 세션 상태 완전 초기화 (더 강력한 방법)

DISCARD ALL;

“`


관련 에러

  • 34000 invalid_cursor_name: 존재하지 않는 커서를 FETCH하거나 CLOSE하려 할 때 발생합니다. 42P03과 반대 상황으로, 커서가 열려있지 않을 때 접근 시 나타납니다.
  • 55000 object_not_in_prerequisite_state: 커서가 올바른 상태가 아닐 때(예: 이미 닫힌 커서에 FETCH 시도) 발생할 수 있는 관련 에러입니다.
  • 25P02 in_failed_sql_transaction: 트랜잭션이 실패한 상태에서 커서 작업을 시도할 때 발생하며, 커서 에러와 함께 복합적으로 나타나는 경우가 많습니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기