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

25008
2026년 06월 23일 | DBMS Error 가이드

이 글에서 다루는 내용

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

25008 held cursor requires same isolation level 는?

PostgreSQL 에러 코드 25008은 “held cursor requires same isolation level” 오류로, 트랜잭션 경계를 넘어 유지되는 커서(Holdable Cursor)가 현재 트랜잭션의 격리 수준과 일치하지 않을 때 발생합니다. WITH HOLD 옵션으로 선언된 커서는 트랜잭션이 커밋된 이후에도 살아남을 수 있는데, 이 커서를 다른 격리 수준의 트랜잭션에서 접근하려 할 때 PostgreSQL이 이 에러를 던집니다. 특히 SERIALIZABLE 또는 REPEATABLE READ 격리 수준과 혼용되는 환경에서 자주 목격되며, 데이터 일관성을 보호하기 위한 PostgreSQL의 안전장치입니다.


주요 발생 원인

1. WITH HOLD 커서를 다른 격리 수준의 트랜잭션에서 재사용

WITH HOLD 커서는 커서를 생성한 트랜잭션의 격리 수준 컨텍스트를 내부적으로 기억합니다. 예를 들어 SERIALIZABLE 격리 수준에서 커서를 열고, 이후 READ COMMITTED 트랜잭션 안에서 동일한 커서를 FETCH하려 하면 PostgreSQL은 격리 수준 불일치를 감지하고 25008 에러를 반환합니다. 이는 단순 실수처럼 보이지만, 커넥션 풀링 환경에서는 개발자가 인지하지 못한 채 발생하는 경우가 많습니다.

2. 커넥션 풀이나 미들웨어에 의한 세션 격리 수준 변경

PgBouncer, pgpool-II 같은 커넥션 풀러를 사용할 때, 세션 격리 수준이 풀 내부에서 재설정되거나 다른 클라이언트의 설정이 혼입되는 경우가 있습니다. WITH HOLD 커서가 살아있는 상태에서 백엔드 세션의 기본 격리 수준이 바뀌면 다음 트랜잭션에서 해당 커서에 접근할 때 이 에러가 발생합니다. 커넥션 풀 환경에서는 커서의 생명주기를 엄격하게 관리해야 하는 이유가 여기에 있습니다.

3. 명시적 SET TRANSACTION 또는 BEGIN 블록에서 격리 수준 불일치

애플리케이션 코드에서 트랜잭션마다 명시적으로 격리 수준을 다르게 설정하는 경우, WITH HOLD 커서의 격리 수준과 현재 트랜잭션의 격리 수준이 달라질 수 있습니다. 특히 레거시 코드나 ORM 레이어에서 트랜잭션 설정을 자동으로 오버라이드하는 경우 이 문제가 잠복해 있다가 특정 실행 경로에서만 표면화되어 디버깅이 어렵습니다.


해결 방법

원인 1 해결: 커서 생성 시와 동일한 격리 수준 사용

가장 직접적인 해결책은 WITH HOLD 커서를 FETCH하는 트랜잭션의 격리 수준을 커서를 생성했을 때와 동일하게 맞추는 것입니다.

-- 잘못된 예시: SERIALIZABLE에서 커서를 열고 READ COMMITTED에서 FETCH
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
DECLARE my_cursor CURSOR WITH HOLD FOR
    SELECT id, name FROM employees ORDER BY id;
COMMIT;

-- 다른 트랜잭션에서 READ COMMITTED로 접근 시도 → 에러 발생
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
FETCH NEXT FROM my_cursor;  -- ERROR: 25008
ROLLBACK;

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

원인 2 해결: 커서 사용 전 세션 격리 수준 명시적 확인 및 설정

커넥션 풀 환경에서는 커서를 열기 전에 항상 세션 격리 수준을 확인하고, 필요하다면 명시적으로 맞춰 주세요.

-- 현재 세션의 격리 수준 확인
SHOW transaction_isolation;

-- 세션 기본 격리 수준을 일치시킨 후 커서 선언
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;
DECLARE batch_cursor CURSOR WITH HOLD FOR
    SELECT order_id, total_amount
    FROM orders
    WHERE status = 'PENDING'
    ORDER BY created_at;
COMMIT;

