대상 : ASP.NET을 이용하여 스스로 일반 게시판이 작성가능하거나,
Taeyo's ASP.NET v1.0 서적을 통해서 게시판 만들기를 이미 공부하신 분
많이 늦은 감이 또한 없진 않지만, 이제 계층형 게시판의 꽃이라 할 수 있는 계층형 출력에 대해서 설명드릴 시간이 다가왔네요. 앗? 네??? 계층형 게시판 강좌가 있기나 했었냐구요?? 허허~~~
아주 제대로 삐지신 것이로구만요… 하하하.. 새삼 스럽게 삐지시고 그러셨습니까? 산다는게 원래 그런 것 아니겠습니까??? 엣?? (쓔욱~~ 뚝!!) 허걱, 왠 돌이??? 엇?? 이런… 텨=3=3=3=
페이징을 위해서 반드시 필요한 것은 당근! 페이징을 위한 쿼리입니다. 이 부분은 성능과 상당히 밀접한 부분이기에 성능을 고려한 여러가지 방법이 거론되고 있지요. 제가 사용하려는 쿼리는 단지 가장 보편적인 페이징 방법 중 하나입니다만(이 방법의 효능에 대해서는 말들이 많은 편입니다), 더 나은 방법을 원하시는 분들은 인터넷에서 발품을 팔아 돌아다녀보시면 쓸만한 것들을 구할 수도 있을 것입니다. 제가 이번 강좌에서 제시하는 페이징용 쿼리가 가장 뛰어난 것은 아니라는 사실에 의심의 여지는 없어 보입니다. 더 좋고, 더 뛰어난 쿼리가 어디엔가 많이 존재하고 있을테니까요. 만일, 그러한 쿼리를 여러분이 생각해 냈다거나, 혹은 어디선가에서 그러한 쿼리를 얻어냈다고 한다면 차후에 이 쿼리 프로시저를 그러한 로직으로 바꾸어 주시면 될 것입니다.
여러분의 질문 : 태오 사이트도 이 쿼리를 사용하고 있나요? 솔직히 그렇지는 않습니다. ㅠ_ㅠ; 제 사이트에서 사용하는 쿼리는 이 쿼리를 조금 개선한 것이지요. 하지만, 쿼리의 기본형태는 이 쿼리의 것을 그대로 사용하고 있습니다. 데이터 량이 늘어남에 따라 바꿔야 한다는 중압감이....지금 지용군과 더 좋은 쿼리를 준비하고 있으니 완성되는 대로 공개하도록 해보겠습니다. (어이 지용군~ 됐어. 시간 벌었어… -_-;;;) |
그렇다면, 이 강좌에서 제시하는 쿼리를 일단 먼저 보도록 하겠습니다.
SELECT TOP [가져올 레코드 수] seq, thread, 컬럼A, 컬럼B, … FROM ThreadBoard WHERE thread NOT IN (SELECT Top [건너뛸 레코드 수] thread FROM ThreadBoard ORDER BY thread DESC) ORDER BY thread DESC |
그렇습니다. 이미 많은 분들이 알고 있는 NOT IN 쿼리입니다. 이 쿼리의 출처는 정확히 알려져 있지 않습니다만, 카이스트의 재학중인 어떤 분이 제시했다는 유력한 설만이 떠돌 뿐이지요. 지금은 이 쿼리를 개선한 쿼리들도 많이 나와있지만, 이 강좌에서는 이 쿼리를 그대로 사용하도록 하겠습니다. 더 나은 쿼리는 스스로 한번 고민해보시기 바랍니다(이 부분에 대해 참고할만한 사이트로는 www.knhead.pe.kr 이 있습니다)
쿼리에 대해서 간략하게 설명을 드리면, 이 쿼리는 [건너뛸 레코드 수]를 지정함으로써 우리가 가져오고자 하는 페이지에 해당하는 레코드를 가져오는 방법을 제공힙니다. 만일, [가져올 레코드 수]를 10으로 가정한 상태에서, [건너뛸 레코드 수]가 0 이라면, 첫 번째 페이지에 해당하는 10개의 레코드를 가져올 것이구요. [건너뛸 레코드 수]가 10이라면 10개의 레코드를 건너뛴 두 번째 페이지에 해당하는 10개의 레코드를 가져오게 될 것입니다. 만일, [건너뛸 레코드 수]를 20으로 지정한다면, 결과적으로 20개의 레코드(2페이지 분량)을 건너뛰게 되어 3번째 페이지에 해당하는 10개의 레코드를 가져오게 되겠지요?
그러므로, [건너뛸 레코드 수]라는 것은 논리적으로 [가져올 레코드 수] x [가져올 페이지 번호 - 1] 이라고 볼 수 있습니다. 간단한 논리의 수학이기에 이해하는 데 그다지 어렵지는 않을 것이라 생각합니다. ^^. 굳이 방정식까지 들먹이지 않는다 해도 말입니다.
그렇다면, 일단 SQL 서버의 [쿼리 분석기]를 통해서 이러한 쿼리가 제대로 동작하는 지를 한번 확인한 다음에, 페이징이 적용된 저장 프로시저를 작성해 보도록 하겠습니다. 사실, 페이징 기능은 저장 프로시저만 확정되면, 기존 소스를 크게 바꿀 것은 없거든요.
자. 그렇다면, [쿼리 분석기]를 실행하시기 바랍니다. 그리고, 일단 ThreadBoard 테이블에 여러분이 저장해 두었을지도 모르는 모든 레코드를 삭제한 다음에, 페이징을 위해서 약 100 개 정도의 레코드를 넣어보도록 하겠습니다.
"여러분~ 모두~~~ 100개의 레코드를 넣어주세요~~"
라고 하면, 아마도 내일에나 강좌를 제대로 시작할 수 있을 것 같네요. -_-;;; 그렇기에, 우리는 단순하게 다음과 같은 쿼리를 통해서 동적으로 100 개의 무의미한 레코드를 한번 넣어볼까 합니다. 지금 우리의 관심은 페이징이 제대로 동작하느냐이지, 각각의 글이 얼마나 유용하냐?인 것은 아니니까요.
DELETE FROM ThreadBoard GO
Declare @count int Declare @threadID int
SET @count = 1 WHILE (@count <= 100) BEGIN SET @threadID = @count * 1000
INSERT INTO ThreadBoard (thread, depth, writer, pwd, title, mode, content) Values (@threadID, 0, 'ME', 'pwd', STR(@count) + ' 번째 글입니다', 1, '본문 내용입니다')
SET @count = @count + 1 END GO |
자. 이 명령을 [쿼리 분석기]에서 실행하시면, 약간의 시간이 소요된 후에, ThreadBoard 테이블에는 100 개의 레코드가 들어가게 될 것입니다. 이제, 데이터도 준비가 되었네요. 그렇다면, 이제 앞에서 설명드린 페이징용 쿼리를 한번 테스트 해 볼까요? 일단, 첫번째 페이지부터 데이터를 제대로 가져오는지 확인해 보도록 하겠습니다. 첫 번째 페이지를 가져오기 위한 쿼리는 다음과 같아야 하겠죠? (우리는 가져올 레코드 수를 10이라고 가정하겠습니다)
SELECT TOP 10 seq, thread, depth, writer, title, readcount, transdate FROM ThreadBoard WHERE thread NOT IN (SELECT Top 0 thread FROM ThreadBoard ORDER BY thread DESC) ORDER BY thread DESC |
결과는 다음과 같을 것입니다,.

