실전 — 게시판 프로시저 세트
목록·상세·저장·답글·삭제를 한 벌로 만듭니다.
배운 것을 한 벌로 모읍니다
여기서 새로 배우는 문법은 없습니다. 지금까지 나눠서 본 것들을 실제로 쓸 수 있는 한 벌로 묶습니다.
| 프로시저 | 하는 일 | 쓰는 것 |
|---|---|---|
| P_POST_LIST | 목록·검색·다음 쪽 | 계층 정렬(3.9) · 키셋 페이징(5.6) |
| P_POST_SELECT | 상세와 댓글 | 결과 집합 둘(4.3) |
| P_POST_SAVE | 새 글·답글·고치기 | 트랜잭션(4.6) · 오류(4.5) · 잠금(5.5) |
| P_POST_DELETE | 지우기 | 권한 확인 · 자리 메우기 |
표는 3부에서 만든 것을 그대로 씁니다.
Board.POSTS 에 계층 열
(parent_num ·
group_num ·
depth ·
sort_no)이 있고,
IX_POSTS_list 가 목록 정렬을 받칩니다.
한 프로시저로 목록·검색·다음 쪽
CREATE OR ALTER PROCEDURE Board.P_POST_LIST @board_num int, @category_num int = NULL, -- 말머리로 거를 때 @keyword nvarchar(100) = NULL, -- 제목 검색 @group_num int = NULL, -- 앞 쪽 마지막 행의 키(5.6) @sort_no int = NULL, @size int = 20 AS BEGIN SET NOCOUNT ON; SELECT TOP (@size + 1) -- 한 건 더 — 다음 쪽 여부(5.6) p.num, p.board_num, p.category_num, p.user_num, p.group_num, p.depth, p.sort_no, p.title, p.hit_count, p.reg_date, p.mod_date, u.nickname, (SELECT COUNT(*) FROM Board.COMMENTS c WHERE c.post_num = p.num) AS comment_count FROM Board.POSTS p JOIN Member.USERS u ON u.num = p.user_num WHERE p.board_num = @board_num AND (@category_num IS NULL OR p.category_num = @category_num) AND (@keyword IS NULL OR p.title LIKE N'%' + @keyword + N'%') AND (@group_num IS NULL -- 첫 쪽 OR (p.group_num <= @group_num -- 범위를 따로(5.4 · 5.6) AND (p.group_num < @group_num OR p.sort_no > @sort_no))) ORDER BY p.group_num DESC, p.sort_no OPTION (RECOMPILE); -- 매개 변수가 여럿이라(5.3) END
| 부르는 법 | POSTS | USERS | COMMENTS |
|---|---|---|---|
| 첫 쪽 | 15 | 42 | 42 |
| 다음 쪽 | 15 | 42 | 42 |
| 제목 검색 | 15 | — | — |
| 말머리로 거르기 | 15 | 42 | 42 |
여기서 정한 것들
| 정한 것 | 까닭 |
|---|---|
| TOP (@size + 1) | 총 건수를 세지 않고 다음 쪽 여부만 봅니다(5.6) |
| 범위 조건을 따로 적음 | 그래야 SEEK 에 들어갑니다(5.4) |
| OPTION (RECOMPILE) | 매개 변수 조합마다 최적 계획이 다릅니다(5.3) |
| 본문(content)을 내지 않음 | 목록에 필요 없습니다. 커버링에서 빠집니다(5.2) |
| 댓글 수는 상관 서브쿼리 | 조인하면 글이 중복됩니다(2.4) |
댓글 수 42장이 눈에 걸립니다. 21행마다 한 번씩 세기
때문입니다. 목록이 자주 열린다면
Board.POSTS 에 댓글 수 열을 두고 트리거로
맞추는 편이 낫습니다(3.6 · 4.7). 지금은 507행이라 문제가 없지만,
그 판단은 재 보고 하십시오.
검색을 LIKE N'%…%' 로 둔 것도
임시입니다. 5.4 에서 본 대로 앞에 % 가
붙으면 인덱스를 사용하지 못합니다. 글이 수십만 건이 되면
전문 검색으로 바꿔야 합니다(5.8).
결과 집합을 둘 냅니다
CREATE OR ALTER PROCEDURE Board.P_POST_SELECT @num int, @hit bit = 1 -- 미리보기에서는 0 으로 부릅니다 AS BEGIN SET NOCOUNT ON; IF @hit = 1 UPDATE Board.POSTS SET hit_count = hit_count + 1 WHERE num = @num; -- 첫째 — 글 SELECT p.num, p.board_num, p.category_num, p.user_num, p.parent_num, p.group_num, p.depth, p.sort_no, p.title, p.content, p.hit_count, p.reg_date, p.mod_date, u.nickname FROM Board.POSTS p JOIN Member.USERS u ON u.num = p.user_num WHERE p.num = @num; -- 둘째 — 댓글 SELECT c.num, c.user_num, c.content, c.reg_date, u.nickname FROM Board.COMMENTS c JOIN Member.USERS u ON u.num = c.user_num WHERE c.post_num = @num ORDER BY c.num; END
| 표 | 읽기 |
|---|---|
| POSTS | 2 |
| USERS | 2 + 2 |
| COMMENTS | 11 |
결과 집합을 둘 내면 왕복이 하나로 줄어듭니다. 응용 프로그램에서는 이렇게 받습니다.
await using var r = await cmd.ExecuteReaderAsync(); if (await r.ReadAsync()) post = ReadPost(r); // 첫째 — 글 if (await r.NextResultAsync()) // 둘째로 넘어갑니다 while (await r.ReadAsync()) comments.Add(ReadComment(r));
조회수를 여기서 올리는 것이 걸립니다. 5.5 에서 본 대로
인기 글일수록 그 한 줄에 사람이 몰립니다.
@hit 매개 변수를 둔 것은 미리보기나 관리자
화면에서 올리지 않기 위해서이고, 사람이 몰리는 곳이라면 모았다가
반영하는 쪽으로 바꿔야 합니다(5.7 의 연습 1).
새 글·답글·고치기를 하나로
셋을 따로 만들 수도 있지만, 번호가 있으면 고치기, 없으면 새 글, 부모가 있으면 답글로 갈라 하나에 담았습니다.
CREATE OR ALTER PROCEDURE Board.P_POST_SAVE @num int = NULL, -- 있으면 고치기 @board_num int, @category_num int = NULL, @user_num int, @parent_num int = NULL, -- 있으면 답글 @title nvarchar(200), @content nvarchar(max) = NULL, @result int = 0 OUTPUT -- 글 번호를 돌려줍니다 AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; -- 4.5 IF LEN(LTRIM(RTRIM(@title))) = 0 THROW 50010, N'제목을 입력하십시오.', 1; DECLARE @outer bit = CASE WHEN @@TRANCOUNT > 0 THEN 1 ELSE 0 END; -- 4.6 BEGIN TRY IF @outer = 0 BEGIN TRAN; /* 고치기 */ IF @num IS NOT NULL BEGIN UPDATE Board.POSTS SET category_num = @category_num, title = @title, content = @content, mod_date = SYSDATETIME() WHERE num = @num AND user_num = @user_num; -- 권한을 조건에 IF @@ROWCOUNT = 0 THROW 50011, N'글이 없거나 고칠 권한이 없습니다.', 1; SET @result = @num; END /* 새 글 · 답글 */ ELSE BEGIN DECLARE @group int, @depth smallint, @sort int; IF @parent_num IS NULL BEGIN SET @depth = 0; SET @sort = 0; END ELSE BEGIN -- UPDLOCK 으로 잡아 두 사람이 같은 자리를 노리지 않게 합니다(5.5). SELECT @group = group_num, @depth = depth + 1, @sort = sort_no + 1 FROM Board.POSTS WITH (UPDLOCK) WHERE num = @parent_num; IF @group IS NULL THROW 50012, N'답글을 달 원글이 없습니다.', 1; -- 뒤에 있는 것들을 한 칸씩 밉니다(3.9). UPDATE Board.POSTS SET sort_no = sort_no + 1 WHERE group_num = @group AND sort_no >= @sort; END INSERT INTO Board.POSTS (board_num, category_num, user_num, parent_num, group_num, depth, sort_no, title, content, hit_count, reg_date) VALUES (@board_num, @category_num, @user_num, @parent_num, ISNULL(@group, 0), @depth, @sort, @title, @content, 0, SYSDATETIME()); SET @result = SCOPE_IDENTITY(); -- 1.8 -- 원글은 제 번호가 덩어리 번호가 됩니다. IF @parent_num IS NULL UPDATE Board.POSTS SET group_num = @result WHERE num = @result; END IF @outer = 0 COMMIT; END TRY BEGIN CATCH IF @outer = 0 AND XACT_STATE() <> 0 ROLLBACK; THROW; END CATCH END
| num | depth | sort_no | title |
|---|---|---|---|
| 514 | 0 | 0 | 실전 시험 원글 |
| 516 | 1 | 1 | 나중 답글 |
| 515 | 1 | 2 | 첫 답글 |
| 517 | 2 | 3 | 답답글 |
여기서 정한 것들
| 정한 것 | 까닭 |
|---|---|
| 권한을 WHERE 에 넣음 | 먼저 조회해 확인하면 그 사이에 바뀔 수 있습니다 |
| @@ROWCOUNT = 0 으로 판정 | 글이 없는 것과 권한이 없는 것을 구분하지 않습니다 |
| UPDLOCK | 두 사람이 같은 원글에 동시에 답글을 달 때(5.5) |
| @outer 로 감쌈 | 부르는 쪽 트랜잭션을 건드리지 않습니다(4.6) |
| 오류 번호를 정해 둠 | 응용 프로그램이 갈라 처리합니다(6.5) |
글이 없는 것과 권한이 없는 것을 나누지 않은 것은 일부러입니다. "그 글은 있는데 당신 것이 아닙니다" 라고 알려 주면 번호를 바꿔 가며 어떤 글이 있는지 알아낼 수 있습니다. 둘을 한 문구로 두면 그것을 막습니다.
지우고 자리를 메웁니다
CREATE OR ALTER PROCEDURE Board.P_POST_DELETE @num int, @user_num int, @result int = 0 OUTPUT 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; DECLARE @owner int, @group int, @sort int; SELECT @owner = user_num, @group = group_num, @sort = sort_no FROM Board.POSTS WITH (UPDLOCK) WHERE num = @num; IF @owner IS NULL THROW 50013, N'글이 없습니다.', 1; IF @owner <> @user_num THROW 50014, N'지울 권한이 없습니다.', 1; IF EXISTS (SELECT 1 FROM Board.POSTS WHERE parent_num = @num) THROW 50015, N'답글이 달려 있어 지울 수 없습니다.', 1; -- 딸린 것부터 지웁니다. 외래 키 순서입니다(3.3). DELETE FROM Board.COMMENTS WHERE post_num = @num; DELETE FROM Board.FILES WHERE post_num = @num; DELETE FROM Board.POSTS WHERE num = @num; SET @result = @@ROWCOUNT; -- 뒤에 있던 것들을 한 칸씩 당깁니다. UPDATE Board.POSTS SET sort_no = sort_no - 1 WHERE group_num = @group AND sort_no > @sort; IF @outer = 0 COMMIT; END TRY BEGIN CATCH IF @outer = 0 AND XACT_STATE() <> 0 ROLLBACK; THROW; END CATCH END
| num | depth | sort_no | title |
|---|---|---|---|
| 514 | 0 | 0 | 실전 시험 원글 |
| 515 | 1 | 1 | 첫 답글 |
| 517 | 2 | 2 | 답답글 |
정말 지울지, 표시만 할지 먼저 정하십시오. 3.10 에서 본
soft delete 라면 DELETE 대신
usable 열을 내리고, 목록 조건에 그것을
더합니다. 답글이 달린 글은 지울 수 없는 문제도 함께
사라집니다 — 표시만 바꾸면 되니까요.
바깥 트랜잭션에서 부를 때
이 프로시저들을 바깥 트랜잭션 안에서 여러 번 부르면 뜻밖의 오류를 만납니다.
BEGIN TRAN; BEGIN TRY EXEC Board.P_POST_DELETE @num = @p1, @user_num = 1; -- 50015 로 실패 END TRY BEGIN CATCH PRINT ERROR_MESSAGE(); -- 잡았습니다 END CATCH EXEC Board.P_POST_DELETE @num = @p3, @user_num = 3; -- 이번엔 될 줄 알았는데
SET XACT_ABORT ON 인 프로시저에서 오류가 나면
바깥 트랜잭션이 "커밋할 수 없는" 상태가 됩니다. 오류를
CATCH 로 잡아도 그 상태는 남습니다.
롤백하는 수밖에 없습니다.
-- 바깥에서 잡았다면 상태를 확인하고 롤백해야 합니다(4.6). BEGIN CATCH PRINT ERROR_MESSAGE(); IF XACT_STATE() = -1 ROLLBACK; -- -1 이면 되돌리는 수밖에 없습니다 END CATCH
그래서 프로시저를 부르는 쪽에서 트랜잭션을 여는 일을 줄이십시오. 프로시저 하나가 하나의 일을 온전히 마치게 두면(4.6) 이런 상태를 다룰 일이 없습니다. 여럿을 묶어야 한다면 그 묶음을 담는 프로시저를 따로 만드는 편이 낫습니다.
직접 해보기
댓글을 달고 지우는 프로시저를 이 한 벌에 맞춰 만들어 보세요.
같은 규약을 지키십시오 — 오류 번호, 권한 확인,
@outer.
글이 50만 건이 되자 목록이 느려졌습니다. 이 프로시저에서 무엇을 어떤 순서로 고치겠습니까. 5부에서 잰 값들을 근거로 적으십시오.
이 단원에서 만든 것을 지웁니다.
DROP PROCEDURE Board.P_POST_LIST, Board.P_POST_SELECT,
Board.P_POST_SAVE, Board.P_POST_DELETE;
- 목록 하나로 첫 쪽·다음 쪽·검색·말머리 거르기를 처리합니다. 키셋이라 어느 쪽을 봐도 POSTS 15장입니다(5.6).
- 총 건수를 세지 않고
TOP (@size + 1)로 다음 쪽 여부만 봅니다. - 키셋 조건은 범위를 따로 적어야
SEEK에 들어갑니다(5.4). - 상세는 결과 집합을 둘 내 왕복을 하나로 줄입니다. 응용 프로그램은
NextResult로 넘깁니다(6.5). - 저장은 번호가 있으면 고치기, 부모가 있으면 답글로 갈라 하나에 담았습니다.
- 권한을
WHERE에 넣고@@ROWCOUNT로 판정합니다. 먼저 조회해 확인하면 그 사이에 바뀔 수 있습니다. - 글이 없는 것과 권한이 없는 것을 한 문구로 뭉칩니다. 나누면 번호를 바꿔 가며 글의 존재를 알아낼 수 있습니다.
- 답글 자리를 만들 때
UPDLOCK으로 잡습니다. 두 사람이 같은 자리를 노리는 것을 막습니다(5.5). - 지운 뒤에는
sort_no를 당겨 촘촘하게 만듭니다. 빈자리를 두면 페이징이 어긋납니다. XACT_ABORT ON인 프로시저가 오류를 내면 바깥 트랜잭션이 커밋 불가 상태(3930)가 됩니다.XACT_STATE() = -1이면 롤백하는 수밖에 없습니다(4.6).