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

오류 처리

TRY...CATCH · THROW · XACT_ABORT 로 잘못됐을 때를 다룹니다. 오류가 나도 다음 문장이 실행된다는 데서 시작합니다.

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

오류가 나도 다음 문장이 실행됩니다

많은 언어에서 예외가 발생하면 그 뒤가 실행되지 않습니다. T-SQL 은 그렇지 않습니다. 확인해 봅니다.

SQL
BEGIN TRAN;

-- 없는 글에 댓글을 답니다. 외래 키에 막힙니다(3.3).
INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date)
VALUES (99999, 1, N'없는 글에 댓글', '2026-03-01');

PRINT N'>> 이 줄이 실행됩니까?';

INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date)
VALUES (1, 1, N'정상 댓글', '2026-03-01');

SELECT COUNT(*) AS 들어간댓글 FROM Board.COMMENTS
WHERE content IN (N'없는 글에 댓글', N'정상 댓글');

ROLLBACK;
결과
메시지 547 The INSERT statement conflicted with the FOREIGN KEY constraint "FK_COMMENTS_POSTS". … The statement has been terminated. >> 이 줄이 실행됩니까? 들어간댓글 ---------- 1
문장 하나만 끝났습니다. 배치는 계속 돌았고 뒤의 INSERT 는 성공했습니다.

"The statement has been terminated" 는 문장이 끝났다는 뜻이지 배치가 끝났다는 뜻이 아닙니다. 트랜잭션도 살아 있습니다 — @@TRANCOUNT 는 1 이고 XACT_STATE() 도 1 입니다. 커밋하면 그대로 들어갑니다.

그래서 자료가 어긋납니다

4.2 에서 만든 답글 프로시저에는 오류 처리가 없습니다. 없는 회원 번호로 답글을 달아 봅니다. 뒤를 미는 데는 성공하고 넣는 데서 실패합니다.

SQL
BEGIN TRAN;
SELECT num, sort_no FROM Board.POSTS WHERE group_num = 6 ORDER BY sort_no;

-- 99999 번 회원은 없습니다.
EXEC Board.P_POST_REPLY @parent_num = 6, @user_num = 99999, @title = N'x';

SELECT num, sort_no FROM Board.POSTS WHERE group_num = 6 ORDER BY sort_no;
ROLLBACK;
결과 — 부르기 전
numsort_no
60
3521
4562
결과 — 실패한 뒤
numsort_no
60
3522
4563
1 번 자리가 비었습니다. 밀어 놓고 넣지 못했기 때문입니다.

오류 메시지는 나왔는데 자료는 이미 어긋났습니다. 이 상태가 쌓이면 목록의 차례가 조금씩 벌어지고, 어디서부터 잘못되었는지 뒤에 알 수 없습니다. 4.5 는 이것을 막는 단원입니다.

TRY CATCH

오류를 잡습니다

SQL
BEGIN TRY
    INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date)
    VALUES (99999, 1, N'없는 글에 댓글', '2026-03-01');

    PRINT N'>> 이 줄은 실행되지 않습니다';
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS 번호, ERROR_SEVERITY() AS 심각도,
           ERROR_STATE() AS 상태, ERROR_LINE() AS 줄,
           ISNULL(ERROR_PROCEDURE(), N'(배치)') AS 프로시저,
           ERROR_MESSAGE() AS 메시지;
END CATCH
결과
번호심각도상태프로시저
5471603(배치)
TRY 안에서 오류가 나면 그 자리에서 CATCH 로 넘어갑니다. PRINT 는 실행되지 않았습니다.

오류 정보는 CATCH 안에서만 읽을 수 있습니다. 밖에서 부르면 NULL 입니다. ERROR_PROCEDURE() 는 프로시저 밖에서 났으면 NULL 이므로 ISNULL 로 감싸 두면 읽기 좋습니다.

잡히지 않는 것도 있습니다

SQL
-- (가) 같은 배치 안의 이름 오타
BEGIN TRY
    SELECT * FROM Board.POSTZ;
END TRY
BEGIN CATCH
    PRINT N'>> CATCH 에 들어왔습니다';
END CATCH
GO
-- (나) 프로시저 안의 같은 오타
CREATE OR ALTER PROCEDURE Board.P_TYPO AS
BEGIN SET NOCOUNT ON; SELECT * FROM Board.POSTZ; END
GO
BEGIN TRY
    EXEC Board.P_TYPO;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS 번호, ERROR_PROCEDURE() AS 프로시저;
END CATCH
결과 (가) — 잡히지 않습니다
메시지 208 Invalid object name 'Board.POSTZ'.
결과 (나) — 잡힙니다
번호프로시저
208Board.P_TYPO

