14. Micro ORM인 Dapper 다루기

  • 38 minutes to read

데이터베이스 프로그래밍할 때 순수 ADO.NET을 사용해도 됩니다. 하지만, 좀 더 생산성과 유지 보수성을 높이려면 ORM 기술을 도입할 수 있습니다. ASP.NET에서 사용할 수 있는 ORM 기술중에서도 가볍고 빠른 Dapper를 사용하는 방법을 살펴보겠습니다.

14.1 Dapper(Micro ORM)를 사용하여 코드 생산성과 유지보수 편의성 높이기 지금까지 데이터 처리 관련 클래스들의 집합인 ADO.NET을 학습하였습니다. ADO.NET의 주요 클래스를 사용하여 데이터베이스 관련 프로그램을 작성하면 반복되는 코드가 발생하기 쉽다. 이때 ORM(Object Relational Mapper)이라는 프레임워크를 활용하면 데이터베이스 처리 관련 코드가 상당히 줄어들어 생산성을 향상시킬 수 있습니다. ORM 중에서는 특히 마이크로소프트에서 만들고 지원하는 Entity Framework가 널리 사용됩니다. 그러나 Entity Framework는 책 한 권으로 다룰 만한 방대한 내용이므로 이 책에서는 ORM의 작은 버전을 의미하는 Micro ORM인 Dapper를 사용하겠습니다. Dapper 또는 Dapper.NET이라 불리는 Micro ORM은 스택오버플로(StackOverflow)에서 만들고 사용하는 오픈 소스이면서 가볍고 빠른 ORM 도구다. ASP.NET 웹 앱은 항상 데이터베이스와 함께 사용되므로 앞서 살펴본 ADO.NET을 사용하여 DB와 연동하는 코드의 기본 패턴인 CRUD(Create, Read, Update, Delete)를 처리할 수 있습니다. 하지만 Dapper.NET을 사용하면 간결한 소스를 유지할 수 있고, 성능도 빠르며, 학습 시간도 단축됩니다.

<참고> 이번 강의에서 사용하는 Dapper에 대한 내용을 미리 살펴볼 수 있는 "Micro ORM인 Dapper를 사용하여 데이터베이스 처리 기능 구현하기"라는 제목의 동영상 강좌로도 마련하였으니 참고하기 바랍니다.  https://youtu.be/jXtNx40w18s  </참고>

14.1.1 ORM 장점 <추가 />  생산성(Productivity)  유지보수(Maintenance)  응용 프로그램 디자인(Application design)  관심의 분리(Clean separation of code)

14.1.2 ORM 종류 <추가 />  .NET 개발 환경  Entity Framework Core  Java 개발 환경  JPA + Hibernate

14.2 [실습] Micro ORM인 Dapper를 사용한 DB 코드 간소화하기 14.2.1 소개 웹 프로젝트를 진행할 때 데이터베이스 사용은 필수이며 매우 빈번히 사용됩니다. 이때 ASP.NET은 데이터베이스에 데이터를 저장하고 읽어오는 등의 일반적인 작업을 진행하며 ADO.NET이라는 데이터베이스 처리 기술을 사용합니다. 이를 좀더 사용하기 편리하게 만들어 놓은 기술이 바로 ORM 기술입니다. ORM 기술도 종류가 많지만, 이번 실습에서는 ORM 중에서도 가장 간결하고 속도가 제일 빠른 Dapper라는 Micro ORM으로 데이터베이스 처리를 연습하겠습니다. 전체적인 진행 순서는 다음과 같습니다.

  1. 프로젝트 생성 및 Dapper 참조 추가
  2. Model(모델) 클래스 및 LocalDb에 데이터베이스 생성
  3. 데이터 입력, 출력, 상세, 수정, 삭제에 대한 기본 코드 작성
  4. ASP.NET 웹 폼에서 CRUD 테스트

이번 실습을 통해서 명언(Max)을 입력하고 출력하는 간단한 앱을 만들어보겠습니다.

14.2.2 따라하기 (1) Visual Studio를 실행하고 ASP.NET 웹 응용 프로그램을 다음과 같이 생성합니다. 이름 위치 ASP.NET 템플릿 선택 폴더 및 핵심 참조 추가 DevDapper C:\ASP.NET ASP.NET 4.7 템플릿-Empty Web Forms, MVC, Web API

그림 14 1 DevDapper 프로젝트 생성

(2) <보기 > 다른 창 > 패키지 관리자 콘솔>을 클릭하여 패키지 관리자 콘솔을 실행하면 NuGet을 통해 Dapper에 대한 참조를 추가할 수 있습니다. 또는 직접 솔루션 탐색기의 <참조>에 마우스 오른쪽 버튼을 클릭하여 나타나는 메뉴에서 <NuGet 패키지 관리>를 선택해도 됩니다. 그림 14 2 패키지 관리자 콘솔 창 열기

(3) <패키지 관리자 콘솔> 창에서 그림과 같이 “Install-Package Dapper” 명령어를 입력하고, 엔터키를 입력합니다. <솔루션 탐색기>의 <참조> 영역에 Dapper 어셈블리가 추가됩니다. 이러한 NuGet 패키지는 NuGet 패키지 관리자에서 직접 검색해서 설치해도 됩니다.

그림 14 3 NuGet 명령어로 Dapper 추가하기

(4) 프로젝트의 Models 폴더에 마우스 오른쪽 버튼 클릭 후 <추가 > 클래스>를 클릭합니다. Maxim.cs란 이름으로 새 클래스를 추가하고 다음과 같이 클래스 속성을 입력합니다.

그림 14 4 Maxim 모델 클래스 추가하기

