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

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

이 글에서 다루는 내용

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

25002 branch transaction already active 는?

PostgreSQL 에러 코드 25002, branch transaction already active는 분산 트랜잭션(Distributed Transaction) 환경에서 동일한 트랜잭션 브랜치(branch)가 이미 활성화된 상태에서 또다시 동일 브랜치를 시작하려 할 때 발생합니다. 이 에러는 주로 XA 트랜잭션 프로토콜 또는 2단계 커밋(Two-Phase Commit, 2PC)을 사용하는 미들웨어, WAS(Web Application Server), 또는 분산 데이터베이스 연동 환경에서 나타납니다. 쉽게 말해, 하나의 글로벌 트랜잭션 식별자(Global Transaction ID, GTID)에 묶인 브랜치 트랜잭션이 이미 살아있는데 동일한 브랜치를 다시 열려는 충돌 상황입니다.


주요 발생 원인

1. 동일한 XA 트랜잭션 ID로 중복 시작 시도

가장 흔한 원인은 애플리케이션 또는 미들웨어에서 동일한 XID(Transaction ID)를 재사용하는 경우입니다. XA 트랜잭션에서는 글로벌 트랜잭션 ID와 브랜치 ID의 조합이 전 세계적으로 유일해야 합니다. 커넥션 풀 설정 오류나 트랜잭션 관리 코드의 버그로 인해 이미 열려있는 브랜치에 동일한 ID로 다시 접근하면 이 에러가 즉시 발생합니다.

2. 2단계 커밋(2PC) 과정에서 비정상 종료 후 재연결

애플리케이션 서버가 PREPARE 단계에서 비정상 종료(crash)되거나 네트워크 단절이 발생한 후, 재시작된 애플리케이션이 기존에 PREPARED 상태로 남아있는 트랜잭션을 인지하지 못하고 동일한 브랜치를 다시 열려고 시도하는 경우입니다. PostgreSQL은 PREPARED 트랜잭션을 pg_prepared_xacts 시스템 뷰에 보관하며, 이 잔류 트랜잭션이 정리되지 않은 상태에서 중복 시작이 발생하면 25002 에러로 이어집니다. 이는 특히 Java EE / Jakarta EE 환경의 JTA(Java Transaction API) 구현체에서 자주 관찰됩니다.

3. 커넥션 풀의 트랜잭션 상태 불일치

HikariCP, PgBouncer(transaction pooling 모드), c3p0 등의 커넥션 풀 환경에서 이전 세션의 트랜잭션이 완전히 종료(COMMIT 또는 ROLLBACK)되지 않은 채로 커넥션이 풀에 반환되고, 이 커넥션을 다른 요청이 가져가서 새로운 XA 트랜잭션을 시작하려 할 때 발생합니다. 특히 PgBouncer를 transaction pooling 모드로 운영할 경우 2PC와 호환되지 않아 이 에러가 빈번하게 발생할 수 있습니다.


해결 방법

원인 1 해결: 잔류 PREPARED 트랜잭션 확인 및 정리

먼저 현재 PREPARED 상태로 남아있는 트랜잭션을 조회합니다.

-- 현재 PREPARED 상태의 트랜잭션 전체 조회
SELECT gid, prepared, owner, database, transaction
FROM pg_prepared_xacts
ORDER BY prepared;

문제가 되는 트랜잭션을 발견했다면, 상황에 따라 COMMIT 또는 ROLLBACK으로 정리합니다.

-- 특정 PREPARED 트랜잭션을 롤백하여 정리
ROLLBACK PREPARED 'my_global_txn_branch_001';

-- 또는 커밋이 맞는 상황이라면
COMMIT PREPARED 'my_global_txn_branch_001';

오래된 PREPARED 트랜잭션을 일괄 정리하고 싶다면 아래 쿼리를 활용하세요.

-- 1시간 이상 지난 PREPARED 트랜잭션 목록 확인
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts
WHERE prepared < NOW() - INTERVAL '1 hour';

-- DO 블록으로 일괄 롤백 처리 (주의: 운영 환경에서는 반드시 검토 후 실행)
DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN
        SELECT gid FROM pg_prepared_xacts
        WHERE prepared < NOW() - INTERVAL '1 hour'
    LOOP
        EXECUTE 'ROLLBACK PREPARED ' || quote_literal(r.gid);
        RAISE NOTICE 'Rolled back prepared transaction: %', r.gid;
    END LOOP;
