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

25008
2026년 08월 27일 | DBMS Error 가이드

이 글에서 다루는 내용

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

25008 held cursor requires same isolation level 는?

PostgreSQL 에러 코드 25008은 “held cursor requires same isolation level”로, 트랜잭션 경계를 넘어 유지되는 커서(WITH HOLD 커서)를 사용할 때 트랜잭션 격리 수준이 커서 생성 시점과 다를 경우 발생합니다. 즉, WITH HOLD 옵션으로 선언된 커서는 트랜잭션이 커밋된 이후에도 살아있는데, 이후 새로운 트랜잭션에서 해당 커서를 사용할 때 격리 수준이 달라지면 PostgreSQL이 데이터 일관성을 보장할 수 없어 이 에러를 발생시킵니다. 특히 READ COMMITTED와 SERIALIZABLE 같이 서로 다른 격리 수준이 혼용될 때 주로 나타납니다.


주요 발생 원인

  • WITH HOLD 커서 생성 후 격리 수준 변경

가장 흔한 원인입니다. WITH HOLD 옵션으로 커서를 생성한 트랜잭션의 격리 수준과, 이후 그 커서를 FETCH 하려는 트랜잭션의 격리 수준이 서로 다를 때 발생합니다. PostgreSQL은 커서가 생성된 시점의 스냅샷 또는 격리 수준 정보를 내부적으로 유지하므로, 격리 수준이 달라지면 데이터 일관성을 보장하기 어렵다고 판단하여 에러를 반환합니다.

  • 애플리케이션 커넥션 풀에서 세션 재사용 시 격리 수준 불일치

커넥션 풀(예: PgBouncer, pgpool-II)을 사용하는 환경에서 세션이 재사용될 때, 이전 세션에서 WITH HOLD 커서를 열어두고 커밋한 뒤 커넥션이 풀로 반환되면, 다음 사용자가 해당 커넥션을 재사용할 때 격리 수준이 다를 수 있습니다. 이 경우 커서는 여전히 이전 세션에 남아 있으므로, 격리 수준 불일치로 인해 25008 에러가 발생합니다.

  • 저장 프로시저 또는 함수 내부에서의 동적 격리 수준 설정

PL/pgSQL 함수나 저장 프로시저 내에서 SET TRANSACTION ISOLATION LEVEL 명령을 동적으로 실행하거나, 함수 호출 전후로 격리 수준이 변경되는 경우에도 이 에러가 발생할 수 있습니다. 특히 중첩 트랜잭션이나 SAVEPOINT를 사용하는 복잡한 로직에서 격리 수준이 의도치 않게 변경되는 경우가 많습니다.


해결 방법

원인 1 해결: 커서 생성 트랜잭션과 동일한 격리 수준 사용

WITH HOLD 커서를 FETCH하는 트랜잭션의 격리 수준을 커서를 생성한 트랜잭션과 동일하게 맞춰주어야 합니다.

-- 잘못된 예시: 격리 수준이 다른 트랜잭션에서 커서 생성 후 FETCH
-- 1단계: SERIALIZABLE로 커서 생성 및 커밋
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
DECLARE my_cursor CURSOR WITH HOLD FOR
    SELECT id, name FROM employees ORDER BY id;
COMMIT;

-- 2단계: 격리 수준 없이(기본 READ COMMITTED) FETCH 시도 -> 에러 발생
BEGIN;
FETCH ALL FROM my_cursor; -- ERROR: 25008
COMMIT;

-- 올바른 예시: 동일한 격리 수준으로 FETCH
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
FETCH ALL FROM my_cursor; -- 정상 동작
COMMIT;
CLOSE my_cursor;

원인 2 해결: 커넥션 풀 환경에서 커서 수명 관리

커넥션을 풀에 반환하기 전에 반드시 WITH HOLD 커서를 닫아야 합니다.

-- 커넥션 반환 전 커서 명시적 종료
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE batch_cursor CURSOR WITH HOLD FOR
    SELECT order_id, amount FROM orders WHERE status = 'PENDING';
COMMIT;

-- 작업 수행
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
FETCH 100 FROM batch_cursor;
-- ... 비즈니스 로직 처리 ...
COMMIT;

-- 반드시 커서를 닫고 커넥션을 풀에 반환
CLOSE batch_cursor;

-- 애플리케이션 레벨에서의 안전한 커서 관리 예시 (PL/pgSQL)
DO $$
DECLARE
    v_cursor REFCURSOR;
    v_row employees%ROWTYPE;
BEGIN
    OPEN v_cursor FOR SELECT * FROM employees;
    -- 작업 수행
    FETCH v_cursor INTO v_row;
    -- 반드시 닫기
    CLOSE v_cursor;
EXCEPTION
    WHEN OTHERS THEN
        -- 예외 발생 시에도 커서 닫기
        IF v_cursor IS NOT NULL THEN
            CLOSE v_cursor;
        END IF;
        RAISE;
END;
$$;

원인 3 해결: 함수/프로시저에서 일관된 격리 수준 유지

저장 프로시저나 함수 내에서 격리 수준을 명시적으로 고정하고, 동적 변경을 피합니다.

