ASP.NET 마스터링하는 방법: 사용자 지정 엔터티 클래스 소개

 

칼 세긴

2005년 3월

요약: 형식화되지 않은 데이터 세트가 데이터 조작에 가장 적합한 솔루션이 아닐 수 있는 경우가 있습니다. 이 가이드의 목표는 DataSets 의 대안인 사용자 지정 엔터티 및 컬렉션을 살펴보는 것입니다. (32페이지 인쇄)

콘텐츠

소개
데이터 세트 문제
사용자 지정 엔터티 클래스
Object-Relational 매핑
사용자 지정 컬렉션
관계 관리
기본 사항 외
결론

소개

ADODB의 일입니다. RecordSet 및 잊어버린 MoveNext는 사라졌으며 Microsoft ADO.NET 강력하고 유연한 기능으로 대체되었습니다. 새로운 무기는 번개처럼 빠른 DataReaders, 기능이 풍부한 DataSets를 갖춘 System.Data 네임스페이스이며, 지원되는 개체 지향 모델에 패키징됩니다. 우리가 그러한 도구를 처분할 수 있다는 것은 놀라운 일이 아닙니다. 모든 3계층 아키텍처는 견고한 DAL(데이터 액세스 계층)을 사용하여 데이터 계층을 비즈니스 계층에 우아하게 연결합니다. 품질 DAL은 코드 재사용을 촉진하고, 좋은 성능의 핵심이며, 완전히 투명합니다.

도구가 발전함에 따라 개발 패턴도 발전했습니다. MoveNext에게 작별 인사를 하는 것은 번거로운 구문을 없애는 것 이상이었습니다. 연결이 끊긴 데이터에 대한 마음을 열었고, 이는 애플리케이션을 빌드하는 방법에 큰 영향을 미쳤습니다.

DataReaders는 익숙했을 수도 있지만(RecordSets처럼 동작) DataAdapters,DataSets,DataTablesDataViews를 탐색하는 데는 그리 오랜 시간이 걸리지 않았습니다. 이러한 새로운 개체를 악용하는 기술이 성장하면서 개발 방식이 바뀌었습니다. 연결이 끊어진 데이터를 통해 새로운 캐싱 기술을 활용할 수 있으므로 애플리케이션의 성능이 크게 향상되었습니다. 이러한 클래스의 기능을 통해 보다 스마트하고 강력한 함수를 작성하는 동시에 일반적인 활동에 필요한 코드 양을 줄일 수 있습니다.

프로토타입, 소규모 시스템 및 지원 유틸리티와 같이 DataSets 가 특히 적합한 여러 상황이 있습니다. 그러나 출시 시간보다 유지 관리의 용이도가 더 중요한 엔터프라이즈 시스템에서 이를 사용하는 것이 최선의 솔루션이 아닐 수 있습니다. 이 가이드의 목표는 사용자 지정 엔터티 및 컬렉션과 같은 유형의 작업에 맞춰진 DataSets 에 대한 대안을 살펴보는 것입니다. 다른 대안이 있지만 동일한 기능을 제공하거나 더 많은 지원을 제공하는 것은 없습니다. 첫 번째 작업은 해결하려는 문제를 이해하기 위해 DataSets 의 단점을 살펴보는 것입니다.

모든 솔루션에는 고유한 장점과 단점이 있으므로 DataSets 의 단점이 사용자 지정 엔터티(여기서도 설명)보다 더 선호될 수 있습니다. 사용자와 팀은 프로젝트에 더 적합한 솔루션을 결정해야 합니다. 변경 요구 사항의 특성과 실제로 코드를 개발하는 것보다 사후 프로덕션에 더 많은 시간을 소비할 가능성을 포함하여 솔루션의 총 비용을 고려해야 합니다. 마지막으로 DataSets를 참조할 때 실제로 untyped-DataSets와 관련된 몇 가지 단점을 해결하는 typed-DataSets를 의미하지는 않습니다.

데이터 세트 문제

추상화 부족

대안을 고려해야 하는 첫 번째이자 가장 명백한 이유는 DataSet에서 코드를 데이터베이스 구조와 분리할 수 없기 때문입니다. DataAdapters 는 코드를 기본 데이터베이스 공급업체(Microsoft, Oracle, IBM, ...)에 맹목적으로 만드는 작업을 수행하지만 데이터베이스의 핵심 구성 요소인 테이블, 열 및 관계를 추상화하지 못합니다. 이러한 핵심 데이터베이스 구성 요소는 DataSet의 핵심 구성 요소이기도 합니다.DataSets 및 데이터베이스는 일반적인 구성 요소 이상의 것을 공유합니다. 아쉽게도 스키마도 공유합니다. 다음 select 문을 지정합니다.

SELECT UserId, FirstName, LastName
   FROM Users

DataSet 내의 UserId,FirstNameLastNameDataColumns에서 값을 사용할 수 있습니다.

왜 그렇게 나쁜가요? 기본적인 일상적인 예를 살펴보겠습니다. 먼저 간단한 DAL 함수가 있습니다.

'Visual Basic .NET
Public Function GetAllUsers() As DataSet
 Dim connection As New SqlConnection(CONNECTION_STRING)
 Dim command As SqlCommand = New SqlCommand("GetUsers", connection)
 command.CommandType = CommandType.StoredProcedure
 Dim da As SqlDataAdapter = New SqlDataAdapter(command)
 Try
  Dim ds As DataSet = New DataSet
  da.Fill(ds)
  Return ds
 Finally
  connection.Dispose()
  command.Dispose()
  da.Dispose()
 End Try
End Function

//C#
public DataSet GetAllUsers() {
 SqlConnection connection = new SqlConnection(CONNECTION_STRING);
 SqlCommand command = new SqlCommand("GetUsers", connection);
 command.CommandType = CommandType.StoredProcedure;
 SqlDataAdapter da = new SqlDataAdapter(command);
 try {
  DataSet ds = new DataSet();
  da.Fill(ds);
  return ds;
 }finally {
  connection.Dispose();
  command.Dispose();
  da.Dispose();
 }            
}

다음으로 모든 사용자를 표시하는 반복기가 있는 페이지가 있습니다.

<HTML>
 <body>
   <form id="Form1" method="post" runat="server">
     <asp:Repeater ID="users" Runat="server">
        <ItemTemplate>
           <%# DataBinder.Eval(Container.DataItem, "FirstName") %>
           <br />
        </ItemTemplate>
     </asp:Repeater>
   </form>
 </body>
</HTML>
<script runat="server">
  public sub page_load
     users.DataSource = GetAllUsers()
     users.DataBind()
  end sub
</script>

여기서 볼 수 있듯이 ASPX 페이지에서는 반복기의 DataSource에 대해 DAL 함수 GetAllUsers를 사용합니다. 어떤 이유로든 데이터베이스 스키마가 변경되면(성능 저하, 명확성을 위한 정규화, 요구 사항 변경) 변경 내용이 ASPX, 즉 Databinder.Eval 줄로 흘러 "FirstName" 열 이름을 사용합니다. 그러면 즉시 빨간색 플래그가 표시됩니다. 데이터베이스 스키마가 ASPX 코드로 흘러들어오나요? N 계층으로 들리지 않나요?

간단한 열 이름 바꾸기만 하면 이 예제의 변경 내용은 복잡하지 않습니다. 그러나 GetAllUsers 가 수많은 장소에서 사용되거나 웹 서비스로 노출되어 수많은 소비자에게 먹이를 주면 어떨까요? 변경 내용이 얼마나 쉽고 안전하게 전파될 수 있나요? 이 기본 예제의 경우 저장 프로시저 자체는 추상화 계층으로 사용되므로 충분할 수 있습니다. 그러나 이에 대한 가장 기본적인 보호를 제외한 모든 것에 대한 저장 프로시저에 의존하면 도로 에서 더 큰 문제가 발생할 수 있습니다. 이것을 하드 코딩의 한 형태로 생각하십시오. 기본적으로 DataSets 를 사용하는 경우 열 이름 또는 서수 위치를 사용하는지 여부에 관계없이 데이터베이스 스키마와 애플리케이션/비즈니스 계층 간에 엄격한 연결을 만들 수 있습니다. 과거의 경험(또는 논리)이 하드 코딩이 유지 관리 및 향후 개발에 미치는 영향을 가르쳐 주셨으면 합니다.

