대상 : ASP.NET을 이용하여 스스로 일반 게시판이 작성가능하거나,
Taeyo's ASP.NET v1.0 서적을 통해서 게시판 만들기를 이미 공부하신 분
이제, 다시금 계층형 게시판을 계속해서 만들어 나가 보도록 하겠습니다.
이번 시간에 작성할 부분은 글을 수정하는 부분입니다. 게시판에 이골이 나신 분들은 아시겠지만, 글을 수정하는 페이지는 일반적으로 UI는 Insert.aspx의 모습을 띄고 있지만, 내부적인 로직은 Content.aspx 형태를 일부 가지고 있는 편입니다. 해서, 사실상 이 페이지에서 어려운 부분은 그다지 없는 편이지요. 보안적인(?) 부분을 제외한다면 말입니다.
그렇다면, 한번 시작해 볼까요?
우선, 여러분의 프로젝트에 Edit.aspx 라는 웹 폼을 하나 추가해 보도록 하겠습니다. 그리고, 그 웹 폼의 UI는 Insert.aspx와 거의 유사하게 다음과 같이 꾸며주도록 하세요. 사실, Insert.aspx 페이지에서 <form> 태그 안쪽에 있는 전체 코드를 싸그리~~ 복사해서, Edit.aspx의 <form> 안에 붙여 넣은 다음, 약간을 수정해 주셔두 됩니다. 이렇게 하면 금새 Edit.aspx 페이지를 만드실 수 있지요. 저의 경우는 이렇게 작업해 보았는데요. 만일, 여러분도 그렇게 하실 생각이시라면... 그렇게 하신 뒤에, 몇가지를 수정하시면 되는데요. 수정해 주어야 하는 부분은 다음과 같습니다. ^^
1. 폼의 상단에 보면 머리글이 "글 작성하기"라고 되어져 있을 겁니다. 그것은 "글 수정하기"라고 바꾸어 버리도록 하세염~~~
2. 비밀번호 입력과 관계된 <tr> 행 자체를 제거해 버리세요. 비밀번호는 편집의 대상이 아니니까요.
3. 이름을 입력하는 <tr> 행에 들어있는 Textbox 컨트롤과 그 컨트롤을 위한 requiredfieldvalidator 유효성 검사 컨트롤도 제거해 버리세요. 그리고, 대신에 <asp:Label runat="server" id="name" /> 를 그 위치에 두도록 하세요. 이렇게 하는 이유는 이름은 편집의 대상이 아니기 때문이랍니다. ^^;
4. 제일 밑쪽의 태그를 보면, reset 이라는 이미지에 걸려있는 하이퍼링크의 태그가 요로코롬 <a href="javascript:document.Insert.reset();"> 라고 되어져 있을 겁니다. 현재 Edit.aspx 페이지의 폼 이름은 Edit 이니깐, 이 부분도 바꾸어 주셔야 합니다. <a href="javascript:document.Edit.reset();">라고 말이죠~
요기까지~~
이렇게 하면, Edit.aspx 용 폼이 간단하게 완성이 됩니다. 디자인도 아주 호환성있게 구성되고 말입니다. 음헤헤... 카피 앤 페이스트... !! 이걸 유용하다고 해야할지는 모르겠습니다만... 일단은 뭐...
사실, UI 재사용성과 관계된 부분은 User Control의 몫이긴 합니다만.. 이 부분을 그렇게까지 해야할 이유는 없어보이니깐.. 저의 경우는 그냥 이렇게 하드코딩 해 보았습니다.
"엔터프라이즈 급 솔루션의 경우에서는 가급적 코드의 reusability를 생각해서, 이 부분을 사용자 정의 컨트롤로 분리하고, 별도의 컨테이너로 가져가는 것이 바람직하지 않을까요???"
라고, 말씀하시고 싶으신 분은 그렇게 하셔도 됩니다. (-_-)+++ 근데 말입니다, 언제나... 생각해야 할 것은, 지금 내가 너무 앞서서 생각하고 있는 것은 아닌가??하고 스스로에게 물어보는 것이라 생각합니다. 실제로 엔터프라이즈급이 아닌 경우라면, 그게 진짜로 더 도움이 되는 방법이 맞을까요? 재사용성은 그렇다쳐도, 그렇게 개발하기 위해 소모해야 하는 시간은 어떻게 감당을???
해서, 저의 경우는 고민할 거 없이, 걍, copy & paste를 이용해 보았습니다. 반론이 있으신 분들이 있다면, 본인의 의지대로 더욱 멋지게 이를 구성해 보는 것도 좋을 것 같습니다. ^^; (절대 비꼬는 거 아님. 실은 이야기하면서 속으로 태오도.. 한번 그렇게 해볼까?? 하고 있었음... -_-;)
자. 이제 유저 인터페이스는 갖춰진 것 같네요. 그럼, Edit.aspx 페이지가 가장 먼저 해야할 일은 무엇일까요? 일단, 이 페이지는 하나의 가정을 하고 시작합니다. 그 가정이란 무엇이냐? Content.aspx 페이지에서 우선적으로 비밀번호 검사를 수행하고, 비밀번호가 일치할 경우, Edit.aspx 페이지로 현재 수정해야 하는 글의 seq 값을 넘겨줄 것이라는 가정이지요!!!
웹 페이지들은 단순해서리 직접적으로 값을 넘겨주지 않으면 지가 알아서 값을 막 가져오고 그러지는 못합니다. 한마디로 단순무식 페이지인 것이쥐요~ 저랑 비슷해 보입니다. 흐음~~~
그렇다면, 이러한 가정이 성립되도록 Content.aspx 페이지에서 이러한 작업을 해주어야 할 것입니다. 그렇죠? 좋습니다. 그렇다면, 작업 들어갑니다... 슬그머니~~~ Content.aspx 페이지를 열어줍니다. 그리고, 그 웹 폼에서 Edit 이미지를 더블클릭!!! 해서, 코드 비하인드 페이지에다가 btnEdit_Click 이벤트 처리기를 다음과 같이 작성해 주는 것이지요. (주의!! 이 코드는 Content.aspx 페이지에 작성합니다)
private void btnEdit_Click(object sender, System.Web.UI.ImageClickEventArgs e) { bool isValid = IsPasswordValid();
if(isValid) { Response.Redirect("Edit.aspx?seq=" + ViewState["seq"].ToString()); } else { string script; script = "<script>alert('비밀번호가 일치하지 않습니다');</script>";
this.RegisterStartupScript("pwd", script); } } |
이 코드는 여러분이 이전 시간에... 작성했었던, btnDelete_Click 이벤트 처리기와 아주아주 흡사합니다. 단지, 비밀번호가 일치할 경우, Edit.aspx 페이지로 재이동한다는 것을 제외하면 말입니다. ^^ 그쵸?? 해서, 코드에서는 그 부분만을 파란색상으로 부각시켜 보았습니다요~
이 코드에 대해서는 별도의 설명이 필요없을 것 같습니다. 이전 시간에 충분히 다루었던 부분이니까요. (물론, 강좌가 매우 오랜만에 올라오는 것이라... 기억이 가물하신 분들도 있을 것이고, 그 분들에게는 무지하게 죄송합니다만.... ) 여하튼!!! 그렇습니다. 해서, 여러분은 이러한 코드를 통해서, 비밀번호가 일치할 경우에만 Edit.aspx 페이지로 seq 값을 가지고 Redirect 될 것임을 확신하실 수 있을 것입니다. ^^;
OK!!
그럼, 이제 본격적인 Edit.aspx 페이지의 작성에 들어갑니다. djt서두가 길었죠??? ^^
Edit.aspx 페이지에서 우선적으로 처리해야 할 작업은 무엇일까요? 그렇습니다. Content.aspx 페이지로부터 넘어오는 seq 값을 일단 받아서 내부적인 변수에 저장해 두어야 하겠죠? 해서, Page_Load 이벤트 처리기에다가는 다음과 같이 코드를 작성하시면 되는 것입니다.
public class Edit : System.Web.UI.Page { protected System.Web.UI.WebControls.Label name; protected System.Web.UI.WebControls.TextBox pwd; //...중략...
private string seq = string.Empty; private SqlConnection Con; private SqlCommand Cmd; private const string CONNECTSTR = "server=(local);database= TEST;userid=sa;password=";
private void Page_Load(object sender, System.EventArgs e) { // 여기에 사용자 코드를 배치하여 페이지를 초기화합니다. if(!IsPostBack) { if(Request.Params["seq"] != null) seq = Request.Params["seq"].ToString();
if(seq == string.Empty) { Response.Redirect("list.aspx"); Response.End(); } else { ViewState["seq"] = seq; }
getContent(); } else { seq = ViewState["seq"].ToString(); } } |
엇따??? 예상보다 코드가 길다구요?? 그렇습니다. 조금 깁니다. 하지만, 알고보면 그리 긴 코드도 아니랍니다. 그렇다면, 한번 살펴볼까요?
아참. 그 전에 Page_Load 이벤트 처리기 바로 윗쪽에 있는 코드를 잠시 보고 넘어가겠습니다. 뭐~ 대단한 것은 아니구요. 몇몇 변수들을 전역적으로 선언해두고 있는 것일 뿐입니다. 그러한 변수에는 seq 변수를 비롯하여, SqlConnection, SqlCommand, 데이터베이스 연결문자열 정도가 있습니다. ^^;
우선 코드는 예상대로 페이지가 처음 로드되는 경우(!IsPostBack)에, Request.Params 컬렉션을 사용하여 seq 값을 받아오고 있습니다. 일단, 그 값을 가져오기 위해서, Request.Params["seq"] 가 null 값이 아닌지 여부를 검사하고 있구요. 만일 null이 아니라면 그 값을 seq 라는 클래스 변수에 넣구 있습니다.
Edit.aspx 페이지는 특성상 이 seq 값이 없으면, 오류가 날 수 밖에 없는 페이지이니까요.(그 값이 없으면 어떤 글을 수정해야 하는지 알수가 없잖아요???) 만일, 그 값이 없다면 list.aspx 페이지로 이동하게 합니다. 정상적인 루트를 통해서 Edit.aspx 페이지에 도달했다면 그 seq 값이 없을리가 없으니깐... 만일, 이 값이 없다면 뭔가 꽁수를 부려서 Edit.aspx 페이지로 들어온 거겠죠? 그렇다면, 인정사정 볼 것 없이 반사~~!!!! 하는 것입지요. 하하하... 오래된 개그입니다. 반사~~~ 짜잔 (-_-)/
해서, 코드에서는 seq == string.Empty 비교를 통해서, 그 값이 빈 값인지 여부를 검사하고, 만일 그렇다면 list.aspx 페이지로 Redirect 시키고 있습니다. 재미있는 것은 그 비교 IF 문의 else 구역인데요. 거기서는 현재의 seq 값을 뷰상태(Viewstate)에 저장하고 있는 것을 보실 수 있을 겁니다.
왜 그래야 하는 걸까요?? 그것은 매우 간단합니다. 이 값이 포스트백(Postback) 시에 필요하기 때문이지요. 실제로 사용자가 글을 수정한 다음 Save 버튼을 누를 경우, 데이터는 실제로 데이터베이스에 업데이트 될 것인데요. 그 경우, 바로 포스트백이 일어나게 되지 않습니까요?? 근데, seq 값을 이렇게 뷰상태에 넣어두지 않으면, 포스트백시에는 현재의 seq 값이 사라져버리게 되어서리... 문제가 생길 수 있게 되기에, 이렇게 한 것입니다.
물론, 반드시 뷰상태를 사용해야 하는 것은 아닙니다. 글 수정 후, Save 버튼을 클릭할 때, 다른 방식으로 seq 값을 전송할 수도 있을 것입니다.(예를 들면, get 방식을 통해서나 URL 쿼리스트링으로 전달하거나, 폼의 hidden 컨트롤에 그 값을 넣어서 폼 전송 시 함께 전송하거나 할 수 있습죠) 하지만, 뷰상태를 사용하는 방법이 여러면에서 제일 간단해 보여서 저는 이 방법을 선택한 것 뿐입니다. ^^
그리고, 코드에서 보면 getContent() 라는 요상한 함수를 호출하고 있네요!!! 이 함수는 곧 알아볼 함수입니다만... 역할은 매우 간단합니다. 즉, 현재 seq 값을 가지고 데이터베이스를 호출해서 그 글에 해당하는 모든 컬럼들을 가져와서 TextBox 컨트롤들에 출력해 주는 것이지요. 그래야, 사용자가 자신이 기존에 쓴 글을 수정할 수 있을테니까요. 혹시 기억나십니까? Content.aspx 페이지에도 이러한 함수가 있었습니다. 역할도 거의 비슷했지요. 물론, 그 경우는 데이터베이스로부터 가져온 데이터를 Textbox가 아닌 Label에 출력했지만 말입니다. 단지, 출력을 어디에다가 하느냐만을 제외한다면, 이 함수의 역할과 코드는 동일합니다.
이제 남은 코드는 무엇인가여?? 남은 부분은 if 문에서의 else 부분입니다. 말로 설명하자면, 페이지가 처음 로드되는 경우가 아닌 경우, 그러니깐, 즉, 포스트백이 일어나는 경우에 실행될 코드입지요. 다시 한번 강조하지만, Edit.aspx 페이지는 페이지가 처음 로드되던, 포스트백으로 로드되던 무조건 seq 값이 있어야만 합니다. 그 값이 없으면 Edit.aspx 페이지는 아무런 일도 수행할 수 없습니다. 그 값이 없으면 Edit.aspx 페이지는 정말 정말 슬픔에 목이 잠겨 애처로이 울 수 밖에 없다는 것입니다. 엉엉엉 말입니다.
이전의 설명을 통해서 기억하고 계시겠지만, 페이지가 처음 로드되는 경우 우리는 Seq 값을 뷰 상태에 저장해 두었습니다.(바로 이전 코드 참조!!!) 그러므로, 포스트백 시에는 아주 당연하게 그 값을 가져올 수 있을 것이구요. 해서, 요 부분의 코드는 그 뷰상태 값을 seq 변수에 다시 지정하고 있는 것이랍니다. ^^
자. 여기까지의 코드를 통해서 우리가 확실하게 한 부분은 Edit.aspx 페이지는 페이지가 로드되는 경우(처음 로드되던, 포스트백된 것이던), 확실하게 seq 값을 얻을 수 있다는 것입니다. 그리고, 만일 그 값이 얻어지지 않는다면 list.aspx 페이지로 반사!!! 된다는 것!! 그렇습니다. 그렇게 되도록 코드를 작성한 것입니다.
이제 다음 단계는 무엇일까요? 역시나 현명하신 여러분들은 다음 단계가 무엇인지 알고 있습니다. 그것은 바로... getContent() 함수를 작성하는 것이지요. 즉, 얻어온 seq 값을 가지고 데이터베이스에 쿼리를 날려 현재 글에 대한 데이터로 Textbox들을 채우는 것입니다. 그렇다면, 한번 해볼까요? 참고로 이 코드는 이미 말씀드렸다시피, Content.aspx 페이지의 getContent() 함수와 거의 동일합니다. (어느 정도로 동일하냐면 말입니다. 저장프로시저도 같은 것을 사용하고 있습니다. 우핫핫 -_-;;;)
private void getContent() { Con = new SqlConnection(CONNECTSTR); Cmd = new SqlCommand(); Cmd.Connection = Con;
Cmd.CommandType = CommandType.StoredProcedure;
Cmd.Parameters.Add("@seq", SqlDbType.Int); Cmd.Parameters["@seq"].Value = seq;
try { Con.Open();
//레코드를 읽어오는 프로시저로 지정 Cmd.CommandText = "UP_SELECT_BOARDCONTENT";
SqlDataReader reader = Cmd.ExecuteReader(CommandBehavior.CloseConnection); if(reader.Read()) { //seq, thread, depth, writer, email, title, mode, ip, readcount, transdate, content name.Text = reader[3].ToString(); mail.Text = reader[4].ToString(); title.Text = reader[5].ToString(); bool mode = (bool)reader[6]; if(mode) UseHTML.Checked = true;
content.Text = reader[10].ToString(); } else { string script = "<script>alert('글이 존재하지 않습니다'); history.back();</script>"; Page.RegisterClientScriptBlock("done", script); }
reader.Close(); } catch(Exception ex) { lblError.Text = "ERROR : " + ex.Source + " - " + ex.Message; lblError.Visible = true; }
if(Con.State == ConnectionState.Open) Con.Close();
Cmd = null; Con = null;
} |
그렇습니다. 코드를 보니 딱!! 기억이 나시죠??? 아마도 Content.aspx 페이지에서 사용했던 코드와 거의 동일하다는 것을 금새 눈치채실 수 있을 것입니다. 하하하...
이 함수에서 사용하고 있는 프로시저도 이전 Content.aspx 에서 사용했던 프로시저를 그대로 사용하고 있는데요. 사실, 프로시저는 새로이 작성되는 것이 좋습니다. 왜냐하면, 이번 함수에서 필요한 컬럼들은 단지, name, email, title, mode, content 정도 뿐이니까요. 다시 말해서, UP_SELECT_BOARDCONTENT라는 저장 프로시저가 반환하는 컬럼들은 현재 함수에 필요한 것 이상으로 많기 때문입니다. 사실, UP_SELECT_BOARDCONTENT 프로시저는 글의 본문내용을 가져오기 위해서 준비된 것이지, 이번 작업을 위해서 특별히 준비된 것은 아니거든요. 하지만, 같이 사용해도 큰 문제는 없기에 여기서는 공유해서 사용하고 있는 것일 뿐, 좀 더 성능을 좋게 하기 위해서는 이번 작업에 딱 맞는 저장 프로시저를 별도로 만드는 것이 바람직할 것입니다. 자~~ 그 부분은 누가 만들어봐야 할까염? 그렇습니다. 여러분입지여~~ 하하
여기까지 OK 입니다.
일단, 여러 준비를 해보았으니까요. 이제... 일단 페이지를 실행해 보도록 합시다. 페이지를 실행하라고 했다고 해서 무조건 Edit.aspx 페이지를 실행하면 안되겠죠? Edit.aspx 페이지는 자기 혼자서 동작하는 페이지가 아니라, Content.aspx 페이지로부터 기인하는 페이지 이니까요. 그러니깐, 여러분은 일단 list.aspx 페이지를 시작으로 하여 Edit.aspx 까지 진행해와야 할 것입니다. 물론, Edit.aspx 페이지를 바로 접근해도 상관은 없습니다. 그런 경우, 자동으로 여러분은 list.aspx 페이지로 이동하도록 코드가 제작되어져 있으니까요. 하핫.