END;
$$;

원인 2 해결: XA 트랜잭션 ID 유일성 보장

애플리케이션 레벨에서 XA 트랜잭션 ID를 생성할 때 반드시 글로벌 유일성을 보장해야 합니다. 아래는 PostgreSQL에서 직접 2PC를 사용하는 예제입니다.

-- 올바른 2단계 커밋 흐름 예시

-- 1단계: 트랜잭션 시작 및 작업 수행
BEGIN;

INSERT INTO orders (customer_id, amount, status)
VALUES (1001, 50000, 'PENDING');

UPDATE inventory SET stock = stock - 1
WHERE product_id = 42;

-- 2단계: PREPARE (고유한 GID 사용 - 타임스탬프 + UUID 조합 권장)
PREPARE TRANSACTION 'txn_20240115_143022_a1b2c3d4';

-- 별도 확인 후 커밋 또는 롤백
-- 성공 시:
COMMIT PREPARED 'txn_20240115_143022_a1b2c3d4';

-- 실패 시:
-- ROLLBACK PREPARED 'txn_20240115_143022_a1b2c3d4';

원인 3 해결: 커넥션 반환 전 트랜잭션 상태 초기화

커넥션 풀에서 커넥션 반환 시 반드시 트랜잭션을 정리하는 로직을 추가합니다.

-- 현재 세션의 트랜잭션 상태 확인
SELECT pg_current_xact_id_if_assigned() AS current_txid,
       txid_current_if_assigned() AS txid,
       current_setting('transaction_isolation') AS isolation;

-- 커넥션 풀 반환 전 강제 롤백 (connection validation query로 활용)
ROLLBACK;

-- PgBouncer 사용 시 server_reset_query 설정 권장
-- pgbouncer.ini 설정 예시:
-- server_reset_query = DISCARD ALL;
-- (2PC 환경에서는 session pooling 모드 사용 권장)

예방 방법

1. max_prepared_transactions 모니터링 및 PREPARED 트랜잭션 주기적 감시

PostgreSQL의 max_prepared_transactions 파라미터를 적절히 설정하고, 잔류 PREPARED 트랜잭션을 주기적으로 모니터링하는 알림 체계를 구축하세요. 아래 쿼리를 Cron 또는 모니터링 도구(Prometheus + postgres_exporter 등)에 등록하여 일정 시간 이상 남아있는 PREPARED 트랜잭션이 발견되면 즉시 알림을 받도록 설정하는 것이 Best Practice입니다.

-- 모니터링용 쿼리: 10분 이상 된 PREPARED 트랜잭션 감지
SELECT COUNT(*) AS stale_prepared_count,
       MIN(prepared) AS oldest_prepared_at
FROM pg_prepared_xacts
WHERE prepared < NOW() - INTERVAL '10 minutes';

2. PgBouncer 사용 시 2PC와 호환되는 풀링 모드 선택

2PC(2단계 커밋)를 사용하는 애플리케이션에서는 PgBouncer를 session pooling 모드로 운영해야 합니다. Transaction pooling 모드는 2PC와 근본적으로 호환되지 않으며, 이는 공식 문서에도 명시된 제한 사항입니다. 분산 트랜잭션이 필요한 마이크로서비스 아키텍처라면, Saga 패턴 또는 이벤트 소싱(Event Sourcing)으로 전환하여 2PC 의존성 자체를 줄이는 것도 장기적으로 고려해야 할 방향입니다.


관련 에러

  • 25000 (in_failed_sql_transaction): 이미 실패한 트랜잭션 블록 안에서 SQL을 실행하려 할 때 발생하며, 25002와 마찬가지로 트랜잭션 상태 관리 실패에서 비롯됩니다.
  • 25001 (active_sql_transaction): 이미 활성화된 트랜잭션 내에서 허용되지 않는 명령을 실행할 때 발생합니다. SET TRANSACTION 등의 명령을 트랜잭션 중간에 실행하는 경우가 대표적입니다.
  • 40001 (serialization_failure): 직렬화 격리 수준에서 트랜잭션 충돌 시 발생하며, 분산 트랜잭션 환경에서 25002와 함께 복합적으로 나타날 수 있습니다.
  • 57P03 (cannot_connect_now): 데이터베이스 복구 중 발생하며, 비정상 종료 이후 PREPARED 트랜잭션 처리 과정에서 연관될 수 있습니다.
DBMS 에러 코드 시리즈

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

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

댓글 남기기