DataSets가 적절한 추상화에 실패하는 또 다른 방법은 개발자에게 기본 스키마를 알도록 요구하는 것입니다. 기본 지식이 아니라 열 이름, 형식 및 관계에 대한 완전한 지식에 대해 이야기합니다. 이 요구 사항을 제거하면 방금 본 것처럼 코드가 중단될 가능성이 낮아질 뿐만 아니라 더 쉽게 작성하고 유지 관리할 수 있습니다. 간단히 말하면 다음과 같습니다.

Convert.ToInt32(ds.Tables[0].Rows[i]["userId"]);

은 읽기 어렵고 열 이름 및 해당 형식에 대한 친밀한 지식이 필요합니다. 이상적으로 비즈니스 계층은 기본 데이터베이스, 데이터베이스 스키마 또는 SQL에 대해 아무것도 알지 않습니다. 이전 코드 문자열에서 표현된 대로 DataSets 를 사용하는 경우( CodeBehind 를 사용하면 더 나은 것은 아닙니다) 비즈니스 계층이 매우 얇을 수 있습니다.

Weakly-Typed

DataSets 는 약한 형식이므로 오류가 발생하기 쉬울 수 있으며 개발 노력에 영향을 줄 수 있습니다. 즉 , DataSet 에서 값을 검색할 때마다 변환해야 하는 System.Object 형식으로 제공됩니다. 실행하는 위험은 변환이 실패한다는 것입니다. 아쉽게도 이 오류는 컴파일 시간에 발생하지 않고 런타임에 발생합니다. 또한 Microsoft Visual Studio.NET(VS.NET)과 같은 도구는 약한 형식의 개체와 관련하여 개발자를 지원하는 데 별로 도움이 되지 않습니다. 이전에 스키마에 대한 심층적인 지식이 필요한 것에 대해 이야기했을 때 이것이 의미하는 바입니다. 다시 한 번 일반적인 예제를 살펴보겠습니다.

'Visual Basic.NET
Dim userId As Integer = 
?      Convert.ToInt32(ds.Tables(0).Rows(0)("UserId"))
Dim userId As Integer = CInt(ds.Tables(0).Rows(0)("UserId"))
Dim userId As Integer = CInt(ds.Tables(0).Rows(0)(0))

//C#
int userId = Convert.ToInt32(ds.Tables[0].Rows[0]("UserId"));

이 코드는 DataSet에서 값을 검색하는 가능한 방법을 나타냅니다. 코드가 여기저기서 이 값을 가질 가능성이 있습니다(변환을 수행하지 않고 Visual Basic .NET을 사용하는 경우 Option Strict off가 있을 수 있습니다. 이 경우 더 깊은 문제가 발생할 수 있습니다).

아쉽게도 이러한 각 줄은 다음과 같은 수많은 런타임 오류를 생성할 수 있습니다.

  1. 다음과 같은 이유로 변환이 실패할 수 있습니다.
    • 값은 null일 수 있습니다.
    • 개발자가 기본 데이터 형식에 대해 잘못되었을 수 있습니다(다시 한 번 데이터베이스 스키마에 대한 자세한 지식이 필요).
    • 서수 값을 사용하는 경우 실제로 X 위치에 있는 열을 아는 사람입니다.
  2. Ds. Tables(0) 는 Null 참조를 반환할 수 있습니다(DAL 메서드 또는 저장 프로시저의 일부가 실패한 경우).
  3. "UserId"는 다음과 같은 이유로 유효한 열 이름이 아닐 수 있습니다.
    • 이름이 변경되었을 수 있습니다.
    • 저장 프로시저에서 반환되지 않을 수 있습니다.
    • 오타가 있을 수 있습니다.

코드를 수정하고 더 방어적으로 작성할 수 있습니다. 즉 , null/nothing 에 대한 검사를 추가하고 변환을 중심으로 try/catch 를 추가하면 개발자에게 도움이 됩니다.

최악의 상황은 우리가 설명한 것처럼 추상화되지 않는다는 것입니다. 이는 dataSet에서 userId를 가져올 때마다 이전에 나열된 위험을 실행하거나 동일한 방어 단계를 다시 프로그래밍해야 한다는 것입니다(허용된 경우 유틸리티 함수는 이를 완화하는 데 도움이 됩니다). 약한 형식의 개체는 항상 자동으로 검색되고 쉽게 수정되는 디자인 타임 또는 컴파일 시간에서 프로덕션에서 노출될 위험이 있고 정확히 파악하기 어려운 런타임으로 오류를 이동합니다.

Object-Oriented 않음

DataSets가 개체이고 C# 및 Visual Basic .NET이 OO(개체 지향) 언어라고 해서 개체 지향 언어를 자동으로 사용할 수는 없습니다. OO 프로그래밍의 "hello world"는 일반적으로 Employee 클래스에서 하위 클래스로 분류되는 Person 클래스입니다. 그러나 DataSets는 이러한 유형의 상속 또는 대부분의 다른 OO 기술을 가능하거나 최소한 자연스럽고 직관적으로 만들지 않습니다. 클래스 엔터티의 솔직한 지지자인 Scott Hanselman은 가장 잘 설명합니다.

"DataSet이 개체인가요? 하지만 도메인 개체가 아니라 'Apple' 또는 'Orange'가 아니라 'DataSet' 형식의 개체입니다. DataSet은 지원 데이터 저장소에 대해 알고 있는 그릇입니다. DataSet은 행 및 열을 보유하는 방법을 알고 있는 개체입니다. 데이터베이스에 대해 LOT를 알고 있는 개체입니다. 그러나 나는 그릇을 반환하고 싶지 않아. 'Apples'와 같은 도메인 개체를 반환하려고 합니다." 1

DataSets는 데이터를 관계형 형식으로 유지하므로 관계형 데이터베이스에서 강력하고 쉽게 사용할 수 있습니다. 아쉽게도 이는 OO의 모든 이점을 잃게 된다는 것을 의미합니다.

DataSets는 도메인 개체 역할을 할 수 없으므로 기능을 추가할 수 없습니다. 일반적으로 개체에는 클래스의 instance 대해 동작하는 필드, 속성 및 메서드가 있습니다. 예를 들어 User 개체와 연결된 Promote 또는 CalcuateOvertimePay 함수가 있을 수 있습니다. 이 함수는 someUser.Promote() 또는 someUser.CalculateOverTimePay()를 통해 깔끔하게 호출할 수 있습니다. 메서드를 DataSet에 추가할 수 없으므로 유틸리티 함수를 사용하고, 약한 형식의 개체를 처리하고, 코드 전체에 하드 코딩된 값의 인스턴스가 더 많이 분산되어야 합니다. 기본적으로 DataSet 에서 데이터를 계속 가져오거나 로컬 변수에 번거롭게 저장하여 전달하는 절차 코드로 끝납니다. 두 방법 모두 단점이 있으며 장점도 없습니다.

DataSet에 대한 사례

데이터 액세스 계층의 아이디어가 DataSet 을 반환하는 것이라면 몇 가지 중요한 이점이 누락될 수 있습니다. 그 이유 중 하나는 얇거나 존재하지 않는 비즈니스 계층을 사용할 수 있기 때문에 무엇보다도 추상화 능력을 제한할 수 있기 때문입니다. 또한 미리 빌드된 제네릭 솔루션을 사용하므로 OO 기술을 활용하기가 어렵습니다. 마지막으로 Visual Studio.NET 같은 도구는 DataSets 와 같이 약한 형식의 개체를 사용하여 개발자에게 쉽게 권한을 부여할 수 없으므로 생산성을 줄이면서 버그의 가능성을 높일 수 있습니다.

