2026년 06월 25일 | DBMS Error 가이드
이 글에서 다루는 내용
25P02 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
25P02 in failed sql transaction 는?
PostgreSQL 에러 코드 25P02 (in\_failed\_sql\_transaction) 는 이미 오류가 발생하여 실패 상태에 빠진 트랜잭션 내에서 추가적인 SQL 명령을 실행하려고 할 때 발생하는 에러입니다. PostgreSQL은 트랜잭션 내에서 하나의 SQL 문이 실패하면 해당 트랜잭션 전체를 “aborted” 상태로 전환하며, 이 상태에서는 ROLLBACK 명령 외의 모든 SQL 실행이 거부됩니다. 즉, 트랜잭션이 이미 망가진 상태에서 계속 쿼리를 날리면 이 에러가 반복적으로 출력되며, 반드시 ROLLBACK으로 트랜잭션을 종료하고 다시 시작해야 합니다.
주요 발생 원인
1. 트랜잭션 내 SQL 오류 후 계속 실행 시도
가장 흔한 원인으로, BEGIN 블록 안에서 실행된 SQL 문 하나가 실패(예: 존재하지 않는 테이블 조회, 제약 조건 위반 등)한 이후에도 애플리케이션이 에러를 무시하고 다음 쿼리를 계속 실행하려는 경우입니다. PostgreSQL은 첫 번째 오류 이후 트랜잭션을 자동으로 aborted 상태로 만들기 때문에, 이후 모든 SQL 명령은 25P02 에러를 반환합니다. 이는 Python의 psycopg2, Java의 JDBC 등 다양한 드라이버에서 예외 처리를 제대로 하지 않을 때 자주 발생합니다.
2. 애플리케이션의 예외 처리 미흡 (Exception Handling 부재)
애플리케이션 코드에서 데이터베이스 예외를 catch하지 않거나, catch하더라도 롤백을 수행하지 않고 동일한 커넥션을 재사용하는 경우 이 에러가 연쇄적으로 발생합니다. 특히 커넥션 풀(Connection Pool) 환경에서 실패한 트랜잭션이 롤백되지 않은 채 풀로 반환되면, 다음 요청에서도 동일한 에러가 재현됩니다. 이는 단일 오류가 서비스 전체의 DB 요청 실패로 이어지는 심각한 장애를 유발할 수 있습니다.
3. PL/pgSQL 함수 내 예외 처리 누락
저장 프로시저나 PL/pgSQL 함수 내에서 내부적으로 발생한 예외를 EXCEPTION 블록으로 처리하지 않으면, 함수 호출 자체가 실패하면서 상위 트랜잭션까지 aborted 상태로 만듭니다. 이후 해당 트랜잭션에서 다른 쿼리를 실행하면 25P02가 발생합니다. 복잡한 비즈니스 로직을 DB 함수로 구현할 때 특히 주의가 필요한 부분입니다.
해결 방법
원인 1 해결: 트랜잭션 오류 후 즉시 ROLLBACK
트랜잭션 내에서 에러가 발생하면 반드시 ROLLBACK을 수행하고 트랜잭션을 새로 시작해야 합니다.
-- 잘못된 패턴: 에러 발생 후 계속 실행
BEGIN;
INSERT INTO orders (id, amount) VALUES (1, 500);
INSERT INTO orders (id, amount) VALUES (1, 300); -- 중복 키 오류 발생!
-- 이 시점부터 트랜잭션은 aborted 상태
SELECT * FROM orders; -- 25P02 에러 발생!
COMMIT; -- 이것도 실패, ROLLBACK과 동일하게 처리됨
-- 올바른 패턴: 에러 감지 후 ROLLBACK
BEGIN;
INSERT INTO orders (id, amount) VALUES (1, 500);
INSERT INTO orders (id, amount) VALUES (1, 300); -- 오류 발생
ROLLBACK; -- 반드시 롤백 후 트랜잭션 종료
-- 새 트랜잭션으로 재시작
BEGIN;
INSERT INTO orders (id, amount) VALUES (2, 300); -- 다른 키로 재시도
COMMIT;
원인 2 해결: SAVEPOINT를 활용한 부분 롤백
트랜잭션 전체를 롤백하지 않고 특정 지점으로만 되돌리고 싶다면 SAVEPOINT를 활용하세요. 이렇게 하면 트랜잭션을 유지하면서 오류 발생 지점만 선택적으로 취소할 수 있습니다.
BEGIN;
INSERT INTO customers (id, name) VALUES (100, 'Alice');
SAVEPOINT before_order; -- 세이브포인트 설정
INSERT INTO orders (id, customer_id, amount) VALUES (1, 100, 500);
INSERT INTO orders (id, customer_id, amount) VALUES (1, 100, 300); -- 중복 오류!
-- 오류 발생 시 세이브포인트로 롤백
ROLLBACK TO SAVEPOINT before_order;
-- 트랜잭션은 여전히 활성 상태, 오류 없이 계속 진행 가능
INSERT INTO orders (id, customer_id, amount) VALUES (2, 100, 300); -- 재시도
COMMIT; -- customers + orders(id=2) 모두 커밋 성공
원인 3 해결: PL/pgSQL 함수 내 EXCEPTION 블록 추가
-- 잘못된 패턴: 예외 처리 없는 함수
CREATE OR REPLACE FUNCTION create_order(p_customer_id INT, p_amount NUMERIC)
RETURNS VOID AS $$
BEGIN
INSERT INTO orders (customer_id, amount) VALUES (p_customer_id, p_amount);
-- 오류 발생 시 호출자의 트랜잭션 전체가 aborted 상태가 됨
END;
$$ LANGUAGE plpgsql;
-- 올바른 패턴: EXCEPTION 블록으로 안전하게 처리
CREATE OR REPLACE FUNCTION create_order_safe(p_customer_id INT, p_amount NUMERIC)
RETURNS TEXT AS $$
BEGIN
INSERT INTO orders (customer_id, amount) VALUES (p_customer_id, p_amount);
RETURN 'SUCCESS';
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE '중복 주문 감지: customer_id=%', p_customer_id;
RETURN 'DUPLICATE';
WHEN foreign_key_violation THEN
RAISE NOTICE '존재하지 않는 고객: customer_id=%', p_customer_id;
RETURN 'INVALID_CUSTOMER';
WHEN OTHERS THEN
RAISE NOTICE '알 수 없는 오류: %', SQLERRM;
RETURN 'ERROR';
END;
$$ LANGUAGE plpgsql;
-- 호출 예시
BEGIN;
SELECT create_order_safe(100, 500.00);
SELECT create_order_safe(101, 300.00);
COMMIT; -- 두 호출 모두 트랜잭션 유지됨
현재 트랜잭션 상태 확인 방법
-- 현재 세션의 트랜잭션 상태 확인
SELECT pg_current_xact_id_if_assigned() AS current_xid,
current_setting('transaction_isolation') AS isolation_level;
-- 실패한 트랜잭션 정보 확인 (pg_stat_activity)
SELECT pid,
usename,
state,
query,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE state = 'idle in transaction (aborted)';
-- 즉시 롤백으로 정상화
ROLLBACK;
예방 방법
1. 애플리케이션 레벨에서 철저한 예외 처리 및 자동 롤백 구현
모든 데이터베이스 작업은 try-catch (또는 언어별 동등 구문) 블록으로 감싸고, 예외 발생 시 반드시 ROLLBACK을 수행한 후 커넥션을 풀에 반환하도록 설계해야 합니다. 커넥션 풀 라이브러리(예: PgBouncer, HikariCP)를 사용하는 경우, 커넥션 반환 전 트랜잭션 상태를 초기화하는 설정(reset_query 등)을 적극 활용하세요. 또한 pg_stat_activity 뷰를 모니터링하여 idle in transaction (aborted) 상태의 세션이 쌓이지 않도록 주기적으로 점검하는 것이 중요합니다.
2. idle_in_transaction_session_timeout 파라미터 설정
postgresql.conf 또는 세션 수준에서 idle_in_transaction_session_timeout 값을 설정하면, 트랜잭션이 aborted 상태로 일정 시간 이상 방치될 경우 PostgreSQL이 자동으로 해당 세션을 종료합니다. 이를 통해 실패한 트랜잭션이 리소스를 계속 점유하는 상황을 방지할 수 있습니다.
-- 세션 수준에서 30초 타임아웃 설정
SET idle_in_transaction_session_timeout = '30s';
-- 또는 특정 역할에 대해 설정
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '30s';
관련 에러
- 25001 (active\_sql\_transaction): 이미 활성화된 트랜잭션 내에서 트랜잭션을 시작할 수 없는 명령을 실행할 때 발생합니다.
- 40001 (serialization\_failure): 직렬화 격리 수준에서 트랜잭션 충돌로 인해 롤백이 필요할 때 발생하며, 재시도 로직이 필요합니다.
- 40P01 (deadlock\_detected): 두 트랜잭션이 서로의 락을 기다리는 교착 상태로, 발생 즉시 트랜잭션이 aborted 상태가 되어 25P02로 이어질 수 있습니다.
- 23505 (unique\_violation): 유니크 제약 조건 위반으로, 트랜잭션을 aborted 상태로 만들어 25P02의 직접적인 원인이 되는 경우가 많습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.