Maxim 클래스의 세부 코드는 다음과 같습니다.

using System;
 
namespace DevDapper.Models
{
    /// <summary>
    /// Maxim 클래스 === Maxims 테이블
    /// </summary>
    public class Maxim
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Content { get; set; }
        public DateTime CreationDate { get; set; }
    }
}

(5) 프로젝트에서 사용할 데이터베이스는 LocalDb다. 데이터베이스를 추가해보겠습니다. 프로젝트의 App_Data 폴더에 마우스 오른쪽 버튼을 클릭한 후 <추가 > 새 항목>을 클릭합니다. 화면에서 다음 그림과 같이 SQL Server 데이터베이스를 선택하고, DevDapper.mdf라는 이름을 지정한 뒤 추가 학습: 버튼을 클릭합니다. 참고로 App_Data 폴더에 데이터베이스를 생성하지 않고 직접 SQL Server 정식 버전에 구성해도 되지만, 이번 실습에서는 학습 목적으로 App_Data 폴더를 사용해보겠습니다.

그림 14 5 App_Data 폴더에 SQL Server 데이터베이스 추가하기

(6) SQL Server 데이터베이스인 LocalDb 데이터베이스가 추가된 프로젝트 모습입니다. 그림 14 6 LocalDB가 추가된 상태

(7) LocalDb 데이터베이스인 App_Data 폴더의 DevDapper.mdf 파일에 마우스 오른쪽 버튼을 클릭하여 <열기>를 실행하면 서버 탐색기를 통해서 데이터베이스가 연결됩니다.

그림 14 7 LocalDb 열기

(8) 서버 탐색기에서 DevDapper 데이터베이스가 연결된 모습입니다. 그림 14 8 서버 탐색기에서 데이터 연결

(9) App_Data 폴더에 DB를 만들면 기본적으로 <열기> 메뉴를 실행하면 서버 탐색기가 열린다. 하지만 Visual Studio에는 앞장에서 사용해봤던 좀 더 향상된 기능을 제공하는 SQL Server 개체 탐색기를 제공하기에 이를 사용해서 테이블을 생성하도록 하겠습니다.

<참고> 서버 탐색기에서 테이블 생성하기 다음 그림과 같이 서버 탐색기에서 DevDapper.mdf 데이터베이스의 테이블에 마우스 오른쪽 버튼을 클릭하여 <새 테이블 추가>를 실행하면 테이블을 추가할 수도 있습니다.

그림 14 9 서버 탐색기에서 <새 테이블 추가>

</참고>

(10) Visual Studio에서 <보기 > SQL Server 개체 탐색기>를 사용하여 SQL Server 개체 탐색기를 실행합니다. 그림 14 10 SQL Server 개체 탐색기 열기

(11) SQL Server 개체 탐색기에서 LocalDb의 기본 인스턴스 이름인 (localdb)\MSSQLLocalDB 항목을 선택하여 메뉴를 확장하면 다음 그림과 같이 App_Data 폴더에 생성한 DevDapper.mdf 데이터베이스가 목록으로 나타난다. 데이터베이스를 선택하면 데이터베이스에 자동 연결됩니다. 다음 그림은 SQL Server 개체 탐색기를 사용하여 LocalDb인 DevDapper.mdf에 접속한 모습입니다.

그림 14 11 SQL Server 개체 탐색기에서 LocalDB 열기

(12) SQL Server 개체 탐색기에서 DevDapper 데이터베이스를 연결한 후 테이블에 마우스 오른쪽 버튼을 클릭한 후 <새 테이블 추가>를 선택합니다. 그림 14 12 새 테이블 추가

테이블 생성 화면이 나타나면 다음과 같이 Maxims란 이름으로 테이블을 구성하고 저장합니다. Maxims 이름으로 테이블을 만들고 <업데이트> 버튼을 클릭하면 데이터베이스에 테이블이 생성됩니다. Maxim.cs 파일에서 C#의 클래스 이름은 단수형으로 작성했지만, 테이블 이름은 Maxims로 복수형으로 표현한 것을 주의하기 바랍니다. 그림 14 13 Maxims 테이블 생성하기

CREATE TABLE [dbo].[Maxims] (
    [Id]           INT            IDENTITY (1, 1) NOT NULL,
    [Name]         NVARCHAR (25)  NOT NULL,
    [Content]      NVARCHAR (140) NOT NULL,
    [CreationDate] DATETIME       DEFAULT (getdate()) NULL,
    PRIMARY KEY CLUSTERED ([Id] ASC)
);

(13) Web.config 파일에 다음과 같이 LocalDb 데이터베이스에 대한 연결문자열을 기록합니다. 그림 14 14 Web.config 파일에 데이터베이스 연결 문자열 설정

<connectionStrings>
	<!-- DevDapper.mdf 데이터베이스에 대한 연결 문자열 -->
	<add name="ConnectionString" 
		 connectionString="
			Data Source=(LocalDb)\MSSQLLocalDB;
			AttachDbFilename=|DataDirectory|DevDapper.mdf;
			Initial Catalog=DevDapper;
			Integrated Security=True;
		" 
		 providerName="System.Data.SqlClient" />
</connectionStrings>

웹 프로젝트의 App_Data 폴더에 있는 MDF 파일에 열결하는 데이터베이스 연결 문자열은 앞서 살펴본 모양과 조금은 다릅니다. AttachDbFilename 속성에 |DataDirectory|DvDapper.mdf 값이 지정된 것을 볼 수 있습니다.