-- FETCH 시에도 동일 격리 수준 보장
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
FETCH 100 FROM batch_cursor;
COMMIT;

-- 작업 완료 후 반드시 커서 닫기
CLOSE batch_cursor;

원인 3 해결: 커서 생명주기를 단일 트랜잭션 내로 제한

가능하면 WITH HOLD를 피하고, 커서를 하나의 트랜잭션 안에서 열고 닫도록 리팩토링하세요. 정말 필요한 경우에는 커서 생성 시 격리 수준을 주석 또는 애플리케이션 변수로 명시적으로 추적하세요.

-- WITH HOLD를 사용하지 않는 안전한 패턴
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;

DECLARE safe_cursor CURSOR FOR
    SELECT customer_id, email
    FROM customers
    WHERE is_active = true;

FETCH ALL FROM safe_cursor;

CLOSE safe_cursor;
COMMIT;

-- 배치 처리가 필요한 경우: LIMIT/OFFSET 또는 keyset pagination 활용
-- (WITH HOLD 대신 권장되는 대안)
SELECT customer_id, email
FROM customers
WHERE is_active = true
  AND customer_id > :last_seen_id
ORDER BY customer_id
LIMIT 1000;

긴급 복구: 문제가 된 커서 강제 닫기

이미 에러가 발생한 상태라면, 열려 있는 WITH HOLD 커서를 즉시 닫아 세션을 정리하세요.

-- 현재 세션의 열린 커서 목록 확인
SELECT name, statement, holdable, held
FROM pg_cursors
WHERE held = true;

-- 문제 커서 닫기
CLOSE my_cursor;

-- 모든 커서 일괄 정리가 필요한 경우 (세션 재연결이 가장 확실)
-- pg_cursors 뷰를 통해 held 커서 모두 식별 후 닫기
DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN SELECT name FROM pg_cursors WHERE held = true LOOP
        EXECUTE 'CLOSE ' || quote_ident(r.name);
    END LOOP;
END $$;

예방 방법

1. WITH HOLD 커서의 격리 수준을 애플리케이션 레벨에서 명시적으로 관리

WITH HOLD 커서를 사용하는 모든 코드 경로에서 커서 생성 시의 격리 수준을 반드시 문서화하고, FETCH 시 동일한 격리 수준을 강제하는 래퍼 함수 또는 클래스를 만들어 사용하세요. 팀 코드 리뷰 단계에서 WITH HOLD 사용 시 격리 수준 설정 코드가 반드시 동반되는지 체크리스트 항목으로 추가하는 것을 권장합니다. 가능하다면 WITH HOLD 자체를 사용하지 않고, 서버 사이드 커서 대신 애플리케이션 레벨의 페이지네이션이나 배치 ID 추적 방식으로 대체하는 것이 장기적으로 안전합니다.

2. 커넥션 풀 환경에서 세션 상태 초기화 보장

PgBouncer나 pgpool 같은 커넥션 풀러를 사용하는 환경에서는 세션이 풀로 반환되기 전에 반드시 모든 WITH HOLD 커서를 명시적으로 닫고, 격리 수준을 기본값(READ COMMITTED)으로 리셋하는 정리 로직을 추가하세요. PgBouncer의 server_reset_query 설정에 CLOSE ALL; RESET ALL; 등을 포함시켜 세션 상태 오염을 방지하는 것도 효과적인 방법입니다.


관련 에러

  • 25001 (active_sql_transaction): 이미 활성 트랜잭션이 있는 상태에서 트랜잭션을 시작하려 할 때 발생하며, 커서 관련 트랜잭션 관리 오류와 함께 나타날 수 있습니다.
  • 25006 (read_only_sql_transaction): 읽기 전용 트랜잭션에서 쓰기 작업을 시도할 때 발생하며, 격리 수준 설정과 함께 트랜잭션 특성 관리 실수로 동반되는 경우가 있습니다.
  • 34000 (invalid_cursor_name): 존재하지 않는 커서를 참조할 때 발생하며, WITH HOLD 커서가 예기치 않게 닫혔거나 세션이 재연결된 경우 25008 이후 연쇄적으로 발생할 수 있습니다.
  • 55006 (object_in_use): 커서나 객체가 이미 사용 중일 때 발생하며, 동시 접근 환경에서 25008과 유사한 맥락에서 나타납니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기