ASP.NET에서 MVC(Model-View-Controller) 구현

? Data Column: select for more on pattern organization Application Column: select for more on pattern organization Deployment Column: select for more on pattern organization Infrastructure Column: select for more on pattern organization
Architecture Row: select for more on pattern organization Data Architecture: select for more on pattern organization Application Architecture: select for more on pattern organization Deployment Architecture: select for more on pattern organization Infrastructure Architecture: select for more on pattern organization
Design Row: select for more on pattern organization Data Design: select for more on pattern organization Application Design: select for more on pattern organization Deployment Design: select for more on pattern organization Infrastructure Design: select for more on pattern organization
Implementation Row Data Implementation: select for more on pattern organization Application Implementation: select for more on pattern organization Deployment Implementation: select for more on pattern organization Infrastructure Implementation: select for more on pattern organization
? Complete List of patterns & practices Complete List of patterns & practices Complete List of patterns & practices Complete List of patterns & practices

버전 1.0.1

이 패턴에 대한 공동 작업을 위한 DotDotNet 커뮤니티

patterns & practives 전체 목록 (영문)

상황

Microsoft ASP.NET에서 웹 응용 프로그램을 구축 중인데 응용 프로그램이 복잡하기 때문에 코드 중복을 줄이고 변경 사항이 전파되는 것을 제한하려면 프로그램의 서로 다른 기능을 분리해야 합니다.

구현 전략

소프트웨어의 모델, 뷰 및 컨트롤러 역할을 분리하여 제공된 값 및 ASP.NET에서 MVC(Model-View-Controller)패턴을 구현하는 방법을 설명하기 위해, 다음 예에서는 이 세 역할을 분리하지 않은 단일 페이지 솔루션을 이러한 역할을 분리한 솔루션으로 리팩토링합니다. 이 예제 응용 프로그램은 드롭다운 목록이 있는 단일한 웹 페이지(그림 1)로서, 데이터베이스에 저장되어 있는 기록을 표시합니다.

그림 1: 예제 웹 페이지

사용자는 드롭다운 목록에서 특정 기록을 선택한 다음 전송(Submit) 단추를 클릭합니다. 그러면 응용 프로그램이 데이터베이스의 이 기록에서 모든 트랙의 목록을 검색하여 그 결과를 표로 표시합니다. 이 패턴에 설명된 세 솔루션 모두 정확하게 동일한 기능을 구현합니다.

단일 ASP.NET 페이지

ASP.NET에서 이 페이지를 구현하는 방법은 많습니다. 가장 간단하고 단순한 방법은 다음 코드 예에서 볼 수 있는 것처럼 모든 것을 "Solution.aspx"라고 하는 한 파일에 모두 넣는 방법입니다.

 

<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<html>
   <head>
      <title>start</title>
      <script language="c#" runat="server">
         void Page_Load(object sender, System.EventArgs e)
         {
            String selectCmd = "select * from Recording";

            SqlConnection myConnection = 
               new SqlConnection(
                  "server=(local);database=recordings;Trusted_Connection=yes");
            SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, 
               myConnection);

            DataSet ds = new DataSet();
            myCommand.Fill(ds, "Recording");
            
            recordingSelect.DataSource = ds;
            recordingSelect.DataTextField = "title";
            recordingSelect.DataValueField = "id";
            recordingSelect.DataBind();
         }
         
         void SubmitBtn_Click(Object sender, EventArgs e) 
         {   
            String selectCmd = 
               String.Format(
               "select * from Track where recordingId = {0} order by id",
               (string)recordingSelect.SelectedItem.Value);

            SqlConnection myConnection = 
               new SqlConnection(
                  "server=(local);database=recordings;Trusted_Connection=yes");

            SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd,
               myConnection);

            DataSet ds = new DataSet();
            myCommand.Fill(ds, "Track");

            MyDataGrid.DataSource = ds;
            MyDataGrid.DataBind();
         }
      </script>
   </head>
   <body>
      <form id="start" method="post" runat="server">
         <h3>Recordings</h3>
         Select a Recording:<br />
         <asp:dropdownlist id="recordingSelect" runat="server" />
         <asp:button runat="server" text="Submit" OnClick="SubmitBtn_Click" />
         <p/>
         <asp:datagrid id="MyDataGrid" runat="server" width="700" 
               backcolor="#ccccff" bordercolor="black" showfooter="false" 
               cellpadding="3" cellspacing="0" font-name="Verdana" 
               font-size="8pt" headerstyle-backcolor="#aaaadd" 
               enableviewstate="false" />
      </form>
   </body>