이러한 모든 요소는 어떤 식으로든 코드의 유지 관리에 직접적인 영향을 줍니다. 추상화가 부족하면 기능 변경 및 버그 수정이 더 많이 관련되고 위험해집니다. 코드 재사용 또는 OO에서 제공하는 가독성 향상을 최대한 활용할 수 없습니다. 물론 개발자는 비즈니스 논리 또는 프레젠테이션 논리에서 작업하든 기본 데이터 구조에 대해 잘 알고 있어야 합니다.

사용자 지정 엔터티 클래스

DataSets와 관련된 대부분의 문제는 잘 정의된 비즈니스 계층 내에서 OO 프로그래밍의 풍부한 기능을 활용하여 해결할 수 있습니다. 기본적으로 관계형으로 구성된 데이터(데이터베이스)를 가져와서 개체(코드)로 사용하려고 합니다. 자동차에 대한 정보를 보유하는 DataTable 대신 실제로 자동차 개체(사용자 지정 엔터티 또는 도메인 개체라고 함)가 있다는 생각입니다.

사용자 지정 엔터티를 살펴보기 전에 먼저 직면하게 될 과제를 살펴보겠습니다. 가장 확실한 것은 필요한 코드 양입니다. 단순히 데이터를 가져오고 DataSet을 자동으로 채우는 대신 데이터를 가져와서 먼저 만들어야 하는 사용자 지정 엔터티에 수동으로 매핑합니다. 반복적인 작업이므로 코드 생성 도구 또는 O/R 매퍼를 사용하여 완화할 수 있습니다(나중에 자세히 설명). 더 큰 문제는 관계형에서 개체 세계로 데이터를 매핑하는 실제 프로세스입니다. 간단한 시스템의 경우 매핑은 대부분 간단하지만 복잡성이 커짐에 따라 두 세계의 차이점이 문제가 될 수 있습니다. 예를 들어 개체 세계에서 코드 재사용과 유지 관리를 돕는 핵심 기술은 상속입니다. 아쉽게도 상속은 관계형 데이터베이스에 대한 외적인 개념입니다. 또 다른 예는 개체 세계가 별도의 개체에 대한 참조를 유지하고 관계형 세계가 외래 키를 사용하는 관계형 세계와 관계를 다루는 차이점입니다.

관계형 데이터와 개체 간의 차이와 함께 코드 양이 증가함에 따라 이 방법이 더 복잡한 시스템에 적합하지 않은 것처럼 들릴 수 있지만 반대의 경우도 마찬가지입니다. 복잡한 시스템은 단일 계층(다시 자동화할 수 있는) 매핑 프로세스에서 어려움을 격리하여 이 접근 방식을 얻습니다. 또한 이 접근 방식은 이미 매우 널리 사용되고 있으며, 이는 추가된 복잡성을 깔끔하게 처리하기 위해 많은 디자인 패턴이 존재한다는 것을 의미합니다. 이전에 복잡한 시스템의 데이터 세트와 함께 설명한 DataSets 의 단점을 확대하면 빌드에 어려움을 겪는 시스템이 변경될 수 없게 됩니다.

사용자 지정 엔터티란?

사용자 지정 엔터티는 비즈니스 도메인을 나타내는 개체입니다. 따라서 비즈니스 계층의 기초입니다. 사용자 인증 구성 요소(이 가이드 전체에서 사용할 예제)가 있는 경우 사용자역할 개체가 있을 수 있습니다. 전자 상거래 시스템에는 공급업체상품 개체가 있을 수 있으며 부동산 회사에는 주택,객실 및 주소가 있을 수 있습니다. 코드 내에서 사용자 지정 엔터티는 단순히 클래스입니다(OO 프로그래밍에 사용되므로 엔터티와 클래스 간에는 상당히 긴밀한 상관 관계가 있습니다). 일반적인 User 클래스는 다음과 같습니다.

'Visual Basic .NET
Public Class User
#Region "Fields and Properties"
 Private _userId As Integer
 Private _userName As String
 Private _password As String
 Public Property UserId() As Integer
  Get
   Return _userId
  End Get
  Set(ByVal Value As Integer)
    _userId = Value
  End Set
 End Property
 Public Property UserName() As String
  Get
   Return _userName
  End Get
  Set(ByVal Value As String)
   _userName = Value
  End Set
 End Property
 Public Property Password() As String
  Get
   Return _password
  End Get
  Set(ByVal Value As String)
   _password = Value
  End Set
 End Property
#End Region
#Region "Constructors"
 Public Sub New()
 End Sub
 Public Sub New(id As Integer, name As String, password As String)
  Me.UserId = id
  Me.UserName = name
  Me.Password = password
 End Sub
#End Region
End Class

//C#
public class User {
#region "Fields and Properties"
 private int userId;
 private string userName;
 private string password;
 public int UserId {
  get { return userId; }
  set { userId = value; }
  }
 public string UserName {
  get { return userName; }
  set { userName = value; }
 }
 public string Password {
  get { return password; }
  set { password = value; }
 }
#endregion
#region "Constructors"
 public User() {}
 public User(int id, string name, string password) {
  this.UserId = id;
  this.UserName = name;
  this.Password = password;
 }
#endregion
}

왜 유익합니까?

사용자 지정 엔터티를 사용하면 얻을 수 있는 주요 이점은 컨트롤에 완전히 포함된 개체라는 단순한 사실에서 비롯됩니다. 즉, 다음을 수행할 수 있습니다.

  • 상속 및 캡슐화와 같은 OO 기술을 활용합니다.
  • 사용자 지정 동작을 추가합니다.

예를 들어 사용자 클래스는 UpdatePassword 함수를 추가하면 도움이 될 수 있습니다(외부/유틸리티 함수를 사용하여 데이터 세트로 수행할 수 있지만 가독성/유지 관리 비용). 또한 강력한 형식이므로 IntelliSense 지원을 받습니다.

Aa479317.entity_fig01(en-us,MSDN.10).gif

그림 1. User 클래스를 사용하는 IntelliSense

마지막으로 사용자 지정 엔터티는 강력한 형식이므로 오류가 발생하기 쉬운 캐스트가 덜 필요합니다.

Dim userId As Integer = user.UserId
'versus
Dim userId As Integer = 
?         Convert.ToInt32(ds.Tables("users").Rows(0)("UserId"))

Object-Relational 매핑

앞에서 설명한 것처럼 이 방법의 기본 과제 중 하나는 관계형 데이터와 개체의 차이점을 다루는 것입니다. 데이터는 관계형 데이터베이스에 지속적으로 저장되므로 두 세계를 연결할 수밖에 없습니다. 이전 사용자 예제에서는 다음과 같은 사용자 테이블이 데이터베이스에 있을 것으로 예상할 수 있습니다.

Aa479317.entity_fig02(en-us,MSDN.10).gif

그림 2. 사용자의 데이터 뷰

이 관계형 스키마에서 사용자 지정 엔터티로 매핑하는 것은 다음과 같은 간단한 문제입니다.

'Visual Basic .NET
Public Function GetUser(ByVal userId As Integer) As User
 Dim connection As New SqlConnection(CONNECTION_STRING)
 Dim command As New SqlCommand("GetUserById", connection)
 command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId
 Dim dr As SqlDataReader = Nothing
 Try
  connection.Open()
  dr = command.ExecuteReader(CommandBehavior.SingleRow)
  If dr.Read Then
   Dim user As New User
   user.UserId = Convert.ToInt32(dr("UserId"))
   user.UserName = Convert.ToString(dr("UserName"))
   user.Password = Convert.ToString(dr("Password"))
   Return user
  End If
  Return Nothing
 Finally
  If Not dr is Nothing AndAlso Not dr.IsClosed Then
   dr.Close()
  End If
  connection.Dispose()
  command.Dispose()
  End Try
End Function

