2026년 07월 02일 | DBMS Error 가이드
이 글에서 다루는 내용
3B001 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
3B001 invalid savepoint specification 는?
PostgreSQL 에러 코드 3B001 invalid savepoint specification은 트랜잭션 내에서 세이브포인트(Savepoint)를 잘못 지정하거나 존재하지 않는 세이브포인트를 참조할 때 발생하는 에러입니다. 세이브포인트는 트랜잭션 도중 특정 지점을 저장해두고, 이후 문제가 발생했을 때 해당 지점으로 롤백할 수 있게 해주는 PostgreSQL의 중요한 기능입니다. 주로 잘못된 이름으로 세이브포인트를 참조하거나, 세이브포인트가 이미 해제(RELEASE)된 이후에 접근하려 할 때 이 에러가 트리거됩니다.
주요 발생 원인
1. 존재하지 않는 세이브포인트 이름 참조
가장 흔한 원인으로, ROLLBACK TO SAVEPOINT 또는 RELEASE SAVEPOINT 명령을 실행할 때 이전에 생성되지 않은 이름을 지정하는 경우입니다. 오타나 대소문자 불일치, 혹은 다른 트랜잭션 블록에서 생성된 세이브포인트를 현재 트랜잭션에서 참조하려는 실수가 이에 해당합니다. 실무에서는 동적 SQL을 생성하는 애플리케이션 코드에서 세이브포인트 이름을 문자열로 조합할 때 특히 자주 발생합니다.
2. 이미 해제(RELEASE)된 세이브포인트 재참조
RELEASE SAVEPOINT 명령으로 세이브포인트를 명시적으로 해제한 뒤, 동일한 이름의 세이브포인트로 다시 롤백하려는 시도가 이 에러를 유발합니다. 세이브포인트는 일회성으로 사용하는 개념이 아니라 명시적으로 해제하기 전까지 유효한 자원이지만, 한번 RELEASE되면 더 이상 해당 이름으로 참조할 수 없습니다. 특히 루프 처리나 배치 작업에서 세이브포인트를 재사용하려는 로직에서 자주 발견됩니다.
3. 트랜잭션 블록 외부에서 세이브포인트 명령 실행
세이브포인트는 반드시 활성 트랜잭션 블록(BEGIN ~ COMMIT/ROLLBACK) 내부에서만 사용 가능합니다. BEGIN 없이 또는 이미 트랜잭션이 종료된 상태에서 SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT 명령을 실행하면 이 에러가 발생할 수 있습니다. 자동 커밋(autocommit) 모드가 활성화된 클라이언트 도구나 ORM 프레임워크 사용 시 이런 상황이 의도치 않게 발생하기도 합니다.
해결 방법
원인 1 해결: 세이브포인트 이름 확인 및 정확한 참조
세이브포인트를 생성할 때와 참조할 때 정확히 동일한 이름을 사용해야 합니다. PostgreSQL에서 세이브포인트 이름은 대소문자를 구분하지 않지만, 애플리케이션 레이어에서 문자열 조합 시 주의가 필요합니다.
-- 잘못된 예: 존재하지 않는 세이브포인트 참조
BEGIN;
SAVEPOINT my_savepoint;
INSERT INTO orders (customer_id, amount) VALUES (1001, 500.00);
-- 오타로 인한 에러 발생 (my_savepint -> 존재하지 않음)
ROLLBACK TO SAVEPOINT my_savepint; -- ERROR: 3B001
-- 올바른 예: 정확한 이름으로 참조
BEGIN;
SAVEPOINT my_savepoint;
INSERT INTO orders (customer_id, amount) VALUES (1001, 500.00);
-- 문제 발생 시 정확한 이름으로 롤백
ROLLBACK TO SAVEPOINT my_savepoint; -- 정상 동작
-- 정상 처리 후 커밋
INSERT INTO orders (customer_id, amount) VALUES (1002, 750.00);
COMMIT;
현재 트랜잭션에서 활성화된 세이브포인트 목록을 확인하려면 pg_current_snapshot() 또는 애플리케이션 레벨에서 세이브포인트 이름을 별도로 관리하는 것이 좋습니다.
원인 2 해결: RELEASE 후 재사용 방지
세이브포인트를 RELEASE한 후에는 동일한 이름으로 새로운 세이브포인트를 재생성하거나, 참조를 시도하지 않아야 합니다. 배치 처리에서 세이브포인트를 반복 사용해야 한다면 아래 패턴을 사용하세요.
-- 잘못된 예: RELEASE 후 동일 세이브포인트 롤백 시도
BEGIN;
SAVEPOINT batch_sp;
INSERT INTO log_table (message) VALUES ('step 1');
RELEASE SAVEPOINT batch_sp; -- 세이브포인트 해제
-- 이미 해제된 세이브포인트로 롤백 시도 -> ERROR: 3B001
ROLLBACK TO SAVEPOINT batch_sp;
-- 올바른 예: 루프에서 세이브포인트 재생성 패턴
BEGIN;
-- 첫 번째 배치 처리
SAVEPOINT batch_sp;
INSERT INTO orders (customer_id, amount) VALUES (1001, 300.00);
-- 오류가 없으면 RELEASE 후 다음 작업 진행
RELEASE SAVEPOINT batch_sp;
-- 두 번째 배치 처리 (새로운 세이브포인트 생성)
SAVEPOINT batch_sp; -- 동일 이름으로 새로 생성 가능 (RELEASE 후)
INSERT INTO orders (customer_id, amount) VALUES (1002, 450.00);
RELEASE SAVEPOINT batch_sp;
COMMIT;
-- 에러 발생 시 세이브포인트로 부분 롤백하는 실용적인 패턴
BEGIN;
SAVEPOINT sp_before_risky_op;
DO $$
DECLARE
v_error_occurred BOOLEAN := FALSE;
BEGIN
BEGIN
-- 위험한 작업 시도
INSERT INTO inventory (product_id, quantity) VALUES (999, -10);
EXCEPTION WHEN OTHERS THEN
v_error_occurred := TRUE;
RAISE NOTICE '오류 발생: %, 세이브포인트로 롤백합니다.', SQLERRM;
END;
IF v_error_occurred THEN
ROLLBACK TO SAVEPOINT sp_before_risky_op;
ELSE
RELEASE SAVEPOINT sp_before_risky_op;
END IF;
END;
$$;
COMMIT;
원인 3 해결: 트랜잭션 블록 내에서만 세이브포인트 사용
세이브포인트 명령은 반드시 BEGIN으로 시작된 트랜잭션 내부에서만 실행해야 합니다.
-- 잘못된 예: 트랜잭션 없이 세이브포인트 사용 시도
-- (autocommit 모드에서 각 명령이 독립 트랜잭션)
SAVEPOINT orphan_sp; -- WARNING 또는 에러 발생 가능
-- 올바른 예: 명시적 트랜잭션 블록 사용
BEGIN;
SAVEPOINT validate_sp;
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 101;
-- 잔액 검증
DO $$
DECLARE
v_balance NUMERIC;
BEGIN
SELECT balance INTO v_balance FROM accounts WHERE account_id = 101;
IF v_balance < 0 THEN
ROLLBACK TO SAVEPOINT validate_sp;
RAISE EXCEPTION '잔액 부족: 계좌 101번의 잔액이 마이너스가 됩니다.';
END IF;
END;
$$;
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 202;
RELEASE SAVEPOINT validate_sp;
COMMIT;
예방 방법
1. 세이브포인트 이름을 상수 또는 변수로 중앙 관리
애플리케이션 코드에서 세이브포인트 이름을 하드코딩하거나 문자열 조합으로 동적 생성하는 방식 대신, 상수 또는 열거형으로 중앙에서 관리하세요. PL/pgSQL 함수 내에서는 변수를 통해 세이브포인트 이름을 관리하고, 상태 플래그를 두어 세이브포인트의 활성 여부를 추적하는 것이 좋습니다. 이렇게 하면 오타나 중복 참조로 인한 3B001 에러를 원천 차단할 수 있습니다.
-- PL/pgSQL에서 세이브포인트 이름을 변수로 관리하는 예
CREATE OR REPLACE FUNCTION process_order_safely(p_order_id INT)
RETURNS VOID AS $$
DECLARE
v_sp_name TEXT := 'sp_order_' || p_order_id::TEXT;
v_sp_active BOOLEAN := FALSE;
BEGIN
EXECUTE format('SAVEPOINT %I', v_sp_name);
v_sp_active := TRUE;
-- 비즈니스 로직 처리
UPDATE orders SET status = 'processing' WHERE order_id = p_order_id;
INSERT INTO order_log (order_id, action) VALUES (p_order_id, 'started');
EXECUTE format('RELEASE SAVEPOINT %I', v_sp_name);
v_sp_active := FALSE;
EXCEPTION WHEN OTHERS THEN
IF v_sp_active THEN
EXECUTE format('ROLLBACK TO SAVEPOINT %I', v_sp_name);
END IF;
RAISE;
END;
$$ LANGUAGE plpgsql;
2. 예외 처리 블록으로 세이브포인트 생명주기 보장
세이브포인트 생성, 사용, 해제의 전체 생명주기를 명확히 관리하기 위해 반드시 예외 처리 블록(EXCEPTION WHEN OTHERS)을 함께 사용하세요. 세이브포인트 이후 발생하는 모든 오류를 캐치하여 적절히 ROLLBACK TO SAVEPOINT 또는 RELEASE SAVEPOINT를 실행하는 구조를 표준화하면, 세이브포인트가 미해제 상태로 남거나 잘못 참조되는 상황을 방지할 수 있습니다. 특히 중첩 트랜잭션 패턴을 사용하는 복잡한 비즈니스 로직에서는 이 원칙이 더욱 중요합니다.
관련 에러
3B000savepoint_exception: 세이브포인트 관련 일반 예외의 부모 에러 클래스로,3B001은 이 카테고리의 하위 에러입니다.25000invalid_transaction_state: 트랜잭션 상태가 올바르지 않을 때 발생하며, 세이브포인트 명령을 트랜잭션 외부에서 실행하는 경우 이 에러와 함께 나타날 수 있습니다.25P01no_active_sql_transaction: 활성 트랜잭션이 없는 상태에서 세이브포인트 관련 명령을 실행할 때 발생하며,3B001과 유사한 상황에서 트리거됩니다.40001serialization_failure: 직렬화 격리 수준에서 충돌 발생 시 나타나며, 이 경우 세이브포인트로의 롤백이 아닌 전체 트랜잭션 재시도가 필요합니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.