이렇게 list.aspx 페이지로 오셨다면, 이제 맘에 드는 글을 클릭해서, Content.aspx 로 이동합니다. 혹은, 새로운 글을 하나 입력하고 그 글의 본문으로 갑니다. (엇? 왜 이렇게 구체적으로 설명하고 있는 것이지? 제가 왜 이러는 것일까염??? 오랜만에 쓰는 강좌라 좀 친절이 과한 것은 아닌가 하는... -_-;)

이제, 비밀번호를 입력하고 Edit 버튼을 눌러야 하겠져??? 그러면, 당연한 이야기지만 비밀번호가 일치할 경우에만 Edit.aspx 페이지로 이동하게 될 것이구요~~ 그 Edit.aspx 페이지의 모습은 다음과 같을 것입니다.

그쵸? 그쵸??? 그렇습니다. 현재 글의 내용도 잘 나오고 있고, 사용자가 글을 수정할 수 있는 모든 준비가 완성된 상태인 것이지요. 아주 맘에 드네요.... 좋습니다.
이제 우리가 해야할 일은 무엇일까요? 기렇습니다~~~~ 사용자가 모든 수정을 마치고, "Save"를 누를 경우에 실제로 현재 변경된 내용들을 데이터베이스로 UPDATE 하는 것이 되겠습니다.
그렇다면, 웹 폼에서 Save 이미지를 따부루 클릭 하여~~~ 코드 비하인드에 그러한 코드를 작성해야 하겠죠? 그렇습니다. 하지만, 그 전에 먼저.... 데이터베이스에 저장 프로시저를 하나 만들어 주어야 할 것입니다. 해당 글을 업데이트 하는 프로시저를 말입지요. 다들 쉽게 작성하실 수 있을 것이라 믿어 의심치 않지만서두.. 그 SP를 보여드리면 다음과 같습니다.
CREATE PROC dbo.UP_UPDATE_BOARD @seq int, @Email Varchar(100), @Title Varchar(100), @Mode Bit, @Ip Varchar(15), @Content Text AS SET NOCOUNT ON
UPDATE ThreadBoard SET email= @Email, title= @Title, mode= @Mode, ip= @Ip, content= @Content WHERE seq = @seq
GO |
프로시저는 이와 같습니다. 실제로 업데이트될 필요가 있는 컬럼들은 email, title, mode, ip(사실, ip를 업데이트할 필요가 있나 하는 부분은 주관적이겠습니다만...), content 이니까요. 그 컬럼들만 업데이트 하고 있는 것이죠.
이제, 프로시저도 준비되었겠다. 코드 비하인드에 PostButton_Click 이벤트 처리기를 작성해 볼까요? 이 작업은 여러분이 기존에 했던 작업들과 무지하게 유사한 작업인지라.. (사실, 어케보면 insert.aspx에서의 작업과도 유사하죠) 크게 어려울 것이 없을 겁니다.
private void PostButton_Click(object sender, System.Web.UI.ImageClickEventArgs e) { Con = new SqlConnection(CONNECTSTR); Cmd = new SqlCommand("UP_UPDATE_BOARD", Con); Cmd.CommandType = CommandType.StoredProcedure;
Cmd.Parameters.Add("@seq", SqlDbType.Int); Cmd.Parameters.Add("@Email", SqlDbType.VarChar, 100); Cmd.Parameters.Add("@Title", SqlDbType.VarChar, 100); Cmd.Parameters.Add("@Mode", SqlDbType.Bit); Cmd.Parameters.Add("@Ip", SqlDbType.VarChar, 15); Cmd.Parameters.Add("@Content", SqlDbType.Text);
Cmd.Parameters["@seq"].Value = seq; Cmd.Parameters["@Email"].Value = mail.Text; Cmd.Parameters["@Title"].Value = title.Text; Cmd.Parameters["@Mode"].Value = (UseHTML.Checked == true ? 1 : 0); Cmd.Parameters["@Ip"].Value = Request.UserHostAddress; Cmd.Parameters["@Content"].Value = content.Text;
string script;
try { Con.Open(); Cmd.ExecuteNonQuery(); Con.Close();
script = "<script>alert('변경되었습니다');location.href='Content.aspx?seq={0}';</script>"; script = string.Format(script, seq); Page.RegisterClientScriptBlock("done", script); } catch(Exception ex) { lblError.Text = "ERROR : " + ex.Source + " - " + ex.Message; lblError.Visible = true; }
if(Con.State == ConnectionState.Open) Con.Close();
Cmd = null; Con = null; } |
단지, 기존 코드와 약간 다른 부분이라면... 코드중에 파란색으로 표시된 부분, 즉, string.Format() 메서드일 것입니다. 바로 이 부분이지요
script = "<script>alert('변경되었습니다');location.href='Content.aspx?seq={0}';</script>";
script = string.Format(script, seq);
뭔가 기대하는 눈빛들이 여실한데요.... 기대하지 마세염. 그렇게 대단한 부분은 아닙니다. 단지, string 클래스의 Format 이라는 메서드가 문자열을 쉽게 포맷팅할 수 있게 도와준다는 것을 거론하고 싶었을 뿐이니까요. Format 메서드는 문자열에서 {0} 이나 {1} 과 같은 위치지정자를 여러분이 지정한 인자로 바꿔치기 해주는 역할을 합니다. 간단하게 예를 들자면,
string str = "저는 그레이트한 {0} 인, {1} 입니다";
str = string.Format(str, "초보", "태오");
이렇게 코드가 작성되면, string.Format 메서드의 두번째 인자는 기존 문자열에서의 {0} 부분을, 세번째 인자는 {1} 부분을 대체하게 되는 것이죠~~ 쉽죠?
우리의 소스에서는 데이터베이스로의 UPDATE 후에, 사용자에게 변경된 내용을 보여주기 위해서 Content.aspx 페이지로 이동하게 하고 있는데요. 이동 시, 반드시 seq 값을 넘겨주어야 하기에 이러한 방법을 써 보았습니다. 물론, 기존 ASP에서 했던 방식으로 문자열을 마구 복잡하게 결합해서도 같은 결과를 얻을 수 있긴 합니다만... 이 방법이 더 나은 방법이 아닌가 싶은 생각에 한번 소개해 보았어요~~~
자. 이제 코드도 완성되었으니, 한번 실행해 보도록 해요~ 실행을 하면.... 우리의 기대대로 글의 수정은 완변하게 동작할 것입니다. 수정 후에는 "변경되었습니다" 라는 메시지박스가 뜨면서 Content.aspx 페이지로 자동 이동하기도 하구 말이져~~~ ㅋㅋㅋ