같은 배치 안에서 난 컴파일 오류는 잡히지 않습니다. 배치 자체가 컴파일되지 않으므로 TRY 에 들어가기 전에 끝납니다. 프로시저 안이면 잡힙니다 — 4.2 에서 본 지연 이름 확인 덕분에 그 오류는 실행 시점에 나기 때문입니다.

심각도 10 이하는 오류가 아니라 알림입니다. RAISERROR (N'경고입니다', 10, 1)CATCH 로 가지 않고 메시지만 남긴 뒤 다음 줄이 실행됩니다. 반대로 심각도 20 이상은 연결이 끊어져 역시 잡을 수 없습니다.

THROW

실패를 알립니다

4.3 에서 반환 코드로 실패를 알리는 방식을 미뤄 두었습니다. 부르는 쪽이 확인하지 않으면 그냥 지나가기 때문입니다. THROW 는 확인하지 않으면 지나갈 수 없습니다.

SQL
THROW 50001, N'답글을 달 원글을 찾지 못했습니다', 1;
--    번호      메시지                              상태
결과
메시지 50001, 수준 16, 상태 1 답글을 달 원글을 찾지 못했습니다
번호는 50000 보다 커야 합니다. 심각도는 늘 16 입니다.

잡아서 기록하고 그대로 다시 던집니다

THROW 를 인수 없이 적으면 지금 잡은 오류를 그대로 다시 던집니다. 번호도 메시지도 원래 것이 유지됩니다.

SQL
BEGIN TRY
    INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date)
    VALUES (99999, 1, N'x', '2026-03-01');
END TRY
BEGIN CATCH
    PRINT N'>> 기록을 남기고 다시 던집니다';
    THROW;                       -- 인수를 적지 않습니다
END CATCH
결과
>> 기록을 남기고 다시 던집니다 메시지 547, 수준 16, 상태 1 The INSERT statement conflicted with the FOREIGN KEY constraint "FK_COMMENTS_POSTS". …
547 이 그대로 나갔습니다. 오류를 삼키지 않으면서 중간에 할 일을 할 수 있습니다.

RAISERROR 와 다른 점

THROWRAISERROR
번호적은 그대로50000 고정(메시지를 등록하지 않으면)
심각도16 고정고를 수 있음
서식없음%d · %s 사용
다시 던지기THROW;직접 조립해야 함

새 오류를 던질 때는 THROW 를 사용하십시오. 번호를 그대로 쓸 수 있어 부르는 쪽이 무엇이 일어났는지 구분할 수 있습니다. RAISERROR 는 서식이 필요하거나 심각도 10 으로 알림만 남길 때 씁니다.

THROW 메시지에 % 를 넣지 마십시오

SQL
THROW 50001, N'없는 글에는 답글을 달 수 없습니다', 1;   -- (가)
THROW 50001, N'값 %d 가 잘못되었습니다', 1;             -- (나)

-- (다) 값을 끼우려면 변수로 조립합니다.
DECLARE @m nvarchar(200) = N'글 번호 ' + CAST(99999 AS nvarchar(10)) + N' 를 찾지 못했습니다';
THROW 50001, @m, 1;
결과 — CATCH 에서 ERROR_MESSAGE() 를 읽으면
적은 것번호메시지
(가) % 없음50001없는 글에는 답글을 달 수 없습니다
(나) % 있음50001(비어 있음)
(다) 변수로 조립50001글 번호 99999 를 찾지 못했습니다
(나)는 오류가 나지 않고 메시지만 사라집니다.

% 가 하나라도 들어가면 메시지가 통째로 비어 버립니다. 경고도 오류도 없습니다. 퍼센트를 보여야 하는 문구라면 RAISERROR 를 사용하거나 변수로 조립해 넘기십시오.

트랜잭션

XACT_ABORT 와 커밋할 수 없는 상태

CATCH 에 들어왔을 때 트랜잭션이 어떤 상태인지 알아야 롤백할지 커밋할지 정할 수 있습니다. XACT_STATE() 가 알려 줍니다.

할 수 있는 것
1살아 있음커밋도 롤백도
0트랜잭션 없음둘 다 안 됨
-1커밋할 수 없음롤백만
SQL
-- (가) 기본값입니다.
SET XACT_ABORT OFF;
BEGIN TRAN;
INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date) VALUES (99999, 1, N'x', '2026-03-01');
SELECT @@TRANCOUNT AS 트랜잭션수, XACT_STATE() AS 상태;
IF @@TRANCOUNT > 0 ROLLBACK;
GO
-- (나) 켜고 같은 일을 합니다.
SET XACT_ABORT ON;
BEGIN TRY
    BEGIN TRAN;
    INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date) VALUES (99999, 1, N'x', '2026-03-01');
    COMMIT;
END TRY
BEGIN CATCH
    SELECT @@TRANCOUNT AS 트랜잭션수, XACT_STATE() AS 상태, ERROR_NUMBER() AS 번호;
    IF XACT_STATE() <> 0 ROLLBACK;
