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

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

이 글에서 다루는 내용

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

25003 inappropriate access mode for branch transaction 는?

PostgreSQL 에러 코드 25003inappropriate access mode for branch transaction이라는 메시지로 발생하며, 분산 트랜잭션(Distributed Transaction) 환경에서 브랜치 트랜잭션(Branch Transaction)에 허용되지 않는 접근 모드를 사용하려 할 때 나타납니다. 이 에러는 주로 XA(eXtended Architecture) 트랜잭션 또는 2단계 커밋(Two-Phase Commit, 2PC) 프로토콜을 사용하는 환경에서 발생하며, 트랜잭션의 상태와 접근 권한이 일치하지 않을 때 PostgreSQL이 이를 거부하는 것입니다. 예를 들어, 읽기 전용(read-only)으로 준비된 브랜치 트랜잭션에서 쓰기 작업을 수행하거나, 반대로 읽기-쓰기 트랜잭션에서 잘못된 모드로 접근할 경우 이 에러가 트리거됩니다.


주요 발생 원인

1. 읽기 전용 브랜치 트랜잭션에서 쓰기 시도

2PC 환경에서 트랜잭션을 READ ONLY로 시작한 후, 해당 브랜치에서 데이터 변경(INSERT, UPDATE, DELETE)을 시도하면 이 에러가 발생합니다. 브랜치 트랜잭션은 선언된 접근 모드를 엄격하게 따르기 때문에, 선언과 실제 작업 간의 불일치가 생기면 PostgreSQL은 즉시 에러를 반환합니다. 이는 분산 트랜잭션의 일관성을 보장하기 위한 PostgreSQL의 보호 메커니즘입니다.

2. PREPARE TRANSACTION 이후 잘못된 접근 모드로 재참여

PREPARE TRANSACTION으로 트랜잭션을 준비 상태로 만든 뒤, 다른 세션이나 프로세스에서 해당 트랜잭션에 잘못된 접근 모드로 재참여(rejoin)하려 할 때 에러가 발생합니다. 준비된 트랜잭션은 특정 접근 모드로 고정되어 있으며, 이를 무시하고 다른 모드로 접근하는 것은 허용되지 않습니다. 특히 미들웨어나 커넥션 풀링 레이어에서 트랜잭션 상태를 제대로 추적하지 못할 경우 자주 발생합니다.

3. 애플리케이션 레벨에서의 트랜잭션 모드 불일치

Java의 JTA(Java Transaction API), Spring의 분산 트랜잭션 매니저, 또는 기타 ORM 프레임워크에서 트랜잭션 접근 모드를 잘못 설정하거나 자동으로 관리하는 과정에서 브랜치 트랜잭션과 충돌이 발생합니다. 애플리케이션이 트랜잭션을 READ ONLY로 선언하고 시작했는데, 내부 로직에서 실제로는 쓰기 작업을 수행하려 하면 PostgreSQL은 이를 25003 에러로 거부합니다. 이 경우 에러의 근본 원인이 애플리케이션 코드에 있기 때문에 DB 레벨에서만 해결하기 어렵습니다.


해결 방법

원인 1: 읽기 전용 트랜잭션에서 쓰기 작업 분리

트랜잭션을 시작할 때 접근 모드를 명확하게 지정하고, 쓰기 작업이 필요한 경우 반드시 READ WRITE 모드를 사용하세요.

-- 잘못된 예시: READ ONLY로 선언 후 쓰기 시도
BEGIN TRANSACTION READ ONLY;
INSERT INTO orders (product_id, quantity) VALUES (101, 5); 
-- ERROR: 25003 inappropriate access mode for branch transaction

-- 올바른 예시: 쓰기 작업이 필요하면 READ WRITE 명시
BEGIN TRANSACTION READ WRITE;
INSERT INTO orders (product_id, quantity) VALUES (101, 5);
COMMIT;

-- 읽기만 필요한 경우 READ ONLY 유지
BEGIN TRANSACTION READ ONLY;
SELECT * FROM orders WHERE product_id = 101;
COMMIT;

원인 2: PREPARE TRANSACTION 올바른 사용 패턴

2PC를 사용할 때는 트랜잭션 준비 전 접근 모드를 명확히 하고, COMMIT PREPARED 또는 ROLLBACK PREPARED로 정리해야 합니다.

-- 2PC 올바른 사용 예시

-- 세션 1: 트랜잭션 시작 및 준비
BEGIN;
SET TRANSACTION READ WRITE;
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 1;
PREPARE TRANSACTION 'txn_branch_001';