</html>
 

이 파일은 이 패턴의 세 역할을 모두 구현하지만 이 역할을 서로 다른 파일이나 클래스로 구분하지는 않습니다. 뷰 역할은 HTML 고유의 렌더링 코드에 의해 표시됩니다. 이 페이지는 바운드 데이터 제어 구현을 사용하여 데이터베이스에서 반환된 DataSet 개체를 표시합니다. 한편 모델 역할은 Page_Load 및 SubmitBtn_Click 함수에서 구현됩니다. 컨트롤러 역할은 직접 표시되지 않으며 ASP.NET에 내재되어 있습니다. 이에 대해서는 페이지 컨트롤러를 참조하십시오. 페이지는 사용자가 요청하면 업데이트됩니다. MVC(Model-View-Controller)는 이것을 수동적인 컨트롤러로 설명합니다. ASP.NET가 컨트롤러 역할을 구현하지만 그 컨트롤러가 응답하는 이벤트에 작업을 연결하는 책임은 프로그래머에게 있기 때문입니다. 이 예에서는 컨트롤러가 Page_Load 함수를 호출하면 페이지가 로드됩니다. 그리고 사용자가 전송(Submit) 단추를 클릭하면 컨트롤러가 SubmitBtn_Click 기능을 호출합니다.

이 페이지는 아주 단순하며 독립적입니다. 이 구현은 유용하며, 응용 프로그램이 작고 자주 변경되지 않는 경우 사용하기 좋습니다. 하지만 개발 중 다음과 같은 상황이 발생하는 경우에는 이 방법을 변경하는 방안을 고려해 보아야 합니다.

  • 병렬 처리를 늘리고 오류 가능성을 줄이고자 할 경우. 뷰 코드와 모델 코드를 서로 다른 사람이 작업하여 병렬 처리 양을 늘리고 오류 발생 가능성을 제한하고자 할 수 있습니다. 예를 들어, 모든 코드가 한 페이지에 있는 경우 개발자는 DataGrid 서식을 변경하고 데이터베이스를 액세스하는 원본 코드 일부를 무심코 변경할 수 있습니다. 하지만 페이지는 다시 보기를 해야만 컴파일되므로 페이지를 다시 보기 전까지는 오류를 발견할 수 없습니다.

  • 여러 페이지에 데이터베이스 액세스 코드를 다시 사용하고자 할 경우. 이 구현에서는 코드를 중복하지 않고는 다른 페이지에 코드를 다시 사용할 수 있는 방법이 없습니다. 중복 코드를 사용하면 데이터베이스 코드가 변경될 경우 그 데이터베이스를 액세스하는 모든 페이지를 수정해야 하므로 유지 관리가 어렵습니다.

    이러한 문제를 처리하기 위해 ASP.NET 구현자들은 코드 비하인드 기능을 도입했습니다.

    코드 비하인드 리팩토링

    Microsoft Visual Studio .NET 개발 시스템의 코드 비하인드 기능을 사용하면 프레젠테이션(뷰) 코드를 모델 컨트롤러 코드에서 쉽게 분리할 수 있습니다. 개별 ASP.NET 페이지에는 그 페이지에서 호출된 메서드가 별도의 클래스에서 구현되도록 하는 메커니즘이 있습니다. 이 메커니즘은 Visual Studio .NET에 의해 활성화되며 Microsoft IntelliSense 기술과 같은 많은 이점이 있습니다. 코드 비하인드 기능을 사용하여 페이지를 구현할 때에는 IntelliSense를 사용하여 해당 페이지 뒤의 코드에서 사용하고 있는 개체의 사용 가능한 메서드 목록을 표시할 수 있습니다. IntelliSense는 .aspx 페이지에서는 작동되지 않습니다.

    다음은 동일한 예로, 이번에는 코드 비하인드 기능을 사용하여 ASP.NET을 구현합니다.

    이 프레젠테이션 코드는 이제 Solution.aspx라고 하는 별도의 파일에 들어 있습니다.

     
    
    <%@ Page language="c#" Codebehind="Solution.aspx.cs" 
       AutoEventWireup="false" Inherits="Solution" %>
    <html>
       <head>
          <title>Solution</title>
       </head>
       <body>
          <form id="Solution" method="post" runat="server">
             <h3>Recordings</h3>
             Select a Recording:<br/>
             <asp:dropdownlist id="recordingSelect" runat="server" />
             <asp:button id="submit" runat="server" text="Submit" 
                enableviewstate="False" />
             <p/>
             <asp:datagrid id="MyDataGrid" runat="server" width="700"
                   backcolor="#ccccff" bordercolor="black" showfooter="false"
                   cellpadding="3" cellspacing="0" font-name="Verdana" font-size="8pt"
                   headerstyle-backcolor="#aaaadd" enableviewstate="false" />
          </form>
       </body>
    </html>
     

    이 코드의 대부분은 첫 번째 구현에서 사용된 코드와 비슷하며 가장 큰 차이점은 첫 번째 줄입니다.

     
    
    <%@ Page language="c#" Codebehind="Solution.aspx.cs" 
       AutoEventWireup="false" Inherits="Solution" %>
     

    이 줄은 코드 비하인드 클래스가 이 페이지에서 참조되는 메서드를 구현하는 ASP.NET 환경을 나타냅니다. 이 페이지에는 데이터베이스를 액세스하는 코드가 없으므로, 데이터베이스가 코드 변경 사항을 액세스하더라도 이 페이지를 수정해야 할 필요가 없습니다. 사용자 인터페이스 디자인에 익숙한 사람은 데이터베이스 액세스 코드에 오류를 유발하지 않고도 이 코드를 수정할 수 있습니다.

    모델 컨트롤러

    이 솔루션의 두 번째 부분은 다음과 같은 코드 비하인드 페이지입니다.

     
    
    using System;
    using System.Data;
    using System.Data.SqlClient;
    
    public class Solution : System.Web.UI.Page
    {
       protected System.Web.UI.WebControls.Button submit;
       protected System.Web.UI.WebControls.DataGrid MyDataGrid;
       protected System.Web.UI.WebControls.DropDownList recordingSelect;
       
       private void Page_Load(object sender, System.EventArgs e)
       {
          if(!IsPostBack)
          {
             String selectCmd = "select * from Recording";
    
             SqlConnection myConnection = 
                new SqlConnection(
                   "server=(local);database=recordings;Trusted_Connection=yes");
             SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
    
             DataSet ds = new DataSet();
             myCommand.Fill(ds, "Recording");
    
             recordingSelect.DataSource = ds;
             recordingSelect.DataTextField = "title";
             recordingSelect.DataValueField = "id";
             recordingSelect.DataBind();
          }
       }
    
       void SubmitBtn_Click(Object sender, EventArgs e) 
       {   
          String selectCmd = 
             String.Format(
             "select * from Track where recordingId = {0} order by id",
             (string)recordingSelect.SelectedItem.Value);
    
          SqlConnection myConnection = 
             new SqlConnection(
                "server=(local);database=recordings;Trusted_Connection=yes");
          SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
    
          DataSet ds = new DataSet();
          myCommand.Fill(ds, "Track");
    
          MyDataGrid.DataSource = ds;
          MyDataGrid.DataBind();
       }
    
       #region Web Form Designer generated code
       override protected void OnInit(EventArgs e)
       {
          //
          // CODEGEN: This call is required by the ASP.NET Web Form Designer.
          //
          InitializeComponent();
          base.OnInit(e);
       }
          
       /// <summary>
       /// Required method for Designer support - do not modify
       /// the contents of this method with the code editor.
       /// </summary>
       private void InitializeComponent()
       {    
          this.submit.Click += new System.EventHandler(this.SubmitBtn_Click);
          this.Load += new System.EventHandler(this.Page_Load);
    
       }
       #endregion
    }
     

    이 코드는 단일 ASP.NET 페이지에서 자체 파일로 옮겨졌습니다. 이 두 엔터티를 하나로 연결하려면 몇몇 구문을 변경해야 합니다. 이 클래스에 정의된 구성원 변수는 Solution.aspx 파일에 참조된 이름과 동일한 이름을 공유합니다. 명시적으로 정의되어야 하는 또 다른 부분은, 이 컨트롤러가 반드시 실행되어야 하는 작업이 발생한 이벤트에 어떻게 연결하는가 입니다. 이 예에서는 InitializeComponent 메서드가 이 두 이벤트를 연결합니다. 첫 번째 이벤트는 Load로, Page_Load 함수로 연결됩니다. 그리고 두 번째 이벤트는 Click으로, 전송(Submit) 단추를 클릭하면 SubmitBtn_Click 함수가 실행되도록 만듭니다.

    코드 비하인드 기능은 뷰 역할을 모델 및 컨트롤러 역할에서 분리하는 훌륭한 메커니즘입니다. 코드 비하인드 기능은 코드 비하인드 클래스에 있는 코드를 다른 페이지에 재사용해야 하는 경우 불충분해질 수 있습니다. 코드 비하인드 페이지에서 코드를 재사용하는 것이 기술적으로는 가능하지만 코드 비하인드 클래스를 공유하는 모든 페이지의 결합이 증가하기 때문에 바람직하지 않습니다.

    MVC(Model-View-Controller) 리팩토링

    마지막 문제를 해결하기 위해서는 컨트롤러에서 모델 코드를 분리해야 합니다. 뷰 코드는 이전 구현에서 사용한 코드와 동일합니다.

    모델

    다음 코드 예는 모델을 표시한 것으로, 데이터베이스에만 의존합니다. 따라서 뷰 의존 코드(ASP.NET 종속성 코드)는 포함되어 있지 않습니다.

     
    
    using System;
    using System.Collections;
    using System.Data;
    using System.Data.SqlClient;
    
    public class DatabaseGateway
    {
       public static DataSet GetRecordings()
       {
          String selectCmd = "select * from Recording";
    
          SqlConnection myConnection = 
             new SqlConnection(
                "server=(local);database=recordings;Trusted_Connection=yes");
          SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
    
          DataSet ds = new DataSet();
          myCommand.Fill(ds, "Recording");
          return ds;
       }
    
       public static DataSet GetTracks(string recordingId)
       {
          String selectCmd = 
             String.Format(
             "select * from Track where recordingId = {0} order by id",
             recordingId);
    
          SqlConnection myConnection = 
             new SqlConnection(
                "server=(local);database=recordings;Trusted_Connection=yes");
          SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
    
          DataSet ds = new DataSet();
          myCommand.Fill(ds, "Track");
          return ds;
       }
     

    이제 이 파일이 데이터베이스에 의존하는 유일한 파일입니다. 이 클래스는 테이블 데이터 게이트웨이의 훌륭한 예입니다. 테이블 데이터 게이트웨이에는 단일 테이블 또는 뷰를 액세스하는 모든 SQL 코드(선택, 삽입, 업데이트, 삭제)가 있습니다. 다른 코드는 이 데이터베이스와의 모든 상호 작용을 위해 자체 메서드를 호출합니다. [Fowler03]

    컨트롤러

    이 리팩토링은 코드 비하인드 기능을 사용하여 그 페이지에 있는 데이터 제어에 모델 코드를 적용하고 컨트롤러가 전달하는 이벤트를 특정 작업 메서드로 매핑합니다. 이 모델은 여기에서 DataSet 개체를 반환하기 때문에 작업이 단순합니다. 이 코드는 뷰 코드와 마찬가지로 데이터베이스에서 데이터가 검색되는 방법에 의존하지 않습니다.

     
    
    using System;
    using System.Data;
    using System.Collections;
    using System.Web.UI.WebControls;
    
    public class Solution : System.Web.UI.Page
    {
       protected System.Web.UI.WebControls.Button submit;
       protected System.Web.UI.WebControls.DataGrid MyDataGrid;
       protected System.Web.UI.WebControls.DropDownList recordingSelect;
       
       private void Page_Load(object sender, System.EventArgs e)
       {
          if(!IsPostBack)
          {
             DataSet ds = DatabaseGateway.GetRecordings();
             recordingSelect.DataSource = ds;
             recordingSelect.DataTextField = "title";
             recordingSelect.DataValueField = "id";
             recordingSelect.DataBind();
          }
       }
    
       void SubmitBtn_Click(Object sender, EventArgs e) 
       {   
          DataSet ds = 
             DatabaseGateway.GetTracks(
             (string)recordingSelect.SelectedItem.Value);
    
          MyDataGrid.DataSource = ds;
          MyDataGrid.DataBind();
       }
    
       #region Web Form Designer generated code
       override protected void OnInit(EventArgs e)
       {
          //
          // CODEGEN: This call is required by the ASP.NET Web Form Designer.
          //
          InitializeComponent();
          base.OnInit(e);
       }
          
       /// <summary>
       /// Required method for Designer support - do not modify
       /// the contents of this method with the code editor.
       /// </summary>
       private void InitializeComponent()
       {    
          this.submit.Click += new System.EventHandler(this.SubmitBtn_Click);
          this.Load += new System.EventHandler(this.Page_Load);
    
       }
       #endregion
    }
     

    테스트

    ASP.NET 환경에서 모델을 분리하면 모델 코드를 테스트하기가 더 쉬워집니다. ASP.NET 환경 내에서 이 코드를 테스트하려면 이 프로세스의 출력을 테스트해야 합니다. 이는, HTML을 읽고 정확한지를 판단해야 하는 것을 의미하는데 그 작업은 아주 지루하고 오류가 발생하기 쉽습니다. 모델을 분리하여 ASP.NET 없이 실행할 수 있게 되면 이러한 지루한 작업을 피할 수 있으며 코드만 분리하여 테스트할 수 있습니다. 다음은 NUnit (http://nunit.org)의 모델 코드 샘플 단위 테스트입니다.

     
    
    using System;
    
    using NUnit.Framework;
    using System.Collections;
    using System.Data;
    using System.Data.SqlClient;
    
    [TestFixture]
    public class GatewayFixture
    {
       [Test]
       public void Tracks1234Query()
       {
    
          DataSet ds = DatabaseGateway.GetTracks("1234");
          Assertion.AssertEquals(10, ds.Tables["Track"].Rows.Count);
       }
    
       [Test]
       public void Tracks2345Query()
       {
          DataSet ds = DatabaseGateway.GetTracks("2345");
          Assertion.AssertEquals(3, ds.Tables["Track"].Rows.Count);
       }
    
       [Test]
       public void Recordings()
       {
          DataSet ds = DatabaseGateway.GetRecordings();
          Assertion.AssertEquals(4, ds.Tables["Recording"].Rows.Count);
    
          DataTable recording = ds.Tables["Recording"];
          Assertion.AssertEquals(4, recording.Rows.Count);
    
          DataRow firstRow = recording.Rows[0];
          string title = (string)firstRow["title"];
          Assertion.AssertEquals("Up", title.Trim());
       }
    }
      

    결과

    ASP.NET에서 MVC를 구현하면 다음과 같은 이점과 단점이 있습니다.

    장점

  • 종속성 감소.ASP.NET 페이지는 프로그래머가 페이지 내에서 메서드를 구현할 수 있도록 해줍니다. 단일 ASP.NET 페이지에서 볼 수 있는 것처럼 이 페이지는 프로토타입 및 작고 수명이 짧은 웹 응용 프로그램에 유용할 수 있습니다. 하지만 페이지가 복잡하고 페이지 간 코드 공유 필요성이 증가하면 코드의 일부를 분리하는 것이 더욱 유용합니다.

  • 코드 중복 감소. DatabaseGateway 클래스의 GetRecordingsGetTracks 메서드를 이제 다른 페이지에서도 사용할 수 있습니다. 따라서 메서드를 여러 뷰로 복사할 필요가 없습니다.

  • 작업 및 문제점 분리. ASP.NET 페이지를 수정하는 데 필요한 기술은 데이터베이스를 액세스하는 코드 작성에 필요한 기술과 다릅니다. 앞서 설명한 것처럼 뷰와 모델을 분리하면 각 분야의 전문가가 동시에 작업할 수 있습니다.

  • 기회의 최적화. 앞서 설명한 것처럼 책임을 특정 클래스로 분리하면 최적화를 위한 기회가 증가됩니다. 앞에서 설명한 예에서, 요청이 있을 때마다 데이터베이스에서 데이터가 로드됩니다. 특정 상황에서 데이터를 캐싱하는 것이 가능하므로 응용 프로그램의 전체적인 성능이 향상될 수 있습니다. 하지만 이는 코드를 분리하지 않으면 어렵거나 불가능합니다.

  • 테스트 가능. 뷰에서 모델을 격리하면 ASP.NET 환경 밖에서 모델을 테스트할 수 있습니다.

    단점

    추가 코드 및 복잡성. 앞에 제시된 예는 더 많은 파일과 코드를 추가하므로 세 역할 모두를 변경해야 할 경우 코드 유지 관리 비용이 높아집니다. 어떤 경우에는 한 파일을 변경하는 것이 변경 사항을 여러 파일로 분리하는 것보다 더 쉬울 수 있습니다. 이 추가 비용은 코드 분리 이유에 불리하게 작용합니다. 작은 응용 프로그램의 경우 이러한 비용을 들일 만한 가치가 없을 수도 있습니다.

    관련 패턴

    자세한 정보는 다음 관련 패턴을 참조하십시오.

  • 테이블 데이터 게이트웨이. 이 패턴은 데이터베이스 테이블에 대해 게이트웨이 역할을 하는 개체입니다. 한 인스턴스가 한 테이블의 모든 역할을 처리합니다. [Fowler03]

  • 바운드 데이터 제어. 이 패턴은 데이터 원본에 바인딩된 사용자 인터페이스 구성 요소로서 화면 또는 페이지에 렌더링할 수 있습니다.

    참고 자료

    [Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.

    Patterns Practices

  • Posted by 나비:D
    :
    BLOG main image
    by 나비:D

    공지사항

    카테고리

    분류 전체보기 (278)
    Programming? (0)
    ---------------------------.. (0)
    나비의삽질 (5)
    Application (177)
    SQL (51)
    Web (27)
    etc. (14)
    Omnia (0)
    ---------------------------.. (0)

    최근에 올라온 글

    최근에 달린 댓글

    최근에 받은 트랙백

    글 보관함

    달력

    «   2024/05   »
    1 2 3 4
    5 6 7 8 9 10 11
    12 13 14 15 16 17 18
    19 20 21 22 23 24 25
    26 27 28 29 30 31
    Total :
    Today : Yesterday :