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

24000
2026년 06월 22일 | DBMS Error 가이드

이 글에서 다루는 내용

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

24000 invalid cursor state 는?

PostgreSQL 에러 코드 24000: invalid cursor state는 커서(Cursor)가 올바르지 않은 상태에서 작업을 수행하려 할 때 발생하는 에러입니다. 커서는 SELECT 쿼리 결과를 한 행씩 순차적으로 처리하기 위한 데이터베이스 객체인데, 이 커서가 열리지 않은 상태에서 FETCH를 시도하거나, 이미 닫힌 커서에 접근하거나, 트랜잭션 외부에서 커서를 사용하려 할 때 이 에러가 발생합니다. 주로 PL/pgSQL 함수, 저장 프로시저, 또는 애플리케이션 레벨의 커서 핸들링 코드에서 자주 목격되며, 커서의 생명 주기(lifecycle)를 제대로 관리하지 않을 경우 반드시 마주치게 되는 에러입니다.


주요 발생 원인

1. 트랜잭션 외부에서 커서를 사용하는 경우

PostgreSQL에서 커서는 기본적으로 트랜잭션 범위(transaction scope) 내에서만 유효합니다. DECLARE 문으로 커서를 선언했더라도, COMMIT 또는 ROLLBACK으로 트랜잭션이 종료된 이후에 해당 커서에 FETCHCLOSE를 시도하면 커서는 이미 무효화된 상태이므로 invalid cursor state 에러가 발생합니다. 특히 자동 커밋(autocommit) 환경의 애플리케이션(예: psycopg2의 autocommit 모드)에서 커서를 사용할 때 이 문제가 빈번하게 발생합니다.

2. 커서를 OPEN 하지 않고 FETCH 또는 CLOSE를 시도하는 경우

PL/pgSQL 코드 내에서 커서 변수를 선언만 하고 실제로 OPEN 하지 않은 상태에서 FETCH 또는 CLOSE 명령을 실행하면 이 에러가 발생합니다. 프로그램 로직 상의 조건 분기(if-else)나 예외 처리(exception handling) 코드가 복잡할수록, 커서를 여는 코드를 건너뛰고 닫거나 읽으려는 실수가 발생하기 쉽습니다. 특히 루프 내에서 커서를 조건부로 여는 경우, 커서가 열린 적이 없는 상태에서 FETCH가 호출될 위험이 있습니다.

3. 이미 닫힌 커서에 다시 접근하는 경우

CLOSE cursor_name 명령으로 커서를 명시적으로 닫은 후, 혹은 트랜잭션 종료로 인해 암묵적으로 닫힌 커서에 대해 다시 FETCH를 시도하면 invalid cursor state 에러가 발생합니다. 재사용 가능한 함수나 루프 구조 안에서 커서를 반복해서 열고 닫을 때, 이중으로 닫거나(double close) 닫힌 커서에서 읽으려는 실수가 발생하는 경우가 많습니다. 커서의 현재 상태를 추적하지 않는 코드 구조에서 특히 취약합니다.


해결 방법

원인 1 해결: 트랜잭션 블록 안에서 커서 사용하기

커서는 반드시 BEGINCOMMIT/ROLLBACK 트랜잭션 블록 안에서 사용해야 합니다. 아래는 올바른 사용 예시입니다.

-- 올바른 커서 사용 방법: 트랜잭션 블록 내에서 사용
BEGIN;

DECLARE my_cursor CURSOR FOR
    SELECT id, name FROM employees WHERE department = 'Engineering';

FETCH 10 FROM my_cursor;  -- 최초 10건 가져오기

FETCH NEXT FROM my_cursor; -- 다음 행 가져오기

CLOSE my_cursor;

COMMIT;

WITH HOLD 옵션을 사용하면 트랜잭션이 종료된 이후에도 커서를 유지할 수 있습니다. 단, WITH HOLD 커서는 결과를 임시 저장소에 버퍼링하므로 대용량 데이터에는 주의가 필요합니다.

-- WITH HOLD 커서: 트랜잭션 종료 후에도 사용 가능
BEGIN;

DECLARE my_hold_cursor CURSOR WITH HOLD FOR
    SELECT id, name FROM employees ORDER BY id;

COMMIT; -- 트랜잭션 종료 후에도 커서 유효

FETCH 5 FROM my_hold_cursor; -- 트랜잭션 밖에서도 사용 가능

CLOSE my_hold_cursor;

원인 2 해결: PL/pgSQL에서 커서 OPEN 여부 확인 후 FETCH

PL/pgSQL 함수 내에서 커서를 사용할 때는 반드시 OPEN 이후 FETCH를 호출해야 합니다. 아래는 안전하게 커서를 열고 닫는 패턴입니다.