//C#
public User GetUser(int userId) {
 SqlConnection connection = new SqlConnection(CONNECTION_STRING);
 SqlCommand command = new SqlCommand("GetUserById", connection);
 command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId;
 SqlDataReader dr = null;
 try{
  connection.Open();
  dr = command.ExecuteReader(CommandBehavior.SingleRow);
  if (dr.Read()){
   User user = new User();
   user.UserId = Convert.ToInt32(dr["UserId"]);
   user.UserName = Convert.ToString(dr["UserName"]);
   user.Password = Convert.ToString(dr["Password"]);
   return user;            
  }
  return null;
 }finally{
  if (dr != null && !dr.IsClosed){
   dr.Close();
  }
  connection.Dispose();
  command.Dispose();
 }
}

일반적으로와 같이 연결 및 명령 개체를 여전히 설정하지만 User 클래스의 새 instance 만들고 DataReader에서 채웁습니다. 이 함수 내에서 DataSet을 계속 사용하여 사용자 지정 엔터티에 매핑할 수 있지만 DataReader를 통해 DataSets의 주요 이점은 연결이 끊긴 데이터 보기를 제공한다는 것입니다. 이 경우 사용자 instance 연결이 끊긴 보기를 제공하여 DataReader의 속도를 활용할 수 있습니다.

잠깐만 기다리세요! 당신은 아무것도 해결하지 않았다!

관찰 독자는 DataSets 에서 지적한 문제 중 하나는 강력한 형식이 아니므로 생산성이 저하되고 런타임 오류가 발생할 가능성이 증가한다는 것을 알 수 있습니다. 또한 개발자는 기본 데이터 구조에 대한 심층적인 지식을 갖도록 요구합니다. 이전 코드를 살펴보면 정확히 동일한 함정이 숨어 있는 것을 알 수 있습니다. 그러나 코드의 매우 격리된 영역 내에서 이러한 문제를 캡슐화했습니다. 즉, 클래스 엔터티(웹 인터페이스, 웹 서비스 소비자, Windows 양식)의 소비자는 이러한 문제를 전혀 인식하지 못합니다. 반대로 DataSets 를 사용하면 이러한 문제가 코드 전체에 분산됩니다.

기능

이전 코드는 매핑의 기본 아이디어를 표시하는 데 유용했지만 이를 개선하기 위해 두 가지 주요 향상을 수행할 수 있습니다. 먼저 다시 사용할 가능성이 있으므로 채우기 코드를 자체 함수로 추출하려고 합니다.

'Visual Basic .NET
Public Function PopulateUser(ByVal dr As IDataRecord) As User
 Dim user As New User
 user.UserId = Convert.ToInt32(dr("UserId"))
 'example of checking for NULL
 If Not dr("UserName") Is DBNull.Value Then
  user.UserName = Convert.ToString(dr("UserName"))
 End If
 user.Password = Convert.ToString(dr("Password"))
 Return user
End Function

//C#
public User PopulateUser(IDataRecord dr) {
 User user = new User();
 user.UserId = Convert.ToInt32(dr["UserId"]);
 //example of checking for NULL
 if (dr["UserName"] != DBNull.Value){
  user.UserName = Convert.ToString(dr["UserName"]);   
 }
 user.Password = Convert.ToString(dr["Password"]);
 return user;
}

두 번째로 주목해야 할 점은 매핑 함수에 SqlDataReader 를 사용하는 대신 IDataRecord 를 사용한다는 것입니다. 모든 DataReaders 가 구현하는 인터페이스입니다. IDataRecord를 사용하면 매핑 프로세스 공급업체가 독립적입니다. 즉, 이전 함수를 사용하여 OleDbDataReader를 사용하는 경우에도 Access 데이터베이스에서 사용자를 매핑할 수 있습니다. 이 특정 접근 방식을 공급자 모델 디자인 패턴(링크 1, 링크 2)과 결합하면 다양한 데이터베이스 공급업체에 쉽게 사용할 수 있는 코드가 있습니다.

마지막으로 위의 코드는 캡슐화가 얼마나 강력한지 보여 줍니다. DataSets에서 NULL을 처리하는 것은 가장 쉬운 일이 아닙니다. 즉, 값을 끌어올 때마다 NULL인 경우 검사 필요가 있기 때문입니다. 위의 인구 방법으로 우리는 편리하게 한 곳에서 이것을 처리하고 소비자가 그것을 처리할 필요가 없도록 했습니다.

매핑 위치

별도의 클래스의 일부 또는 적절한 사용자 지정 엔터티의 일부로 이러한 데이터 액세스 및 매핑 함수가 속하는 위치에 대한 몇 가지 논쟁이 있습니다. 사용자 사용자 지정 엔터티의 일부로 모든 사용자 관련 작업(데이터 가져오기, 업데이트 및 매핑)을 갖는 것은 확실히 좋은 우아함입니다. 데이터베이스 스키마가 사용자 지정 엔터티와 매우 유사할 때(이 예제와 같이) 잘 작동하는 경향이 있습니다. 시스템이 복잡해지고 두 세계의 차이점이 나타나기 시작하면 데이터 계층과 비즈니스 계층을 명확하게 분리하면 유지 관리를 간소화하는 데 크게 도움이 될 수 있습니다(데이터 액세스 계층이라고 부르는 것을 좋아함). 자체 계층 내에 액세스 및 매핑 코드를 갖는 부작용인 DAL은 레이어를 명확하게 분리할 수 있는 좋은 규칙을 제공한다는 것입니다.

"DAL에서 System.Data 또는 자식 네임스페이스에서 클래스를 반환하지 마세요."

사용자 지정 컬렉션

지금까지 개별 엔터티를 다루는 것만 살펴보았습니다. 그러나 여러 개체를 처리해야 하는 경우가 많습니다. 간단한 솔루션은 Arraylist 와 같은 제네릭 컬렉션 내에 여러 값을 저장하는 것입니다. 이 솔루션은 DataSets와 관련하여 겪었던 몇 가지 문제, 즉 다음을 다시 도입하기 때문에 이상적인 솔루션입니다.

  • 강력한 형식이 아니며
  • 사용자 지정 동작을 추가할 수 없습니다.

요구 사항에 가장 적합한 솔루션은 자체 사용자 지정 컬렉션을 만드는 것입니다. 다행히 Microsoft .NET Framework 이 목적을 위해 특별히 상속되는 클래스인 CollectionBase를 제공합니다.CollectionBase는 모든 형식의 개체를 프라이빗 Arraylist 내에 저장하지만 User 개체와 같은 특정 형식만 사용하는 메서드를 통해 이러한 프라이빗 컬렉션에 대한 액세스를 노출하는 방식으로 작동합니다. 즉, 약한 형식의 코드는 강력한 형식의 API 내에 캡슐화됩니다.

사용자 지정 컬렉션은 많은 코드처럼 보일 수 있지만 대부분은 코드 생성 또는 잘라내기 및 붙여넣기이며, 종종 검색 및 바꾸기가 하나만 필요한 경우가 많습니다. 사용자 클래스에 대한 사용자 지정 컬렉션을 구성하는 다양한 부분을 살펴보겠습니다.

'Visual Basic .NET
Public Class UserCollection
   Inherits CollectionBase
 Default Public Property Item(ByVal index As Integer) As User
  Get
   Return CType(List(index), User)
  End Get
  Set
   List(index) = value
  End Set
 End Property
 Public Function Add(ByVal value As User) As Integer
  Return (List.Add(value))
 End Function
 Public Function IndexOf(ByVal value As User) As Integer
  Return (List.IndexOf(value))
 End Function
 Public Sub Insert(ByVal index As Integer, ByVal value As User)
  List.Insert(index, value)
 End Sub
 Public Sub Remove(ByVal value As User)
  List.Remove(value)
 End Sub
 Public Function Contains(ByVal value As User) As Boolean
  Return (List.Contains(value))
 End Function
End Class

//C#
public class UserCollection : CollectionBase {
 public User this[int index] {
  get {return (User)List[index];}
  set {List[index] = value;}
 }
 public int Add(User value) {
  return (List.Add(value));
 }
 public int IndexOf(User value) {
  return (List.IndexOf(value));
 }
 public void Insert(int index, User value) {
  List.Insert(index, value);
 }
 public void Remove(User value) {
  List.Remove(value);
 }
 public bool Contains(User value) {
  return (List.Contains(value));
 }
}