END CATCH
결과
설정트랜잭션수XACT_STATE()
XACT_ABORT OFF11
XACT_ABORT ON1-1
켜면 커밋할 수 없는 상태가 됩니다. 실수로 커밋하는 길이 막힙니다.

XACT_ABORT 를 켜면 오류가 난 트랜잭션을 커밋할 수 없습니다. 끄면 앞에서 본 대로 살아 있어, 오류를 잡고도 실수로 커밋해 반쪽만 들어간 자료를 남길 수 있습니다.

프로시저 앞머리에 SET XACT_ABORT ON 을 적으십시오. SET NOCOUNT ON 과 짝입니다. 그리고 CATCH 에서는 @@TRANCOUNT 가 아니라 XACT_STATE() 로 판단하십시오 — 커밋할 수 없는 상태에서도 @@TRANCOUNT 는 1 이기 때문입니다.

완성

답글 프로시저를 고칩니다

4.2 에서 만든 것에 지금까지의 것을 붙입니다.

SQL
CREATE OR ALTER PROCEDURE Board.P_POST_REPLY
    @parent_num int,
    @user_num   int,
    @title      nvarchar(200)
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRY
        IF NOT EXISTS (SELECT 1 FROM Board.POSTS WHERE num = @parent_num)
            THROW 50001, N'답글을 달 원글을 찾지 못했습니다', 1;

        DECLARE @grp int, @dep smallint, @sort int;

        BEGIN TRAN;

        SELECT @grp = group_num, @dep = depth + 1, @sort = sort_no + 1
        FROM Board.POSTS WHERE num = @parent_num;

        UPDATE Board.POSTS SET sort_no = sort_no + 1
        WHERE group_num = @grp AND sort_no >= @sort;

        INSERT INTO Board.POSTS (board_num, category_num, user_num, parent_num,
                                 group_num, depth, sort_no, title, hit_count, reg_date)
        SELECT board_num, category_num, @user_num, @parent_num, @grp, @dep, @sort,
               @title, 0, '2026-03-01'
        FROM Board.POSTS WHERE num = @parent_num;

        COMMIT;

        SELECT SCOPE_IDENTITY() AS 새글번호;
    END TRY
    BEGIN CATCH
        IF XACT_STATE() <> 0 ROLLBACK;
        THROW;
    END CATCH
END
결과 — 없는 원글
메시지 50001, 수준 16, 상태 1, 프로시저 Board.P_POST_REPLY, 줄 12 답글을 달 원글을 찾지 못했습니다
결과 — 없는 회원
메시지 547, 수준 16, 상태 1, 프로시저 Board.P_POST_REPLY, 줄 24 The INSERT statement conflicted with the FOREIGN KEY constraint "FK_POSTS_USERS". …
결과 — 그 뒤의 자료
numsort_no
60
3521
4562
두 번 실패했는데 차례가 그대로입니다. 글 수도 507 그대로입니다.

내가 던진 오류와 엔진이 낸 오류가 같은 방식으로 나갑니다. 부르는 쪽은 번호로 구분합니다 — 50001 이면 원글이 없는 것이고, 547 이면 제약에 걸린 것입니다.

BEGIN TRAN 을 관문 뒤에 두었습니다. 검사에서 걸릴 것을 트랜잭션 안에서 할 이유가 없습니다. 트랜잭션은 짧을수록 좋고, 그 까닭은 4.6 에서 다룹니다.

연습

직접 해보기

1. 오류를 표에 기록합니다 난이도 하

오류가 났을 때 번호와 메시지를 Board.ERROR_LOG 에 남기려 합니다. 아래처럼 적으면 기록이 남지 않습니다. 까닭을 찾고 고쳐 보세요.

SQL
SET XACT_ABORT ON;
BEGIN TRY
    BEGIN TRAN;
    INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date)
    VALUES (99999, 1, N'x', '2026-03-01');
    COMMIT;
END TRY
BEGIN CATCH
    INSERT INTO Board.ERROR_LOG (err_num, err_msg, err_proc, err_line)
    VALUES (ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_PROCEDURE(), ERROR_LINE());
    IF XACT_STATE() <> 0 ROLLBACK;
