작성자 : 한동훈 제목 : 효율적인웹응용프로그램구축 들어가기두번째섹션에서는먼저데이터액세스계층 (DAL: Data Access Layer) 작성에대한이야기를했습니다. 따라서데이터액세스계층에대해서먼저살펴본다음에 ASP.NET을기준으로 'Everything in one page(or method)' 안티패턴에대해서얘기하며이를풀기위해 MVC(Model-View-Controller) 패턴과이패턴의다양한변형들에대해서살펴보도록하겠습니다. 마지막으로는캐싱에대해서살펴볼것입니다. Section 2. 웹응용프로그램작성및설계 2.1 데이터액세스계층데이터액세스응용프로그램에서흔히나타나는실수들중에는기본키 (Primary Key) 값만전달해주면특정레코드를가져오거나특정필드의값을가져오는함수, 값을비교해서다양한처리를하는함수, 특정테이블에데이터를업데이트하는함수, 추가하는함수를작성하는것입니다. 이러한종류의함수는해당응용프로그램에서해당하는작업을반복하는것을피하는데도움을줄수있으나재사용성이떨어지며추상화가제대로이뤄진것이아니며자신이하는작업의공통된부분이무엇인지이해하지못한것입니다. 어떤테이블에액세스하는가, 어떤필드에액세스하는가하는것은중요하지않습니다. 중요한것, 여러분이파악해야할것은이들작업들의공통점입니다. 데이터를액세스하는응용프로그램의작업유형은다음과같이나눌수있습니다. 1. 질의를수행해서한필드의값을가져오는경우 2. 질의에해당하는레코드가몇건이나존재하는가여부. 3. 질의를수행해서하나의레코드를반환하는경우. ex. 각종조회화면들 4. 질의를수행해서여러개의레코드를반환하는경우. 5. 질의를수행하고성공여부만을반환하는경우. ex. 각종 UPDATE, INSERT, DELETE 질의 처음부터이러한다섯가지를분류해내는것은쉬운일이아니며개발자에따라더세분화하는분들도 있을것입니다. 그러나 DB 설계와마찬가지로응용프로그램작성에있어도절대적인원칙이나답은 없습니다. 다만이런방법도있구나라고생각했으면합니다. 세상은상대성과절대성이있으며사람수만큼다양한생각이존재한다는것을인정하는마음은상 대성이라생각합니다. 프랑스의브리지트바르도의한국고유문화비판이나미국의이라크비판은
자신의척도를제일로삼는문화절대주의이며, 서로이해하려는자세가부족한것이라생각합니다. 2.1.1 PHP 버전 PHP 웹응용프로그램에서는두개의파일을먼저작성합니다. 첫번째는데이터베이스연결정보나 응용프로그램정보를저장하기위한것으로다음과같습니다. 예제 : settings.inc.php // database hostname $ma_config['dbhost'] = 'localhost'; // database user name $ma_config['dbuser'] = 'mona'; // database user password $ma_config['dbpassword'] = 'monauser'; // database name $ma_config['dbname'] = 'mona'; define(disable_attachment, 0); define(enable_attachment, 1); 예제에서각설정정보를 $dbhost와같은변수의형태가아닌키와값으로된해쉬테이블형태로저장하는것은 URL을통한변조를예방하기위한것입니다. URL을통한변조예방은 $_GET, $_POST를사용하는방법과 php.ini 설정을변경하는방법이있으며이에대한것은관련도서를참고하시기바랍니다.($_GET, $_POST는 PHP 4.2 이상에서만제공됩니다 ) 두번째로흥미있는부분은 define으로정의된부분인데여기서는 ma_configuration.mcf_ispds 열에서사용하는 0과 1이라는값을정의한것입니다. 숫자에의미를부여하는매직넘버는종종코드를읽기어렵게만들고프로그래머도각숫자의의미를쉽게혼동하여프로그래밍하는실수를하게됩니다. 이러한버그는찾기도어렵습니다. 보다읽기쉽게하고, 실수를줄이기위해매직넘버 (Magic Number - 숫자를이용한해결책 ) 에대한상수를정의합니다. 다음은실제데이터베이스에액세스하는함수들을다룰차례입니다. 이들은 database-mysql.inc.php 로작성된것이며전체가아닌일부만소개해드리겠습니다.
먼저연결을얻는부분은다음과같습니다. 연결은데이터연결 (Data Connection) 을의미하는 $dc 에 저장하며이는전역변수로사용하고있습니다. $dc = mm_openconnection(); // open db connection function mm_openconnection() global $mm_config; $dc = @mysql_connect ($mm_config['dbhost'], $mm_config['dbuser'], $mm_config['dbpassword']); if (@mysql_select_db ($mm_config['dbname'], $dc)) return $dc; 위와같이연결을얻기때문에 settings.inc.php와 database-mysql.inc.php를 include 문을사용하여차례대로코드에포함시키는것으로데이터베이스연결을얻게됩니다. 모든함수는 @ 로시작하는데이는에러가발생할경우어떠한메시지도출력하지않도록하는것입니다. 나중에어떻게에러메시지를출력하고디버깅할수있는지살펴보도록하겠습니다. 데이터베이스액세스프로그램의유형중에첫번째부터살펴보겠습니다. 2.1.1.1. 질의를수행해서한필드의값을가져오는경우 이에대한함수는다음과같습니다. function mm_queryvalue($query) global $dc; $query = stripslashes($query); $rows = @mysql_query($query, $dc); $row = @mysql_fetch_array($rows); return $row[0];
위질의를수행하는예는다음과같다. $name = mm_queryvalue("select Name FROM user where id = 'mona'"); echo $name; mm_queryvalue와같은함수에서특정레코드가존재하지않는경우반환값은 '0' 이되기때문에적절한비교를통해서데이터베이스에해당하는레코드가존재하는지판단할수있습니다. 그러나이런경우는질의에해당하는레코드가몇건이나존재하는가를알려주는함수를먼저사용하는것이좋습니다. 2.1.1.2. 질의에해당하는레코드가몇건이나존재하는가여부. function mm_queryscalar($query) global $dc; $query = stripslashes($query); $rows = @mysql_query($query, $dc); return @mysql_num_rows($rows); 위함수는다음과같은방법으로보다세련되게사용할수있습니다. If(!mm_queryScalar( $query ) ) $name = mm_queryvalue( $query ); echo $name; 세번째는질의를수행해서하나의레코드를반환하는경우로주로게시물을조회하는것과같이어떤 정보를조회하는화면을위해자주사용됩니다.. 네번째는질의를수행해서여러개의레코드를반환하는경우로주로게시물의목록을보여주는것과같이어떤정보에대한목록을보여주기위해자주사용됩니다. 세번째를확장한것이네번째이기때문에세번째와네번째는각각의함수를작성하는대신하나의함수로작성하였습니다. 2.1.1.3. 질의를수행해서하나의레코드를반환하는경우와 2.1.1.4. 질의를수행해서여러개의레
코드를반환하는경우. function mm_getrecordsbyfields($query) global $dc; $query = stripslashes($query); $result = @mysql_query($query, $dc); if ( 0 == $result ) return false; exit; for( $loopctr = 0; $loopctr < @mysql_num_rows($result); $loopctr++ ) $row_array = @mysql_fetch_row($result); for( $ctr = 0; $ctr < @mysql_num_fields($result); $ctr++ ) $field_name = @mysql_field_name($result, $ctr); $data_set[$loopctr][$field_name] = $row_array[$ctr]; // end mm_getrecords return $data_set; 쿼리를실행한결과는 2 차원배열로반환됩니다. 결과집합 $data_set[m][n] 에서 m 은각레코드의번 호이며 n 은각레코드에대한열을가리킵니다. 일반적인사용법은다음과같습니다. define( NL, '<br/>' ); $query = "SELECT ma_id, ma_title FROM ma_articles ORDER BY ma_id DESC LIMIT 1, 20"; if( $rs = mm_getrecordsbyfields( $query ) ): $rscount = count( $rs ); for( $ctr = 0; $ctr < $rscount; $ctr++ ):
echo $rs[$ctr]['ma_id']; echo $rs[$ctr]['ma_title']; echo NL; endfor; endif; 하나의레코드를질의하는경우에는루프를반복하지않고다음과같이쓸수있습니다. <? $number = $rs[0]['ma_id']; $title = $rs[0]['ma_title'];?> <font color=red><?= $number?></font><br> <?= $title?> 이와같은방법을사용하여 PHP 에서도템플릿의도움없이도최대한디자인과 UI 를분리시킬수있습 니다. 물론, $number, $title 과같이중간단계를만들어야하는것은번거롭습니다. mm_getrecordsbyfields와비슷한함수로 mm_getrecords 함수가있는데이함수가반환하는이차배열은모두숫자인덱스를통해서접근합니다. 동일한함수를이와같이작성한이유는템플릿이나다른함수들에서프레젠테이션을모두자동화하기위해서입니다. 다음은쿼리를실행한다음에결과를받을필요가없는경우를위한함수입니다. 이러한함수는질의를수행하고성공여부만을반환하며되며 UPDATE, INSERT, DELETE 질의등이대표적입니다. 2.1.1.5. 질의를수행하고성공여부만을반환하는경우. function mm_querynonresult($query) global $dc; $query = stripslashes($query); @mysql_query($query, $dc); return @mysql_affected_rows($dc); 단순히쿼리를실행하고 @mysql_affected_rows 의결과를반환합니다. 이값을검사하여 1 이상의값 이나오면쿼리가성공한것을알수있습니다. 따라서쿼리의성공여부와몇개의레코드를추가, 갱신, 삭제했는지알수있습니다.
이상 4가지함수는데이터액세스를유형별로나누어추상화한것이며일반적인레코드처리에대한함수는 mm_ 로정의되어있습니다. 이들함수는 PHP 함수에하나의레이어를추가한것에불과합니다. 디버깅이필요한경우에에러메시지를어떻게볼수있는지알아보기위해다음함수를살펴보겠습니다. 2.1.1.6 일반함수 function mm_error () global $dc; return @mysql_error($dc); 디버깅이필요한곳에다음과같은코드를사용하여에러메시지를출력할수있습니다. echo mm_error(); 연결을닫는함수는다음과같다. function mm_closeconnection() global $dc; mysql_close($dc); 위함수를매번호출하는것은매우어려운일입니다. 이런경우에는 dispose.inc.php 와같은별도의 파일을작성하고, 이파일에서 mm_closeconnection 함수를비롯한각종리소스를해제하는코드를추 가하고각페이지의마지막에 dispose.inc.php 를포함시킵니다. mm_error 와마찬가지로나머지함수들역시 PHP 함수를포장한것에불과합니다. function mm_affectedrows() global $dc; return @mysql_affected_rows($dc);
나머지함수들에대해서는직접살펴보기바라며, 이들을이용한 PHP 응용프로그램의전체구조는 다음과같습니다. <? define('mm_path', '/var/run'); require(mm_path."/common/libraries/settings.inc.php"); require(mm_path."/common/libraries/database-mysql.inc.php"); require(mm_path."/common/header.inc.php"); require(mm_path."/common/menu.inc.php"); // PHP 코드들, 임시변수를사용해 UI 부분에는코드를사용하지않게한다.?> HTML 코드와약간의 for 루프 <? require(mm_path."/common/footer.inc.php"); require(mm_path."/common/copyright.inc.php"); require(mm_path."/common/libraries/dispose.inc.php");?> mm_path 와같은상수를정의함으로써페이지를어느위치에옮기더라도한번의수정으로페이지전 체를수정한효과를누릴수있으며절대경로를이용하면서도상대경로를이용하는이점을누릴수 있게됩니다. 단순히현재디렉터리를지정하는경우에는 define( 'mm_path', '.' ); 과같이설정하면됩니다. 2.1.2 C# 버전 C# 버전은별도로작성하지않고 Microsoft Application Building Blocks 으로소개된 Data Access 를 소개합니다. public static int ExecuteNonQuery(SqlConnection connection, CommandType commandtype, string commandtext, params SqlParameter[] commandparameters)
//create a command and prepare it for execution SqlCommand cmd = new SqlCommand(); PrepareCommand(cmd, connection, (SqlTransaction)null, commandtype, commandtext, commandparameters); //finally, execute the command. int retval = cmd.executenonquery(); // detach the SqlParameters from the command object, so they can be used again. cmd.parameters.clear(); return retval; 이것은 PHP에서살펴본 mm_querynonresult와같은역할을하며언어는다르지만코드는비슷한방식으로동작하는것을알수있습니다. SqlCommand.ExecuteNonQuery를호출하여쿼리를실행하고결과로수행된레코드수를반환받습니다. 이러한종류의함수는 PHP, ADO.NET 뿐만아니라 ASP, VB, JSP 와같은다른언어에서도모두있으므로비슷한함수를쉽게작성하실수있습니다. PHP 버전보다 C# 버전이더복잡해보이는이유는오버로딩을통한다형성을지원하고있으며 MySQL 에서는기대할수없는저장프로시저를다루기때문입니다. PHP 버전에서는레코드의반환타입으로 2차원배열이나 mm_result를사용한 MySQL Resource Identifier로불리는참조만을반환하는두가지유형만을사용하지만 C# 버전은 ADO.NET에서제공하는데이터컨테이너종류에따라함수를별도로작성하기때문에가지수가증가합니다. C# 버전에대한보다자세한소스코드나사용법은도움말에맡기고생략하겠습니다. 설치하면 C# & VB.NET 버전이모두설치되며도움말도제공됩니다. 2.2 Everything in one page 모든코드가한페이지에있는것은매우흔합니다. 초기의웹프로그래밍언어들에서이러한문제는흔히발견됩니다. 다음은 ASP.NET 코드지만한페이지에모든것을작성했으며이것은 ASP, PHP에서도가장보편적입니다. <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <html> <head> <title>single</title> </head> <script language="c#" runat="server"> protected string page; protected string mcfid; void Page_Load(object sender, System.EventArgs e) // Initialize page = Request[ "page" ]; mcfid = Request[ "mcf" ]; if( page == null ) page = "1"; if( mcfid == null ) mcfid = "1"; string dsn = ConfigurationSettings.AppSettings[ "dsn" ]; SqlConnection dc = new SqlConnection( dsn ); dc.open(); // Execute SqlCommand cmd = new SqlCommand( "usp_articlesbypagesplit", dc ); cmd.commandtype = CommandType.StoredProcedure; cmd.parameters.add( "@PAGE", page ); cmd.parameters.add( "@MCF_ID", mcfid ); SqlDataAdapter adapter = new SqlDataAdapter( cmd ); DataSet ds = new DataSet();
adapter.fill( ds, "articles" ); DataTable table = ds.tables[0]; rptlist.datasource = table; rptlist.databind(); // Terminate dc.close(); private void btnwrite_click(object sender, System.Web.UI.ImageClickEventArgs e) Response.Redirect( "write.aspx?mcf=" + mcfid ); </script> <body MS_POSITIONING="GridLayout"> <form id="form1" method="post" runat="server"> <!-- Begin ArticleList --> <asp:repeater id="rptlist" runat="server"> <HeaderTemplate> <table align="center" class="listtitle" cellspacing="0"> <tr class="listtitle"> <td><b> </b></td> <td> 제 목 </td> <td> 이름 </td> <td width="150px"> 등록일 </td> </tr> </HeaderTemplate> <ItemTemplate> <tr class="ltlist"> <td width="50"> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_id" ) ) %> </td> <td class="ltlisttitle"> <a href='view.aspx?id=<%# Convert.ToString(
DataBinder.Eval( Container.DataItem, "ma_id" )) %>'> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_title" )) %> </a> </td> <td> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_userid" )) %> </td> <td width="150px"><%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_recordeddate" ))%> </td> </tr> </ItemTemplate> <FooterTemplate> </table> </FooterTemplate> </asp:repeater> <!-- End ArticleList --> <!-- Begin IconSection --> <table class="headercell"> <tr valign="bottom"> <td align="right"> <a href="list.aspx?mcf=<%= mcfid %>&page=<%= Convert.ToInt32(page) + 1 %>"><img src="images/form/mai_next.gif" border="0" alt="next Page"></a> <!-- Write --> <asp:imagebutton id="btnwrite" runat="server" ImageUrl="images/form/mi_write.gif" AlternateText=" 글쓰기 " /> </td> </tr> </table> <!-- End IconSection --> </form> </body>
</html> 예제를간단하게하기위해게시판에서목록을보여주는기능외에는아무것도구현하지않았습니다. 그러나게시물을등록, 삭제하는과정은모두저장프로시저로작성해두었기때문에그사용방법은크게다르지않습니다. 게시물목록을보여주기위해 Repeater 컨트롤을사용했습니다. 이처럼한페이지에모든것을작성하는것은코드재사용성이낮으며다른페이지와자원을공유하 기어렵습니다. 따라서 ASP.NET 에서제공하는코드숨김 (Code-Behind) 기능을이용해서코드와프레 젠테이션을분리합니다. 이구조는 View-Model Controller 라얘기합니다. Model-View-Controller 에서제공하는이야기는 GoF 의디자인패턴, 마틴파울러의 Pattern of Enterprise Application Architecture, 마이크로소프트의 Enterprise Solution Patterns using Microsoft.Net 에소개된내용을옮기는것에불과합니다. 2.3 View-Model Controller 2.3.1 View <%@ Page language="c#" Codebehind="Model-View_Controller.aspx.cs" AutoEventWireup="false" Inherits="MonaArticle1.Model_View_Controller" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML> <HEAD> <title>webform1</title> </HEAD> <body> <form id="form1" method="post" runat="server"> <!-- Begin ArticleList --> <asp:repeater id="rptlist" runat="server"> <HeaderTemplate> <table align="center" class="listtitle" cellspacing="0"> <tr class="listtitle"> <td><b> </b></td> <td> 제 목 </td> <td> 이름 </td> <td width="150px"> 등록일 </td>
</tr> </HeaderTemplate> <ItemTemplate> <tr class="ltlist"> <td width="50"> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_id" ) ) %> </td> <td class="ltlisttitle"> <a href='view.aspx?id=<%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_id" )) %>'> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_title" )) %> </a> </td> <td> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_userid" )) %> </td> <td width="150px"><%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_recordeddate" ))%> </td> </tr> </ItemTemplate> <FooterTemplate> </table> </FooterTemplate> </asp:repeater> <!-- End ArticleList --> <!-- Begin IconSection --> <table class="headercell"> <tr valign="bottom"> <td align="right"> <a href="list.aspx?mcf=<%= mcfid %>&page=<%= Convert.ToInt32(page) + 1 %>"><img src="images/form/mai_next.gif" border="0" alt="next Page"></a> <!-- Write --> <asp:imagebutton id="btnwrite" runat="server" ImageUrl="images/form/mai_write.gif"
AlternateText=" 글쓰기 " /> </td> </tr> </table> <!-- End IconSection --> </form> </body> </HTML> 2.3.2 Model Controller UI 로부터분리되어데이터를다루는부분을 Model Controller 라하며, ASP.NET 에서는코드숨김 (Code Behind) 이이역할을담당한다. using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Data.SqlClient; using System.Configuration; namespace MonaArticle1 public class Model_View_Controller : System.Web.UI.Page protected string page; protected System.Web.UI.WebControls.Repeater rptlist; protected System.Web.UI.WebControls.ImageButton btnwrite; protected string mcfid; private void Page_Load(object sender, System.EventArgs e)
// Initialize page = Request[ "page" ]; mcfid = Request[ "mcf" ]; if( page == null ) page = "1"; if( mcfid == null ) mcfid = "1"; string dsn = ConfigurationSettings.AppSettings[ "dsn" ]; SqlConnection dc = new SqlConnection( dsn ); dc.open(); // Execute SqlCommand cmd = new SqlCommand( "usp_articlesbypagesplit", dc ); cmd.commandtype = CommandType.StoredProcedure; cmd.parameters.add( "@PAGE", page ); cmd.parameters.add( "@MCF_ID", mcfid ); SqlDataAdapter adapter = new SqlDataAdapter( cmd ); DataSet ds = new DataSet(); adapter.fill( ds, "articles" ); DataTable table = ds.tables[0]; rptlist.datasource = table; rptlist.databind(); // Terminate dc.close(); private void btnwrite_click(object sender, System.Web.UI.ImageClickEventArgs e)
Response.Redirect( "write.aspx?mcf=" + mcfid ); 위예제코드에서 VS.NET에의해생성되는코드는생략하였다. View와 Model Controller로구분하였지만버튼을클릭하면처리를담당하는 Controller와 Model이분리가되어있지않으며 Model과 Controller가코드숨김클래스에강하게결합되어있습니다. 재사용성을높이기위해서는이들을분리해야합니다. 다음은이를분리한 Model-View-Controller(MVC) 패턴입니다. 2.4 Model-View-Controller 2.4.1 Model Model에서는뷰와관련된코드를포함하지않으며오로지모델에관련된코드와데이터베이스액세스에대한코드만포함합니다. Model은데이터를다루며다른것에는간여하지않으므로 C# 클래스형태로작성합니다. using System; using System.Collections; using System.Data; using System.Data.SqlClient; using System.Configuration; namespace MonaArticle1 public class Model1 public Model1() public static DataTable ExecuteDataSet( string page, string mcfid ) // Initialize string dsn = ConfigurationSettings.AppSettings[ "dsn" ];
SqlConnection dc = new SqlConnection( dsn ); dc.open(); // Execute SqlCommand cmd = new SqlCommand( "usp_articlesbypagesplit", dc ); cmd.commandtype = CommandType.StoredProcedure; cmd.parameters.add( "@PAGE", page ); cmd.parameters.add( "@MCF_ID", mcfid ); SqlDataAdapter adapter = new SqlDataAdapter( cmd ); DataSet ds = new DataSet(); adapter.fill( ds, "articles" ); DataTable table = ds.tables[0]; // Terminate dc.close(); return table; 여기에는 View 나 Controller 와관련된코드가존재하지않습니다. Model 에대한코드만작성했습니다. 2.4.2 View-Controller View는 aspx 페이지가되며코드숨김클래스는 Controller가됩니다. View는앞의예제와동일하며여기서는 Controller만소개합니다. using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI;
using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; namespace MonaArticle1 /// <summary> /// Summary description for Controller1. /// </summary> public class Controller1 : System.Web.UI.Page protected System.Web.UI.WebControls.Repeater rptlist; protected System.Web.UI.WebControls.ImageButton btnwrite; protected string page; protected string mcfid; private void Page_Load(object sender, System.EventArgs e) // Initialize page = Request[ "page" ]; mcfid = Request[ "mcf" ]; if( page == null ) page = "1"; if( mcfid == null ) mcfid = "1"; // Execute DataTable table = Model1.ExecuteDataSet( page, mcfid ); rptlist.datasource = table; rptlist.databind(); private void btnwrite_click(object sender, System.Web.UI.ImageClickEventArgs e) Response.Redirect( "write.aspx?mcf=" + mcfid );
Page_Load 메서드에서볼수있는것처럼모델을분리함으로써코드가더단순해지는것을알수있 습니다. Model1 클래스는다시 Model 부분과데이터액세스층 (Data Access Layer) 으로분리될것입 니다. 자카르타스트러츠의 Model2와같은 MVC 변형이등장하게되는것처럼웹응용프로그램에서 MVC는문제점이있습니다. 인터페이스와비즈니스로직을분리하는데는 MVC 패턴이성공적이지만웹페이지간에네비게이션은정적이라는문제점이있습니다. 비즈니스로직과 UI 관련코드를분리시켜야할것입니다. 2.5. Page Controller PageController, Model, View 는위와같이상호작용하며, 각페이지에 PageController 를작성하는것 은많은코드중복을만들어냅니다. 따라서 PageController 에서공통된부분을추출하여일반화 (Generalization) 를시키면다음과같습니다. 웹응용프로그램은사용자요청에매우의존적입니다. 쿼리스트링, 폼요소의제출, 멀티파트폼 등의처리는 Controller 를테스트하는것을어렵게만들며이러한작업은매우시간소모적이며지루 합니다. 따라서 BaseController 클래스는다시웹종속코드와웹독립코드로분리합니다.
2.5.1. PageController PageController에는 Template Mothod 패턴을적용합니다. Template Method는하위클래스에서알고리즘의구조를변경하지않고알고리즘의특정단계를재정의하는데유리합니다. 웹응용프로그램은각페이지에따라서다양한초기화를수행해야합니다. 따라서 BasePage에서는알고리즘의구조를정의하고, 하위클래스에서알고리즘의특정단계즉 PageLoadEvent() 에서알고리즘을재정의하는것이유리합니다. 행위패턴으로알려진 Template Method 패턴은알고리즘구조가변하지않는곳에서사용되며, 하위클래스에서공통적인부분에대한코드중복을줄이기위해공통된부분을 Base Class로일반화 (Generalization) 하는것입니다. 따라서 PageController를일반화시킨 BaseController를살펴봅니다.
2.5.1.1 BaseController C# 클래스 BasePage.cs를추가합니다. 소스코드는다음과같습니다. 웹응용프로그램의오퍼레이션을정의하는것이기때문에 System.Web.UI.Page 클래스를상속하며웹폼디자이너가생성해주는것과같이 Load 이벤트등을정의하고있다는것이주의하십시오. 또한각하위클래스에서재정의하게될 PageLoadEvent를선언하고있습니다. using System; using System.Web.UI.WebControls; using System.Web.UI; namespace MonaArticle1 public class BasePage : System.Web.UI.Page protected virtual void PageLoadEvent(object sender, System.EventArgs e) private void Page_Load(object sender, System.EventArgs e) PageLoadEvent(sender, e); #region Web Form Designer generated code override protected void OnInit(EventArgs e) InitializeComponent(); base.oninit(e); private void InitializeComponent() this.load += new System.EventHandler(this.Page_Load); #endregion
모든페이지에공통적으로들어갈헤더파일도작성합니다. 이헤더파일의이름은 BasePage.inc로합니다. <style> A:link FONT-WEIGHT: normal; COLOR: black; TEXT-DECORATION: none A:visited FONT-WEIGHT: normal; COLOR: black; TEXT-DECORATION: none A:active FONT-WEIGHT: normal; COLOR: red; TEXT-DECORATION: none A:hover FONT-WEIGHT: normal; COLOR: #177c15; TEXT-DECORATION: none BODY FONT-WEIGHT: normal; FONT-SIZE: 9px; FONT-FAMILY: Arial, Verdana, 굴림 TR.ListTitle FONT-WEIGHT: 500; COLOR: #177c15; FONT-FAMILY: Verdana, 돋움 ; BACKGROUND- COLOR: #d8e8d8; TEXT-ALIGN: center TABLE.ListTitle BORDER-RIGHT: 0px; PADDING-RIGHT: 0px; BORDER-TOP: 0px; PADDING-LEFT: 0px; FONT-SIZE: 9pt; PADDING-BOTTOM: 0px; MARGIN: 0px; BORDER-LEFT: 0px; WIDTH: 700px; PADDING-TOP: 0px; BORDER-BOTTOM: 0px; FONT-FAMILY: verdana, Arial, 돋움 ; TEXT-ALIGN: center TR.LTList PADDING-RIGHT: 3px; PADDING-LEFT: 3px; FONT-WEIGHT: normal; FONT-SIZE: 9pt; PADDING-BOTTOM: 3px; COLOR: dimgray; LINE-HEIGHT: 18pt; PADDING-TOP: 3px; FONT-FAMILY: 굴림 ; BACKGROUND-COLOR: white TD.LTListTitle COLOR: dimgray; FONT-FAMILY: 굴림 ; TEXT-ALIGN: left TABLE.HeaderCell BORDER-RIGHT: 0px; PADDING-RIGHT: 0px; BORDER-TOP: 0px; PADDING-LEFT: 0px; PADDING-BOTTOM: 0px; MARGIN: 1px; BORDER-LEFT: 0px; WIDTH: 700px; PADDING-TOP: 0px; BORDER-BOTTOM: 0px; TEXT-ALIGN: center; CellSpacing: 0px </style> <table width=100% border=0 cellspacing=0 cellpadding=5> <tr> <td valign=bottom align=right> <font size=-2 face=verdana> Everything here is <a href="http://www.liebmona.net/">liebmona</a>, 2002.<br> All rights reserved. </font> </td> </tr> <tr> <td colspan=2> </td> </tr>
</table> 2.5.1.2 Model Model 부분은 C# 클래스 DataAccess.cs에서처리합니다. 이와같은 DataAccess는마틴파울러의 POEAA에따르면 Table Data Gateway 패턴입니다. using System; using System.Data.SqlClient; using System.Data; using System.Configuration; namespace MonaArticle1 public class DataAccess public DataAccess() public static DataTable ExecuteDataSet( string page, string mcfid ) // Initialize string dsn = ConfigurationSettings.AppSettings[ "dsn" ]; SqlConnection dc = new SqlConnection( dsn ); dc.open(); // Execute SqlCommand cmd = new SqlCommand( "usp_articlesbypagesplit", dc ); cmd.commandtype = CommandType.StoredProcedure; cmd.parameters.add( "@PAGE", page ); cmd.parameters.add( "@MCF_ID", mcfid ); SqlDataAdapter adapter = new SqlDataAdapter( cmd ); DataSet ds = new DataSet(); adapter.fill( ds, "articles" );
DataTable table = ds.tables[0]; // Terminate dc.close(); return table; 2.5.1.3 SubPage SubPage는웹폼이며 System.Web.UI.Page 클래스를상속하는대신 BasePage 클래스를상속하여동작합니다. 여기서는 SubPage의예로게시판목록을사용했습니다. 이름 : SubPage.aspx <%@ Page language="c#" Codebehind="SubPage.aspx.cs" AutoEventWireup="false" Inherits="MonaArticle1.SubPage" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML> <HEAD> <title>subpage</title> <meta name="generator" Content="Microsoft Visual Studio.NET 7.1"> <meta name="code_language" Content="C#"> <meta name="vs_defaultclientscript" content="javascript"> <meta name="vs_targetschema" content="http://schemas.microsoft.com/intellisense/ie5"> </HEAD> <body MS_POSITIONING="FlowLayout"> <!-- #include virtual="basepage.inc" --> <form id="form1" method="post" runat="server"> <!-- Begin ArticleList --> <asp:repeater id="rptlist" runat="server"> <HeaderTemplate> <table align="center" class="listtitle" cellspacing="0">
<tr class="listtitle"> <td><b> </b></td> <td> 제 목 </td> <td> 이름 </td> <td width="150px"> 등록일 </td> </tr> </HeaderTemplate> <ItemTemplate> <tr class="ltlist"> <td width="50"> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_id" ) ) %> </td> <td class="ltlisttitle"> <a href='view.aspx?id=<%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_id" )) %>'> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_title" )) %> </a> </td> <td> <%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_userid" )) %> </td> <td width="150px"><%# Convert.ToString( DataBinder.Eval( Container.DataItem, "ma_recordeddate" ))%> </td> </tr> </ItemTemplate> <FooterTemplate> </table> </FooterTemplate> </asp:repeater> <!-- End ArticleList --> <!-- Begin IconSection --> <table class="headercell"> <tr valign="bottom">
<td align="right"> <a href="list.aspx?mcf=<%= mcfid %>&page=<%= Convert.ToInt32(page) + 1 %>"><img src="images/form/mai_next.gif" border="0" alt="next Page"></a> <!-- Write --> <asp:imagebutton id="btnwrite" runat="server" ImageUrl="images/form/mi_write.gif" AlternateText=" 글쓰기 " /> </td> </tr> </table> <!-- End IconSection --> </form> </body> </HTML> 이름 : SubPage.aspx.cs( 코드숨김파일 ) using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; namespace MonaArticle1 public class SubPage : BasePage protected string page; protected System.Web.UI.WebControls.Repeater rptlist; protected System.Web.UI.WebControls.ImageButton btnwrite; protected string mcfid; private void Page_Load(object sender, System.EventArgs e)
// Put user code to initialize the page here protected override void PageLoadEvent(object sender, System.EventArgs e) // Initialize page = Request[ "page" ]; mcfid = Request[ "mcf" ]; if( page == null ) page = "1"; if( mcfid == null ) mcfid = "1"; // Execute DataTable table = DataAccess.ExecuteDataSet( page, mcfid ); rptlist.datasource = table; rptlist.databind(); SubPage.aspx를시작페이지로설정한후컴파일하면잘동작하는것을알수있습니다. 이와같은패턴이갖는장점은다음과같습니다. - PageController를웹종속적인부분과웹독립적인부분으로나누었기때문에각페이지에서는매우한정된부분만을작업하는것으로페이지를동적으로생성할수있으며코드를최대한단순하게유지할수있습니다. 웹페이지의네비게이션을고려한패턴으로는 MVC외에 Intercepting Filter, Front Controller 등이있습니다. 이들은여기서소개한방법들보다네비게이션에대해더강력한방법을제공하지만보다복잡해집니다. 2.6 캐싱
캐싱을할수있는종류는웹프로그래밍언어마다조금씩차이가있으나대부분은동일합니다. ASP.NET 에서는캐싱할수있는종류로는 Class, Configuration( 설정정보 ), Output Caching, Object Caching 이있으며, 데이터베이스연결을캐싱하는연결풀링 (Connection Pooling) 이있습니다. 2.6.1 Output 캐싱대부분의사용자들에게시물목록페이지의 1-3페이지정도를보며, 게시물을조회하는페이지도평균 5개이내의게시물을조회한다는것을생각하면아주작은캐싱만으로도매우큰퍼포먼스를얻을수있습니다. ASP.NET에서캐싱을적용하는방법은 aspx 페이지의처음에다음과같이하면됩니다. <%@ OutputCache Duration="1" VaryByParam="none"%> <%@ OutputCache Duration="1" VaryByParam="page;mcf"%> 첫번째는정적인페이지를캐싱하는경우에적당하며두번째는게시물목록페이지에서와같이매개변수 page, mcf의값을기준으로캐싱하라는것을의미합니다. Duration은캐시를보유할시간을나타내며여기서는 1분으로설정하였습니다. 그러나신문사, 도서정보, 영화정보, 쇼핑몰과같이컨텐트제공이위주인사이트에서는페이지캐싱을 30-60분정도로설정하는것만으로도많은성능을얻을수있습니다. 메모리에저장된캐시를가져오는비용은데이터베이스연결을생성하고, 데이터를가져오는비용과비교할수없을만큼적습니다. 이러한캐싱은적게는 200% 에서많게는 2000% 의성능향상을보여줍니다. 모든매개변수에대해서캐싱을하고싶은경우에는 VaryByParam="*" 과같이설정하시면됩니다. @OutputCache에는 Location을사용하여캐시된정보를어디에저장할지지정할수있습니다. 이값은 Client, DownStream, Server, None, Any가있습니다. 기본값은 Any이며이는 Client, DownStream, Server 무엇이든될수있습니다. None은캐싱을사용하지않는것을의미합니다. Client는사용자브라우저에캐시정보를저장하는것을의미하며 DownStream은웹서버로부터요청을받은서버가되며대부분의경우에프록시서버에저장하는것을의미합니다. Server는여러웹서버중에요청을받은웹서버에캐시정보를저장하는것을의미합니다. 2.6.2 Class 캐싱클래스를이용한캐싱은공유자원을 static으로선언하는것으로각페이지를여러인스턴스들간에데이터를공유할수있습니다. 클래스캐싱은어떤멤버든지 static으로선언할수있으면모두이용할수있으며 Hashtable, Dictionary, Vector 등을이용하여캐싱을직접구현하는경우에도사용할수있습니다. 직접캐싱을구현할때주의할점은언제캐싱을만료할것인지결정해야한다는것입니다. 여기서는간단히 int counter를 static으로선언하여방문자수를카운트하는경우를설명하였
습니다. 웹서버에서이페이지를실행한다음에다른 PC 에서이페이지를실행하시면결과를볼수 있습니다. 이름 : SimpleCache.aspx <%@ Page language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false" Inherits="SimpleCache.WebForm1" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML> <HEAD> <title>class Cache</title> </HEAD> <body MS_POSITIONING="FlowLayout"> <form id="form1" method="post" runat="server"> <FONT face=" 굴림 ">Counter: </FONT> <asp:label id="label1" runat="server">label</asp:label> </form> </body> </HTML> 이름 : SimpleCache.aspx.cs using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; namespace SimpleCache public class WebForm1 : System.Web.UI.Page
protected System.Web.UI.WebControls.Label Label1; public static int counter; private void Page_Load(object sender, System.EventArgs e) // Put user code to initialize the page here ++counter; Label1.Text = counter.tostring(); ASP.NET 과같이자체캐싱을지원하지않는경우에는 WAS(Web Application Server) 에서제공하는캐 싱이나전용캐싱솔루션을구입하는방법도있으며결과를임시파일로저장하는방법도있습니다. 그러나캐싱에는단점이있습니다. 캐싱도자원을소비하는것이기때문에히트율이높은페이지만을캐싱해야효율적이며히트율이낮은페이지까지캐싱하는것은메모리를차지하는결과를가져옵니다. 캐싱은디스크캐싱, 메모리캐싱의경우에사용되는디스크양과메모리양을정확히관리하는것이필요합니다. 2.6.3 조각캐싱조각캐싱은닷넷에서사용자컨트롤 ascx 페이지에 @OutputCache를추가하여컨트롤을캐싱하는것입니다. 캐싱을사용하는사용자컨트롤을페이지에추가하면사용자컨트롤에대해서캐싱되기때문에전체페이지를실행하는것보다나은성능을보여주게됩니다. 앞에서작성한 BasePage.inc 를사용자컨트롤을사용하여작성하면다음과같습니다. 이름 : BasePage.ascx <%@ Control Language="c#" AutoEventWireup="false" Codebehind="BasePage.ascx.cs" Inherits="MonaArticle1.BasePage1" TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%> <%@ OutputCache Duration="60" VaryByParam="none" %> <table width=100% border=0 cellspacing=0 cellpadding=5> <tr> <td valign=bottom align=right> <font size=-2 face=verdana>
Everything here is <a href="http://www.liebmona.net/">liebmona</a>, 2002.<br> All rights reserved. </font> </td> </tr> <tr> <td colspan=2> </td> </tr> </table> 사용자컨트롤 BasePage.ascx을사용하는페이지예제는다음과같습니다. <%@ Page language="c#" Codebehind="BasePageUsage.aspx.cs" AutoEventWireup="false" Inherits="MonaArticle1.BasePageUsage" %> <%@ Register TagPrefix="Mona" TagName="Header" Src="BasePage.ascx" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <html> <head> <title>basepageusage</title> </head> <body MS_POSITIONING="FlowLayout"> <Mona:Header runat="server" /> <form id="form1" method="post" runat="server"> </form> </body> </html> 2.6.4 객체캐싱 static 키워드를이용한클래스캐싱보다나은방법은닷넷프레임워크에서제공하는 Cache 클래스를이용하는것입니다. 다음은 Cache 클래스를사용하여객체캐싱을하는예제이며그리드를채우기위해 SQL Server의예제데이터베이스인 Northwind를사용했습니다. 이름 : Cache.aspx <%@ Page language="c#" Codebehind="Cache.aspx.cs" AutoEventWireup="false"
Inherits="CacheView.WebForm1" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML> <HEAD> <title>object Cache</title> </HEAD> <body> <form id="form1" method="post" runat="server"> <FONT face=" 굴림 "> <asp:button id="button1" runat="server" Text="Button"></asp:Button> <asp:label id="label1" runat="server">label</asp:label> <asp:datagrid id="datagrid1" runat="server"></asp:datagrid></font> </form> </body> </HTML> 이름 : Cache.aspx.cs using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Data.SqlClient; namespace CacheView public class WebForm1 : System.Web.UI.Page protected System.Web.UI.WebControls.Label Label1; protected System.Web.UI.WebControls.DataGrid DataGrid1;
protected System.Web.UI.WebControls.Button Button1; private void Page_Load(object sender, System.EventArgs e) // Put user code to initialize the page here DataView view = new DataView(); string date; view = (DataView) Cache[ "CacheView" ]; date = (string) Cache[ "CacheDate" ]; if( view == null date == null ) SqlConnection sc = new SqlConnection( "server=(local); database=northwind; uid=sa; pwd=ndsndkealva" ); SqlDataAdapter adapter = new SqlDataAdapter( "SELECT * FROM Products", sc ); DataSet ds = new DataSet(); adapter.fill( ds, "Product" ); view = ds.tables[0].defaultview; date = DateTime.Now.ToString(); Cache.Insert( "CacheView", view ); Cache.Insert( "CacheDate", date ); Label1.Text = "I came from DB. " + date; else Label1.Text = "I came from cached data. " + date; // end if DataGrid1.DataSource = view; DataGrid1.DataBind();
private void Button1_Click(object sender, System.EventArgs e) if( Cache["CacheView"] == null ) Cache.Remove("CacheView"); Cache.Remove("CacheDate"); Cache 에추가하기위해 Cache.Insert 를사용하고, 캐시된항목을삭제하기위해 Cache.Remove 를사용 합니다. 2.6.5 연결캐싱 동적으로생성되는페이지를캐싱하는방법외에연결을캐싱하는방법이있습니다. ASP.NET 에서는연 결문자열에다음과같이설정하면됩니다. <add key="dsn" value="server=(local); uid=mona2; password=mona2user; database=mona2; Min Pool Size=0; Max Pool Size=20;" /> ASP.NET에서연결을캐싱하는기준은연결문자열의값이같은지의여부입니다. 따라서같은데이터베이스에연결하는경우라해도연결문자열이다른경우에는동일한채널로인식하지않습니다. PHP 도마찬가지로 dbhost, dbuser 등을조합하여하나의고유한연결로인식합니다. PHP 자체에서 Persistent Connection( 연결지속 ) 을제공하더라도이들조합의요소가달라지면다른연결로인식합니다. PHP 에서는연결을캐싱하기위해 sqlrelay 를사용합니다. 앞서소개한 database-mysql.inc.php 를 sqlrelay 버전으로다시작성한 database-sqlrelay.inc.php 에서는다음과같은방법을사용합니다. $dc = mm_openconnection(); $cursor = mm_opencursor();
// open db connection function mm_openconnection() global $mm_config; //global $dc; $dc = sqlrcon_alloc( $mm_config['dbhost'], 9000, "", $mm_config['dbuser'], $mm_config['dbpassword'], 0, 1); return $dc; 여러개의레코드를가져오기위해사용한 mm_getrecordsbyfields 는다음과같이재작성됩니다. function mm_getrecordsbyfields($query) global $dc; global $cursor; $query = stripslashes($query); // buffering the result set all at once sqlrcur_setresultsetbuffersize( $cursor, 0 ); sqlrcur_openquery( $cursor, $query ); sqlrcon_endsession( $dc ); if ( 0 == $cursor ) return false; exit; for( $loopctr = 0; $loopctr < sqlrcur_rowcount( $cursor ); $loopctr++)
$row_array = sqlrcur_getrow( $cursor, $loopctr ); for( $ctr = 0; $ctr < sqlrcur_colcount( $cursor ); $ctr++ ) $field_name = sqlrcur_getcolumnname( $cursor, $ctr ); $data_set[$loopctr][$field_name] = $row_array[$ctr]; // end mm_getrecords return $data_set; 현재연결캐싱은다양한웹프로그래밍언어에서제공되거나 sqlrelay 와같이별도의라이브러리형 태로제공됩니다. 마치며웹응용프로그램에서는오늘날웹응용프로그램에서널리쓰이는 MVC와 MVC의변형에대해서살펴보았습니다. 그러나시간상의제약으로인해 Front Controller, Intercepting Filter 등을다루지못했습니다. 또한웹응용프로그램에서유용하게적용될수있는 Observer, Command, 분산환경응용프로그램에적용될수있는 Broker, Data Transfer Object, Serialize 등을다루지못한아쉬움이남습니다. 지금까지소개한것들중에진정제자신의것은없습니다. 모두책, 인터넷, 뉴스그룹을통해서얻은것들일뿐입니다. 따라서익명으로많은지식을인터넷을통해전파하는많은분들에게감사드립니다. 이강좌를통해여러분의웹응용프로그래밍에도움이되었으면바랄것이없겠습니다.