2026년 08월 28일 | DBMS Error 가이드
이 글에서 다루는 내용
25004 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
25004 inappropriate isolation level for branch transaction 는?
PostgreSQL 에러 코드 25004는 “inappropriate isolation level for branch transaction” 으로, 분산 트랜잭션(branch transaction) 환경에서 허용되지 않는 격리 수준(isolation level)을 설정하려 할 때 발생합니다. 이 에러는 주로 XA(eXtended Architecture) 트랜잭션 또는 2단계 커밋(Two-Phase Commit, 2PC)을 사용하는 분산 데이터베이스 환경에서 나타납니다. PostgreSQL은 분산 트랜잭션의 브랜치에서 특정 격리 수준만을 허용하며, 이 규칙을 위반할 경우 즉시 이 에러를 발생시켜 데이터 일관성을 보호합니다.
주요 발생 원인
1. XA 트랜잭션 브랜치에서 SERIALIZABLE 이외의 격리 수준 강제 설정
XA 트랜잭션을 사용할 때, 일부 미들웨어나 ORM 프레임워크는 트랜잭션 브랜치가 시작된 이후에 SET TRANSACTION ISOLATION LEVEL을 호출하여 격리 수준을 변경하려 시도합니다. PostgreSQL의 분산 트랜잭션 브랜치는 이미 격리 수준이 결정된 상태이기 때문에 사후 변경을 허용하지 않으며, 이 경우 25004 에러가 발생합니다. 특히 Java의 JTA(Java Transaction API)나 Spring의 JtaTransactionManager를 사용할 때 이런 상황이 자주 발생합니다.
2. 2단계 커밋(2PC) 환경에서의 잘못된 격리 수준 설정 순서
PREPARE TRANSACTION을 사용하는 2PC 환경에서, 트랜잭션이 이미 준비(PREPARE) 단계에 진입한 이후 격리 수준을 변경하거나, 브랜치 트랜잭션 시작 후 BEGIN 없이 격리 수준을 설정하려는 경우 이 에러가 발생합니다. 2PC 트랜잭션의 브랜치는 독립적인 라이프사이클을 가지므로, 격리 수준은 반드시 브랜치가 시작되기 전에 지정되어야 합니다. 이는 애플리케이션 코드와 DB 드라이버 간의 트랜잭션 제어 타이밍 문제로 자주 발생합니다.
3. 분산 트랜잭션 미들웨어(예: Pgpool-II, PgBouncer)와의 격리 수준 불일치
Pgpool-II나 기타 커넥션 풀러가 분산 쿼리를 브랜치 트랜잭션으로 분리할 때, 풀러 레벨에서 설정된 격리 수준과 개별 PostgreSQL 인스턴스에서 허용하는 격리 수준이 충돌하는 경우 이 에러가 발생할 수 있습니다. 특히 READ UNCOMMITTED 격리 수준은 PostgreSQL이 내부적으로 READ COMMITTED로 처리하기 때문에, 미들웨어가 명시적으로 READ UNCOMMITTED를 브랜치에 강제 적용하려 할 때 예기치 않은 동작이 발생합니다. 이 문제는 미들웨어의 버전 업그레이드 또는 설정 변경 후에 갑자기 나타나는 경우가 많습니다.
해결 방법
원인 1 해결: XA 트랜잭션에서 격리 수준을 브랜치 시작 전에 설정
XA 트랜잭션을 사용할 때는 반드시 XA START 이전에 격리 수준을 지정해야 합니다.
-- 잘못된 방법: XA START 이후 격리 수준 변경 시도 (25004 에러 발생)
XA START 'txn_branch_001';
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 에러 발생!
-- 올바른 방법: XA START 이전에 격리 수준 설정
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
XA START 'txn_branch_001';
-- 이후 DML 작업 수행
INSERT INTO orders (order_id, customer_id, amount) VALUES (1001, 42, 9900);
XA END 'txn_branch_001';
XA PREPARE 'txn_branch_001';
XA COMMIT 'txn_branch_001';
원인 2 해결: 2PC 환경에서 올바른 트랜잭션 시작 순서 적용
PREPARE TRANSACTION을 사용하는 경우, 트랜잭션 시작과 함께 격리 수준을 즉시 지정합니다.
-- 잘못된 방법: BEGIN 이후 별도 SET TRANSACTION 없이 PREPARE 직전에 변경 시도
BEGIN;
INSERT INTO accounts (id, balance) VALUES (1, 10000);
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 이미 트랜잭션 시작 후라 위험
PREPARE TRANSACTION 'my_distributed_txn';
-- 올바른 방법: BEGIN과 함께 격리 수준을 즉시 명시
BEGIN ISOLATION LEVEL SERIALIZABLE;
INSERT INTO accounts (id, balance) VALUES (1, 10000);
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
PREPARE TRANSACTION 'my_distributed_txn';
-- 다른 노드에서 처리 완료 후
COMMIT PREPARED 'my_distributed_txn';
-- 준비된 트랜잭션 목록 확인 (모니터링용)
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts
ORDER BY prepared;
원인 3 해결: 미들웨어 설정에서 기본 격리 수준 통일
Pgpool-II나 PgBouncer 설정에서 기본 격리 수준을 PostgreSQL과 일치시키고, 애플리케이션 레벨에서 명시적으로 제어합니다.
-- PostgreSQL 서버의 기본 격리 수준 확인
SHOW default_transaction_isolation;
-- 세션 레벨에서 기본 격리 수준 통일 (커넥션 풀에서 연결 후 실행)
SET SESSION default_transaction_isolation = 'read committed';
-- 특정 트랜잭션에서만 격리 수준 변경이 필요한 경우
BEGIN;
SET LOCAL default_transaction_isolation = 'repeatable read';
-- 작업 수행
SELECT * FROM inventory WHERE product_id = 500 FOR UPDATE;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 500;
COMMIT;
-- postgresql.conf 또는 ALTER SYSTEM으로 서버 전체 기본값 설정
ALTER SYSTEM SET default_transaction_isolation = 'read committed';
SELECT pg_reload_conf();
-- 특정 사용자나 데이터베이스에 대한 기본 격리 수준 설정
ALTER ROLE app_user SET default_transaction_isolation = 'read committed';
ALTER DATABASE myapp_db SET default_transaction_isolation = 'read committed';
예방 방법
1. 트랜잭션 격리 수준을 항상 트랜잭션 시작 시점에 명시적으로 선언하는 코딩 표준 수립
분산 트랜잭션 환경에서 가장 흔한 실수는 격리 수준을 트랜잭션 중간에 변경하려는 것입니다. 팀 전체의 코딩 표준으로 BEGIN ISOLATION LEVEL {level} 구문을 사용하도록 강제하고, 코드 리뷰 단계에서 SET TRANSACTION ISOLATION LEVEL이 트랜잭션 블록 내부에서 호출되는 패턴을 반드시 검출하도록 합니다. CI/CD 파이프라인에 정적 분석 도구를 통합하여 이러한 안티패턴을 자동으로 감지하는 것이 장기적으로 가장 효과적인 예방책입니다.
-- 권장 패턴: 항상 BEGIN과 함께 격리 수준 선언
BEGIN ISOLATION LEVEL READ COMMITTED;
-- 모든 DML은 여기서부터
COMMIT;
2. 분산 트랜잭션 환경에서 정기적인 pg_prepared_xacts 모니터링 및 좀비 트랜잭션 정리 자동화
준비(prepared) 상태에서 멈춘 트랜잭션은 격리 수준 관련 에러의 간접적 원인이 될 수 있습니다. 아래 쿼리를 크론잡 또는 모니터링 도구에 통합하여 일정 시간 이상 지속되는 prepared 트랜잭션을 자동으로 알림 및 정리하는 체계를 구축하세요.
-- 1시간 이상 미완료된 준비된 트랜잭션 탐지 및 알림
SELECT gid,
prepared,
now() - prepared AS duration,
owner,
database
FROM pg_prepared_xacts
WHERE now() - prepared > INTERVAL '1 hour'
ORDER BY prepared;
-- 필요 시 좀비 트랜잭션 강제 롤백 (주의: 반드시 확인 후 실행)
ROLLBACK PREPARED 'zombie_txn_gid';
관련 에러
- 25000 (invalid_transaction_state): 잘못된 트랜잭션 상태에서 명령을 실행할 때 발생하는 상위 범주 에러로, 25004는 이 에러의 하위 코드입니다.
- 25001 (active_sql_transaction): 이미 활성화된 SQL 트랜잭션 내에서 허용되지 않는 작업을 수행할 때 발생하며, 격리 수준 설정 문제와 함께 나타날 수 있습니다.
- 25P01 (no_active_sql_transaction): 트랜잭션 블록 외부에서 트랜잭션 제어 명령을 실행할 때 발생하며, 2PC 환경에서
COMMIT PREPARED나ROLLBACK PREPARED를 잘못 호출할 때 함께 나타날 수 있습니다. - 25P02 (in_failed_sql_transaction): 실패한 트랜잭션 내에서 추가 명령을 실행하려 할 때 발생하며, 분산 트랜잭션에서 브랜치 실패 후 복구 처리 시 주의가 필요합니다.
- 40001 (serialization_failure): SERIALIZABLE 격리 수준 사용 시 직렬화 충돌로 발생하며, 분산 트랜잭션 환경에서 격리 수준을 높일 때 함께 고려해야 하는 에러입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.