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

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

이 글에서 다루는 내용

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

42P03 duplicate cursor 는?

PostgreSQL 에러 코드 42P03 duplicate_cursor는 이미 동일한 이름의 커서(Cursor)가 현재 트랜잭션 내에서 열려 있는 상태에서, 같은 이름으로 새로운 커서를 선언(DECLARE)하려 할 때 발생하는 에러입니다. PostgreSQL에서 커서는 트랜잭션 범위 내에서 고유한 이름을 가져야 하며, 동일 트랜잭션 안에서 같은 이름의 커서를 중복으로 열 수 없습니다. 이 에러는 주로 PL/pgSQL 프로시저, 함수, 또는 복잡한 트랜잭션 로직을 다루는 애플리케이션 코드에서 자주 목격되며, 대용량 데이터 처리 배치 작업에서도 빈번하게 나타납니다.


주요 발생 원인

1. 동일 트랜잭션 내 중복 커서 선언

가장 흔한 원인으로, 하나의 트랜잭션 블록 안에서 이미 열린 커서를 닫지(CLOSE) 않은 상태로 같은 이름의 커서를 다시 선언하는 경우입니다. 개발자가 루프 처리나 반복 로직을 작성하면서 커서의 생명 주기를 명확히 관리하지 않으면 이 문제가 발생합니다. 특히 PL/pgSQL 블록에서 예외 처리 없이 커서를 반복 선언할 때 매우 자주 나타납니다.

2. PL/pgSQL 함수 또는 프로시저 내 재귀 호출 혹은 반복 실행

PL/pgSQL 함수 내부에서 고정된 이름의 커서를 선언해 두고, 해당 함수가 동일 트랜잭션 내에서 반복 호출되거나 재귀적으로 실행될 때 에러가 발생합니다. 함수가 처음 실행되면서 커서를 열고, 비정상 종료되거나 커서를 닫지 않은 채 함수가 다시 호출되면 이미 존재하는 커서 이름과 충돌합니다. 이 경우 동적으로 커서 이름을 생성하거나, 함수 종료 전에 반드시 커서를 명시적으로 닫아야 합니다.

3. 애플리케이션 레벨에서의 커넥션 풀링 및 트랜잭션 관리 미흡

JDBC, psycopg2, asyncpg 등의 드라이버를 사용하는 애플리케이션에서 커넥션 풀을 통해 커서를 열고, 트랜잭션을 제대로 종료하거나 롤백하지 않은 상태로 커넥션이 풀에 반환된 경우 문제가 생깁니다. 이후 동일 커넥션이 재사용될 때 이전 트랜잭션의 커서가 여전히 열려 있어 새로운 커서 선언 시 충돌이 발생합니다. 이는 특히 장기 실행 트랜잭션이나 예외 처리가 미흡한 애플리케이션에서 자주 나타납니다.


해결 방법

원인 1 해결: 커서 선언 전 명시적 CLOSE 처리

커서를 재선언하기 전에 반드시 기존 커서를 닫아야 합니다. 아래 예제는 잘못된 패턴과 올바른 패턴을 비교합니다.

-- ❌ 잘못된 예시: 커서를 닫지 않고 재선언
BEGIN;

DECLARE my_cursor CURSOR FOR SELECT id, name FROM employees WHERE dept = 'HR';
FETCH ALL FROM my_cursor;

-- 커서를 닫지 않고 동일 이름으로 다시 선언 → 42P03 에러 발생
DECLARE my_cursor CURSOR FOR SELECT id, name FROM employees WHERE dept = 'IT';
FETCH ALL FROM my_cursor;

COMMIT;
-- ✅ 올바른 예시: 커서를 닫은 후 재선언
BEGIN;

DECLARE my_cursor CURSOR FOR SELECT id, name FROM employees WHERE dept = 'HR';
FETCH ALL FROM my_cursor;
CLOSE my_cursor;  -- 반드시 닫기

-- 이제 안전하게 동일 이름으로 재선언 가능
DECLARE my_cursor CURSOR FOR SELECT id, name FROM employees WHERE dept = 'IT';
FETCH ALL FROM my_cursor;
CLOSE my_cursor;

COMMIT;

원인 2 해결: PL/pgSQL에서 동적 커서 이름 사용 및 EXCEPTION 블록 처리

-- ✅ PL/pgSQL에서 동적 커서 이름 생성 및 안전한 처리
CREATE OR REPLACE FUNCTION process_department(p_dept TEXT)
RETURNS VOID AS $$
DECLARE
    -- 고유한 커서 이름을 동적으로 생성
    v_cursor_name TEXT := 'cur_' || p_dept || '_' || extract(epoch FROM clock_timestamp())::bigint;
    v_ref REFCURSOR;
    v_row employees%ROWTYPE;
BEGIN
    -- REFCURSOR를 활용한 안전한 방법
    OPEN v_ref FOR
        SELECT * FROM employees WHERE dept = p_dept;

    LOOP
        FETCH v_ref INTO v_row;
        EXIT WHEN NOT FOUND;
        -- 각 행 처리 로직
        RAISE NOTICE 'Processing employee: %', v_row.name;
    END LOOP;

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