CollectionBase를 구현하여 더 많은 작업을 수행할 수 있지만 이전 코드는 사용자 지정 컬렉션에 필요한 핵심 기능을 나타냅니다. Add 함수를 살펴보면 User 개체만 허용하는 함수에서 List.Add(Arraylist)에 대한 호출을 단순히 래핑하는 방법을 확인할 수 있습니다.

사용자 지정 컬렉션 매핑

관계형 데이터를 사용자 지정 컬렉션에 매핑하는 프로세스는 사용자 지정 엔터티에 대해 검사한 프로세스와 매우 유사합니다. 단일 엔터티를 만들고 반환하는 대신 컬렉션에 엔터티를 추가하고 다음 엔터티에 루프를 추가합니다.

'Visual Basic .NET
Public Function GetAllUsers() As UserCollection
 Dim connection As New SqlConnection(CONNECTION_STRING)
 Dim command As New SqlCommand("GetAllUsers", connection)
 Dim dr As SqlDataReader = Nothing
 Try
  connection.Open()
  dr = command.ExecuteReader(CommandBehavior.SingleResult)
  Dim users As New UserCollection
  While dr.Read()
   users.Add(PopulateUser(dr))
  End While
  Return users
 Finally
  If Not dr Is Nothing AndAlso Not dr.IsClosed Then
   dr.Close()
  End If
  connection.Dispose()
  command.Dispose()
 End Try
End Function

//C#
public UserCollection GetAllUsers() {
 SqlConnection connection = new SqlConnection(CONNECTION_STRING);
 SqlCommand command =new SqlCommand("GetAllUsers", connection);
 SqlDataReader dr = null;
 try{
  connection.Open();
  dr = command.ExecuteReader(CommandBehavior.SingleResult);
  UserCollection users = new UserCollection();
  while (dr.Read()){
   users.Add(PopulateUser(dr));
  }
  return users;
 }finally{
  if (dr != null && !dr.IsClosed){
   dr.Close();
  }
  connection.Dispose();
  command.Dispose();
 }
}

데이터베이스에서 데이터를 가져와 사용자 지정 컬렉션을 만들고 결과를 반복하여 각 User 개체를 만들고 컬렉션에 추가합니다. PopulateUser 매핑 함수를 다시 사용하는 방법도 확인합니다.

사용자 지정 동작 추가

사용자 지정 엔터티에 대해 이야기할 때 클래스에 사용자 지정 동작을 추가하는 기능만 언급했습니다. 엔터티에 추가할 기능 유형은 대부분 구현하는 비즈니스 논리 유형에 따라 달라지지만 사용자 지정 컬렉션에서 구현하려는 몇 가지 일반적인 기능이 있을 수 있습니다. 이러한 예 중 하나는 userId를 기반으로 하는 사용자와 같은 일부 키를 기반으로 단일 엔터티를 반환하는 것입니다.

'Visual Basic .NET
Public Function FindUserById(ByVal userId As Integer) As User
 For Each user As User In List
  If user.UserId = userId Then
   Return user
  End If
 Next
 Return Nothing
End Function

//C#
public User FindUserById(int userId) {
 foreach (User user in List) {
  if (user.UserId == userId){
   return user;
  }
 }
 return null;
}

또 다른 하나는 부분 사용자 이름과 같은 특정 조건에 따라 사용자의 하위 집합을 반환하는 것일 수 있습니다.

'Visual Basic .NET
Public Function FindMatchingUsers(ByVal search As String) As UserCollection
 If search Is Nothing Then
  Throw New ArgumentNullException("search cannot be null")
 End If
 Dim matchingUsers As New UserCollection
 For Each user As User In List
  Dim userName As String = user.UserName
  If Not userName Is Nothing And userName.StartsWith(search) Then
   matchingUsers.Add(user)
  End If
 Next
 Return matchingUsers
End Function

//C#
public UserCollection FindMatchingUsers(string search) {
 if (search == null){
  throw new ArgumentNullException("search cannot be null");
 }
 UserCollection matchingUsers = new UserCollection();
 foreach (User user in List) {
  string userName = user.UserName;
  if (userName != null && userName.StartsWith(search)){
   matchingUsers.Add(user);
  }
 }
 return matchingUsers;
}

DataTable.Select와 동일한 방식으로 DataSets를 사용할 수 있습니다. 고유한 기능을 만들면 코드를 절대 제어할 수 있지만 Select 메서드는 동일한 작업을 수행하는 매우 편리하고 코드 없는 수단을 제공합니다. 반대로 선택 하려면 개발자가 기본 데이터베이스를 알아야 하며 강력한 형식이 아닙니다.

사용자 지정 컬렉션 바인딩

살펴본 첫 번째 예제는 DataSet 을 ASP.NET 컨트롤에 바인딩하는 예제입니다. 이것이 얼마나 일반적인지 고려할 때 사용자 지정 컬렉션이 쉽게 바인딩된다는 것을 알게 되어 기쁩니다( CollectionBase 는 바인딩에 사용되는 Ilist 를 구현하기 때문입니다). 사용자 지정 컬렉션은 이를 노출하는 모든 컨트롤의 DataSource 역할을 할 수 있으며 DataBinder.EvalDataSet과 마찬가지로 사용할 수 있습니다.

'Visual Basic .NET
Dim users as UserCollection = DAL.GetallUsers()
repeater.DataSource = users
repeater.DataBind()

//C#
UserCollection users = DAL.GetAllUsers();
repeater.DataSource = users;
repeater.DataBind();

<!-- HTML -->
<asp:Repeater onItemDataBound="r_IDB" ID="repeater" Runat="server">
 <ItemTemplate>
  <asp:Label ID="userName" Runat="server">
   <%# DataBinder.Eval(Container.DataItem, "UserName") %><br />
  </asp:Label>
 </ItemTemplate>
</asp:Repeater>

열 이름을 DataBinder.Eval의 두 번째 매개 변수로 사용하는 대신 표시할 속성 이름(이 경우 UserName)을 지정합니다.

많은 데이터 바인딩된 컨트롤에서 노출되는 OnItemDataBound 또는 OnItemCreated에서 처리를 수행하는 경우 DataRowViewe.Item.DataItem을 캐스팅할 수 있습니다. 사용자 지정 컬렉션에 바인딩할 때는 Item.DataItem이 대신 사용자 지정 엔터티로 캐스팅됩니다. 이 예제에서 User 클래스는 다음과 같습니다.

'Visual Basic .NET
Protected Sub r_ItemDataBound (s As Object, e As RepeaterItemEventArgs)
 Dim type As ListItemType = e.Item.ItemType
 If type = ListItemType.AlternatingItem OrElse
?   type = ListItemType.Item Then
  Dim u As Label = CType(e.Item.FindControl("userName"), Label)
  Dim currentUser As User = CType(e.Item.DataItem, User)
  If Not PasswordUtility.PasswordIsSecure(currentUser.Password) Then
   ul.ForeColor = Drawing.Color.Red
  End If
 End If
End Sub

//C#
protected void r_ItemDataBound(object sender, RepeaterItemEventArgs e) {
 ListItemType type = e.Item.ItemType;
 if (type == ListItemType.AlternatingItem || 
?    type == ListItemType.Item){
  Label ul = (Label)e.Item.FindControl("userName");
  User currentUser = (User)e.Item.DataItem;
  if (!PasswordUtility.PasswordIsSecure(currentUser.Password)){
   ul.ForeColor = Color.Red;
  }
 }
}

관계 관리

가장 간단한 시스템 내에서도 엔터티 간의 관계가 존재합니다. 관계형 데이터베이스를 사용하면 외세 키를 통해 관계가 유지됩니다. 개체를 사용하여 관계는 단순히 다른 개체에 대한 참조일 뿐입니다. 예를 들어 이전 예제를 기반으로 하는 경우 User 개체에 Role이 있어야 합니다.