(14) DevDapper 웹 프로젝트 루트에 Repositories란 폴더를 생성하고, MaximServiceRepository.cs 클래스 파일을 만듭니다. 그리고 다음과 같이 뼈대를 잡는 코드를 구성합니다. 네임스페이스 선언부에 using 구문을 사용하여 Dapper에 대한 참조를 추가합니다. Web.config 파일에 지정한 데이터베이스 연결문자열을 바탕으로 커넥션 개체인 db 개체를 생성하는 코드를 추가합니다. 완성된 전체 코드는 (15)번을 참고하라. 그림 14 15 MaximServiceRepository.cs 파일에 Dapper 관련 코드 뼈대 잡기

(15) MaximServiceRepository.cs 클래스 파일에 대한 전체 소스는 다음과 같습니다. 그림 14 16 MaximServiceRepository.cs 파일에 Dapper 관련 코드 완성

using Dapper;
using DevDapper.Models;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;

namespace DevDapper.Repositories
{
    /// <summary>
    /// 명언(Maxim) 서비스에 대한 DB 연동 코드 부분
    /// </summary>
    public class MaximServiceRepository
    {
        // Database Connection 개체 생성
        private IDbConnection db = new SqlConnection(
            ConfigurationManager.ConnectionStrings[
                "ConnectionString"].ConnectionString);

        // 입력
        public Maxim AddMaxim(Maxim model)
        {
            string sql = @"
                Insert Into Maxims (Name, Content) Values (@Name, @Content);
                Select Cast(SCOPE_IDENTITY() As Int);
            ";
            var id = this.db.Query<int>(sql, model).Single();
            model.Id = id;
            return model; 
        }

        // 출력
        public List<Maxim> GetMaxims()
        {
            string sql = 
                "Select Id, name, Content, CreationDate From Maxims Order By Id Asc";
            return this.db.Query<Maxim>(sql).ToList(); 
        }

        // 상세
        public Maxim GetMaximById(int id)
        {
            string sql = 
                "Select Id, name, Content, CreationDate From Maxims Where Id = @Id";
            return this.db.Query<Maxim>(sql, new { Id = id }).SingleOrDefault(); 
        }

        // 수정
        public Maxim UpdateMaxim(Maxim model)
        {
            string sql = 
                "Update Maxims Set Name = @Name, Content = @Content Where Id = @Id";
            this.db.Execute(sql, model);
            return model; 
        }

        // 삭제
        public void RemoveMaxim(int id)
        {
            string sql = "Delete Maxims Where Id = @Id";
            this.db.Execute(sql, new { Id = id }); 
        }
    }
}

ADO.NET의 커넥션, 커멘드, 데이터 리더 등의 클래스 사용 대신에 Dapper를 사용하여 코드가 훨씬 간결해진 것을 알 수 있습니다.

(16) 이제 앞서 생성한 리포지토리 클래스인 MaximServiceRepository.cs를 테스트해보겠습니다. 웹 폼 다섯 개를 다음 그림과 같이 웹 프로젝트 루트에 생성합니다.  FrmMaximWrite.aspx: 입력 테스트  FrmMaximList.aspx: 전체 출력 테스트  FrmMaximView.aspx: 상세 보기 테스트  FrmMaximModify.aspx: 수정 테스트  FrmMaximDelete.aspx: 삭제 테스트 그림 14 17 웹 폼을 사용한 DB 입출력 테스트 페이지 작성

(17) FrmMaximWrite.aspx 페이지와 이에 대한 코드 숨김 파일의 코드는 다음과 같습니다.

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="FrmMaximWrite.aspx.cs" Inherits="DevDapper.FrmMaximWrite" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>입력</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>    
        이름: <asp:TextBox ID="txtName" runat="server"></asp:TextBox>
        <br />
        명언: <asp:TextBox ID="txtContent" runat="server"></asp:TextBox>
        <br />
        <asp:Button ID="btnWrite" runat="server" Text="저장" 
            OnClick="btnWrite_Click" />
        <br />
        <asp:Label ID="lblDisplay" runat="server"></asp:Label>
        <hr />
        <asp:HyperLink ID="lnkList" runat="server" 
            NavigateUrl="~/FrmMaximList.aspx">리스트</asp:HyperLink>    
    </div>
    </form>
</body>
</html>
using System;
using DevDapper.Models;
using DevDapper.Repositories;

namespace DevDapper
{
    public partial class FrmMaximWrite : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        protected void btnWrite_Click(object sender, EventArgs e)
        {
            Maxim maxim = new Maxim();
            maxim.Name = txtName.Text;
            maxim.Content = txtContent.Text;

            MaximServiceRepository repo = new MaximServiceRepository();
            maxim.Id = repo.AddMaxim(maxim).Id;

            lblDisplay.Text = 
                maxim.Id.ToString() + "번 데이터가 입력되었습니다.";
        }
    }
}

(18) FrmMaximList.aspx 페이지와 이에 대한 코드 숨김 파일의 코드는 다음과 같습니다.

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="FrmMaximList.aspx.cs" Inherits="DevDapper.FrmMaximList" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>출력</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:GridView ID="lstMaxims" runat="server">
            <Columns>
                <asp:HyperLinkField Text="상세보기" 
                    DataNavigateUrlFormatString="~/FrmMaximView.aspx?Id={0}" 
                    DataNavigateUrlFields="Id" />
            </Columns>
        </asp:GridView>
        <hr />
        <asp:HyperLink ID="lnkWrite" runat="server" 
            NavigateUrl="~/FrmMaximWrite.aspx">입력</asp:HyperLink>
    </div>
    </form>
</body>
</html>
using System;
using System.Web.UI;
using DevDapper.Repositories;

