2026년 08월 29일 | DBMS Error 가이드
이 글에서 다루는 내용
25P02 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
25P02 in failed sql transaction 는?
PostgreSQL 에러 코드 25P02 (in_failed_sql_transaction)는 이미 오류가 발생하여 실패 상태에 빠진 트랜잭션 내에서 추가적인 SQL 명령을 실행하려 할 때 발생합니다. PostgreSQL은 트랜잭션 내에서 한 번 오류가 발생하면 해당 트랜잭션을 자동으로 중단(abort) 상태로 전환하며, 이 상태에서는 ROLLBACK 또는 ROLLBACK TO SAVEPOINT 외의 모든 명령이 거부됩니다. 주로 애플리케이션 레벨에서 예외 처리가 미흡하거나, 트랜잭션의 생명 주기를 제대로 관리하지 못할 때 연쇄적으로 발생하는 에러입니다.
주요 발생 원인
1. 트랜잭션 내 예외 처리 누락 후 추가 쿼리 실행
트랜잭션 블록 안에서 특정 SQL이 실패했음에도 불구하고, 애플리케이션이 해당 오류를 적절히 처리하지 않고 계속해서 다음 SQL 명령을 실행하려 할 때 이 에러가 발생합니다. 예를 들어, Python의 psycopg2나 Java의 JDBC를 사용할 때 try-catch 블록 없이 연속 쿼리를 수행하면 첫 번째 쿼리 실패 이후 모든 후속 쿼리가 25P02 에러를 반환합니다.
2. 존재하지 않는 테이블/컬럼 참조 또는 제약 조건 위반
트랜잭션 내에서 존재하지 않는 테이블이나 컬럼을 참조하거나, NOT NULL, UNIQUE, FOREIGN KEY 같은 제약 조건을 위반하는 쿼리가 실행되면 트랜잭션이 즉시 실패 상태로 전환됩니다. 이 상태에서 롤백 없이 새로운 쿼리를 시도하면 25P02가 반복적으로 발생하며, 특히 ORM(Hibernate, SQLAlchemy 등)을 사용할 때 내부적으로 이러한 상황이 숨겨져 디버깅이 어려워질 수 있습니다.
3. 커넥션 풀링 환경에서 오염된 커넥션 재사용
PgBouncer나 애플리케이션 레벨 커넥션 풀(HikariCP 등)을 사용하는 환경에서, 이전 트랜잭션이 실패한 후 롤백 없이 반환된 커넥션을 다음 요청이 재사용할 경우 25P02가 발생합니다. 커넥션이 여전히 실패한 트랜잭션 상태를 유지하고 있기 때문이며, 이는 간헐적으로 발생하여 원인 파악이 특히 까다롭습니다.
해결 방법
원인 1 해결: 트랜잭션 롤백 후 재시작
오류 발생 시 즉시 ROLLBACK을 실행하고 트랜잭션을 다시 시작해야 합니다.
-- 잘못된 패턴: 오류 발생 후 롤백 없이 계속 실행
BEGIN;
INSERT INTO orders (id, product_id) VALUES (1, 9999); -- FK 위반 오류 발생
INSERT INTO payments (order_id, amount) VALUES (1, 100); -- 25P02 에러 발생!
COMMIT;
-- 올바른 패턴: 오류 감지 후 롤백 처리
BEGIN;
INSERT INTO orders (id, product_id) VALUES (1, 9999); -- 오류 발생
-- 즉시 롤백
ROLLBACK;
-- 문제를 수정한 후 새로운 트랜잭션 시작
BEGIN;
INSERT INTO orders (id, product_id) VALUES (1, 1001); -- 유효한 product_id
INSERT INTO payments (order_id, amount) VALUES (1, 100);
COMMIT;
원인 1 해결: SAVEPOINT를 활용한 부분 롤백
트랜잭션 전체를 롤백하지 않고 특정 지점으로만 되돌리고 싶을 때 SAVEPOINT를 활용합니다.
BEGIN;
INSERT INTO customers (id, name) VALUES (1, 'Alice');
SAVEPOINT before_order;
INSERT INTO orders (id, product_id) VALUES (1, 9999); -- 오류 발생 가능성 있는 쿼리
-- 오류 발생 시 SAVEPOINT로 롤백
ROLLBACK TO SAVEPOINT before_order;
-- 수정된 데이터로 재시도
INSERT INTO orders (id, product_id) VALUES (1, 1001);
RELEASE SAVEPOINT before_order;
COMMIT;
원인 2 해결: 제약 조건 사전 확인
데이터를 삽입하거나 수정하기 전에 조건을 미리 확인하는 방어적 쿼리를 작성합니다.
-- 삽입 전 존재 여부 확인
BEGIN;
DO $$
DECLARE
v_product_exists BOOLEAN;
BEGIN
SELECT EXISTS(SELECT 1 FROM products WHERE id = 9999)
INTO v_product_exists;
IF v_product_exists THEN
INSERT INTO orders (id, product_id) VALUES (1, 9999);
ELSE
RAISE NOTICE '유효하지 않은 product_id입니다. 트랜잭션을 취소합니다.';
RAISE EXCEPTION 'INVALID_PRODUCT' USING ERRCODE = 'P0001';
END IF;
END;
$$;
COMMIT;
-- INSERT ... ON CONFLICT를 활용한 안전한 삽입
BEGIN;
INSERT INTO orders (id, product_id, status)
VALUES (1, 1001, 'pending')
ON CONFLICT (id) DO UPDATE
SET product_id = EXCLUDED.product_id,
status = EXCLUDED.status;
COMMIT;
원인 3 해결: 커넥션 반환 전 상태 확인 및 정리
커넥션 풀에 반환하기 전 반드시 트랜잭션 상태를 확인하고 정리합니다.
-- 현재 트랜잭션 상태 확인
SELECT pg_current_xact_id_if_assigned() IS NOT NULL AS in_transaction;
-- 트랜잭션 상태 확인 후 조건부 롤백
-- (애플리케이션 레벨에서 아래 쿼리를 커넥션 반환 전 실행)
DO $$
BEGIN
-- 현재 세션의 트랜잭션 상태 확인
IF current_setting('transaction_status', true) = 'idle in transaction (aborted)' THEN
ROLLBACK;
END IF;
END;
$$;
-- psql에서 트랜잭션 상태 확인
SELECT state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE pid = pg_backend_pid();
예방 방법
1. 애플리케이션 레벨에서 철저한 예외 처리 구현
모든 데이터베이스 작업은 반드시 예외 처리 블록으로 감싸고, 오류 발생 시 즉시 롤백을 수행하는 패턴을 코딩 표준으로 정착시켜야 합니다. PostgreSQL에서는 트랜잭션이 실패 상태에 빠지면 오직 ROLLBACK만이 해당 트랜잭션을 정상 상태로 되돌릴 수 있다는 점을 팀 전체가 명확히 인지해야 하며, CI/CD 파이프라인에서 트랜잭션 처리 패턴을 검증하는 자동화 테스트를 반드시 포함하시기 바랍니다.
-- PL/pgSQL에서의 올바른 예외 처리 패턴
CREATE OR REPLACE FUNCTION safe_insert_order(
p_order_id INT,
p_product_id INT,
p_amount NUMERIC
) RETURNS VOID AS $$
BEGIN
INSERT INTO orders (id, product_id) VALUES (p_order_id, p_product_id);
INSERT INTO payments (order_id, amount) VALUES (p_order_id, p_amount);
EXCEPTION
WHEN foreign_key_violation THEN
RAISE NOTICE '외래 키 위반: product_id=% 가 존재하지 않습니다.', p_product_id;
-- 트랜잭션은 자동으로 롤백됨
WHEN unique_violation THEN
RAISE NOTICE '중복 키 위반: order_id=% 이미 존재합니다.', p_order_id;
WHEN OTHERS THEN
RAISE NOTICE '예기치 않은 오류 발생: %', SQLERRM;
END;
$$ LANGUAGE plpgsql;
2. 커넥션 풀 헬스 체크 및 검증 쿼리 설정
커넥션 풀 설정에서 커넥션 유효성 검사(validation query)를 활성화하여 오염된 커넥션이 재사용되는 것을 방지해야 합니다. HikariCP의 경우 connectionTestQuery=SELECT 1, PgBouncer의 경우 server_reset_query=DISCARD ALL 또는 RESET ALL; SET SESSION AUTHORIZATION DEFAULT; 설정을 통해 커넥션 상태를 초기화하는 것이 Best Practice입니다.
-- 커넥션 상태 모니터링 쿼리 (DBA 모니터링 대시보드용)
SELECT
pid,
usename,
application_name,
state,
xact_start,
query_start,
state_change,
left(query, 100) AS current_query
FROM pg_stat_activity
WHERE state = 'idle in transaction (aborted)'
ORDER BY xact_start;
-- 오래된 실패 트랜잭션 강제 종료 (운영 환경 주의)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction (aborted)'
AND xact_start < NOW() - INTERVAL '5 minutes';
관련 에러
- 25001 (active_sql_transaction):
BEGIN없이 이미 활성화된 트랜잭션 내에서 트랜잭션을 시작하려 할 때 발생합니다. - 25006 (read_only_sql_transaction): 읽기 전용 트랜잭션에서 쓰기 작업을 시도할 때 발생하며,
SET TRANSACTION READ ONLY이후INSERT/UPDATE/DELETE실행 시 나타납니다. - 40001 (serialization_failure):
SERIALIZABLE격리 수준에서 직렬화 충돌이 발생할 때 나타나며, 이 에러 이후 롤백 없이 쿼리를 실행하면 25P02로 이어질 수 있습니다. - 40P01 (deadlock_detected): 교착 상태 감지 에러로, 데드락 발생 후 트랜잭션이 강제 중단되고 이후 25P02가 연쇄적으로 발생할 수 있습니다.
- 23503 (foreign_key_violation): 외래 키 위반으로 트랜잭션이 실패 상태로 빠지는 대표적인 원인 에러입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.