'Visual Basic .NET
Public Class User
 Private _role As Role
 Public Property Role() As Role
  Get
   Return _role
  End Get
  Set(ByVal Value As Role)
   _role = Value
  End Set
 End Property
End Class

//C#
public class User {
 private Role role;
 public Role Role {
  get {return role;}
  set {role = value;}
 }
}

또는 역할의 컬렉션:

'Visual Basic .NET
Public Class User
 Private _roles As RoleCollection
 Public ReadOnly Property Roles() As RoleCollection
  Get
   If _roles Is Nothing Then
    _roles = New RoleCollection
   End If
   Return _roles
  End Get
 End Property
End Class

//C#
public class User {
 private RoleCollection roles;
 public RoleCollection Roles {
  get {
   if (roles == null){
    roles = new RoleCollection();
   }
   return roles;
  }
 }
}

이 두 예제에서는 User 및UserCollection 클래스와 같은 다른 사용자 지정 엔터티 또는 컬렉션 클래스인 가상의 Role 클래스 또는 RoleCollection 클래스가 있습니다.

관계 매핑

실제 문제는 관계를 매핑하는 방법입니다. 간단한 예제를 살펴보겠습니다. 사용자의 역할과 함께 userId 를 기반으로 사용자를 검색하려고 합니다. 먼저 관계형 모델을 살펴보겠습니다.

Aa479317.entity_fig03(en-us,MSDN.10).gif

그림 3. 사용자와 역할 간의 관계

여기서 는 사용자 테이블과 역할 테이블이 표시됩니다. 이 테이블은 사용자 지정 엔터티에 간단하게 매핑할 수 있습니다. 사용자와 역할 간의 다대다 관계를 나타내는 UserRoleJoin 테이블도 있습니다.

다음으로 저장 프로시저를 사용하여 두 개의 개별 결과인 사용자에 대한 첫 번째 결과와 해당 역할의 두 번째 결과를 가져옵니다.

CREATE PROCEDURE GetUserById(
  @UserId INT
)AS
SELECT UserId, UserName, [Password]
  FROM Users
  WHERE UserId = @UserID
SELECT R.RoleId, R.[Name], R.Code
  FROM Roles R INNER JOIN
     UserRoleJoin URJ ON R.RoleId = URJ.RoleId
  WHERE  URJ.UserId = @UserId

마지막으로 관계형 모델에서 개체 모델로 매핑합니다.

'Visual Basic .NET
Public Function GetUserById(ByVal userId As Integer) As User
 Dim connection As New SqlConnection(CONNECTION_STRING)
 Dim command As New SqlCommand("GetUserById", connection)
 command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId
 Dim dr As SqlDataReader = Nothing
 Try
  connection.Open()
  dr = command.ExecuteReader()
  Dim user As User = Nothing
  If dr.Read() Then
   user = PopulateUser(dr)
   dr.NextResult()
   While dr.Read()
    user.Roles.Add(PopulateRole(dr))
   End While
  End If
  Return user
 Finally
  If Not dr Is Nothing AndAlso Not dr.IsClosed Then
   dr.Close()
  End If
  connection.Dispose()
  command.Dispose()
 End Try
End Function

//C#
public User GetUserById(int userId) {
 SqlConnection connection = new SqlConnection(CONNECTION_STRING);
 SqlCommand command = new SqlCommand("GetUserById", connection);
 command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId;
 SqlDataReader dr = null;
 try{
  connection.Open();
  dr = command.ExecuteReader();
  User user = null;
  if (dr.Read()){
   user = PopulateUser(dr);
   dr.NextResult();
   while(dr.Read()){
    user.Roles.Add(PopulateRole(dr));
   }            
  }
  return user;
 }finally{
  if (dr != null && !dr.IsClosed){
   dr.Close();
  }
  connection.Dispose();
  command.Dispose();
 }
}

사용자 instance 만들어지고 채워집니다. 다음 결과/선택 및 반복으로 이동하여 역할을 채우고 User 클래스의 RolesCollection 속성에 추가합니다.

기본 사항 외

이 가이드의 목적은 사용자 지정 엔터티 및 컬렉션의 개념과 사용을 소개하는 것이었습니다. 사용자 지정 엔터티를 사용하는 것은 업계에서 널리 사용되는 사례이며, 다양한 시나리오를 처리하기 위해 수많은 패턴이 문서화되었습니다. 디자인 패턴은 여러 가지 이유로 적합합니다. 첫째, 특정 상황을 해결할 때 주어진 문제에 가장 먼저 직면하지 않을 가능성이 있습니다. 디자인 패턴을 사용하면 시도되고 테스트된 솔루션을 지정된 문제에 다시 사용할 수 있습니다(디자인 패턴은 100% 잘라내고 붙여넣는 것은 아니지만 거의 항상 솔루션에 건전한 기반을 제공합니다). 이렇게 하면 시스템이 널리 사용되는 접근 방식일 뿐만 아니라 잘 문서화된 접근 방식이기 때문에 복잡성으로 잘 확장될 것이라는 확신을 갖게 됩니다. 또한 디자인 패턴은 지식 전달 및 학습을 훨씬 쉽게 만들 수 있는 일반적인 어휘를 제공합니다.

디자인 패턴이 사용자 지정 엔터티에만 적용된다는 것은 말할 것도 없으며 실제로 많은 사람들이 그렇지 않습니다. 그러나 사용자에게 기회를 주면 사용자 지정 엔터티 및 매핑 프로세스에 얼마나 잘 문서화된 패턴이 적용되는지 유쾌하게 놀라게 될 것입니다.

이 마지막 섹션은 더 크거나 더 복잡한 시스템이 실행될 가능성이 있는 몇 가지 고급 시나리오를 지적하는 데만 사용됩니다. 대부분의 topics 개별 가이드에 합당하지만 최소한 몇 가지 시작 리소스를 제공하려고 노력할 것입니다.

마틴 파울러의 엔터프라이즈 애플리케이션 아키텍처 패턴은 일반적인 디자인 패턴에 대한 훌륭한 참조(자세한 설명 및 샘플 코드 포함)로 사용될 뿐만 아니라 처음 100페이지는 전체 개념을 중심으로 마음을 감싸는 것입니다. 또는 Fowler에는 이미 개념에 익숙하지만 편리한 참조가 필요한 사람들에게 적합한 패턴의 온라인 카탈로그가 있습니다.

동시성

이전 예제에서는 모두 데이터베이스에서 데이터를 끌어와 해당 데이터에서 개체를 만드는 것을 처리했습니다. 대부분의 경우 데이터를 업데이트, 삭제 및 삽입하는 것은 간단합니다. 비즈니스 계층은 개체를 만들고 데이터 액세스 계층에 전달하며 관계형 세계에 대한 매핑을 처리할 수 있도록 합니다. 예:

'Visual Basic .NET
Public sub UpdateUser(ByVal user As User)
 Dim connection As New SqlConnection(CONNECTION_STRING)
 Dim command As New SqlCommand("UpdateUser", connection)
 'could have a reusable function to inversly map this
 command.Parameters.Add("@UserId", SqlDbType.Int)
 command.Parameters(0).Value = user.UserId
 command.Parameters.Add("@Password", SqlDbType.VarChar, 64)
 command.Parameters(1).Value = user.Password
 command.Parameters.Add("@UserName", SqlDbType.VarChar, 128)
 command.Parameters(2).Value = user.UserName
 Try
  connection.Open()
  command.ExecuteNonQuery()
 Finally
  connection.Dispose()
  command.Dispose()
 End Try
End Sub

//C#
public void UpdateUser(User user) {
 SqlConnection connection = new SqlConnection(CONNECTION_STRING);
 SqlCommand command = new SqlCommand("UpdateUser", connection);
 //could have a reusable function to inversly map this
 command.Parameters.Add("@UserId", SqlDbType.Int);
 command.Parameters[0].Value = user.UserId;
 command.Parameters.Add("@Password", SqlDbType.VarChar, 64);
 command.Parameters[1].Value = user.Password; 
 command.Parameters.Add("@UserName", SqlDbType.VarChar, 128);
 command.Parameters[2].Value = user.UserName;
 try{
  connection.Open();
  command.ExecuteNonQuery();
 }finally{
  connection.Dispose();
  command.Dispose();
 }
}

