SAVEPOINT는 트랜잭션 안에 찍어두는 이름 붙은 북마크
일반적인 트랜잭션은 전부 아니면 전무 방식입니다. BEGIN과 COMMIT 사이의 변경 사항은 통째로 반영되거나, 통째로 사라지죠. 보통은 이게 우리가 원하는 동작이지만, 가끔은 좀 더 세밀한 단위로 다루고 싶을 때가 있습니다. "이 변경 묶음을 시도해보고, 잘못되면 이 부분만 되돌리되 트랜잭션 자체는 살려두고 싶다" 같은 상황 말이죠.
바로 이럴 때 쓰는 게 SQLite의 savepoint입니다. 이름을 붙여 북마크를 하나 찍어두고 작업을 진행한 뒤, 그 결과를 그대로 확정하거나(RELEASE) 북마크 지점으로 되감으면(ROLLBACK TO) 됩니다.
Ada의 출금과 Boris의 입금은 그대로 반영됩니다. Nobody에 잘못 들어간 UPDATE만 롤백됐고, 나머지 트랜잭션은 멀쩡히 살아남죠.
세 가지 명령어
SAVEPOINT API라고 해봐야 사실 명령어 세 개가 전부입니다.
SAVEPOINT name— 북마크를 찍습니다.RELEASE SAVEPOINT name— 북마크 이후의 작업은 유지하고, 북마크만 제거합니다.ROLLBACK TO SAVEPOINT name— 북마크 이후의 모든 변경을 되돌립니다. 북마크 자체는 그대로 남아 있어서 다시 시도할 수 있습니다.
참고로 RELEASE와 ROLLBACK TO 뒤에 붙는 SAVEPOINT는 생략해도 됩니다. 즉, RELEASE risky나 ROLLBACK TO risky라고만 써도 똑같이 동작합니다.
2단계 시도 행은 최종 데이터베이스 입장에서는 애초에 존재한 적도 없는 셈이죠. 나머지 변경사항은 모두 정상적으로 반영됩니다.
외부 트랜잭션 없이 SAVEPOINT 사용하기
재미있는 점이 하나 있습니다. BEGIN 없이도 바로 SAVEPOINT를 쓸 수 있다는 사실이죠. 이 경우 SQLite가 알아서 트랜잭션을 열어 주고, 가장 바깥쪽 savepoint가 트랜잭션 자체의 역할을 대신합니다. 이 savepoint에 RELEASE를 하면 커밋이 되고, ROLLBACK TO를 하면 커밋 없이 되돌려집니다.
그래서 savepoint를 흔히 "이름 있는 트랜잭션"이라고 부르기도 합니다. 다만 실제 코드에서 두 방식을 섞어 쓰면 헷갈리기 쉬우니 하나를 정해서 쓰는 게 좋습니다. 보통은 바깥쪽 트랜잭션 경계는 BEGIN ... COMMIT으로 명시적으로 잡고, 안쪽에서 부분적으로 되돌리고 싶은 지점에만 savepoint를 쓰는 패턴을 많이 씁니다.
SQLite 중첩 savepoint 사용하기
savepoint는 스택처럼 쌓입니다. 하나의 savepoint 안에 또 다른 savepoint를 만들 수 있고, 바깥쪽은 그대로 둔 채 안쪽 savepoint만 롤백할 수도 있습니다:
최종 결과는 a, b, d가 남습니다. inner로 롤백하면 c만 사라지고, inner 이전에 했던 작업(b 삽입)은 그대로 유지되며 트랜잭션도 계속 이어집니다.
바깥쪽 savepoint로 롤백하면 그 안쪽 레벨에서 했던 작업까지 전부 취소됩니다. 즉, 해당 이름 위에 쌓여 있던 스택 전체가 한 번에 풀리는 셈이죠:
b와 c 둘 다 사라졌습니다. ROLLBACK TO outer는 outer 이후에 일어난 모든 작업을 되돌리기 때문에 inner는 물론 c를 insert한 작업까지 함께 취소됩니다.
SAVEPOINT는 언제 쓰면 좋을까?
가장 대표적인 활용 사례는 배치 처리입니다. 개별 항목 하나가 실패하더라도 전체 배치를 통째로 버리고 싶지 않을 때 유용하죠. 각 항목을 SAVEPOINT로 감싸두면, 실패한 항목만 해당 savepoint로 롤백하고 다음 항목으로 넘어갈 수 있습니다:
실제 애플리케이션 코드에서는 잘못된 INSERT가 에러를 던지고, 애플리케이션이 이를 잡아서 ROLLBACK TO를 실행한 뒤 작업을 이어갑니다. 정상적인 두 행은 그대로 들어가고, 문제가 된 행 하나 때문에 배치 전체가 망가지는 일은 일어나지 않죠.
ORM이나 마이그레이션 도구가 중첩 트랜잭션을 구현할 때도 바로 이 패턴을 씁니다. BEGIN 블록을 실제로 중첩시키는 게 아니라(SQLite는 애초에 허용하지 않습니다), 중첩된 호출을 savepoint로 매핑하는 방식입니다.
주의해야 할 몇 가지
SQLite savepoint를 처음 다룰 때 자주 걸려 넘어지는 포인트들입니다.
COMMIT은 언제나 트랜잭션 전체를 커밋합니다. 열려 있는 savepoint가 몇 개든 상관없이,COMMIT(또는 별칭인END)은 가장 바깥쪽 트랜잭션을 통째로 닫습니다.RELEASE를 "부분 커밋"으로 오해하면 안 됩니다. 바깥 트랜잭션이 커밋되기 전까지는 어떤 변경도 영구 저장되지 않습니다.TO없는ROLLBACK은 전부 되돌립니다. 트랜잭션을 종료하고 열려 있던 savepoint도 모두 폐기합니다. 트랜잭션을 살려두고 싶다면 반드시ROLLBACK TO 이름형태를 쓰세요.- savepoint는 RELEASE되거나 롤백으로 지나쳐지기 전까지 계속 열려 있습니다.
RELEASE를 깜빡한다고 데이터가 사라지진 않지만, 책갈피가 트랜잭션 끝까지 그대로 남아 있게 됩니다. - 이름은 유일할 필요가 없습니다.
SAVEPOINT s를 두 번 선언하면ROLLBACK TO s는 가장 최근에 만든 것을 찾아갑니다. 재귀에서는 유용하지만, 실수로 같은 이름을 쓰면 혼란스러워집니다.
다음 주제: 뷰(View)
savepoint는 쓰기 작업을 더 세밀하게 제어하는 도구였습니다. 다음 단계는 읽기 쪽을 다듬는 일입니다. 쿼리를 이름 붙은 재사용 가능한 객체로 저장해 두고, 마치 테이블처럼 SELECT 할 수 있게 만드는 거죠. 그게 바로 뷰(View)이고, 곧이어 살펴볼 주제입니다.
자주 묻는 질문
SQLite에서 savepoint란 무엇인가요?
savepoint는 트랜잭션 안쪽에 찍어두는 이름이 붙은 마커입니다. 나중에 ROLLBACK TO 이름으로 그 지점 이후에 한 작업만 되돌릴 수도 있고, RELEASE로 변경 사항은 그대로 두면서 마커만 정리할 수도 있죠. 즉, 트랜잭션의 일부를 작은 단위로 끊어 복구 가능한 블록처럼 다룰 수 있게 해줍니다.
savepoint와 트랜잭션은 어떻게 다른가요?
트랜잭션은 BEGIN으로 시작해서 COMMIT이나 ROLLBACK으로 끝납니다. 반면 savepoint는 트랜잭션 안에서 SAVEPOINT 이름으로 설정하는 부분 되돌리기 지점이에요. savepoint로 롤백한다고 해서 바깥 트랜잭션이 끝나는 게 아니라서, 계속 작업을 이어가다 나중에 한 번에 커밋할 수 있습니다.
SQLite savepoint도 중첩이 가능한가요?
네, 됩니다. 서로 다른 이름으로 savepoint를 쌓아두면 스택처럼 동작하는데, ROLLBACK TO outer를 하면 그 안쪽에 있는 savepoint들까지 한꺼번에 정리되면서 outer 시점으로 되돌아갑니다. 이름이 꼭 유일할 필요는 없고, 같은 이름이 여러 개면 SQLite는 가장 최근에 만든 것을 기준으로 동작합니다 — 일반적인 스택 방식 그대로예요.