트랜잭션
COMMIT 과 ROLLBACK, 그리고 중첩 트랜잭션이 만드는 함정입니다. 중첩은 중첩되지 않습니다.
전부 되거나 전부 안 되거나
4.5 에서 답글 넣기가 반쪽만 끝나 sort_no 자리가
비는 것을 보았습니다. 밀기와 넣기가 한 덩어리로 묶여야 그런
일이 없습니다. 그 덩어리가 트랜잭션입니다.
BEGIN TRAN 을 적지 않아도
문장 하나는 그 자체로 트랜잭션입니다. 315행을 고치는
UPDATE 가 200행에서 실패하면 200행이 남는 것이
아니라 하나도 바뀌지 않습니다. 이것을 자동 커밋이라고 합니다.
여러 문장을 묶으려면 BEGIN TRAN 으로 시작해
COMMIT 이나
ROLLBACK 으로 끝냅니다. 짝이 맞지 않으면
오류입니다.
COMMIT; -- 시작한 트랜잭션이 없는데 ROLLBACK; -- 마찬가지
중첩 트랜잭션은 중첩되지 않습니다
BEGIN TRAN 을 두 번 적으면 트랜잭션이 둘 생길 것
같습니다. 그렇지 않습니다. 세는 숫자만 늘어납니다.
SELECT @@TRANCOUNT AS 시작; -- 0 BEGIN TRAN; SELECT @@TRANCOUNT AS 한번; -- 1 BEGIN TRAN; SELECT @@TRANCOUNT AS 두번; -- 2 COMMIT; SELECT @@TRANCOUNT AS 안쪽커밋뒤; -- 1 COMMIT; SELECT @@TRANCOUNT AS 바깥커밋뒤; -- 0
| 언제 | @TRANCOUNT |
|---|---|
| 시작 | 0 |
| BEGIN TRAN 한 번 | 1 |
| BEGIN TRAN 두 번 | 2 |
| COMMIT 한 번 | 1 |
| COMMIT 두 번 | 0 |
숫자만 보면 짝이 맞는 것 같습니다. 그런데 안쪽
COMMIT 은 아무것도 커밋하지 않습니다.
BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 99999 WHERE num = 250; BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 88888 WHERE num = 251; COMMIT; -- 안쪽을 커밋했습니다 ROLLBACK; -- 바깥을 되돌립니다 SELECT num, hit_count FROM Board.POSTS WHERE num IN (250, 251) ORDER BY num;
| num | hit_count |
|---|---|
| 250 | 250 |
| 251 | 257 |
반대쪽은 더 놀랍습니다. 안쪽
ROLLBACK 은 전부 되돌립니다.
BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 99999 WHERE num = 250; BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 88888 WHERE num = 251; ROLLBACK; -- 안쪽만 되돌리려 했습니다 SELECT @@TRANCOUNT AS 남은트랜잭션; SELECT num, hit_count FROM Board.POSTS WHERE num IN (250, 251) ORDER BY num;
| 확인한 것 | 값 |
|---|---|
| 남은 트랜잭션 | 0 |
| 250 번 조회수 | 250 |
| 251 번 조회수 | 257 |
정리하면 이렇습니다. 실제 트랜잭션은 하나뿐이고
@@TRANCOUNT 는 그저 세는 숫자입니다.
| 적은 것 | 일어나는 일 |
|---|---|
안쪽 BEGIN TRAN | 숫자만 1 늘어납니다 |
안쪽 COMMIT | 숫자만 1 줄어듭니다. 아무것도 확정되지 않습니다 |
안쪽 ROLLBACK | 전부 되돌리고 숫자가 0 이 됩니다 |
가장 바깥의 COMMIT 만 실제로
커밋합니다. 그러므로 @@TRANCOUNT 가 2
이상인 상태는 대개 실수입니다 — 프로시저가 남의 트랜잭션 안에서 또
BEGIN TRAN 을 적은 것입니다.
짝이 맞지 않으면 밖으로 새어 나갑니다
프로시저 안에서 BEGIN TRAN 을 적고
COMMIT 을 빠뜨리면 어떻게 될까요.
CREATE OR ALTER PROCEDURE Board.P_TRAN_BAD AS BEGIN SET NOCOUNT ON; BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 33333 WHERE num = 250; -- COMMIT 을 잊었습니다 END GO EXEC Board.P_TRAN_BAD; SELECT @@TRANCOUNT AS 프로시저를_나온뒤;
열린 트랜잭션이 그대로 새어 나갑니다. 부르는 쪽이 이것을 모르고 다음 일을 하면 그 일까지 같은 트랜잭션에 묶이고, 아무도 커밋하지 않으면 연결이 끊길 때까지 잠금이 유지됩니다.
반대도 위험합니다. 프로시저 안에서
ROLLBACK 하면 남의 트랜잭션까지 없앱니다.
CREATE OR ALTER PROCEDURE Board.P_TRAN_BAD2 AS BEGIN SET NOCOUNT ON; BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 44444 WHERE num = 250; ROLLBACK; END GO BEGIN TRAN; -- 부르는 쪽이 시작한 트랜잭션입니다 EXEC Board.P_TRAN_BAD2; SELECT @@TRANCOUNT AS 바깥트랜잭션; COMMIT;
프로시저는 자기가 시작한 트랜잭션만 끝내야 합니다. 들어올 때
@@TRANCOUNT 를 보고 판단하는 것이 정석이고, 이
단원 끝에서 그 모양을 정리합니다.
부분만 되돌리기
안쪽 ROLLBACK 이 전부를 되돌린다면
일부만 되돌릴 방법이 필요합니다. 저장점입니다.
BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 11111 WHERE num = 250; SAVE TRANSACTION sp1; -- 여기에 표시를 남깁니다 UPDATE Board.POSTS SET hit_count = 22222 WHERE num = 251; ROLLBACK TRANSACTION sp1; -- 표시까지만 되돌립니다 SELECT @@TRANCOUNT AS 부분롤백뒤; SELECT num, hit_count FROM Board.POSTS WHERE num IN (250,251) ORDER BY num; ROLLBACK;
| 확인한 것 | 값 |
|---|---|
| 부분 롤백 뒤 @TRANCOUNT | 1 |
| 250 번 조회수 | 11111 |
| 251 번 조회수 | 257 |
다만 XACT_ABORT 를 켜면 사용할 수 없습니다
4.5 에서 SET XACT_ABORT ON 을 권했습니다. 그런데
그것을 켜면 오류가 난 트랜잭션이 커밋할 수 없는 상태가 되어
저장점으로도 되돌릴 수 없습니다.
-- 같은 코드를 XACT_ABORT 만 바꿔 두 번 돌립니다. BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 111 WHERE num = 251; SAVE TRANSACTION sp1; BEGIN TRY UPDATE Board.POSTS SET hit_count = 222 WHERE num = 250; INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date) VALUES (99999, 1, N'x', '2026-03-01'); -- 547 로 막힙니다 END TRY BEGIN CATCH SELECT XACT_STATE() AS 상태; IF XACT_STATE() = 1 ROLLBACK TRANSACTION sp1; END CATCH IF @@TRANCOUNT > 0 ROLLBACK;
| 설정 | XACT_STATE() | 저장점 롤백 |
|---|---|---|
| XACT_ABORT ON | -1 | 할 수 없습니다 |
| XACT_ABORT OFF | 1 | 됩니다(250 만 되돌아감) |
둘을 함께 얻을 수는 없습니다. 그래도
XACT_ABORT ON 을 기본으로 삼는 편이 낫습니다 —
부분만 살리는 일보다 반쪽 커밋을 막는 일이 대개 더
중요하기 때문입니다. 부분 롤백이 꼭 필요한 자리라면 그 프로시저에서만
끄고, 대신 CATCH 에서
XACT_STATE() 를 확인하는 것을 빠뜨리지 마십시오.
열어 두면 잠금이 유지됩니다
트랜잭션이 열려 있는 동안 고친 행마다 배타 잠금이 걸려 있습니다. 그동안 다른 연결은 그 행을 읽지도 고치지도 못합니다.
BEGIN TRAN; UPDATE Board.POSTS SET hit_count = hit_count WHERE board_num = 2; SELECT resource_type AS 자원, request_mode AS 방식, COUNT(*) AS 개수 FROM sys.dm_tran_locks WHERE request_session_id = @@SPID AND resource_type <> 'DATABASE' GROUP BY resource_type, request_mode; ROLLBACK;
| 자원 | 방식 | 개수 |
|---|---|---|
| KEY | X | 315 |
| PAGE | IX | 12 |
| OBJECT | IX | 1 |
그래서 트랜잭션은 짧아야 합니다. 4.5 에서
BEGIN TRAN 을 관문 뒤에 둔 것이 이 까닭입니다 —
검사에서 걸릴 것을 트랜잭션 안에서 할 이유가 없습니다.
트랜잭션 안에서 사용자 입력을 기다리거나 외부를 부르지 마십시오. 사람이 화면을 보고 있는 동안 잠금이 유지되면 다른 사람들이 모두 멈춥니다. 잠금과 격리 수준은 5.5 에서 다룹니다.
남의 트랜잭션을 건드리지 않는 프로시저
들어올 때 @@TRANCOUNT 를 보고
내가 시작했는지 기억해 둡니다. 내가 시작했으면 내가 끝내고,
남의 것이면 손대지 않고 다시 던집니다.
CREATE OR ALTER PROCEDURE Board.P_HIT_SET @num int, @hit int AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; -- 들어올 때 바깥에 트랜잭션이 있었는지 기억합니다. DECLARE @outer bit = CASE WHEN @@TRANCOUNT > 0 THEN 1 ELSE 0 END; BEGIN TRY IF @outer = 0 BEGIN TRAN; UPDATE Board.POSTS SET hit_count = @hit WHERE num = @num; IF @hit < 0 THROW 50002, N'조회수는 음수가 될 수 없습니다', 1; IF @outer = 0 COMMIT; END TRY BEGIN CATCH -- 내가 시작한 것만 되돌립니다. IF @outer = 0 AND XACT_STATE() <> 0 ROLLBACK; THROW; -- 바깥이 있으면 바깥이 결정합니다 END CATCH END
| 부른 방법 | 오류 | 나온 뒤 @@TRANCOUNT | 250 번 값 |
|---|---|---|---|
| 혼자 부르고 실패 | 50002 | 0 | 250 (원래대로) |
| 바깥 트랜잭션 안에서 실패 | 50002 | 1 | 바깥이 롤백하면 원래대로 |
| 바깥 트랜잭션 안에서 성공 | — | 1 | 500 (아직 커밋 전) |
세 경우 모두 트랜잭션 개수가 들어올 때와 나갈 때 같습니다.
266 이 나지 않고, 부르는 쪽의 COMMIT 도 갈 곳을
잃지 않습니다.
더 간단한 규칙도 있습니다 — 프로시저에서 트랜잭션을 시작하지 않는 것입니다. 트랜잭션의 경계를 부르는 쪽 한 곳에만 두면 이 판단 자체가 필요 없습니다. 프로시저를 여러 개 묶어 한 트랜잭션으로 처리해야 하는 경우가 많다면 그 편이 낫습니다.
직접 해보기
아래 코드는 250 번을 고치고 커밋까지 했는데 값이 원래대로 돌아갑니다. 까닭을 말하고, 251 번만 되돌리고 250 번은 남기도록 고쳐 보세요.
BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 11111 WHERE num = 250; BEGIN TRAN; UPDATE Board.POSTS SET hit_count = 22222 WHERE num = 251; COMMIT; ROLLBACK;
답글을 달면서 원글의 조회수도 함께 올려야 한다고 합시다.
두 일이 모두 되거나 모두 안 되어야 합니다. 4.5 의
P_POST_REPLY 와 이 단원의
P_HIT_SET 을 묶는 프로시저를 만들어 보세요.
이 단원과 4.5 에서 만든 것을 지웁니다.
DROP PROCEDURE Board.P_TRAN_BAD, Board.P_TRAN_BAD2, Board.P_HIT_SET,
Board.P_POST_REPLY, Board.P_REPLY_SAFE;
DROP TABLE Board.ERROR_LOG;
- 문장 하나는 그 자체로 트랜잭션입니다. 여러 문장을 묶으려면
BEGIN TRAN으로 시작합니다. 짝이 없으면 3902 · 3903 입니다. - 중첩 트랜잭션은 중첩되지 않습니다. 실제 트랜잭션은 하나이고
@@TRANCOUNT는 세는 숫자입니다. - 안쪽
COMMIT은 아무것도 확정하지 않습니다. 안쪽ROLLBACK은 전부 되돌리고@@TRANCOUNT를 0 으로 만듭니다. - 프로시저가 짝을 맞추지 않으면 266 이 나고, 열린 트랜잭션이 새어 나가거나 남의 트랜잭션이 사라집니다.
- 일부만 되돌리려면
SAVE TRANSACTION과ROLLBACK TRANSACTION 이름입니다.@@TRANCOUNT는 줄지 않습니다. XACT_ABORT ON이면 저장점을 사용할 수 없습니다(XACT_STATE()가 -1). 그래도 반쪽 커밋을 막는 쪽이 대개 더 중요합니다.- 열린 트랜잭션은 잠금을 붙잡고 있습니다(315행을 고치면 배타 잠금 315개). 짧게 유지하고 안에서 사람을 기다리지 마십시오.
- 프로시저는 들어올 때
@@TRANCOUNT를 보고 자기가 시작한 것만 끝냅니다. 남의 것이면 다시 던져 바깥이 결정하게 합니다.