-- 세션 2: 준비된 트랜잭션 커밋
COMMIT PREPARED 'txn_branch_001';

-- 또는 롤백이 필요한 경우
ROLLBACK PREPARED 'txn_branch_001';

-- 현재 준비된 트랜잭션 목록 확인
SELECT gid, prepared, owner, database, transaction
FROM pg_prepared_xacts;

원인 3: 트랜잭션 모드 확인 및 재설정

현재 트랜잭션의 접근 모드를 확인하고 올바르게 설정하는 방법입니다.

-- 현재 트랜잭션 접근 모드 확인
SHOW transaction_read_only;

-- 트랜잭션 시작 시 명시적 모드 선언
BEGIN;
SET TRANSACTION READ WRITE;
-- 또는
SET SESSION CHARACTERISTICS AS TRANSACTION READ WRITE;

-- 실제 작업 수행
UPDATE inventory SET stock = stock - 10 WHERE item_id = 42;
INSERT INTO audit_log (action, item_id, ts) VALUES ('STOCK_REDUCE', 42, NOW());
COMMIT;

-- 세션 기본값을 READ WRITE로 설정 (postgresql.conf 또는 세션 레벨)
-- default_transaction_read_only = off  -- postgresql.conf 설정
SET default_transaction_read_only = off;  -- 세션 레벨 설정

-- 고아(orphaned) 준비 트랜잭션 정리
DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN SELECT gid FROM pg_prepared_xacts 
             WHERE database = current_database()
             AND prepared < NOW() - INTERVAL '1 hour'
    LOOP
        EXECUTE 'ROLLBACK PREPARED ' || quote_literal(r.gid);
        RAISE NOTICE 'Rolled back orphaned transaction: %', r.gid;
    END LOOP;
END;
$$;

예방 방법

1. 트랜잭션 접근 모드를 명시적으로 선언하는 코딩 표준 수립

모든 트랜잭션 시작 시 READ ONLY 또는 READ WRITE를 명시적으로 선언하는 팀 내 코딩 컨벤션을 수립하세요. 특히 분산 트랜잭션 환경에서는 접근 모드의 묵시적 상속이 예기치 않은 에러를 유발할 수 있으므로, 각 브랜치 트랜잭션의 시작 지점에서 명확하게 모드를 선언하는 것이 필수적입니다. 또한 CI/CD 파이프라인에 트랜잭션 모드 검증 테스트를 포함시켜 배포 전 미리 확인하는 절차를 만들어두는 것을 권장합니다.

2. pg_prepared_xacts 모니터링 및 고아 트랜잭션 자동 정리 구축

pg_prepared_xacts 시스템 뷰를 주기적으로 모니터링하여 오래된 준비 트랜잭션이 누적되지 않도록 관리하세요. 아래와 같은 모니터링 쿼리를 cron 또는 pg_cron으로 스케줄링하여 고아 트랜잭션을 자동으로 탐지하고 알림을 발송하는 시스템을 구축하는 것이 좋습니다. 준비 트랜잭션이 일정 시간(예: 30분) 이상 커밋되지 않으면 알림을 발생시키는 임계값 기반의 모니터링은 25003 에러를 포함한 다양한 2PC 관련 문제를 사전에 예방하는 데 매우 효과적입니다.

-- pg_cron을 이용한 고아 트랜잭션 모니터링 예시
SELECT cron.schedule(
    'check_prepared_txns',
    '*/15 * * * *',  -- 15분마다 실행
    $$
    SELECT COUNT(*), MAX(EXTRACT(EPOCH FROM (NOW() - prepared))/60) AS max_age_minutes
    FROM pg_prepared_xacts
    WHERE prepared < NOW() - INTERVAL '30 minutes';
    $$
);

관련 에러

  • 25000 – invalid transaction state: 트랜잭션이 올바르지 않은 상태에 있을 때 발생하는 상위 에러 클래스입니다.
  • 25001 – active sql transaction: 이미 활성화된 트랜잭션 내에서 허용되지 않는 명령을 실행할 때 발생합니다.
  • 25002 – branch transaction already active: 동일한 브랜치 트랜잭션이 이미 활성화된 상태에서 다시 시작하려 할 때 발생합니다.
  • 25P01 – no active sql transaction: 트랜잭션이 없는 상태에서 트랜잭션 관련 명령을 실행할 때 발생합니다.
  • 25P02 – in failed sql transaction: 이미 실패한 트랜잭션 블록 내에서 추가 명령을 실행할 때 발생하며, ROLLBACK 이전에 다른 명령을 실행하면 나타납니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기