MSSQL 4.6 · 4부. T-SQL 프로그래밍

트랜잭션

COMMIT 과 ROLLBACK, 그리고 중첩 트랜잭션이 만드는 함정입니다. 중첩은 중첩되지 않습니다.

예상 학습 시간 22분 난이도 중급
개념 설명

전부 되거나 전부 안 되거나

4.5 에서 답글 넣기가 반쪽만 끝나 sort_no 자리가 비는 것을 보았습니다. 밀기와 넣기가 한 덩어리로 묶여야 그런 일이 없습니다. 그 덩어리가 트랜잭션입니다.

BEGIN TRAN 을 적지 않아도 문장 하나는 그 자체로 트랜잭션입니다. 315행을 고치는 UPDATE 가 200행에서 실패하면 200행이 남는 것이 아니라 하나도 바뀌지 않습니다. 이것을 자동 커밋이라고 합니다.

여러 문장을 묶으려면 BEGIN TRAN 으로 시작해 COMMIT 이나 ROLLBACK 으로 끝냅니다. 짝이 맞지 않으면 오류입니다.

SQL
COMMIT;      -- 시작한 트랜잭션이 없는데
ROLLBACK;    -- 마찬가지
오류
메시지 3902 The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION. 메시지 3903 The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
중첩

중첩 트랜잭션은 중첩되지 않습니다

BEGIN TRAN 을 두 번 적으면 트랜잭션이 둘 생길 것 같습니다. 그렇지 않습니다. 세는 숫자만 늘어납니다.

SQL
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 은 아무것도 커밋하지 않습니다.

SQL
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;
결과
numhit_count
250250
251257
둘 다 원래 값입니다. 커밋했다고 믿은 88888 도 사라졌습니다.

반대쪽은 더 놀랍습니다. 안쪽 ROLLBACK 은 전부 되돌립니다.

SQL
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
바깥 UPDATE 까지 되돌아갔고 트랜잭션이 통째로 사라졌습니다.

정리하면 이렇습니다. 실제 트랜잭션은 하나뿐이고 @@TRANCOUNT 는 그저 세는 숫자입니다.

적은 것일어나는 일
안쪽 BEGIN TRAN숫자만 1 늘어납니다
안쪽 COMMIT숫자만 1 줄어듭니다. 아무것도 확정되지 않습니다
안쪽 ROLLBACK전부 되돌리고 숫자가 0 이 됩니다

가장 바깥의 COMMIT 만 실제로 커밋합니다. 그러므로 @@TRANCOUNT 가 2 이상인 상태는 대개 실수입니다 — 프로시저가 남의 트랜잭션 안에서 또 BEGIN TRAN 을 적은 것입니다.

프로시저

짝이 맞지 않으면 밖으로 새어 나갑니다

프로시저 안에서 BEGIN TRAN 을 적고 COMMIT 을 빠뜨리면 어떻게 될까요.

SQL
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 프로시저를_나온뒤;
결과
메시지 266 Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1. 프로시저를_나온뒤 ------------------ 1
오류는 났지만 트랜잭션은 열린 채 남았습니다.

열린 트랜잭션이 그대로 새어 나갑니다. 부르는 쪽이 이것을 모르고 다음 일을 하면 그 일까지 같은 트랜잭션에 묶이고, 아무도 커밋하지 않으면 연결이 끊길 때까지 잠금이 유지됩니다.

반대도 위험합니다. 프로시저 안에서 ROLLBACK 하면 남의 트랜잭션까지 없앱니다.

SQL
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;
결과
메시지 266 … Previous count = 1, current count = 0. 바깥트랜잭션 -------------- 0 메시지 3902 The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
프로시저가 남의 트랜잭션을 없앴고, 부르는 쪽의 COMMIT 이 갈 곳을 잃었습니다.

프로시저는 자기가 시작한 트랜잭션만 끝내야 합니다. 들어올 때 @@TRANCOUNT 를 보고 판단하는 것이 정석이고, 이 단원 끝에서 그 모양을 정리합니다.

저장점

부분만 되돌리기

안쪽 ROLLBACK 이 전부를 되돌린다면 일부만 되돌릴 방법이 필요합니다. 저장점입니다.

SQL
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;
결과
확인한 것
부분 롤백 뒤 @TRANCOUNT1
250 번 조회수11111
251 번 조회수257
251 만 되돌아갔습니다. 트랜잭션은 아직 열려 있어 커밋할 수 있습니다.

다만 XACT_ABORT 를 켜면 사용할 수 없습니다

4.5 에서 SET XACT_ABORT ON 을 권했습니다. 그런데 그것을 켜면 오류가 난 트랜잭션이 커밋할 수 없는 상태가 되어 저장점으로도 되돌릴 수 없습니다.

SQL
-- 같은 코드를 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 OFF1됩니다(250 만 되돌아감)

둘을 함께 얻을 수는 없습니다. 그래도 XACT_ABORT ON 을 기본으로 삼는 편이 낫습니다 — 부분만 살리는 일보다 반쪽 커밋을 막는 일이 대개 더 중요하기 때문입니다. 부분 롤백이 꼭 필요한 자리라면 그 프로시저에서만 끄고, 대신 CATCH 에서 XACT_STATE() 를 확인하는 것을 빠뜨리지 마십시오.

비용

열어 두면 잠금이 유지됩니다

트랜잭션이 열려 있는 동안 고친 행마다 배타 잠금이 걸려 있습니다. 그동안 다른 연결은 그 행을 읽지도 고치지도 못합니다.

SQL
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;
결과
자원방식개수
KEYX315
PAGEIX12
OBJECTIX1
315행을 고쳤더니 배타 잠금 315개가 걸렸습니다. 롤백하면 0 이 됩니다.