-- PL/pgSQL에서 커서를 안전하게 사용하는 예시
CREATE OR REPLACE FUNCTION process_employees(p_dept TEXT)
RETURNS void AS $$
DECLARE
    emp_cursor REFCURSOR;
    emp_record employees%ROWTYPE;
BEGIN
    -- 반드시 OPEN 이후 FETCH 수행
    OPEN emp_cursor FOR
        SELECT * FROM employees WHERE department = p_dept;

    LOOP
        FETCH emp_cursor INTO emp_record;
        EXIT WHEN NOT FOUND;  -- 더 이상 행이 없으면 루프 종료

        -- 각 행에 대한 처리 로직
        RAISE NOTICE 'Processing employee: %', emp_record.name;
    END LOOP;

    CLOSE emp_cursor;  -- 반드시 닫기

EXCEPTION
    WHEN OTHERS THEN
        -- 예외 발생 시에도 커서가 열려있다면 닫기
        IF emp_cursor IS NOT NULL THEN
            BEGIN
                CLOSE emp_cursor;
            EXCEPTION
                WHEN invalid_cursor_state THEN
                    NULL; -- 이미 닫혔다면 무시
            END;
        END IF;
        RAISE;
END;
$$ LANGUAGE plpgsql;

원인 3 해결: 커서 상태를 Boolean 변수로 추적하기

커서가 이미 닫혔는지 여부를 별도의 플래그 변수로 관리하면 이중 닫힘(double close) 문제를 예방할 수 있습니다.

-- 커서 상태를 플래그로 관리하는 패턴
CREATE OR REPLACE FUNCTION safe_cursor_example()
RETURNS void AS $$
DECLARE
    my_cursor REFCURSOR;
    rec       RECORD;
    is_cursor_open BOOLEAN := FALSE;  -- 커서 상태 추적 플래그
BEGIN
    OPEN my_cursor FOR SELECT id, name FROM employees LIMIT 100;
    is_cursor_open := TRUE;  -- 열렸음을 기록

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

    -- 커서가 열려있을 때만 닫기
    IF is_cursor_open THEN
        CLOSE my_cursor;
        is_cursor_open := FALSE;
    END IF;

EXCEPTION
    WHEN OTHERS THEN
        IF is_cursor_open THEN
            CLOSE my_cursor;
            is_cursor_open := FALSE;
        END IF;
        RAISE;
END;
$$ LANGUAGE plpgsql;

예방 방법

1. FOR 루프를 활용한 커서 자동 관리

PL/pgSQL에서는 FOR record IN SELECT ... 구문을 사용하면 커서의 OPEN, FETCH, CLOSE를 PostgreSQL이 자동으로 처리해줍니다. 직접 커서를 선언하고 관리하는 것보다 코드가 간결해지고, invalid cursor state 에러 발생 가능성을 원천적으로 차단할 수 있습니다. 대용량 처리가 필요하지 않거나, 커서를 외부로 반환(REFCURSOR)할 필요가 없는 경우라면 이 패턴을 우선적으로 채택하는 것이 좋습니다.

-- FOR 루프를 이용한 안전한 커서 대체 패턴
CREATE OR REPLACE FUNCTION process_all_employees()
RETURNS void AS $$
DECLARE
    rec RECORD;
BEGIN
    -- 커서를 직접 관리할 필요 없이 FOR 루프 사용
    FOR rec IN SELECT id, name FROM employees WHERE active = TRUE
    LOOP
        RAISE NOTICE 'Processing: id=%, name=%', rec.id, rec.name;
    END LOOP;
    -- 커서는 자동으로 닫힘
END;
$$ LANGUAGE plpgsql;

2. 애플리케이션 레벨에서 트랜잭션 범위와 커서 생명 주기 일치시키기

Python(psycopg2), Java(JDBC) 등 애플리케이션에서 서버 사이드 커서를 사용할 때는 반드시 커서의 생명 주기가 트랜잭션의 생명 주기와 일치하도록 설계해야 합니다. autocommit 모드를 비활성화하고, try-with-resources 또는 context manager 패턴으로 커서와 커넥션을 명시적으로 관리하는 습관을 들이세요. 코드 리뷰 체크리스트에 “커서는 트랜잭션 블록 내에서 사용되는가?”를 항목으로 추가하는 것도 좋은 방법입니다.


관련 에러

  • 34000: invalid cursor name — 선언되지 않았거나 존재하지 않는 커서 이름을 참조할 때 발생합니다. 24000과 함께 커서 관련 작업 시 자주 동반 발생합니다.
  • 25001: active SQL transaction — 트랜잭션 상태와 관련된 에러로, 커서의 트랜잭션 범위 문제를 디버깅할 때 함께 확인해야 할 에러입니다.
  • 55000: object not in prerequisite state — 객체가 요구되는 선행 상태에 있지 않을 때 발생하며, 커서의 잘못된 상태 접근과 유사한 맥락에서 발생할 수 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기