그러나 간단하지 않은 한 가지 영역은 동시성을 처리할 때입니다. 즉, 두 사용자가 동시에 동일한 데이터를 업데이트하려고 하면 어떻게 되나요? 기본 동작(아무 작업도 수행하지 않는 경우)은 데이터를 커밋할 마지막 사람이 이전의 모든 작업을 덮어쓰는 것입니다. 한 사용자의 작업을 자동으로 덮어쓰기 때문에 이상적이지 않을 수 있습니다. 충돌을 완전히 방지하는 한 가지 방법은 비관적 동시성을 사용하는 것입니다. 그러나 이 메서드에는 확장 가능한 방식으로 구현하기 어려울 수 있는 일부 유형의 잠금 메커니즘이 필요합니다. 대안은 낙관적 동시성 기술을 사용하는 것입니다. 첫 번째 커밋이 지배하고 후속 사용자에게 알리는 것은 일반적으로 더 부드럽고 사용자에게 친숙한 접근 방식입니다. 이는 타임스탬프와 같은 일부 유형의 행 버전 관리에서 수행됩니다.

추가 읽기:

성능

합법적인 유연성 및 기능 문제와는 달리, 우리는 너무 자주 미세한 성능 차이에 대해 걱정합니다. 성능은 실제로 중요하지만 가장 간단한 상황을 제외한 모든 것에 대한 일반화된 지침을 제공하는 것은 종종 어렵습니다. 예를 들어 사용자 지정 컬렉션과 DataSets 를 비교해 보세요. 더 빠른 것은 무엇인가요? 사용자 지정 컬렉션을 사용하면 데이터베이스에서 데이터를 더 빠르게 가져올 수 있는 DataReaders 를 많이 사용할 수 있습니다. 요점은, 그래도, 대답은 정말 어떻게에 따라 달라집니다, 어떤 유형의 데이터, 당신은 그들을 사용, 그래서 담요 문은 꽤 쓸모가. 더 중요한 것은 저장할 수 있는 처리 시간이 유지 관리 효율성의 차이에 비해 그다지 많지 않다는 것입니다.

물론 유지 관리가 가능한 고성능 솔루션을 사용할 수 없다고 말한 사람은 아무도 없습니다. 사용 방법에 따라 달라지지만 성능을 최대화하는 데 도움이 되는 몇 가지 패턴이 있습니다. 하지만 먼저 사용자 지정 엔터티 및 컬렉션이 DataSets 뿐만 아니라 캐시되고 동일한 메커니즘(예: HttpCache )을 사용할 수 있다는 것을 알고 있어야 합니다. DataSets 의 한 가지 좋은 점은 Select 문을 작성하여 필요한 정보를 가져오는 기능입니다. 사용자 지정 엔터티를 사용하면 자식 엔터티뿐만 아니라 전체 엔터티를 채워야 하는 경우가 많습니다. 예를 들어 조직 목록을 표시하려는 경우 DataSet을 사용하여 OganizationId,NameAddress를 가져와서 리피터에 바인딩할 수 있습니다. 사용자 지정 엔터티를 사용하면 ISO 인증, 모든 직원 컬렉션, 추가 연락처 정보 등을 말하는 비트 플래그일 수 있는 다른 모든 조직 정보도 항상 가져와야 한다고 생각합니다. 어쩌면 다른 사람들은이 중단을 공유하지 않지만 다행히도 우리는 원하는 경우 사용자 지정 엔터티를 세밀하게 제어 할 수 있습니다. 가장 일반적인 방법은 지연 로드 패턴 유형을 사용하는 것입니다. 이 패턴은 처음 필요할 때만 정보를 가져옵니다(속성에 잘 캡슐화될 수 있음). 개별 속성에 대한 이러한 유형의 제어는 그렇지 않으면 쉽게 달성할 수 없는 엄청난 유연성을 제공합니다( DataColumn 수준에서 비슷한 작업을 수행하려고 한다고 가정).

추가 읽기:

정렬 및 필터링

SQL 및 기본 데이터 구조에 대한 지식이 필요하지만 정렬 및 필터링에 대한 DataView의 기본 제공 지원은 사용자 지정 컬렉션에서 다소 손실되는 편리성입니다. 여전히 정렬 및 필터링할 수 있지만 이렇게 하려면 기능을 작성해야 합니다. 기술이 반드시 고급인 것은 아니지만 코드의 전체 데모는 이 섹션의 scope 외부에 있습니다. 대부분의 기술은 필터 클래스를 사용하여 컬렉션을 필터링하고 정렬을 위해 비교자 클래스를 사용하는 것과 같이, 제가 알고 있는 패턴이 없는 것과 매우 유사합니다. 그러나 많은 리소스가 존재합니다.

코드 생성

개념적 장애물을 지나면 사용자 지정 엔터티 및 컬렉션에 대한 기본 단점은 이러한 모든 유연성, 추상화 및 낮은 유지 관리 비용의 추가 코드 양입니다. 사실, 유지 관리 비용 및 버그 감소에 대한 모든 이야기가 추가 코드와 동일시되지 않는다고 생각할 수 있습니다. 이는 확실히 유효한 지점이지만(다시 한 번 완벽한 솔루션은 없음), CSLA.NET 같은 디자인 패턴 및 프레임워크는 문제를 완화하는 데 큰 도움이 됩니다. 패턴 및 프레임워크와 완전히 다르지만 코드 생성 도구는 실제로 작성해야 하는 코드의 양을 상당히 줄일 수 있습니다. 처음에 이 가이드에는 코드 생성 도구, 특히 인기 있고 무료 CodeSmith에 대해 자세히 설명하는 전체 섹션이 있었습니다. 그러나 제품에 대한 내 지식을 능가하는 수많은 리소스가 존재합니다.

계속하기 전에 코드 생성이 꿈처럼 들린다는 것을 깨달았습니다. 그러나 제대로 사용되고 이해되면 사용자 지정 엔터티를 수행하지 않더라도 도구 모음에 강력한 무기고가 됩니다. 코드 생성이 사용자 지정 엔터티에만 적용되는 것은 아니지만 많은 항목이 이 용도에 맞게 특별히 조정됩니다. 그 이유는 간단합니다. 사용자 지정 엔터티에는 반복적인 코드가 많이 필요합니다.

간단히 말해 코드 생성은 어떻게 작동하나요? 아이디어는 훨씬 페치되거나 비생산적으로 들릴 수 있지만 기본적으로 코드를 생성하기 위한 코드(템플릿)를 작성합니다. 예를 들어 CodeSmith는 데이터베이스에 연결하고 테이블, 열(형식, 크기 등) 및 관계와 같은 모든 속성을 가져올 수 있는 강력한 클래스와 함께 제공됩니다. 이 정보로 무장, 우리가 지금까지 이야기 한 것의 대부분은 자동화 할 수 있습니다. 예를 들어 개발자는 테이블을 선택하고 올바른 템플릿을 사용하여 사용자 지정 엔터티(올바른 필드, 속성 및 생성자 포함), 매핑 함수, 사용자 지정 컬렉션 및 기본 선택, 삽입, 업데이트 및 삭제 기능을 자동으로 만들 수 있습니다. 한 단계 더 나아가 정렬, 필터링 및 기타 고급 기능을 구현할 수도 있습니다.

CodeSmith에는 훌륭한 학습 리소스 역할을 하는 많은 즉시 사용할 수 있는 템플릿도 함께 제공됩니다. 마지막으로 CodeSmith에는 CSLA.NET 프레임워크를 구현하는 여러 템플릿이 있습니다. 처음에 기본 사항을 배우고 CodeSmith에 익숙해지게하는 데 걸린 몇 시간은 말할 수없는 시간을 절약했습니다. 또한 모든 개발자가 동일한 템플릿을 사용하는 경우 코드 전체에서 높은 수준의 일관성을 통해 다른 사람의 함수를 쉽게 작업할 수 있습니다.