그렇습니다.
그렇다면, 이제 완성된 것이냐???? -_-;
그게 그렇지가 않습니다. 이 게시판은 이제야 이야기지만... 사실, 큰 문제가 하나 있습니다. 현재의 구조 상 바로 Edit 시에 아주 큰 보안적인 문제가 있다는 것이죠.
이는 ASP.NET의 문제가 아닌 웹이라는 환경의 비연결 구조로 인한 문제인데요. 여러 페이지에 걸쳐서 하나의 작업이 완료되어야 하는 경우(이러한 하나의 작업 단위를 트랜잭션이라 합니다), 사용자가 그 작업의 중간에 끼어들 수 있어서는 안 되는데, 웹 페이지에서는 그게 가능할 수도 있기 때문에 가끔 문제가 생길 수 있다는 겁니다. 특별한 처리를 해 놓지 않는 한, 웹 상에 존재하는 모든 페이지들은 사용자의 직접적인 요청(Request)이 가능하므로, 영악한 사용자들이 어떤 흐름의 중간에 끼어들 가능성은 여전히 존재합니다. 그리고, 그런 시도를 하는 사용자들은 결코 마음 착하게 구경만하고 지나가지는 않지요.(물론, 간혹 착한 이들도 있기는 합니다만.... -_-++)
예를 들어, 하나의 작업이 A.aspx -> B.aspx -> C.aspx 와 같이 진행되어야 하는데요. 이는 서버측의 처리구조가 그렇게 되어져 있는 것이고, 일반적인 사용자들은 A.aspx 페이지를 요청하면, C.aspx 페이지를 결과로 받아보게 된다고 가정해 봅시당. 그렇담, 사용자들은 B.aspx 라는 페이지가 존재하는지 조차 알 수 없으며, 그렇기에 그러한 페이지를 직접적으로 접근하는 경우는 극히 드물 것입니다.
하지만, 우연히 B.aspx 라는 페이지가 존재한다는 사실을 알았다면? 그렇다면, 어떤 사용자는 B.aspx 페이지로의 접근을 시도할 수 있을 것이며, 운이 좋은 경우(!) 서버를 대상으로 장난을 칠 수도 있겠죠?? 오오~~ 지금 얼굴 표정 딱!!!! 보였어.... "뜨끔" 한 사람들... 오호... 거기... 딱 걸렸어... 어랍쇼~~ 괜히 시선 피하지 말아여... 다 보이니깐... -_-++ 케케케...
제가 갑자기 이러한 이야기를 꺼내는 이유는 무엇일까요? 우리의 게시판이 위에서 이야기한 것과 같은 어떤 유사한 문제점을 가지고 있기 때문입니다. 그것도!!! Edit.aspx 페이지에서 말입니다. 그렇다면, 그 문제점을 직접 테스트를 통해서 한번 확인해 보도록 할까요???
List.aspx 페이지를 실행하고, 목록 중에서 여러분이 작성한 글을 클릭하여, Content.aspx 페이지로 이동해 보도록 하세요. 그리고, 상세 정보 페이지인 Content.aspx 페이지에서 비밀번호를 올바르게 입력한 다음 "Edit" 버튼을 클릭하여 Edit.aspx 페이지로 진행해 보자 이겁니다!!!! 제대로 진행했다면, 분명~~ 기존 글을 편집할 수 있는 Edit.aspx 페이지를 다음과 같이 보게 되겠죠???? 여기서 주목할 부분은 주소창에서 보여지는 부분입니다!!! 주목!!