END CATCH
CATCH 에 들어온 시점의 XACT_STATE() 는 몇입니까. 그 상태에서 표에 쓸 수 있습니까.
-- 커밋할 수 없는 상태(-1)에서는 로그 파일에 쓰는 어떤 작업도 못 합니다. -- 메시지 3930 -- The current transaction cannot be committed and cannot support -- operations that write to the log file. Roll back the transaction. -- 먼저 롤백해야 합니다. 그런데 롤백하면 ERROR_* 함수가 값을 잃으므로 -- 롤백하기 전에 변수에 담아 둡니다. SET XACT_ABORT ON; BEGIN TRY BEGIN TRAN; INSERT INTO Board.COMMENTS (post_num, user_num, content, reg_date) VALUES (99999, 1, N'x', '2026-03-01'); COMMIT; END TRY BEGIN CATCH DECLARE @n int = ERROR_NUMBER(), @m nvarchar(2048) = ERROR_MESSAGE(), @p nvarchar(128) = ERROR_PROCEDURE(), @l int = ERROR_LINE(); IF XACT_STATE() <> 0 ROLLBACK; -- 먼저 롤백하고 INSERT INTO Board.ERROR_LOG (err_num, err_msg, err_proc, err_line) VALUES (@n, @m, @p, @l); -- 그다음 기록합니다 END CATCH -- num err_num 메시지 프로시저 -- 1 547 The INSERT statement conflicted with the… (배치) -- 순서를 지키는 것이 핵심입니다. -- ERROR_* 를 변수에 담고 → 롤백하고 → 기록합니다.
2. 오류를 구분해 다르게 알립니다 난이도 중

P_POST_REPLY 를 부르는 쪽에서 원글이 없는 경우와 그 밖의 오류를 구분해 다르게 알리는 프로시저를 만들어 보세요. 앞의 것은 사용자에게 보여 줄 문구이고, 뒤의 것은 기록만 남기고 다시 던집니다.

직접 던진 오류에는 번호를 정해 두었습니다. 그 번호로 가릅니다.
CREATE OR ALTER PROCEDURE Board.P_REPLY_SAFE @parent_num int, @user_num int, @title nvarchar(200), @message nvarchar(200) = NULL OUTPUT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; BEGIN TRY EXEC Board.P_POST_REPLY @parent_num = @parent_num, @user_num = @user_num, @title = @title; SET @message = N'답글을 달았습니다'; END TRY BEGIN CATCH DECLARE @n int = ERROR_NUMBER(), @m nvarchar(2048) = ERROR_MESSAGE(), @p nvarchar(128) = ERROR_PROCEDURE(), @l int = ERROR_LINE(); IF XACT_STATE() <> 0 ROLLBACK; INSERT INTO Board.ERROR_LOG (err_num, err_msg, err_proc, err_line) VALUES (@n, @m, @p, @l); IF @n = 50001 SET @message = @m; -- 사용자에게 보여 줄 문구입니다 ELSE THROW; -- 그 밖의 것은 다시 던집니다 END CATCH END GO DECLARE @msg nvarchar(200); EXEC Board.P_REPLY_SAFE @parent_num = 99999, @user_num = 1, @title = N'x', @message = @msg OUTPUT; SELECT @msg; -- 답글을 달 원글을 찾지 못했습니다 -- 번호로 가르려면 던질 때 번호를 정해 두어야 합니다. -- 50001 원글 없음 · 50002 권한 없음 처럼 표를 만들어 두고 문서로 남기십시오. -- 그 밖의 것을 삼키지 않는 것이 중요합니다. 제약 위반이나 교착(4.6)을 -- "실패했습니다" 로 뭉뚱그리면 원인을 찾을 수 없습니다.

이 단원에서 만든 것을 지웁니다.
DROP TABLE Board.ERROR_LOG;
DROP PROCEDURE Board.P_POST_REPLY, Board.P_REPLY_SAFE;

요약
  • 오류가 나도 다음 문장이 실행됩니다. 문장만 끝나고 배치는 계속되며 트랜잭션도 살아 있습니다. 그래서 절차 중간에서 실패하면 자료가 반쪽만 바뀝니다.
  • TRY 안에서 오류가 나면 그 자리에서 CATCH 로 넘어갑니다. 오류 정보는 CATCH 안에서만 읽힙니다.
  • 같은 배치의 컴파일 오류는 잡히지 않습니다. 프로시저 안이면 잡힙니다(208). 심각도 10 이하는 CATCH 로 가지 않습니다.
  • 새 오류는 THROW 번호, 메시지, 상태 로 던지고, 잡은 오류는 THROW;그대로 다시 던집니다.
  • THROW 메시지에 % 를 넣지 마십시오. 경고 없이 메시지가 통째로 사라집니다. 값을 끼우려면 변수로 조립합니다.
  • 프로시저 앞머리에 SET XACT_ABORT ON 을 적으십시오. 오류가 난 트랜잭션을 커밋할 수 없는 상태로 만들어 반쪽 커밋을 막습니다(XACT_STATE() 가 -1).
  • CATCH 에서는 @@TRANCOUNT 가 아니라 XACT_STATE() 로 판단합니다.
  • 커밋할 수 없는 상태에서는 기록도 남길 수 없습니다(3930). ERROR_* 를 변수에 담고 → 롤백하고 → 기록하는 순서를 지키십시오.