namespace DevDapper
{
    public partial class FrmMaximList : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                DisplayData();
            }
        }

        private void DisplayData()
        {
            MaximServiceRepository repo = new MaximServiceRepository();

            this.lstMaxims.DataSource = repo.GetMaxims();
            this.lstMaxims.DataBind(); 
        }
    }
}

(19) FrmMaximView.aspx 페이지와 이에 대한 코드 숨김 파일의 코드는 다음과 같습니다.

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="FrmMaximView.aspx.cs" Inherits="DevDapper.FrmMaximView" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>상세</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        번호: <asp:Label ID="lblId" runat="server"></asp:Label>         <br />
        이름: <asp:Label ID="lblName" runat="server"></asp:Label>       <br />
        명언: <asp:Label ID="lblContent" runat="server"></asp:Label>    <br />
        <hr />
        <asp:HyperLink ID="btnModify" runat="server">수정</asp:HyperLink>
        <asp:HyperLink ID="btnDelete" runat="server">삭제</asp:HyperLink>
        <asp:HyperLink ID="lnkList" runat="server" 
            NavigateUrl="~/FrmMaximList.aspx">리스트</asp:HyperLink>
    </div>
    </form>
</body>
</html>
using System;	
using System.Web.UI;
using DevDapper.Models;
using DevDapper.Repositories;

namespace DevDapper
{
    public partial class FrmMaximView : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                DisplayData();
            }
        }

        private void DisplayData()
        {
            int id = Convert.ToInt32(Request.QueryString["Id"]);

            MaximServiceRepository repo = new MaximServiceRepository();
            Maxim maxim = repo.GetMaximById(id);

            this.lblId.Text = id.ToString();
            this.lblName.Text = maxim.Name;
            this.lblContent.Text = maxim.Content;

            this.btnModify.NavigateUrl = "FrmMaximModify.aspx?Id=" + id;
            this.btnDelete.NavigateUrl = "FrmMaximDelete.aspx?Id=" + id;
        }
    }
}

(20) FrmMaximModify.aspx 페이지와 이에 대한 코드 숨김 파일의 코드는 다음과 같습니다.

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="FrmMaximModify.aspx.cs" Inherits="DevDapper.FrmMaximModify" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>수정</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        번호: <asp:Label ID="lblId" runat="server"></asp:Label><br />
        이름: <asp:TextBox ID="txtName" runat="server"></asp:TextBox><br />
        명언: <asp:TextBox ID="txtContent" runat="server"></asp:TextBox><br />
        <asp:Button ID="btnModify" runat="server" Text="수정" 
            OnClick="btnModify_Click" /><br />
        <asp:Label ID="lblDisplay" runat="server"></asp:Label>    
        <hr />
        <asp:HyperLink ID="lnkList" runat="server" 
            NavigateUrl="~/FrmMaximList.aspx">리스트</asp:HyperLink>
    </div>
    </form>
</body>
</html>
using System;
using System.Web.UI;
using DevDapper.Models;
using DevDapper.Repositories;

namespace DevDapper
{
    public partial class FrmMaximModify : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!String.IsNullOrEmpty(Request.QueryString["Id"]))
            {
                if (!Page.IsPostBack)
                {
                    DisplayData();
                }
            }
            else
            {
                Response.Write("잘못된 요청입니다.");
                Response.End(); 
            }
        }

        private void DisplayData()
        {
            int id = Convert.ToInt32(Request.QueryString["Id"]);

            MaximServiceRepository repo = new MaximServiceRepository();
            Maxim maxim = repo.GetMaximById(id);

            this.lblId.Text = id.ToString();
            this.txtName.Text = maxim.Name;
            this.txtContent.Text = maxim.Content;
        }

        protected void btnModify_Click(object sender, EventArgs e)
        {
            Maxim maxim = new Maxim();
            // Id를 채워서 넘겨주자.
            maxim.Id = Convert.ToInt32(Request.QueryString["Id"]); 
            maxim.Name = txtName.Text;
            maxim.Content = txtContent.Text;

            MaximServiceRepository repo = new MaximServiceRepository();
            maxim = repo.UpdateMaxim(maxim);

            lblDisplay.Text = 
                maxim.Id.ToString() + "번 데이터가 수정되었습니다.";

            DisplayData();
        }
    }
}

(21) FrmMaximDelete.aspx 페이지와 이에 대한 코드 숨김 파일의 코드는 다음과 같습니다.

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="FrmMaximDelete.aspx.cs" Inherits="DevDapper.FrmMaximDelete" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>삭제</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Label ID="lblId" runat="server"></asp:Label>
        번 글을 삭제하시겠습니까?
        <asp:Button ID="btnDelete" runat="server" Text="삭제" 
            OnClick="btnDelete_Click" />
        <hr />
        <asp:HyperLink ID="lnkList" runat="server" 
            NavigateUrl="~/FrmMaximList.aspx">리스트</asp:HyperLink>
    </div>
    </form>
</body>
</html>
using System;
using System.Web.UI;
using DevDapper.Repositories;

namespace DevDapper
{
    public partial class FrmMaximDelete : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!String.IsNullOrEmpty(Request.QueryString["Id"]))
            {
                if (!Page.IsPostBack)
                {
                    lblId.Text = Request["Id"]; 
                }
            }
            else
            {
                Response.Write("잘못된 요청입니다.");
                Response.End();
            }
        }

        protected void btnDelete_Click(object sender, EventArgs e)
        {
            int id = Convert.ToInt32(Request.QueryString["Id"]);

            MaximServiceRepository repo = new MaximServiceRepository();
            repo.RemoveMaxim(id);

            // 리스트 페이지로 이동
            Response.RedirectPermanent("FrmMaximList.aspx"); 
        }
    }
}