Edit.aspx 페이지는 비밀번호를 제대로 기입한 다음에야 만나볼 수 있는 페이지라는 것은 기억하시죠?? 진짜??? 그렇게 믿고 있으시겠지만... 사실, 반드시 그런 것만도 아닙니다. 여기에 우리의 게시판의 허점이 있는 것이지요. 현재 브라우저의 URL 창을 보도록 하세요. 위의 그림에서 표시한 것처럼, 그 부분은 다음과 같이 나타나고 있을 겁니다.
http://localhost/AspNetTest/ThreadBoard/Edit.aspx?seq=19
Content.aspx 페이지에서 비밀번호를 검증한 다음, Edit.aspx 페이지로 이동하면서 현재 글의 seq 컬럼 값을 같이 넘기므로, 위의 결과는 당연한 것이긴 합니다. 하지만, 만일 이 URL 뒷 부분의 seq= 19 이라는 값을 우리가 직접 다른 값으로 바꾸어 요청한다면 어떻게 될까요? 예를 들어, 어떤 다른 사람이 올린 글이 있고, 그 글의 seq 값이 18 이라는 것을 알고 있다면, 그 값을 이용해서 이러한 요청을 직접적으로 한다면 과연 어떻게 될까요? 한번 시도해 보세요.
저의 경우는 다른 사람이 작성한 글의 seq 값을 18이라고 가정하였지만, 여러분은 자신이 가진 데이터에 따라 적절한 값을 사용해야 할 것입니다. 이미 ThreadBoard 테이블에 존재하고 있는 어떤 값으로 말이지요.