두 번째 페이지를 가져오려면 어떻게 해야 할까요? 그렇습니다. 단지, 서브쿼리 구역의 Top 쪽 숫자 즉, [건너뛸 레코드 수]를 10으로 지정하면 됩니다. 앞에서 말씀드렸다시피,
[건너뛸 레코드 수] = [가져올 레코드 수] x [가져올 페이지 번호 - 1]
[건너뛸 레코드 수] = 10 x (2 -1) => 10
이 되기 때문이지요. 쉽죠? 다음 그림은 각각 2번째 페이지와 3번째 페이지를 가져올 경우의 결과 화면입니다.


호오.. 예상한대로 결과가 제대로 출력되는 것을 볼 수 있네요. ^^
하지만, 위의 쿼리를 실전에 직접 사용하기는 좀 그렇죠? 가급적 ASP 페이지에서 Query를 직접 작성하는 것은 여러모로 좋지 않으니까요(SQL 인젝션의 위험도 있고 하니깐 말입니다. SQL 인젝션 공격에 대해서는 별도의 강좌로 준비중입니다). 해서, 이 부분은 프로시저로 제작하는 것이 나을 것입니다. 여러모로 말입니다~
그럼 어떻게 프로시저로 만들면 될까요? 아마도 많은 분들이 다음과 같이 생각하실 것 같습니다(저도 이렇게 생각했었습니다).
CREATE PROC UP_SELECT_BOARDLIST_PAGING @Page int AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED SET NOCOUNT ON
SELECT TOP 10 seq, thread, depth, writer, title, readcount, transdate FROM ThreadBoard WHERE seq NOT IN (SELECT Top 10 * (@page-1) seq FROM ThreadBoard ORDER BY seq DESC) ORDER BY thread DESC
GO |
하지만, 이렇게 작성하시고 프로시저를 생성하려 하시면 빠박!! 에러가 다음과 같이 나는 것을 보실 수 있을 겁니다.
서버: 메시지 170, 수준 15, 상태 1, 프로시저 UP_SELECT_BOARDLIST_PAGING, 줄 10 줄 10: '(' 근처의 구문이 잘못되었습니다. |
사실 이러한 에러의 원인은 복합적인 이유로 인해서 발생한 것입니다만, 가장 큰 이유를 설명드리자면, 결정적으로 SQL의 Top 구문 앞에서는 변수를 사용할 수 없다는 것이 되겠습니다.(사실, 현재의 에러의 이유는 이게 아니라 서브쿼리 안에서 연산을 수행하고 있어서 나는 겁니다만, 그 문제를 풀고 나면 최종적으로는 앞서 말씀드린 문제에 봉착하게 됩니다)
그렇다면, 문제를 어떻게 풀어나가야 할까요. 답은 단순합니다. 전체 쿼리 문장을 문자열로 만들고, 그 문자열을 EXEC 라는 명령어를 통해서 실행하게 하는 것이지요. 조금 지저분해 보일 수 있습니다만, 대부분들 다음과 같이 작성하여 사용합니다.
CREATE PROC UP_SELECT_BOARDLIST_PAGING @Page int AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED SET NOCOUNT ON
Declare @strSQL nvarchar(300)
Set @strSQL = ' SELECT TOP 10 seq, thread, depth, writer, title, readcount, transdate ' Set @strSQL = @strSQL + ' FROM ThreadBoard ' Set @strSQL = @strSQL + ' WHERE thread NOT IN ' Set @strSQL = @strSQL + ' (SELECT Top ' + STR(10 * (@page-1)) + ' thread FROM ThreadBoard ORDER BY thread DESC) ' Set @strSQL = @strSQL + ' ORDER BY thread DESC '
Exec sp_executesql @strSQL
GO |
입력 매개변수로 페이지 번호를 받아들여서 그 페이지에 해당하는 레코드들을 10개 가져오는 쿼리입니다. 이전 프로시저와 다른 점이라면 모든 쿼리를 문자열로 결합하고 있다는 점 정도가 차이입니다. 그렇죠??? TOP 구문 앞에 변수를 사용할 수 없기에, 이런 방법을 사용한 것이지요. 잘 보시면 실제로 작성된 전체 쿼리 문자열을 EXEC 명령과 함께 sp_executesql 라는 내부 프로시저를 사용해서 실행하고 있는 것을 보실 수 있을 겁니다.
사실, 이 부분은 그냥 EXEC(@strSQL) 라고 하셔도 문제는 없지만, sp_executesql를 사용하는 것이 쿼리 계획을 재사용하는 것을 기대할 수 있기 때문에, 성능측면에서 보다 낫기에 그를 사용한 것입니다.
참고! 참고!! 만일, 더 구체적인 정보를 원한다 하시면 MSDN에서 sp_executesql로 검색해 보세요. MSDN을 자기 집 드나들듯이 드나드는 습관이야말로 뛰어난 개발자가 되기 위한 기본적인 습관이 아닌가 합니다. |
근데, 현재대로라면 페이지당 가져오는 레코드의 수는 고정적으로 10이 되겠네요. 그 점은 좀 애석하죠? 그렇다면, 가져올 레코드수(페이지당)도 변수로 받아서 사용해 보도록 바꿔봅시다. 어떻게 하면 될까요? 다음처럼 하면 되겠죠?
ALTER PROC UP_SELECT_BOARDLIST_PAGING @Page int, @PageSize int AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED SET NOCOUNT ON
Declare @strSQL nvarchar(300)
Set @strSQL = ' SELECT TOP ' + Str(@PageSize) + ' seq, thread, depth, writer, title, readcount, transdate ' Set @strSQL = @strSQL + ' FROM ThreadBoard ' Set @strSQL = @strSQL + ' WHERE thread NOT IN ' Set @strSQL = @strSQL + ' (SELECT Top ' + STR(@PageSize * (@page-1)) + ' thread FROM ThreadBoard ORDER BY thread DESC) ' Set @strSQL = @strSQL + ' ORDER BY thread DESC '
Exec sp_executesql @strSQL
GO |
파란색으로 처리된 코드가 새로이 추가된 부분입니다. 어려울 것 없죠?? 좋습니다. 일단, 여기까지만 작성하고 이 프로시저가 올바르게 동작하는지부터 확인해 보도록 하겠습니다.기존에 작성해 놓은 List.aspx 페이지를 방금 제작한 UP_SELECT_BOARDLIST_PAGING 프로시저를 사용하도록 한번 수정해 보도록 해요 ^^
저는 일단 간단하게 페이징 이동을 위해서, 텍스트 박스와 버튼을 하나 추가해 보았습니다. ^^ 다음과 같이 List.aspx 페이지의 제일 하단에 말이지요~~
// 기존 코드들 // ...................
<asp:BoundColumn DataField="readcount" HeaderText="읽음"> <HeaderStyle HorizontalAlign="Center" Width="40px"> </HeaderStyle> <ItemStyle HorizontalAlign="Center"></ItemStyle> </asp:BoundColumn> </Columns> </asp:DataGrid> </td> </tr>
<tr> <td align="right"> <asp:TextBox Runat="server" ID="PageNoToGo" Text="1" Width="40"> </asp:TextBox> <asp:Button Runat="server" ID="GoPage" Text="GO"></asp:Button> </td> </tr> | </table> </form> </body> </HTML> |
그리고, 버튼의 클릭 이벤트는 다음과 같이 작성해 보았습니다.
private void GoPage_Click(object sender, System.EventArgs e) { PageNo = int.Parse(PageNoToGo.Text); Listing(); } |
위의 GoPage_Click 이벤트에서는 현재 사용자가 입력한 페이지 번호값을 PageNo라는 클래스 전역적인 Integer 변수에 담고 있습니다. 그리고, 그 값은 게시판 List를 출력하는 Listing()에서 사용될 것이구요 ^^
그렇다면, 일단 PageNo라는 변수를 클래스 변수로 선언해 볼까요? 그리고, 기존에 만들어져 있던 Listing() 함수도 이제 새로운 계층형 프로시저를 사용하는것으로 바꾸어 보도록 하겠습니다. 다음과 같이 하시면 될 것 같아요 (기존 코드에서 변경된 부분은 파란색으로 작성해 보았습니다)
public class list_Paging : System.Web.UI.Page { //.. 기존 코드 .. protected int PageNo = 1; protected const int PageSize = 10;
private void Listing() { string ConnectStr = "server=(local);database=TEST;user id=sa;password=xodh";
SqlConnection Con = new SqlConnection(ConnectStr); SqlCommand Cmd = new SqlCommand(); Cmd.Connection = Con; Cmd.CommandText = "UP_SELECT_BOARDLIST_PAGING"; Cmd.CommandType = CommandType.StoredProcedure;
//----------------- 계층형 추가 구역 ----------------- Cmd.Parameters.Add("@Page", SqlDbType.Int); Cmd.Parameters["@Page"].Value = PageNo; Cmd.Parameters.Add("@PageSize", SqlDbType.Int); Cmd.Parameters["@PageSize"].Value = PageSize; //----------------- 계층형 추가 구역 -----------------
SqlDataAdapter adp = new SqlDataAdapter(Cmd); DataSet ds = new DataSet();
adp.Fill(ds, "ThreadBoard"); DataGrid1.DataSource = ds; DataGrid1.DataBind();
Cmd = null; Con = null; }
//.. 기존 코드 ..
|
다. 되셨다면, 이제 우리 모두 다같이 손뼉을~~~ 짝짝!! 치면서 실행을 한번 해볼까요??? 페이지 이동하기 전에 답변도 좀 달아보고 해보세요~~~ ^^그러면, 다음과 같이 아주 잘 출력되고, 또한 아주 잘 목록이 나열되는 것을 볼 수 있을 겁니다그쵸??? 페이지도 입력한대로 잘 이동될 거예요. 하하하

일단은 우리가 작성한 프로시저가 어느정도 잘 동작한다는 것을 확인해 볼 수 있었습니다. 그렇다면, 이제 이 프로시저를 사용해서 페이저바(Pager Bar)을 꾸미면 모든게 완성이 되는 것일까요?
안타깝게도, 그렇지가 않습니다. 느낌상으로는 여기까지면 다 된 것 같아 보입니다만… 사실 그게 그렇지가 않습니다. 나중에 실제적으로 페이징을 적용한 List 페이지를 만들어 보시면 알겠지만, 페이징이 적용되는 List 페이지는 공통적으로 전체 레코드의 수를 반드시 알아야 할 필요가 있습니다. 물론, 그를 위해서는 별도로 쿼리를 한번 더 날려야 하겠죠. 현재 테이블의 전체 레코드수를 알아오는 쿼리를 말입니다. 근데, 성능적인 측면에서 보면 할 수만 있다면 가급적 웹 페이지에서는 데이터베이스로 쿼리를 적게 날리는 것이 바람직합니다. 해서, 저의 경우는 어차피 페이징이 적용된 모든 List 페이지가 방금 제작한 프로시저를 날려야만 한다면, 전체 레코드수를 가져오는 것도 별도로 처리할 게 아니라, 이 프로시저에서 같이 처리해서 넘겨주면 낫지 않을까? 하고 생각했다는 겁니다. 어차피 전체 레코드 수는 단순한 정수 값일 것이기에, 출력 매개변수(Output Parameter)로도 쉽게 넘길 수 있으니까요 ^^
해서, 저는 그러한 부분까지 추가해 보았습니다(저장 프로시저에서 입력, 출력 매개변수 다루는 방법을 잘 모른다하시는 분은 다음 강좌를 우선적으로 읽어보시기 바랍니다). 해서, 변경된 저장 프로시저는 다음과 같습니다.
http://www.taeyo.net/lecture/10_ADO/SP01.asp
http://www.taeyo.net/lecture/10_ADO/SP02.asp
UP_SELECT_BOARDLIST_PAGING의 최종버전
ALTER PROC UP_SELECT_BOARDLIST_PAGING @Page int, @PageSize int, @TotalRowCount int OUTPUT AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED SET NOCOUNT ON
SELECT @TotalRowCount = Count(seq) FROM ThreadBoard
Declare @strSQLnvarchar(300)
Set @strSQL = ' SELECT TOP ' + Str(@PageSize) + ' seq, thread, depth, writer, title, readcount, transdate ' Set @strSQL = @strSQL + ' FROM ThreadBoard ' Set @strSQL = @strSQL + ' WHERE thread NOT IN ' Set @strSQL = @strSQL + ' (SELECT Top ' + STR(@PageSize * (@page-1)) + ' thread FROM ThreadBoard ORDER BY thread DESC) ' Set @strSQL = @strSQL + ' ORDER BY thread DESC '
Exec sp_executesql @strSQL
GO |
프로시저를 위와 같이 ALTER 하셨다면, 이제 [쿼리 분석기]에서 먼저 이 프로시저가 제대로 동작하는지 확인부터 해보도록 합시다. 다음과 같이 작성하고 실행해 보면 되겠죠????
Declare @TCNT int exec UP_SELECT_BOARDLIST_PAGING 1, 10, @TCNT OUTPUT SELECT @TCNT |
위의 명령은 1번째 페이지에서 10개의 레코드를 가져오는 명령이라는 것은 다들 아시겠죠? 해서, 결과 레코드가 나올 것이구요. 추가적으로 @TCNT라는 변수를 통해서 출력 매개변수 값도 나올 것입니다. 물론, 그 값은 전체 레코드개수가 되겠죠 ^^ 실행결과는 다음과 같을 겁니다.

호오.. 좋죠??? 그렇습니다. 그렇다면, 이제 계층적으로 출력하기 위한 단계 중 거의 80%의 작업이 끝난 것 같네요. 이 즈음되면, DataGrid 컨트롤을잘 다루시는 분들의 경우는 스스로도 결과를 출력해낼런지도 모르겠습니다.
남은 것이라곤, Pager Bar를 구성해주는 것밖에 안 남았으니까요 ^^ Pager Bar가 뭐냐구요? 그것은 바로~~그리드 하단에 나타나곤 하는 페이지 번호들의 링크를 말하는 것입니다. ^^
저희는 이 페이저바를 위해서, 태오가 제작한 컨트롤인 PagingHelper 컨트롤을 사용해 볼까 합니다. -_-;;; 맘에 안드신다구요??? -_-+++ 흥!!!!~~~ 그렇다면, 그분들은 그냥 DataGrid에서 제공하는 페이저 기능을 사용하시면 되겠습니다. 사용방법은 태오의 PagingHelper나 그거나 다를 것이 거의 없으니까요..
혹시 태오를 좋아하시는 분들은 멋진 PagingHelper 컨트롤을 한번 먼저 만나보세요. ^^ 사용법을 강좌로 준비해 두었으니까요. 한번 써보시면..우와~~ 쉽다~~ 하실 겁니다. 혹.. "에이. 퉤퉤... 재섭어~~ 컨트롤 유치뽕짝이야~~" 하시는 분들이 있다면 말입니다. 삐짐임다. -_-++++
PagingHelper 컨트롤 도움문서(예제 포함) :
PagingHelper 설명 페이지
그렇다면, 다음 시간에 어디 한번 그것을 구현해볼까요??? 그리고, 이 모든 작업이 끝난 후에는 마지막으로 검색기능을 달아보도록 하겠습니다. 그 다음에는 이 게시판을클래스화 시켜서 좀 더 모듈스럽게 만들어볼까 합니다. ^^
그럼 다음 시간에 뵈요~~