추가 읽기:

O/R 매퍼

O/R 매퍼에 대한 경험이 부족하여 그것에 대해 이야기하는 것을 신중하게 생각하지만, 그들의 잠재적 가치는 무시할 수 없게 만듭니다. 코드 생성기가 사용자 고유의 소스 코드를 복사하여 붙여넣기 위해 템플릿을 기반으로 하는 코드를 만드는 경우 O/R 매퍼는 일부 유형의 구성 메커니즘에서 런타임에 코드를 동적으로 생성합니다. 예를 들어 XML 파일 내에서 일부 테이블의 X 열이 엔터티의 속성 Y에 매핑되도록 지정할 수 있습니다. 여전히 사용자 지정 엔터티를 만들지만 컬렉션, 매핑 및 기타 데이터 액세스 함수(저장 프로시저 포함)는 모두 동적으로 만들어집니다. 이론적으로 O/R 매퍼는 사용자 지정 엔터티의 문제를 거의 완전히 완화합니다. 관계형 및 개체 세계가 갈라지고 매핑 프로세스가 복잡해짐에 따라 O/R 매퍼는 훨씬 더 중요해집니다. O/R 매퍼의 단점 중 두 가지는 적어도 .NET 커뮤니티에서 보안이 떨어지고 성능이 저하되는 것으로 인식된다는 것입니다. 내가 읽은 내용에서, 나는 그들이 덜 안전하지 않다고 확신하고, 어떤 상황에서는 성능이 저하 될 수 있지만, 그들은 아마 다른 사람에서 우수합니다. O/R 매퍼는 모든 상황에 적합하지 않지만 복잡한 시스템을 처리하는 경우 조사해야 합니다.

추가 정보

.NET Framework 2.0 기능

.NET Framework 예정된 2.0 릴리스는 이 가이드 전체에서 살펴본 구현 세부 정보 중 일부를 변경합니다. 이러한 변경으로 사용자 지정 엔터티를 지원하는 데 필요한 코드의 양이 줄어들고 매핑 문제를 처리하는 데 도움이 됩니다.

제네릭

제네릭에 대한 많은 이야기가 있는 기본 이유 중 하나는 개발자에게 강력한 형식의 컬렉션을 기본적으로 제공하기 위한 것입니다. 약한 형식의 특성 때문에 Arraylists 와 같은 기존 컬렉션에서 벗어났습니다. 제네릭은 현재 컬렉션과 동일한 종류의 편의를 제공하지만 강력한 형식의 방식으로 제공됩니다. 선언에서 형식을 지정하여 이 작업을 수행합니다. 예를 들어 UserCollection을 추가 코드 없이 바꾸고 List<T> 제네릭의 새 instance 만들고 User 클래스를 지정할 수 있습니다.

'Visual Basic .NET
Dim users as new IList(of User)

//C#
IList<User> users = new IList<user>();

선언된 사용자 컬렉션은 User 형식의 개체만 처리할 수 있습니다. 이 개체는 컴파일 시간 검사 및 최적화의 모든 기능을 제공합니다.

추가 정보

nullable 형식

Nullable 형식은 실제로 이전에 나열된 것과 다른 이유로 사용되는 제네릭입니다. 데이터베이스를 처리할 때 직면하는 문제 중 하나는 NULL을 지원하는 열의 적절하고 일관된 처리입니다. 문자열 및 기타 클래스(참조 형식이라고 함)를 처리할 때 코드의 변수에null을 할당할 수 없습니다/.

'Visual Basic .NET
if dr("UserName") Is DBNull.Value Then
   user.UserName = nothing
End If

//C#
if (dr["UserName"] == DBNull.Value){
   user.UserName = null;
}

또는 아무 작업도 수행할 수 없습니다(기본적으로 참조 형식은null없음/). 정 수,부울, 10진 수 등의 값 형식에서도 거의 작동하지 않습니다. 이러한 값에null을 할당/ 수는 없지만 기본값을 할당합니다. 정수만 선언하거나null을 할당하지/ 않으면 변수는 실제로 값 0을 보유합니다. 이렇게 하면 데이터베이스에 다시 매핑하기가 어렵습니다. 값이 0 인가요 null인가요? Null 허용 형식은 값 형식이 실제 값 또는 null을 보유하도록 허용하여 이 문제를 해결합니다. 예를 들어 userId 열에서 null 값을 지원하려는 경우(정확히 현실적이지 않음) 먼저 userId 필드와 해당 속성을 nullable 형식으로 선언합니다.

'Visual Basic .NET
Private _userId As Nullable(Of Integer)
Public Property UserId() As Nullable(Of Integer)
   Get
      Return _userId
   End Get
   Set(ByVal value As Nullable(Of Integer))
      _userId = value
   End Set
End Property


//C#
private Nullable<int> userId;
public Nullable<int> UserId {
   get { return userId; }
   set { userId = value; }
}

그런 다음 HasValue 속성을 사용하여null이 할당되었는지 여부를/ 확인합니다.

'Visual Basic .NET
If UserId.HasValue Then
   Return UserId.Value
Else
   Return DBNull.Value
End If

//C#
if (UserId.HasValue) {
   return UserId.Value;
} else {
   return DBNull.Value;
}

추가 읽기:

Iterators

살펴본 UserCollection 예제는 사용자 지정 컬렉션에 필요할 수 있는 기본 기능만 나타냅니다. 제공된 구현으로는 수행할 수 없는 작업은 foreach 루프의 컬렉션을 통해 루프입니다. 이렇게 하려면 사용자 지정 컬렉션에 IEnumerable 인터페이스를 구현하는 열거자 지원 클래스가 있어야 합니다. 이것은 매우 간단하고 반복적인 프로세스이지만 그럼에도 불구하고 더 많은 코드를 도입합니다. C# 2.0에서는 이 인터페이스의 구현 세부 정보를 처리하는 새로운 수율 키워드(keyword) 도입되었습니다. 현재 새 수율 키워드(keyword) 해당하는 Visual Basic .NET은 없습니다.

추가 정보:

결론

사용자 지정 엔터티 및 컬렉션으로 전환하는 것은 가볍게 결정하는 것이 아닙니다. 고려해야 할 여러 가지 요인이 있습니다. 예를 들어 OO 개념에 대한 숙지, 이 새로운 접근 방식을 사용해야 하는 시간 및 배포하려는 환경이 있습니다. 장점은 일반적으로 중요하지만 특정 상황에 있지 않을 수 있습니다. 경우에 중요한 경우에도 단점이 무효화 될 수 있습니다. 또한 수많은 대체 솔루션이 존재한다는 사실에 유의하세요. Jimmy Nilsson은 5부 시리즈 인 .NET용 데이터 컨테이너 선택 ( 1부, 2, 3, 4, 5부)에서 이러한 대안에 대한 개요를 제공합니다.

사용자 지정 엔터티는 개체 지향 프로그래밍의 풍부한 기능을 지원하며 유지 관리가 가능한 견고한 N 계층 아키텍처를 위한 프레임워크를 설정하는 데 도움이 됩니다. 이 가이드의 목표 중 하나는 일반적인 DataSetsDataTable 대신 시스템을 구성하는 비즈니스 엔터티 측면에서 시스템을 생각하게 하는 것입니다. 또한 선택한 경로, 즉 디자인 패턴, 개체와 관계형 세계 간의 차이점(자세히 보기) 및 N 계층 아키텍처에 관계없이 알아야 할 몇 가지 주요 문제에 대해서도 설명했습니다. 선불로 보낸 시간은 시스템 수명 동안 여러 번 스스로 지불하는 방법이 있다는 것을 기억하십시오.

1http://www.hanselman.com/blog/PermaLink.aspx?guid=d88f7539-10d8-4697-8c6e-1badb08bb3f5http://www.hanselman.com/blog/PermaLink.aspx?guid=d88f7539-10d8-4697-8c6e-1badb08bb3f5

© Microsoft Corporation. All rights reserved.