(22) 입력 페이지를 웹 브라우저로 실행한 모습입니다. 그림 14 18 데이터 입력 페이지

데이터를 입력해하고 저장 버튼을 클릭합니다.
Figure 14 18 데이터 입력 페이지 저장 완료

(23) 다음 그림은 출력 페이지를 웹 브라우저로 실행한 모습입니다. Figure 14 19 데이터 출력 페이지 실행

(24) 다음 그림은 상세보기 페이지의 모습입니다. 그림처럼 반드시 쿼리스트링으로 Id=1 형식의 값을 전달해 주어야만 정상적으로 페이지가 실행됩니다. 그림 14 20 데이터 상세보기 페이지

(25) 다음 그림은 데이터를 수정하는 페이지입니다. 상세보기 페이지와 마찬가지로 쿼리스트링 값이 넘어와야 정상적으로 출력됩니다. 내용을 변경하고 <수정> 버튼을 클릭하면 데이터가 변경됩니다. 그림 14 19 데이터 수정 페이지

(26) 다음 그림은 데이터를 삭제하는 페이지입니다. 이곳도 마찬가지로 ?Id=1 형식의 쿼리스트링이 넘어와야 합니다. <삭제> 버튼을 클릭하면 현재 Id 값에 해당하는 데이터가 삭제되고 리스트 페이지로 이동합니다. 그림 14 20 데이터 삭제 페이지

14.2.3 마무리 이번 실습에서는 Micro ORM 기술인 Dapper를 사용하는 방법을 알아보았습니다. 이 기술로 데이터베이스에 대한 CRUD(Create, Read, Update, Delete) 처리를 ADO.NET 기술보다 좀 더 편하게 사용할 수 있습니다. 앞서 ADO.NET을 사용하여 데이터 입출력 등의 작업을 해보았는데 이 코드와 비교해보면 14장의 코드는 상당히 간결합니다. 이처럼 기본 기능은 ADO.NET으로 처리할 수 있지만, ORM 같은 추가적인 기술을 통해 개발 생산성을 높일 수 있습니다. 앞으로 진행되는 DB 처리는 순수 ADO.NET 기술보다는 Dapper 기술을 주로 사용해서 DB 처리에 대한 간결함과 생산성을 높이도록 하겠습니다.

<참고> 제목: 모델 클래스의 개념과 사용법? http://www.devlec.com/SAYBOARD_BBS/sayboard.say?mtype=C&group=qna&category=devlec_tutor&page=1&idx=27561&b_ref=27561&b_step=0&b_level=0

질문: 안녕하세요. 개발하면서 궁금한점이 있어서 질문 남깁니다. 모델 클래스를 작성할때의 가이드가 있을까 싶어서 여쭤봅니다. 예를 들면 회원테이블이 있고 거기에 회원가입, 수정, 로그인 등등의 DB에 접근해서 필요로하는 데이터의 컬럼개수랑 이런것들이 가입, 수정, 로그인 등에 따라 다른데요. 그렇다면 각각 어떤 동작에 따른 모델클래스를 일일이 다 만들어 주는 방법을 사용해도 되나요? 회원입력모델, 회원수정모델, 로그인모델 클래스 등등을 다 만들어서 필요할때 사용하는지 아니면 테이블과 1:1로 매핑된 모델클래스를 하나 만들고 그것을 이용해서 처리하는 것이 맞는지 궁금합니다. 항상 1:1로만 매핑되지 않기 때문에 이런 차이에서 오는 모델클래스를 각각 필요에 따라서 만드는것이 맞는 방법인지요?

답변: 안녕하세요. 데이터베이스의 테이블과 연관된 모델 클래스를 만드는 것과 관련된 정확한 가이드는 없는 것 같습니다. 개발자들 모임에서 얘기하는 중에 나왔던 주제인데요, 천재 개발자 하나가 나와서 "반드시 이러한 경우에는 이렇게 해야 합니다."라는 가이드를 제시해 주면 좋지 않을까라는 얘기를 했었지만, 현실은 그러한 가이드는 어디에도 존재하지 않는다는 것이지요. 그래도 100%까지는 아니더라도 많은 개발자들 사이에 융통되는 것이 있다면, 귀찮아도 업무에 따라서 모델 클래스를 만드는게 일반적인 것 같습니다. 다만, 회원 테이블을 예를 든다면,(테이블 1개마다 1개 클래스 생성) 기본 회원 테이블과 일대일로 매핑되는 클래스를 만들고, 이것 하나만 사용해서 처리해도 큰 문제는 없습니다. 그런 후 클래스를 더 만들어야 고민해야 하는 부분이 있다면, 회원의 암호 정보를 노출할 필요가 없는 페이지를 위한 또 다른 클래스를 만드는 것입니다. 단순 회원 정보 조회 페이지에서는 암호 정보가 필요없기에 이를 뺀 클래스를 사용하면 좋습니다. Web API에서 JSON으로 클라이언트에게 회원정보를 내려주는 경우에 잘못해서 JSON에 Password 정보가 포함되는 것을 1차적으로도 막아 줄 수 있구요. 제 경험이지만, 테이블과 저장 프로시저의 매개변수 및 반환 값 등에 대한 설계가 나오면, 그와 동일한 클래스를 여러 개 만드는 것부터 시작하게 됩니다. 테이블과 관련해서 자동으로 클래스 만들어주는 확장 기능도 찾아보면 있겠지만요. 저는 테이블의 구조(필드명)를 익히는 차원에서 타이핑으로 모델 클래스를 만듭니다. 테이블 이름이 Users 이면 모델 클래스는 User, UserModel, UserViewModel, UserDto, UserEntity 등의 이름으로 사용합니다. 저는 일반적인 경우에는 테이블과 매핑되는 클래스를 하나 통으로 만들어 놓고 이를 전체 업무로직에서 사용하되, 특정 업무 로직을 위한 클래스를 나누기도 합니다. 그런다음, 시간이 날 때 모델 클래스들을 보기 좋게 재정리하는 시간을 갖기도 합니다. 아무튼, 질문 주신 고민의 내용을 잘 정리해 놓은 자료가 어딘가에는 있을 수도 있겠지만요. 위에 답변드린 내용은 제 경험을 기준으로 말씀드렸으니, 참고로 봐주시면 감사하겠습니다. 이상입니다. </참고>