그래서 트랜잭션은 짧아야 합니다. 4.5 에서 BEGIN TRAN 을 관문 뒤에 둔 것이 이 까닭입니다 — 검사에서 걸릴 것을 트랜잭션 안에서 할 이유가 없습니다.

트랜잭션 안에서 사용자 입력을 기다리거나 외부를 부르지 마십시오. 사람이 화면을 보고 있는 동안 잠금이 유지되면 다른 사람들이 모두 멈춥니다. 잠금과 격리 수준은 5.5 에서 다룹니다.

정석

남의 트랜잭션을 건드리지 않는 프로시저

들어올 때 @@TRANCOUNT 를 보고 내가 시작했는지 기억해 둡니다. 내가 시작했으면 내가 끝내고, 남의 것이면 손대지 않고 다시 던집니다.

SQL
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
결과 — 세 가지로 불러 봅니다
부른 방법오류나온 뒤 @@TRANCOUNT250 번 값
혼자 부르고 실패500020250 (원래대로)
바깥 트랜잭션 안에서 실패500021바깥이 롤백하면 원래대로
바깥 트랜잭션 안에서 성공1500 (아직 커밋 전)
바깥이 있을 때는 커밋도 롤백도 하지 않고 판단을 넘깁니다.

세 경우 모두 트랜잭션 개수가 들어올 때와 나갈 때 같습니다. 266 이 나지 않고, 부르는 쪽의 COMMIT 도 갈 곳을 잃지 않습니다.

더 간단한 규칙도 있습니다 — 프로시저에서 트랜잭션을 시작하지 않는 것입니다. 트랜잭션의 경계를 부르는 쪽 한 곳에만 두면 이 판단 자체가 필요 없습니다. 프로시저를 여러 개 묶어 한 트랜잭션으로 처리해야 하는 경우가 많다면 그 편이 낫습니다.

연습

직접 해보기

1. 커밋했는데 사라졌습니다 난이도 하

아래 코드는 250 번을 고치고 커밋까지 했는데 값이 원래대로 돌아갑니다. 까닭을 말하고, 251 번만 되돌리고 250 번은 남기도록 고쳐 보세요.

SQL
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;
-- 안쪽 COMMIT 은 @@TRANCOUNT 를 1 줄일 뿐 아무것도 확정하지 않습니다. -- 실제로 확정하는 것은 가장 바깥의 COMMIT 뿐인데, 여기서는 ROLLBACK 이므로 -- 250 도 251 도 되돌아갑니다. -- 일부만 되돌리려면 저장점을 사용합니다. 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; -- 251 만 되돌립니다 SELECT num, hit_count FROM Board.POSTS WHERE num IN (250,251) ORDER BY num; -- 250 11111 -- 251 257 COMMIT; -- 250 은 남습니다 -- ROLLBACK TRANSACTION sp1 은 @@TRANCOUNT 를 줄이지 않습니다(1 그대로). -- 이름 없는 ROLLBACK 과 헷갈리지 마십시오.
2. 두 프로시저를 한 덩어리로 난이도 중

답글을 달면서 원글의 조회수도 함께 올려야 한다고 합시다. 두 일이 모두 되거나 모두 안 되어야 합니다. 4.5 의 P_POST_REPLY 와 이 단원의 P_HIT_SET 을 묶는 프로시저를 만들어 보세요.

묶는 쪽이 트랜잭션을 시작하면 안쪽 프로시저들은 무엇을 보게 됩니까.
CREATE OR ALTER PROCEDURE Board.P_REPLY_AND_HIT @parent_num int, @user_num int, @title nvarchar(200) AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @outer bit = CASE WHEN @@TRANCOUNT > 0 THEN 1 ELSE 0 END; DECLARE @hit int; BEGIN TRY IF @outer = 0 BEGIN TRAN; -- 안쪽 프로시저들은 @@TRANCOUNT 가 1 인 것을 보고 -- 자기 트랜잭션을 시작하지 않습니다. EXEC Board.P_POST_REPLY @parent_num = @parent_num, @user_num = @user_num, @title = @title; SELECT @hit = hit_count + 1 FROM Board.POSTS WHERE num = @parent_num; EXEC Board.P_HIT_SET @num = @parent_num, @hit = @hit; IF @outer = 0 COMMIT; END TRY BEGIN CATCH IF @outer = 0 AND XACT_STATE() <> 0 ROLLBACK; THROW; END CATCH END -- 안쪽 프로시저 둘이 모두 이 단원의 규칙을 지키고 있어야 성립합니다. -- 하나라도 자기 판단으로 ROLLBACK 하면 이 트랜잭션이 통째로 사라집니다. -- 그것이 "프로시저는 자기가 시작한 것만 끝낸다" 는 규칙이 필요한 까닭입니다. -- P_POST_REPLY 가 실패하면 P_HIT_SET 은 실행되지 않고, -- 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 TRANSACTIONROLLBACK TRANSACTION 이름 입니다. @@TRANCOUNT 는 줄지 않습니다.
  • XACT_ABORT ON 이면 저장점을 사용할 수 없습니다(XACT_STATE() 가 -1). 그래도 반쪽 커밋을 막는 쪽이 대개 더 중요합니다.
  • 열린 트랜잭션은 잠금을 붙잡고 있습니다(315행을 고치면 배타 잠금 315개). 짧게 유지하고 안에서 사람을 기다리지 마십시오.
  • 프로시저는 들어올 때 @@TRANCOUNT 를 보고 자기가 시작한 것만 끝냅니다. 남의 것이면 다시 던져 바깥이 결정하게 합니다.