오류 처리
TRY...CATCH · THROW · XACT_ABORT 로 잘못됐을 때를 다룹니다. 오류가 나도 다음 문장이 실행된다는 데서 시작합니다.
오류가 나도 다음 문장이 실행됩니다
많은 언어에서 예외가 발생하면 그 뒤가 실행되지 않습니다. T-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;
"The statement has been terminated" 는 문장이 끝났다는 뜻이지 배치가
끝났다는 뜻이 아닙니다. 트랜잭션도 살아 있습니다 —
@@TRANCOUNT 는 1 이고
XACT_STATE() 도 1 입니다. 커밋하면 그대로
들어갑니다.
그래서 자료가 어긋납니다
4.2 에서 만든 답글 프로시저에는 오류 처리가 없습니다. 없는 회원 번호로 답글을 달아 봅니다. 뒤를 미는 데는 성공하고 넣는 데서 실패합니다.
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;
| num | sort_no |
|---|---|
| 6 | 0 |
| 352 | 1 |
| 456 | 2 |
| num | sort_no |
|---|---|
| 6 | 0 |
| 352 | 2 |
| 456 | 3 |
오류 메시지는 나왔는데 자료는 이미 어긋났습니다. 이 상태가 쌓이면 목록의 차례가 조금씩 벌어지고, 어디서부터 잘못되었는지 뒤에 알 수 없습니다. 4.5 는 이것을 막는 단원입니다.
오류를 잡습니다
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
| 번호 | 심각도 | 상태 | 줄 | 프로시저 |
|---|---|---|---|---|
| 547 | 16 | 0 | 3 | (배치) |
오류 정보는 CATCH 안에서만 읽을 수
있습니다. 밖에서 부르면 NULL 입니다.
ERROR_PROCEDURE() 는 프로시저 밖에서 났으면
NULL 이므로 ISNULL 로
감싸 두면 읽기 좋습니다.
잡히지 않는 것도 있습니다
-- (가) 같은 배치 안의 이름 오타 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 | Board.P_TYPO |
같은 배치 안에서 난 컴파일 오류는 잡히지 않습니다. 배치 자체가
컴파일되지 않으므로 TRY 에 들어가기 전에 끝납니다.
프로시저 안이면 잡힙니다 — 4.2 에서 본 지연 이름 확인 덕분에
그 오류는 실행 시점에 나기 때문입니다.
심각도 10 이하는 오류가 아니라 알림입니다.
RAISERROR (N'경고입니다', 10, 1) 은
CATCH 로 가지 않고 메시지만 남긴 뒤 다음 줄이
실행됩니다. 반대로 심각도 20 이상은 연결이 끊어져 역시 잡을
수 없습니다.
실패를 알립니다
4.3 에서 반환 코드로 실패를 알리는 방식을 미뤄 두었습니다.
부르는 쪽이 확인하지 않으면 그냥 지나가기 때문입니다.
THROW 는 확인하지 않으면 지나갈 수 없습니다.
THROW 50001, N'답글을 달 원글을 찾지 못했습니다', 1; -- 번호 메시지 상태
잡아서 기록하고 그대로 다시 던집니다
THROW 를 인수 없이 적으면
지금 잡은 오류를 그대로 다시 던집니다. 번호도 메시지도
원래 것이 유지됩니다.
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
RAISERROR 와 다른 점
| THROW | RAISERROR | |
|---|---|---|
| 번호 | 적은 그대로 | 50000 고정(메시지를 등록하지 않으면) |
| 심각도 | 16 고정 | 고를 수 있음 |
| 서식 | 없음 | %d · %s 사용 |
| 다시 던지기 | THROW; | 직접 조립해야 함 |
새 오류를 던질 때는 THROW 를
사용하십시오. 번호를 그대로 쓸 수 있어 부르는 쪽이 무엇이
일어났는지 구분할 수 있습니다.
RAISERROR 는 서식이 필요하거나 심각도 10 으로
알림만 남길 때 씁니다.
THROW 메시지에 % 를 넣지 마십시오
THROW 50001, N'없는 글에는 답글을 달 수 없습니다', 1; -- (가) THROW 50001, N'값 %d 가 잘못되었습니다', 1; -- (나) -- (다) 값을 끼우려면 변수로 조립합니다. DECLARE @m nvarchar(200) = N'글 번호 ' + CAST(99999 AS nvarchar(10)) + N' 를 찾지 못했습니다'; THROW 50001, @m, 1;
| 적은 것 | 번호 | 메시지 |
|---|---|---|
| (가) % 없음 | 50001 | 없는 글에는 답글을 달 수 없습니다 |
| (나) % 있음 | 50001 | (비어 있음) |
| (다) 변수로 조립 | 50001 | 글 번호 99999 를 찾지 못했습니다 |
% 가 하나라도 들어가면 메시지가 통째로
비어 버립니다. 경고도 오류도 없습니다. 퍼센트를 보여야 하는 문구라면
RAISERROR 를 사용하거나 변수로 조립해 넘기십시오.
XACT_ABORT 와 커밋할 수 없는 상태
CATCH 에 들어왔을 때 트랜잭션이 어떤 상태인지
알아야 롤백할지 커밋할지 정할 수 있습니다.
XACT_STATE() 가 알려 줍니다.
| 값 | 뜻 | 할 수 있는 것 |
|---|---|---|
| 1 | 살아 있음 | 커밋도 롤백도 |
| 0 | 트랜잭션 없음 | 둘 다 안 됨 |
| -1 | 커밋할 수 없음 | 롤백만 |
-- (가) 기본값입니다. 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 OFF | 1 | 1 |
| XACT_ABORT ON | 1 | -1 |
XACT_ABORT 를 켜면 오류가 난 트랜잭션을
커밋할 수 없습니다. 끄면 앞에서 본 대로 살아 있어, 오류를 잡고도
실수로 커밋해 반쪽만 들어간 자료를 남길 수 있습니다.
프로시저 앞머리에 SET XACT_ABORT ON 을
적으십시오. SET NOCOUNT ON 과 짝입니다.
그리고 CATCH 에서는
@@TRANCOUNT 가 아니라
XACT_STATE() 로 판단하십시오 — 커밋할 수 없는
상태에서도 @@TRANCOUNT 는 1 이기 때문입니다.
답글 프로시저를 고칩니다
4.2 에서 만든 것에 지금까지의 것을 붙입니다.
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
| num | sort_no |
|---|---|
| 6 | 0 |
| 352 | 1 |
| 456 | 2 |
내가 던진 오류와 엔진이 낸 오류가 같은 방식으로 나갑니다. 부르는 쪽은 번호로 구분합니다 — 50001 이면 원글이 없는 것이고, 547 이면 제약에 걸린 것입니다.
BEGIN TRAN 을 관문 뒤에 두었습니다.
검사에서 걸릴 것을 트랜잭션 안에서 할 이유가 없습니다. 트랜잭션은 짧을수록
좋고, 그 까닭은 4.6 에서 다룹니다.
직접 해보기
오류가 났을 때 번호와 메시지를 Board.ERROR_LOG
에 남기려 합니다. 아래처럼 적으면 기록이 남지 않습니다.
까닭을 찾고 고쳐 보세요.
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
P_POST_REPLY 를 부르는 쪽에서
원글이 없는 경우와 그 밖의 오류를 구분해 다르게 알리는
프로시저를 만들어 보세요. 앞의 것은 사용자에게 보여 줄 문구이고, 뒤의
것은 기록만 남기고 다시 던집니다.
이 단원에서 만든 것을 지웁니다.
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_*를 변수에 담고 → 롤백하고 → 기록하는 순서를 지키십시오.