14.3 참고: Dapper CRUD 코드의 모든 경우의 수 다음 코드 조각들은 Dapper를 사용하여 처리할 수 있는 코드 유형의 대부분을 보여줍니다. Dapper로 코드 작성시 특정 코드 패턴이 기억나지 않을 땐 아래 코드들을 참고하면 됩니다.

using Dapper;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace DapperDemo.Models
{
    /// <summary>
    /// Tables 테이블과 일대일로 매핑되는 모델 클래스
    /// </summary>
    public class TableViewModel
    {
        public TableViewModel()
        {
            SubTableViewModel = new List<SubTableViewModel>();
        }

        /// <summary>
        /// 일련번호
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 비고
        /// </summary>
        public string Note { get; set; }
        /// <summary>
        /// 하위 테이블에 대한 참조
        /// </summary>
        public List<SubTableViewModel> SubTableViewModel { get; set; }
    }

    /// <summary>
    /// SubTables 테이블과 일대일로 매핑되는 모델 클래스: XXX, XXXModel, XXXViewModel
    /// </summary>
    public class SubTableViewModel
    {
        /// <summary>
        /// 일련번호
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 부모 테이블의 기본키
        /// </summary>
        public int TableId { get; set; }
        /// <summary>
        /// 비고
        /// </summary>
        public string Note { get; set; }
    }

    public interface ITableRepository
    {
        /// <summary>
        /// [0] 기본: DB 컨텍스트 개체를 바로 포함해서 사용
        /// </summary>
        List<TableViewModel> GetTables();

        /// <summary>
        /// [1] 입력 패턴
        /// </summary>
        TableViewModel Add(TableViewModel model);       

        /// <summary>
        /// [2] 출력 패턴(전체): GetAll(), GetTables()
        /// </summary>
        List<TableViewModel> GetAll();                 
        
        /// <summary>
        /// [3] 상세 패턴
        /// </summary>
        TableViewModel GetById(int id);                

        /// <summary>
        /// [4] 수정 패턴
        /// </summary>
        TableViewModel Update(TableViewModel model);   

        /// <summary>
        /// [6] 삭제 패턴
        /// </summary>
        void Remove(int id);                                

        /// <summary>
        /// [7] 검색 패턴
        /// </summary>
        List<TableViewModel> SearchTablesByNote(string note);

        /* More... */

        /// <summary>
        /// 입력: 입력 전용
        /// </summary>
        void AddOnly(TableViewModel model);

        /// <summary>
        /// 벌크 인서트
        /// </summary>
        int BulkInsertRecords(List<TableViewModel> records);

        /// <summary>
        /// 다중 레코드 검색
        /// </summary>
        List<TableViewModel> GetByIds(params int[] ids);

        /// <summary>
        /// 선택 삭제: 코드를 보충으로 추가
        /// </summary>
        void DeleteByIds(params int[] ids);

        /// <summary>
        /// Dynamic 출력
        /// </summary>
        List<dynamic> GetDynamicAll();

        /// <summary>
        /// 저장 프로시저 사용
        /// </summary>
        List<TableViewModel> GetAllWithSp();

        /// <summary>
        /// 매개변수가 있는 저장 프로시저 사용
        /// </summary>
        List<TableViewModel> GetByIdWithSp(int id);

        /// <summary>
        /// 저장 프로시저의 매개변수로 DynamicParameters 사용
        /// </summary>
        List<TableViewModel> GetByIdWithSpWithDynamicParamter(int id);

        /// <summary>
        /// 다중 테이블에서 데이터 가져오기
        /// </summary>
        TableViewModel GetMultiData(int id);

        /// <summary>
        /// 트랜잭션 처리: 다중 삭제 또는 다중 업데이트
        /// </summary>
        void RemoveWith(int id);

        /// <summary>
        /// Tables 테이블의 총 레코드 수 반환
        /// </summary>
        int GetTotalCount(); // 총 레코드 수

        /// <summary>
        /// 페이징 처리 후의 데이터 리스트
        /// </summary>
        List<TableViewModel> GetAllWithPaging(int pageIndex, int pageSize);

        /// <summary>
        /// 페이징 처리된 리스트: 인라인 SQL 사용
        /// </summary>
        List<TableViewModel> GetAllWithPagingInline(int pageIndex, int pageSize = 10);

        /// <summary>
        /// 저장 프로시저의 OUTPUT 매개변수 사용
        /// </summary>
        string GetTablesNoteByIdWithOutput(int id);

        /// <summary>
        /// 날짜 검색
        /// </summary>
        List<TableViewModel> SearchTablesByDate(string start, string end);

        /// <summary>
        /// 특정 Id에 해당하는 레코드의 Note 컬럼의 값이 있는지 없는지(NULL) 체크
        /// </summary>
        bool IsExistNoteById(int id);
    }

    public class TableRepository : ITableRepository
    {
        private IDbConnection db;

        public TableRepository()
        {
            db = new SqlConnection(
                ConfigurationManager.ConnectionStrings[
                    "ConnectionString"].ConnectionString);
        }

        /// <summary>
        /// 기본: DB 컨텍스트 개체를 바로 포함해서 사용
        /// </summary>
        public List<TableViewModel> GetTables()
        {
            using (IDbConnection ctx = new SqlConnection(
                ConfigurationManager.ConnectionStrings[
                    "ConnectionString"].ConnectionString))
            {
                if (ctx.State == ConnectionState.Closed)
                {
                    ctx.Open();
                }

                string sql = "Select * From Tables";
                return ctx.Query<TableViewModel>(sql).ToList();
            }
        }

        /// <summary>
        /// 입력: 입력 전용
        /// </summary>
        public void AddOnly(TableViewModel model)
        {
            var sql = "Insert Into Tables (Note) Values (@Note); ";

            db.Execute(sql, model);
        }

        /// <summary>
        /// 입력: 입력 후 Identity 값 반환
        /// </summary>
        public TableViewModel Add(TableViewModel model)
        {
            var sql =
                "Insert Into Tables (Note) Values (@Note); " +
                "Select Cast(SCOPE_IDENTITY() As Int);";

            var id = db.Query<int>(sql, model).Single();

            model.Id = id;
            return model;
        }

        /// <summary>
        /// 출력
        /// </summary>
        public List<TableViewModel> GetAll()
        {
            string sql = "Select * From Tables";
            return db.Query<TableViewModel>(sql).ToList();
        }

        /// <summary>
        /// 상세: 매개 변수 전달 방식 종류 확인(모델, 익명 형식, 개체 이니셜라이저, ...)
        /// </summary>
        public TableViewModel GetById(int id)
        {
            string sql = "Select * From Tables Where Id = @Id";
            return db.Query<TableViewModel>(sql, new { id }).SingleOrDefault();
        }

        /// <summary>
        /// 상세 패턴: 저장 프로시저 사용
        /// </summary>
        /// <param name="uid">Id</param>
        /// <returns>T</returns>
        //public UserModel GetUserInfo(int uid)
        //{
        //     저장 프로시저 이름 또는 인라인 SQL 문(Ad HOC 쿼리)
        //    string sql = "GetUsers";

        //     파라미터 추가
        //    var parameters = new DynamicParameters();
        //    parameters.Add("@UID", value: uid, 
        //        dbType: DbType.Int32, direction: ParameterDirection.Input);

        //     저장 프로시저 실행
        //    return db.Query<UserModel>(sql, parameters, 
        //        commandType: CommandType.StoredProcedure).SingleOrDefault();
        //}

        /// <summary>
        /// 수정
        /// </summary>
        public TableViewModel Update(TableViewModel model)
        {
            var sql =
                "Update Tables                  " +
                "Set                            " +
                "    Note       =       @Note   " +
                "Where Id = @Id                 ";
            db.Execute(sql, model);
            return model;
        }

        /// <summary>
        /// 삭제
        /// </summary>
        public void Remove(int id)
        {
            string sql = "Delete From Tables Where Id = @Id";
            db.Execute(sql, new { Id = id });
        }

        /// <summary>
        /// 검색 조건 처리시 Like 절 처리 : Note Like N'%홍길동10%'
        /// </summary>
        public List<TableViewModel> SearchTablesByNote(string note)
        {
            string sql =
                "Select * From Tables Where Note Like N'%' + @Note + '%' ";
            return db.Query<TableViewModel>(sql, new { Note = note }).ToList();
        }

        /// <summary>
        /// 벌크 인서트
        /// </summary>
        public int BulkInsertRecords(List<TableViewModel> records)
        {
            if (db.State != ConnectionState.Open)
            {
                db.Open();
            }

            var sql =
                "Insert Into Tables (Note) Values (@Note); " +
                "Select Cast(SCOPE_IDENTITY() As Int);";

            return db.Execute(sql, records);
        }

        /// <summary>
        /// 다중 레코드 검색
        /// </summary>
        public List<TableViewModel> GetByIds(params int[] ids)
        {
            string sql = "Select * From Tables Where Id In @Ids";
            return db.Query<TableViewModel>(
                sql, new { Ids = ids }).ToList();
        }

        /// <summary>
        /// 선택 삭제: 코드를 보충으로 추가
        /// </summary>
        public void DeleteByIds(params int[] ids)
        {
            string sql = "Delete Tables Where Id In @Ids";
            db.Execute(sql, new { Ids = ids });
        }

        /// <summary>
        /// Dynamic 출력
        /// </summary>
        public List<dynamic> GetDynamicAll()
        {
            string sql = "Select * From Tables";
            return db.Query(sql).ToList();
        }

        /// <summary>
        /// 저장 프로시저 사용
        /// </summary>
        public List<TableViewModel> GetAllWithSp()
        {
            string sql = "GetTables";
            return db.Query<TableViewModel>(
                sql, commandType: CommandType.StoredProcedure).ToList();
        }

        /// <summary>
        /// 매개변수가 있는 저장 프로시저 사용
        /// </summary>
        public List<TableViewModel> GetByIdWithSp(int id)
        {
            string sql = "GetTableById";
            return db.Query<TableViewModel>(sql, new { Id = id },
                commandType: CommandType.StoredProcedure).ToList();
        }

        /// <summary>
        /// 저장 프로시저의 매개변수로 DynamicParameters 사용
        /// </summary>
        public List<TableViewModel> GetByIdWithSpWithDynamicParamter(int id)
        {
            string sql = "GetTableById";

            var parameters = new DynamicParameters();

            parameters.Add("@Id", 
                value: id, 
                dbType: DbType.Int32, 
                direction: ParameterDirection.Input);
            //parameters.Add("@Note", "노트");

            return db.Query<TableViewModel>(sql, parameters,
                commandType: CommandType.StoredProcedure).ToList();

            //parameters.Get<int>("@Id");
        }

        /// <summary>
        /// 다중 테이블에서 데이터 가져오기
        /// </summary>
        public TableViewModel GetMultiData(int id)
        {
            var sql =
                "Select * From Tables Where Id = @Id; " +
                "Select * From SubTables Where TableId = @Id ";

            using (var multiRecords = db.QueryMultiple(sql, new { Id = id }))
            {
                var table = 
                    multiRecords.Read<TableViewModel>().SingleOrDefault();
                var subTable =
                    multiRecords.Read<SubTableViewModel>().ToList();
                if (table != null && subTable != null)
                {
                    table.SubTableViewModel.AddRange(subTable); 
                }

                return table;
            }
        }

        /// <summary>
        /// 트랜잭션 처리: 다중 삭제 또는 다중 업데이트
        /// </summary>
        public void RemoveWith(int id)
        {
            using (var tran = new TransactionScope())
            {
                var sqlTables = "Delete Tables Where Id = @Id";
                db.Execute(sqlTables, new { Id = id });

                var sqlSubTables = "Delete SubTables Where TableId = @Id";
                db.Execute(sqlSubTables, new { Id = id });

                tran.Complete(); 
            }
        }

        /// <summary>
        /// 페이징 처리 후의 데이터 리스트
        /// </summary>
        /// <param name="pageIndex">페이지 인덱스(페이지 번호 - 1)</param>
        /// <param name="pageSize">한 페이지에서 보여줄 레코드 수</param>
        public List<TableViewModel> GetAllWithPaging(
            int pageIndex, int pageSize = 10)
        {
            // 인라인 SQL(Ad Hoc 쿼리) 또는 저장 프로시저 지정
            string sql = "GetTablesWithPaging"; // 페이징 저장 프로시저

            // 파라미터 추가
            var parameters = new DynamicParameters();
            parameters.Add("@PageIndex",
                value: pageIndex,
                dbType: DbType.Int32,
                direction: ParameterDirection.Input);
            parameters.Add("@PageSize", 
                value: pageSize, 
                dbType: DbType.Int32, 
                direction: ParameterDirection.Input);

            // 실행
            return db.Query<TableViewModel>(sql, parameters,
                commandType: CommandType.StoredProcedure).ToList();
        }
        
        /// <summary>
        /// 페이징 처리된 리스트: 인라인 SQL 사용
        /// SELECT * FROM Tables ORDER BY Id 
        /// OFFSET @PageSize * (@PageIndex - 1) ROWS FETCH NEXT @PageSize ROWS ONLY; 
        /// </summary>
        public List<TableViewModel> GetAllWithPagingInline(
            int pageIndex, int pageSize = 10)
        {
            string sql = @"
                Select Id, Note
                From 
                    (
                        Select Row_Number() Over (Order By Id Desc) As RowNumbers, 
                        Id, Note From Tables
                    ) As TempRowTables
                Where 
                    RowNumbers
                        Between
                            (@PageIndex * @PageSize + 1)
                        And
                            (@PageIndex + 1) * @PageSize
            ";
            return db.Query<TableViewModel>(
                sql, new { PageIndex = pageIndex, PageSize = pageSize }).ToList();
        }

        /// <summary>
        /// Tables 테이블의 총 레코드 수 반환
        /// </summary>
        public int GetTotalCount()
        {
            var sql = "Select Count(*) From Tables";

            return db.Query<int>(sql).Single();
        }

        /// <summary>
        /// 저장 프로시저의 OUTPUT 매개변수 사용
        /// </summary>
        public string GetTablesNoteByIdWithOutput(int id)
        {
            string sql = "GetTablesNoteByIdWithOutput";

            var parameters = new DynamicParameters();

            parameters.Add("@Id",
                value: id,
                dbType: DbType.Int32,
                direction: ParameterDirection.Input);
            parameters.Add("@Note", 
                value: "",
                dbType: DbType.String, 
                direction: ParameterDirection.InputOutput);

             db.Execute(sql, parameters,
                commandType: CommandType.StoredProcedure);

            return parameters.Get<string>("@Note"); // OUTPUT 매개변수
        }

        /// <summary>
        /// 날짜 검색
        /// </summary>
        public List<TableViewModel> SearchTablesByDate(
            string start, string end)
        {
            string sql = @"
                Select * From Tables 
                Where TimeStamp Between @StartDate And @EndDate";
            return db.Query<TableViewModel>(sql, new {
                StartDate= start, EndDate = end }).ToList();
        }
        
        // 날짜 차이
        //string sql = @"
        //        Select Top 10 UID 
        //        From Users 
        //        Where 
        //            LastLoginDate Is Not Null 
        //            And 
        //            DATEDIFF(day, LastLoginDate, GetDate()) > 365 
        //    ";

        /// <summary>
        /// 특정 Id에 해당하는 레코드의 Note 컬럼의 값이 있는지 없는지(NULL) 체크
        /// </summary>
        public bool IsExistNoteById(int id)
        {
            var sql = "Select Note From Tables Where Id = @Id";

            // Single() : null 값이면 예외 발생(에러가 발생합니다.)
            // SingleOrDefault(): 값이 없으면 null 값을 반환합니다. 
            var result = db.Query<string>(sql, new { Id = id }).SingleOrDefault();

            if (result == null)
            {
                return false;
            }

            return true; 
        }
    }
}

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com