오~~ 마이뉴스~~~ 세상에 이런일이... -_-+;;; 그렇습니다. 위의 그림과 같이 비밀번호를 검증하는 작업을 피해서, Edit.aspx 페이지를 접근할 수 있으며, 다른 사람이 작성한 글을 내 마음대로 수정할 수가 있게 되는 것입니다. 오옷~ 어쩌다 이런 일이!!!
그렇다면, 이러한 문제가 발생하는 원인은 무엇일까요? 그 원인부터 파악을 해보도록 하죠. 일단, 이러한 문제의 근본적인 원인은 사용자가 입력한 비밀번호의 체크가 Content.aspx 페이지에서만 수행되고, Edit.aspx 페이지에서는 수행되지 않는다는 데에 있습니다. 즉, 사용자가 Content.aspx 페이지에서 비밀번호를 제대로 입력하지 않는다면, Edit.aspx 페이지로는 접근조차 할 수 없을 것이다라고 너무 맹신하고 있었던 것이 문제라는 것이지요.
이를 해결하기 위해서 여러분은 Edit.aspx 페이지에서 다시금 비밀번호를 검사할 수도 있겠지만, 그렇게 되면 사용자는 상당히 불편해 할 것입니다. 글을 편집하기 위해서 두 번이나 비밀번호를 입력해야 한다는 것은 별로 달갑지 않은 작업일 테니 말입니다. 만일 게시판을 그렇게 만들었다면, 이미 제 [받은 편지함]은 항의의 글로 가득찼을지도 모릅니다. -_-++
해서, 문제의 해결을 그런 식으로 접근해서는 안될 것입니다. 그렇다면, 어떤 식으로 풀어야 할까요오오~~? 근본적으로 생각해 보면, 사용자가 Edit.aspx 페이지에 직접적으로 접근할 수 있다는 것이 문제인 것은 분명합니다. Edit.aspx 페이지에 접근이 가능한 사용자는 오직 비밀번호 검증에 통과한 사용자여야 하는데, 지금은 누구나 접근할 수 있기 때문에 문제가 생긴 것이지요....
그렇다면, 비밀번호 검증에 통과한 사용자만이 Edit.aspx 페이지를 접근할 수 있게 하고, 그렇지 않은 사용자는 Edit.aspx 페이지에 접근하지 못하게 만든다면, 간단하게 이 문제는 해결될 수 있지 않을까요? 그렇습니다. 그런 식으로 진행하는 것이 어쩌면 가장 바람직할 것입이다. 그러나, 그 방법은 각각의 사용자를 구분할 수 없는 웹 사이트(즉, 사용자 인증이 없는 사이트)에서는 예상보다 구현이 까다로운 편입니다. 사용자가 비밀번호 검사를 통과했다면, 어떤 글에 대한 비밀번호 검사를 통과했는지를 알아내야 할 필요도 있고, 그 글과 현재 Edit.aspx 페이지의 글이 같은 글인지도 검사해야 할 테니 말입이다. 아아.. 뭔가 마구 복잡해지는 느낌이네요... 하지만, 걱정마십시요.. 여기서 그 방법을 사용할 것은 아니니까요~
해서, 태오는 여기서 다른 방법을 하나 제시하고자 하는데, 그것은 바로 Taeyo's ASP.NET v1.0에 상세히~ 나와있습니다. 이미 베스트셀러에 이어 스테디셀러에 들어선 태오의 명강의 Taeyo's ASP.NET v1.0!! 현재 절찬리 판매중에 있습니다. ^____^
그럼.. 이번 강좌는 여기까지~~ 나머지는 베수투셀러인 Taeyo's ASP.NET v1.0에게 뒤를 부탁하도록 하겠습니다...
.
.
.
.
라고... 이야기를 하고 이대로 마치면... 당연히.... 저는 집에 갈때.. 어쩌면.. "마니 무었다 아이가~~" 해야 할지도 모르기에... 제가 사용한 방법을 고스란히 알려드리면...
사용자가 현재 페이지 이전에 거쳐 들어온 페이지가 어디인지를 파악해내서 그 결과를 가지고 Edi.t.aspx 페이지로의 접근을 제한하는 방법을 사용해 볼 수 있습니다.
이를 위해서는 Request 개체의 UrlReferrer 속성을 유용하게 사용할 수 있는데요. 이 속성은 현재 페이지 이전에 어떤 페이지를 참조로 하여 현재 페이지로 들어왔는지를 알려주는 역할을 합니다. 예를 들면, 이 속성은 사용자가 Content.aspx 페이지에서 링크나 버튼을 눌러 Edit.aspx 페이지로 이동해 왔다면 그 사용자의 이전 참조 페이지(UrlReferrer)가 Content.aspx 라고 나타나지만, 만일 사용자가 브라우저의 URL 창에 직접 Edit.aspx 경로를 기입하여 접근했다면 이전 참조 페이지(referrer 페이지)의 값이 나타나지 않게 됩니다. 다시 말해, 반드시 링크나 폼 전송을 통해서 현재 페이지에 접근한 경우에만 Request.UrlReferrer 값이 존재한다는 의미이지요..
그러므로, 이 값을 이용해서 사용자가 Content.aspx 페이지로부터 넘어온 사람인지 아니면 직접 Edit.aspx 페이지로 뛰어든 사람인지를 구분해 낼 수가 있다는 이야기이고, 직접 뛰어든 영악한 사용자의 경우는 접근을 불가능하게 할 수 있다는 이야기인 것입다. 이해가 잘 가지 않는다면, 직접 눈으로 확인해 보도록 하지요~. 먼저, Edit.aspx 페이지의 Page_Load 구역에 제일 하단에.. 다음과 같은 코드를 추가해 보도록 하세요.
private void Page_Load(object sender, System.EventArgs e) { // 여기에 사용자 코드를 배치하여 페이지를 초기화합니다. if(!IsPostBack) { //...중략... } else { seq = ViewState["seq"].ToString(); }
lblError.Visible = true; if(Request.UrlReferrer != null) lblError.Text = Request.UrlReferrer.ToString(); else lblError.Text = "값이 없습니다";
} |
코드는 Request.UrlReferrer 값을 확인해서, 그 값이 있다면 Label 컨트롤에 그 경로를 출력하고, 만일, 값이 없다면 Label에 값이 없다는 메시지를 출력하는 간단한 코드입니다. 테스트의 의미로 이렇게 작성하고, 페이지를 실행해 보도록 하자구요~~~ 자. 그러면 이제 한번 Edit.aspx 페이지로 순차적으로 접근해 보도록 합시당~ 제대로 된 순서를 따라 Edit.aspx 페이지로 왔을 경우에는 다음과 같이 Contentdit.aspx 페이지의 완전한 URL 경로가 Label에 출력되어 나타날 것입니다.

