Savepoint와 partial rollback: 긴 작업을 안전하게 나누는 방법
MySQL Savepoint의 내부 동작과 partial rollback 설계, 잠금·오류 처리·Aurora MySQL 운영 주의점을 정리한다.
Savepoint와 partial rollback: 긴 작업을 안전하게 나누는 방법
하나의 업무 요청이 여러 SQL 문으로 구성되더라도 항상 전체를 처음부터 다시 실행해야 하는 것은 아니다. 주문의 필수 항목은 유지하면서 선택 항목만 취소하거나, 배치의 한 단계를 되돌린 뒤 대체 경로로 계속 진행해야 할 때가 있다. MySQL의 SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT는 하나의 트랜잭션 안에 논리적 복구 지점을 만들고 그 이후 변경만 취소하는 수단이다.
그러나 Savepoint는 긴 트랜잭션의 비용을 줄이거나 잠금을 모두 해제하는 기능이 아니다. 또한 이미 트랜잭션 전체를 종료한 deadlock, 연결 단절, implicit commit을 복구하지 못한다. 따라서 문법보다 중요한 것은 어떤 실패까지 부분 복구하고, 어떤 실패에서는 전체 트랜잭션을 포기할지를 설계하는 일이다.
1. Savepoint가 만드는 경계
Savepoint는 별도 트랜잭션을 중첩해서 만드는 기능이 아니다. 하나의 물리적 트랜잭션 안에 이름이 있는 위치를 기록하고, 필요할 때 해당 위치 이후의 변경을 undo하도록 지시한다.
flowchart LR
A[START TRANSACTION] --> B[필수 단계 A]
B --> C[SAVEPOINT optional_step]
C --> D[선택 단계 B]
D --> E{단계 B 성공?}
E -- 성공 --> F[RELEASE SAVEPOINT]
E -- 실패 --> G[ROLLBACK TO SAVEPOINT]
G --> H[대체 처리 또는 건너뛰기]
F --> I[후속 단계 C]
H --> I
I --> J{전체 불변식 충족?}
J -- 예 --> K[COMMIT]
J -- 아니오 --> L[ROLLBACK]
핵심 명령의 의미는 다음과 같다.
SAVEPOINT name: 현재 트랜잭션 위치에 이름을 부여한다.ROLLBACK TO SAVEPOINT name: 해당 지점 이후 변경을 취소하지만 트랜잭션은 계속 유지한다.RELEASE SAVEPOINT name: 복구 지점의 이름만 제거한다. 데이터 변경을 commit하거나 rollback하지 않는다.COMMIT또는 이름 없는ROLLBACK: 트랜잭션을 끝내며 모든 Savepoint를 제거한다.
ROLLBACK TO SAVEPOINT를 실행하면 지정한 Savepoint보다 뒤에 만들어진 Savepoint는 삭제된다. 반면 되돌아간 대상 Savepoint 자체는 남아 있으므로 필요하면 같은 지점으로 다시 rollback할 수 있다. 사용이 끝났다면 RELEASE SAVEPOINT로 의도를 명확히 하는 편이 좋다.
Savepoint는 autocommit=1 상태에서 독립적으로 짧은 문장을 묶어 주지 않는다. 먼저 START TRANSACTION을 실행하거나 애플리케이션 트랜잭션 API로 명시적 트랜잭션을 시작해야 한다. 또한 Savepoint는 세션 상태이므로 트랜잭션 도중 다른 커넥션으로 이동할 수 없다.
2. InnoDB에서 partial rollback이 실행되는 방식
InnoDB는 데이터 페이지를 이전 상태로 통째로 복사해 두는 방식으로 Savepoint를 구현하지 않는다. 트랜잭션이 수행한 변경에는 undo 정보가 연결되며, ROLLBACK TO SAVEPOINT는 Savepoint 이후의 변경을 역방향으로 적용해 취소한다. 전체 ROLLBACK과 사용하는 원리는 같지만 범위가 트랜잭션의 일부라는 점이 다르다.
이 동작에서 운영자가 기억해야 할 사실은 네 가지다.
- 트랜잭션은 끝나지 않는다. 기존 Read View와 트랜잭션 컨텍스트는 계속 유지된다.
- Savepoint 이전 변경은 유지된다. 최종
COMMIT시 함께 확정된다. - undo 비용은 사라지지 않는다. Savepoint 이후 작업량이 크면 부분 rollback에도 그 변경을 되감는 시간이 필요하다.
- 잠금이 모두 해제된다고 가정하면 안 된다. InnoDB는 Savepoint 이후 획득해 메모리에 기록한 row lock을 partial rollback에서 모두 해제하지 않을 수 있다. 새로 삽입한 행처럼 undo 과정에서 잠금이 해제되는 경우도 있지만, Savepoint를 잠금 범위 축소 장치로 취급해서는 안 된다.
즉, Savepoint는 논리적 변경 범위를 되돌리는 기능이지 긴 트랜잭션의 격리 비용을 초기화하는 기능이 아니다. 오래 유지된 트랜잭션은 여전히 MVCC 이전 버전 정리를 지연시키고, 잠금 대기와 undo 처리량을 늘리며, 연결 장애 시 되돌려야 할 작업량을 키울 수 있다.
3. 기본 예제: 선택 단계만 취소하기
다음 예제는 주문의 필수 항목은 유지하고 선택 항목만 취소한다. 하나의 트랜잭션 안에서 partial rollback 후에도 후속 SQL을 수행하고 최종 commit할 수 있음을 확인한다.
SET NAMES utf8mb4;
DROP TABLE IF EXISTS savepoint_order_line;
CREATE TABLE savepoint_order_line (
order_id BIGINT NOT NULL,
line_no INT NOT NULL,
item_name VARCHAR(50) NOT NULL,
item_kind ENUM('필수', '선택') NOT NULL,
PRIMARY KEY (order_id, line_no)
) ENGINE = InnoDB;
START TRANSACTION;
INSERT INTO savepoint_order_line
VALUES (1001, 1, '기본 상품', '필수');
SAVEPOINT before_optional_items;
INSERT INTO savepoint_order_line
VALUES (1001, 2, '선물 포장', '선택'),
(1001, 3, '추가 보증', '선택');
SELECT line_no, item_name, item_kind
FROM savepoint_order_line
WHERE order_id = 1001
ORDER BY line_no;
ROLLBACK TO SAVEPOINT before_optional_items;
SELECT line_no, item_name, item_kind
FROM savepoint_order_line
WHERE order_id = 1001
ORDER BY line_no;
RELEASE SAVEPOINT before_optional_items;
COMMIT;
SELECT COUNT(*) AS committed_lines
FROM savepoint_order_line
WHERE order_id = 1001;
DROP TABLE savepoint_order_line;
실행 결과(MySQL 8.0.x):
다음은 준비·정리 DDL을 줄이고 핵심 실행 흐름만 발췌한 결과다.
mysql> INSERT INTO savepoint_order_line
-> VALUES (1001, 1, '기본 상품', '필수');
Query OK, 1 row affected (0.01 sec)
mysql> SAVEPOINT before_optional_items;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO savepoint_order_line
-> VALUES (1001, 2, '선물 포장', '선택'),
-> (1001, 3, '추가 보증', '선택');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> SELECT line_no, item_name, item_kind ...;
+---------+---------------+-----------+
| line_no | item_name | item_kind |
+---------+---------------+-----------+
| 1 | 기본 상품 | 필수 |
| 2 | 선물 포장 | 선택 |
| 3 | 추가 보증 | 선택 |
+---------+---------------+-----------+
3 rows in set (0.00 sec)
mysql> ROLLBACK TO SAVEPOINT before_optional_items;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT line_no, item_name, item_kind ...;
+---------+---------------+-----------+
| line_no | item_name | item_kind |
+---------+---------------+-----------+
| 1 | 기본 상품 | 필수 |
+---------+---------------+-----------+
1 row in set (0.00 sec)
mysql> RELEASE SAVEPOINT before_optional_items;
Query OK, 0 rows affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT COUNT(*) AS committed_lines ...;
+-----------------+
| committed_lines |
+-----------------+
| 1 |
+-----------------+
1 row in set (0.00 sec)
첫 번째 조회에서는 세 행이 보이지만 ROLLBACK TO SAVEPOINT 이후에는 필수 항목 한 행만 남는다. 이 한 행은 최종 COMMIT으로 확정된다. 여기서 partial rollback 직후 바로 commit하지 않고 비즈니스 불변식을 다시 검사하는 절차가 중요하다. 선택 단계 실패가 필수 단계의 유효성까지 훼손했다면 부분 rollback이 아니라 전체 rollback이 맞다.
4. 같은 이름을 다시 사용하면 이전 지점이 교체된다
현재 트랜잭션에 같은 이름의 Savepoint가 있으면 새 SAVEPOINT가 이전 지점을 대체한다. 반복 처리에서 일정한 이름을 재사용할 수 있지만, 어느 시점이 현재 이름에 연결되어 있는지 불명확해지면 잘못된 범위만 취소할 수 있다.
DROP TABLE IF EXISTS savepoint_account;
CREATE TABLE savepoint_account (
account_id BIGINT PRIMARY KEY,
balance INT NOT NULL
) ENGINE = InnoDB;
INSERT INTO savepoint_account VALUES (1, 1000);
START TRANSACTION;
SAVEPOINT retry_point;
UPDATE savepoint_account
SET balance = balance - 100
WHERE account_id = 1;
SAVEPOINT retry_point;
UPDATE savepoint_account
SET balance = balance - 200
WHERE account_id = 1;
ROLLBACK TO SAVEPOINT retry_point;
SELECT account_id, balance
FROM savepoint_account;
RELEASE SAVEPOINT retry_point;
COMMIT;
DROP TABLE savepoint_account;
실행 결과(MySQL 8.0.x):
mysql> DROP TABLE IF EXISTS savepoint_account;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE savepoint_account (
-> account_id BIGINT PRIMARY KEY,
-> balance INT NOT NULL
-> ) ENGINE = InnoDB;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO savepoint_account VALUES (1, 1000);
Query OK, 1 row affected (0.00 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> SAVEPOINT retry_point;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE savepoint_account
-> SET balance = balance - 100
-> WHERE account_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SAVEPOINT retry_point;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE savepoint_account
-> SET balance = balance - 200
-> WHERE account_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> ROLLBACK TO SAVEPOINT retry_point;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT account_id, balance
-> FROM savepoint_account;
+------------+---------+
| account_id | balance |
+------------+---------+
| 1 | 900 |
+------------+---------+
1 row in set (0.00 sec)
mysql> RELEASE SAVEPOINT retry_point;
Query OK, 0 rows affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> DROP TABLE savepoint_account;
Query OK, 0 rows affected (0.00 sec)
결과 잔액은 900이다. 두 번째 SAVEPOINT retry_point가 첫 번째 지점을 대체했기 때문에 마지막 200 감소만 취소되고, 앞선 100 감소는 유지된다. 이름을 재사용하는 반복문에서는 item_시작, item_검증완료처럼 의미가 드러나는 이름을 사용하고, 한 반복이 끝날 때 명시적으로 release하는 편이 안전하다.
5. 오류의 범위부터 분류해야 한다
모든 SQL 오류가 같은 rollback 범위를 요구하지는 않는다. 애플리케이션은 오류 코드를 보고 최소한 다음 세 종류를 구분해야 한다.
5.1 현재 문장 또는 선택 단계만 실패한 경우
중복 키, 일부 데이터 검증 실패, 선택 기능 처리 실패처럼 트랜잭션 자체가 살아 있다면 Savepoint로 돌아가 대체 처리를 수행할 수 있다. 다만 오류가 발생한 문장 자체가 statement rollback되었더라도 그 전에 같은 단계에서 성공한 문장이 있을 수 있으므로, 단계 시작점으로 rollback하는 것이 경계를 명확하게 만든다.
5.2 트랜잭션 전체가 이미 rollback된 경우
Deadlock victim으로 선택되면 InnoDB는 일반적으로 해당 트랜잭션 전체를 rollback한다. 이때 기존 Savepoint도 사라졌으므로 ROLLBACK TO SAVEPOINT로 복구하려 해서는 안 된다. 애플리케이션은 새 트랜잭션을 시작하고, 멱등성 키와 재시도 정책에 따라 전체 업무 단위를 다시 실행해야 한다.
연결 단절, 서버 재시작, failover도 마찬가지다. Savepoint는 메모리나 애플리케이션에 저장되는 복구 토큰이 아니라 현재 서버 세션의 트랜잭션 컨텍스트다. 새 연결에서는 이전 Savepoint를 참조할 수 없다.
5.3 문장만 취소됐는지 환경에 따라 달라지는 경우
Lock wait timeout은 기본 설정에서 문제가 된 문장만 rollback하고 트랜잭션은 남을 수 있다. 그러나 innodb_rollback_on_timeout 설정과 오류 처리 계층에 따라 기대가 달라질 수 있다. timeout 뒤에 무조건 commit을 계속하지 말고 다음을 확인해야 한다.
- 현재 커넥션이 유효한가?
- 드라이버가 트랜잭션을 실패 상태로 표시했는가?
- 서버가 문장만 취소했는가, 전체 트랜잭션을 취소했는가?
- Savepoint로 돌아간 뒤에도 비즈니스 불변식이 유지되는가?
불확실하면 전체 rollback 후 새 트랜잭션으로 재시도하는 쪽이 안전하다.
6. 애플리케이션 제어 흐름
부분 rollback은 SQL 몇 줄보다 오류 처리 구조가 더 중요하다. 다음은 특정 언어에 종속되지 않은 제어 흐름이다. 실제 구현에서는 DB 드라이버의 transaction API와 SQLSTATE·MySQL error code를 함께 사용한다.
begin transaction
try:
execute required_step()
savepoint optional_step
try:
execute optional_step()
validate_optional_result()
release savepoint optional_step
catch recoverable_error:
rollback to savepoint optional_step
release savepoint optional_step
record_optional_step_skipped()
validate_whole_business_invariant()
commit
catch deadlock_or_connection_loss:
rollback_if_connection_is_usable
retry_whole_transaction_with_idempotency_key
catch any_unrecoverable_error:
rollback
fail_request
복구 가능한 오류를 지나치게 넓게 잡아서는 안 된다. catch all → rollback to savepoint → commit 구조는 필수 변경까지 논리적으로 오염된 상태를 확정할 위험이 있다. 복구 가능한 오류 목록과 대체 결과를 업무 규칙으로 명시하고, 나머지는 전체 rollback으로 보내야 한다.
7. DDL과 implicit commit 함정
다수의 DDL 문은 실행 전에 활성 트랜잭션을 implicit commit한다. 예를 들어 다음 의사 코드는 의도한 원자성을 제공하지 않는다.
START TRANSACTION;
UPDATE customer_balance ...;
SAVEPOINT before_schema_change;
ALTER TABLE customer_balance ADD COLUMN memo VARCHAR(100);
ROLLBACK TO SAVEPOINT before_schema_change; -- Savepoint가 이미 사라져 실패
ALTER TABLE이 앞선 변경을 commit하고 Savepoint를 제거하기 때문이다. 일반적인 CREATE TABLE, DROP TABLE, CREATE INDEX, TRUNCATE TABLE, 권한·계정 관련 여러 문장도 트랜잭션 설계와 섞기 전에 implicit commit 여부를 확인해야 한다.
CREATE TEMPORARY TABLE처럼 implicit commit은 일으키지 않지만 생성 자체가 rollback되지 않는 예외도 있다. 따라서 “DDL이 트랜잭션 안에 있으니 Savepoint로 되돌릴 수 있다”는 가정은 금물이다. 스키마 변경과 업무 DML은 배포 단계와 트랜잭션 경계를 분리하는 것이 원칙이다.
8. 긴 배치를 잘게 나누는 방법과 한계
Savepoint가 긴 배치의 오류 격리에는 유용하지만, 수만 건을 한 트랜잭션에 계속 쌓는 명분이 되어서는 안 된다. ROLLBACK TO SAVEPOINT를 여러 번 실행해도 트랜잭션 시작 시점, 남아 있는 변경, undo와 잠금의 운영 부담이 모두 초기화되지는 않는다.
배치 경계는 다음 두 방식을 구분해 선택한다.
| 목표 | 적합한 경계 | 의미 |
|---|---|---|
| 전체 묶음의 원자성이 필수 | 하나의 트랜잭션 + Savepoint | 선택 단계만 부분 취소하되 최종 commit은 한 번 수행 |
| 항목별 독립 성공이 허용됨 | 작은 트랜잭션 여러 개 | 일정 건수마다 commit하여 잠금·undo·복구 범위를 제한 |
| 외부 API 호출이 포함됨 | 상태 테이블·Outbox·Saga | DB Savepoint만으로 외부 시스템 부작용을 취소할 수 없음 |
| 실패 항목만 재처리 | 멱등성 키 + 작업 상태 | 성공 항목을 재실행하지 않고 실패 항목을 추적 |
한 트랜잭션을 여러 Savepoint로 나눠도 외부 결제 승인, 메시지 발행, 파일 생성 같은 부작용은 rollback되지 않는다. 데이터베이스와 외부 시스템을 함께 다루면 transactional outbox, 보상 작업, 멱등성 키를 별도로 설계해야 한다.
9. 잠금과 관측 관점
Savepoint 이름과 목록을 직접 보여 주는 범용 Performance Schema 테이블은 없다. 운영에서는 트랜잭션의 수명, 수정 행 수, 잠금 대기, 애플리케이션 단계 로그를 조합해서 관찰해야 한다.
확인할 항목은 다음과 같다.
information_schema.INNODB_TRX: 활성 트랜잭션 시작 시각, 상태, 수정 행 수performance_schema.data_lock_waits: waiting/blocking 트랜잭션 관계performance_schema.data_locks: 현재 관측되는 record/table lock- 애플리케이션 trace: 업무 ID, 트랜잭션 시도 번호, Savepoint 단계명, partial rollback 사유
- 지연 시간: 단계 실행 시간뿐 아니라 rollback 수행 시간과 최종 commit 시간
partial rollback을 실행했다는 사실만으로 차단 관계가 해소됐다고 판단하지 않는다. 긴 트랜잭션이 계속 열려 있다면 blocking session이 남아 있을 수 있다. 긴 작업에서 부분 복구가 자주 발생한다면 오류 횟수뿐 아니라 트랜잭션 전체 체류 시간과 잠금 대기 시간을 함께 봐야 한다.
로그에는 SQL 원문이나 고객 데이터를 과도하게 남기지 말고 다음처럼 구조화된 최소 정보를 기록하는 것이 좋다.
operation_id=<비식별 업무 키>
attempt=2
transaction_phase=optional_inventory_reservation
rollback_scope=savepoint
mysql_error_code=1062
outcome=continued_with_fallback
10. 복제와 Aurora MySQL에서의 해석
Savepoint를 사용해도 최종적으로는 하나의 트랜잭션이다. partial rollback으로 취소된 변경은 최종 확정 상태에 포함되지 않지만, 남은 변경은 최종 COMMIT 경계에서 확정된다. 트랜잭션이 지나치게 크면 source에서의 commit 지연, replica 적용 지연, 장애 복구 시간과 같은 기존의 대형 트랜잭션 위험이 그대로 남는다. Savepoint 개수가 많다는 사실보다 실제 변경량과 트랜잭션 체류 시간이 중요하다.
Aurora MySQL에서도 MySQL 호환 Savepoint 문법과 기본 트랜잭션 의미를 사용할 수 있다. 그러나 분산 스토리지와 관리형 failover가 Savepoint를 영속 복구 지점으로 바꾸지는 않는다.
- writer failover나 연결 단절이 발생하면 기존 세션의 미완료 트랜잭션과 Savepoint를 새 연결에서 이어갈 수 없다.
- 애플리케이션은 writer endpoint 재연결 뒤 전체 트랜잭션을 멱등하게 재시도해야 한다.
- 커넥션 풀이나 프록시는 트랜잭션 동안 같은 물리 세션을 유지해야 한다.
- CloudWatch, Performance Insights, Database Insights에서 긴 SQL만 볼 것이 아니라 긴 트랜잭션과 lock wait도 함께 확인해야 한다.
- parameter group 변경이나 DDL을 업무 트랜잭션의 Savepoint로 보호하려 해서는 안 된다.
Aurora의 자동 백업과 point-in-time recovery도 개별 Savepoint를 애플리케이션 복구 지점으로 노출하지 않는다. Savepoint는 온라인 트랜잭션 내부의 제어 도구이고, 백업·복구 전략의 대체물이 아니다.
11. 자주 발생하는 오해
오해 1: partial rollback이면 잠금도 모두 풀린다
변경은 되돌아가지만 InnoDB가 Savepoint 이후 메모리에 저장한 row lock을 모두 해제한다고 보장할 수 없다. 잠금 해제가 목표라면 작은 트랜잭션으로 경계를 다시 설계해야 한다.
오해 2: Savepoint는 nested transaction이다
Savepoint별로 독립 commit할 수 없다. 안쪽 단계를 release해도 변경은 바깥 트랜잭션의 최종 commit 전까지 확정되지 않는다.
오해 3: Savepoint가 있으면 deadlock 후 계속할 수 있다
Deadlock victim의 트랜잭션은 일반적으로 전체 rollback된다. Savepoint가 아니라 새 트랜잭션과 전체 재시도 정책이 필요하다.
오해 4: DDL도 Savepoint로 되돌릴 수 있다
다수 DDL은 implicit commit을 일으킨다. 예외적인 temporary object 동작까지 포함해 DDL과 DML의 원자성 규칙을 따로 확인해야 한다.
오해 5: Savepoint를 많이 만들면 긴 트랜잭션 비용이 작아진다
Savepoint는 commit 경계를 만들지 않는다. undo, MVCC, 잠금, 장애 복구 부담은 트랜잭션 전체 규모와 수명에 의해 결정된다.
12. 설계 및 운영 체크리스트
트랜잭션 설계
- 사용한 Savepoint를
RELEASE SAVEPOINT
오류와 재시도
운영 관측
-
INNODB_TRX,data_lock_waits,data_locks
13. 결론
Savepoint는 하나의 트랜잭션 안에서 업무 단계를 구분하고, 복구 가능한 실패만 선택적으로 취소하는 정교한 제어 도구다. 올바르게 사용하면 필수 변경을 보존하면서 선택 기능을 건너뛰거나 대체 경로로 진행할 수 있다.
반대로 Savepoint는 nested transaction, commit 경계, 잠금 초기화, failover 복구, 외부 시스템 보상의 대체물이 아니다. 부분 rollback을 채택할 때는 오류 범위, implicit commit, deadlock 처리, 멱등 재시도, 긴 트랜잭션 비용을 함께 설계해야 한다. 이후에는 대형 트랜잭션을 실제 운영에서 식별하고 작은 commit 단위로 분해하는 기준을 살펴볼 필요가 있다.