-- 안전한 WITH HOLD 커서 사용 패턴
CREATE OR REPLACE PROCEDURE process_large_dataset()
LANGUAGE plpgsql
AS $$
DECLARE
    v_record RECORD;
    v_isolation_level TEXT;
BEGIN
    -- 현재 격리 수준 확인
    SELECT current_setting('transaction_isolation') INTO v_isolation_level;
    RAISE NOTICE 'Current isolation level: %', v_isolation_level;

    -- WITH HOLD 커서는 격리 수준을 명시한 트랜잭션에서 생성
    -- 프로시저 내에서는 WITH HOLD 커서 대신 일반 루프 사용 권장
    FOR v_record IN
        SELECT id, name FROM large_table ORDER BY id
    LOOP
        -- 개별 처리 로직
        UPDATE large_table
        SET processed = TRUE
        WHERE id = v_record.id;
        
        -- 대용량 처리 시 중간 커밋이 필요하면 WITH HOLD 커서 사용
        -- 단, 격리 수준을 일관되게 유지할 것
    END LOOP;
END;
$$;

-- WITH HOLD 커서를 활용한 안전한 배치 처리 예시
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE safe_cursor CURSOR WITH HOLD FOR
    SELECT id FROM large_table WHERE processed = FALSE ORDER BY id;
COMMIT;

-- 동일한 격리 수준으로 배치 처리
DO $$
DECLARE
    v_id INTEGER;
    v_count INTEGER := 0;
BEGIN
    LOOP
        -- 반드시 동일 격리 수준으로 트랜잭션 시작
        BEGIN;
        SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
        
        FETCH safe_cursor INTO v_id;
        EXIT WHEN NOT FOUND;
        
        UPDATE large_table SET processed = TRUE WHERE id = v_id;
        v_count := v_count + 1;
        COMMIT;
        
        IF v_count % 1000 = 0 THEN
            RAISE NOTICE 'Processed % rows', v_count;
        END IF;
    END LOOP;
    
    CLOSE safe_cursor;
END;
$$;

예방 방법

  • WITH HOLD 커서 사용 시 격리 수준을 항상 명시하고 문서화하기

WITH HOLD 커서를 사용하는 모든 코드 경로에서 격리 수준을 명시적으로 설정하고, 해당 커서를 사용하는 모든 트랜잭션이 동일한 격리 수준을 사용하도록 코딩 컨벤션 또는 팀 가이드라인을 수립하세요. 아래와 같이 애플리케이션 설정 파일이나 커넥션 초기화 스크립트에 기본 격리 수준을 명시하면 실수를 줄일 수 있습니다.

“`sql

— 커넥션 초기화 시 격리 수준 명시 (postgresql.conf 또는 ALTER ROLE)

ALTER ROLE batch_user SET default_transaction_isolation = ‘read committed’;

— 또는 데이터베이스 단위로 설정

ALTER DATABASE mydb SET default_transaction_isolation = ‘read committed’;

— 현재 격리 수준 확인 쿼리

SHOW transaction_isolation;

SELECT current_setting(‘transaction_isolation’);

“`

  • WITH HOLD 커서의 수명 주기를 명확히 관리하고 모니터링하기

WITH HOLD 커서는 커밋 이후에도 서버 메모리를 점유하므로, 사용 후 즉시 닫는 습관을 들이고, 장시간 열려 있는 커서를 주기적으로 모니터링해야 합니다. 아래 쿼리로 현재 열려 있는 커서 현황을 확인할 수 있으며, 필요 시 자동화된 클린업 스크립트를 운영하는 것을 권장합니다.

“`sql

— 현재 열려 있는 커서 목록 조회

SELECT name, statement, is_holdable, creation_time

FROM pg_cursors

ORDER BY creation_time;

— 오래된 WITH HOLD 커서 감지 (10분 이상 유지된 커서)

SELECT name, statement, creation_time,

NOW() – creation_time AS open_duration

FROM pg_cursors

WHERE is_holdable = TRUE

AND creation_time < NOW() - INTERVAL '10 minutes'

ORDER BY creation_time;

“`


관련 에러

  • 25006 (read_only_sql_transaction): 읽기 전용 트랜잭션에서 쓰기 작업 시도 시 발생하는 에러로, 트랜잭션 격리 및 속성 설정과 관련된 에러군에 속합니다.
  • 25001 (active_sql_transaction): 이미 활성화된 트랜잭션 내에서 트랜잭션 격리 수준을 변경하려 할 때 발생하며, 25008과 함께 격리 수준 관리 실수에서 자주 동반됩니다.
  • 25P01 (no_active_sql_transaction): 활성 트랜잭션이 없는 상태에서 커서를 FETCH하려 할 때 발생하며, WITH HOLD 커서 사용 시 트랜잭션 관리 누락으로 함께 나타나는 경우가 있습니다.
  • 34000 (invalid_cursor_name): 잘못된 커서 이름을 참조할 때 발생하며, WITH HOLD 커서가 예외 처리 없이 닫히거나 세션이 종료된 경우 후속으로 발생할 수 있습니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기