이번에는 URL 창에 Seq 변수의 값을 적절한 값으로 직접 바꾸어서(아까 했던 것처럼요~) 접근해 보도록 하세요. 저는 seq 값을 18로 주어서 접근하고 있지만, 여러분의 경우는 ThreadBoard에 존재하는 어떤 다른 값을 사용해야만 할 것입니다. 그러면 다음과 같은 결과를 보실 수 있을 겁니다.

"값이 없습니다"라는 문자열이 출력되고 있다는 것이 중요한 부분입니다. 이와 같이 페이지를 직접적으로 접근했을 경우에는 Request.UrlReferrer 값이 존재하지 않기 때문이지요. 그러므로, 위의 사실을 기반으로 여러분은 Request.UrlReferrer 값을 검사하여 그 값이 존재할 경우에만 Edit.aspx 페이지를 접근할 수 있도록 하고, 그 값이 없다면 List.aspx 페이지로 자동으로 이동하게 할 수 있는 것입니다. 그렇다면, 코드를 그렇게 한번 바꾸어 보도록 할까요? (좀 전에 테스트를 목적으로 작성한 코드는 모두 지워주셔요~)
private void Page_Load(object sender, System.EventArgs e) { if(Request.UrlReferrer == null) Response.Redirect("List.aspx"); else { string refer = Request.UrlReferrer.ToString(); if(refer.IndexOf("http://localhost/AspNetTest/ThreadBoard") == -1) Response.Redirect("List.aspx"); }
// 여기에 사용자 코드를 배치하여 페이지를 초기화합니다. if(!IsPostBack) { //...중략... } else { seq = ViewState["seq"].ToString(); } } |
위의 코드는 사용자가 Edit.aspx 페이지를 직접 접근하는 경우, List.aspx 페이지로 자동 분기시키고 있구요. 사용자가 이전에 어떠한 페이지를 거쳐서 들어왔다 하더라도, 그 경로가 실제 서버에서의 경로인 http://localhost/TaeyoAspNet/ThreadBoard를 포함하고 있지 않다면, 마찬가지로 List.aspx 페이지로 분기시키고 있습니다. (기왕 접근을 막는 것. 아주 확실하게 막고 있는 것이지요~~)
이를 위해서는 string 클래스의 IndexOf 메서드를 사용하고 있는데요. 이 메서드는 현재의 문자 텍스트 내에서 인자로 지정된 값이 나타나는 위치값(integer)를 알려주는 역할을 합니다. 인자로 지정된 문자 값이 텍스트 내에 존재한다면 그 서수 위치값을 리턴하구요. 만일 지정된 문자열이 텍스트내에 존재하지 않을 경우에는 -1 을 리턴합니다. 그러므로, 그 값이 -1일 경우에는 사용자가 엉뚱한 페이지로부터 Edit.aspx 페이지로 들어온 것이기에 List.aspx 페이지로 분기시키면 되는 것이지요.
소스에서는 URL 경로의 값으로 http://localhost/TaeyoAspNet/ThreadBoard 를 직접적으로 사용하고 있지만 말입니다. 여러분이 이 소스를 다른 곳에 배포하는 경우에는 그 경로가 달라지게 될 것이므로, 그러한 경우에는 이 URL 경로도 상황에 맞게 바꾸어 주어야 할 것입니다. 예를 들어, 여러분이 이 게시판 소스를 http://www.taeyo.net/Board라는 곳에 배포하였다고 가정하면, 위의 코드를 다음과 같이 바꾸어 주어야 한다는 것이지요.
string refer = Request.UrlReferrer.ToString();
if(refer.IndexOf("http://www.taeyo.net/Board") == -1)
Response.Redirect("List.aspx");
하지만, 다른 사이트에 게시판 소스를 배포할 때마다 이러한 경로명을 바꾸어 주어야 한다면, 이는 꽤나 번거로운 작업이 될 것임에 틀림이 없습니다. 해서, 이 부분을 자동으로 얻어내는 코드를 사용하는 것이 더욱 효과적일텐데요, 그러한 코드는 다음과 같습니다. ^^
string refer = Request.UrlReferrer.ToString();
string ServerPath = Request.Url.ToString();
ServerPath = ServerPath.Substring(0, ServerPath.LastIndexOf("/");
if(refer.IndexOf(ServerPath) == -1)
Response.Redirect("List.aspx");
이 코드는 여러분의 파일이 어느 사이트의 어느 위치에 놓이던지 간에, 자신의 디렉터리에 존재하는 Content.aspx 페이지의 경로를 알아내어, 그를 가지고 Request.UrlReferrer과의 비교를 시도할 겁니다. 그리고, 기왕 이렇게 만든 김에... 이러한 부분의 코드를 따로 떼어내어서 별도의 함수로 만들면, 기존의 코드는 더욱 깔끔해보일 수 있겠네요. 해서, 총 정리해 본... Page_Load 이벤트 처리기의 코드는 다음과 같습니다. 참고하세염~
private void Page_Load(object sender, System.EventArgs e) { CheckReferrer();
if(!IsPostBack) { //...중략... } else { seq = ViewState["seq"].ToString(); } }
public void CheckReferrer() { if(Request.UrlReferrer == null) Response.Redirect("list.aspx"); else { string UrlReferrer = Request.UrlReferrer.ToString(); string ServerUrl = Request.Url.ToString(); ServerUrl = ServerUrl.Substring(0, ServerUrl.LastIndexOf("/")); if(UrlReferrer.IndexOf(ServerUrl) == -1) Response.Redirect("list.aspx"); } } |
이제 모든 코드가 완성되었으니 종합적으로 테스트를 해보도록 하세요. 이제는 비밀번호의 입력 없이 직접적으로 Edit.aspx 페이지로 접근하는 것은 불가능할 것이고, 결코 다른 사람이 작성한 글을 편집할 수도 없을 것입니다.
해서, 이것으로 계층형 게시판의 1차 부분은 일단 완료가 된 것 같네요. 다음 강좌부터 올라오는 2차 부분이 실제 계층형 게시판의 본격적인 부분이 될텐데요.. 그 부분은 답변을 다는 부분과, list.aspx에서의 페이징 기능이 될 것입니다. 그리고, 3차에 들어서면... 이제 이 게시판을 모듈별로 컴포넌트화 시켜서리.... 좀 더 개선하는 작업을 하게 될 것이구요..
앞으로는 배신 안 때리구... 꾸준히 다시 예전처럼 강좌를 올릴 것이니까여~~ 기대해 주시기 바랍니다. 만에 하나, 제가 강좌를 올릴 수 없는 불행한 사태(?)가 발생하게 된다고 하더라도... 이 강좌만큼은 어떻게든 마무리를 지을 예정이니까요... 힘을 조금만 주셨으면 합니다. 힘 주는 방법이야 뭐 있겠습니까?? 많이 읽어주시는 것이지요~~~ ^^;;
그럼 다음주에 또 뵈어요~~~ 좋은 하루 되세요~~