EXCEPTION
    WHEN OTHERS THEN
        -- 예외 발생 시에도 커서 닫기 시도
        IF v_ref IS NOT NULL THEN
            CLOSE v_ref;
        END IF;
        RAISE;  -- 예외 재발생
END;
$$ LANGUAGE plpgsql;
-- ✅ 고정 이름 커서를 사용하는 경우: 함수 내에서 안전하게 관리
CREATE OR REPLACE FUNCTION safe_cursor_function()
RETURNS TABLE(emp_id INT, emp_name TEXT) AS $$
DECLARE
    v_cursor CURSOR FOR
        SELECT id, name FROM employees ORDER BY id;
    v_id INT;
    v_name TEXT;
BEGIN
    OPEN v_cursor;

    LOOP
        FETCH v_cursor INTO v_id, v_name;
        EXIT WHEN NOT FOUND;
        emp_id := v_id;
        emp_name := v_name;
        RETURN NEXT;
    END LOOP;

    CLOSE v_cursor;
    RETURN;

EXCEPTION
    WHEN OTHERS THEN
        IF v_cursor%ISOPEN THEN
            CLOSE v_cursor;
        END IF;
        RAISE;
END;
$$ LANGUAGE plpgsql;

원인 3 해결: 애플리케이션에서 트랜잭션 및 커서 생명 주기 명확히 관리

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

-- ✅ 특정 커서가 이미 열려 있는지 확인 후 조건부 처리 (PL/pgSQL)
DO $$
DECLARE
    v_cursor_exists BOOLEAN;
BEGIN
    -- pg_cursors를 통해 커서 존재 여부 확인
    SELECT EXISTS (
        SELECT 1 FROM pg_cursors WHERE name = 'my_cursor'
    ) INTO v_cursor_exists;

    IF v_cursor_exists THEN
        RAISE NOTICE '커서가 이미 존재합니다. 닫기를 시도합니다.';
        CLOSE my_cursor;
    END IF;

    -- 안전하게 커서 선언
    DECLARE my_cursor CURSOR FOR SELECT id FROM employees;
    FETCH ALL FROM my_cursor;
    CLOSE my_cursor;
END;
$$;
-- ✅ Python psycopg2 예시: 트랜잭션 관리 강화
-- (SQL 레벨 예시로 표현)
-- 애플리케이션에서 항상 커서 사용 후 명시적 CLOSE 및 트랜잭션 COMMIT/ROLLBACK

BEGIN;
DECLARE app_cursor CURSOR FOR SELECT * FROM orders WHERE status = 'PENDING';
FETCH 100 FROM app_cursor;
-- 처리 완료 후
CLOSE app_cursor;
COMMIT;

예방 방법

1. 커서 사용 시 반드시 EXCEPTION 블록과 함께 명시적 CLOSE 패턴 적용

PL/pgSQL 코드를 작성할 때는 커서를 열면 반드시 닫는 것을 코딩 컨벤션으로 정착시켜야 합니다. EXCEPTION 블록을 활용해 비정상 종료 시에도 커서가 반드시 닫히도록 보장하고, %ISOPEN 속성으로 커서 상태를 확인한 후 닫는 방어적 코딩을 습관화하세요. 코드 리뷰 단계에서도 커서의 열기/닫기 쌍이 맞는지 반드시 체크리스트 항목으로 추가하는 것을 강력히 권장합니다.

2. 고정 커서 이름 대신 REFCURSOR 또는 동적 이름 사용

함수나 프로시저에서 커서를 사용할 때는 하드코딩된 고정 이름 대신 REFCURSOR 타입의 변수를 사용하거나, clock_timestamp() 또는 gen_random_uuid()를 활용해 고유한 동적 커서 이름을 생성하세요. 이렇게 하면 동일 함수가 동일 트랜잭션 내에서 재호출되더라도 이름 충돌 가능성을 원천 차단할 수 있습니다. 또한 pg_cursors 뷰를 정기적으로 모니터링하여 장기간 열려 있는 커서가 없는지 확인하는 운영 루틴을 도입하세요.


관련 에러

  • 34000 invalid_cursor_name: 존재하지 않는 커서 이름을 참조할 때 발생하며, 42P03과 반대 상황에서 나타납니다. FETCH나 CLOSE 시 커서 이름을 잘못 입력했을 때 주로 경험하게 됩니다.
  • 25P02 in_failed_sql_transaction: 트랜잭션이 실패한 상태에서 커서 관련 명령을 실행할 때 발생하며, 커서 에러 후 트랜잭션 처리를 잘못했을 때 연쇄적으로 나타날 수 있습니다.
  • 55006 object_in_use: 커서가 사용 중인 리소스를 다른 작업이 점유하려 할 때 발생하며, 커서와 잠금(Lock) 문제가 복합적으로 얽힌 상황에서 함께 나타나기도 합니다.
  • 42P02 undefined_cursor: 선언되지 않은 커서를 참조할 때 발생하는 에러로, 42P03과 함께 커서 생명 주기 관리 실수에서 비롯되는 대표적인 에러 쌍입니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기