Karl Seguin 요약: 경우에 따라 형식화되지 않은 DataSet가 데이터 조작을 위한 최선의 솔루션이 되지 못하는 상황이 있을 수 있습니다. 이 가이드에서는 사용자 지정 엔터티 및 컬렉션이라는 DataSet의 대안을 살펴도록 하겠습니다. 목차소개 소개ADODB.RecordSet과 곧잘 잊혀지던 MoveNext의 시대는 가고 이제는 그 자리를 Microsoft ADO.NET의 강력하고 유연한 기능이 대신하게 되었습니다. Microsoft에서 내놓은 새로운 방법은 탁월한 속도의 DataReader와 풍부한 기능의 DataSet를 갖추고 뛰어난 개체 지향 모델에 패키지화되는 System.Data 네임스페이스입니다. 이러한 도구를 마음대로 사용할 수 있는 상황은 충분히 예견된 일입니다. 모든 3 계층 아키텍처는 강력한 DAL(데이터 액세스 계층)을 사용하여 데이터 계층을 비즈니스 계층에 안정적으로 연결합니다. 양질의 DAL은 코드 재사용률을 높이고 뛰어난 성능을 위한 핵심 역할을 수행할 뿐 아니라 완전히 투명하게 나타납니다(이 기사에는 영문 페이지 링크가 포함되어 있습니다). 도구가 발전을 거듭함에 따라 일정한 개발 패턴을 갖게 되었습니다. MoveNext와 작별을 고한 것은 성가신 구문으로부터 벗어난 수준을 뛰어넘어 연결이 끊어진 데이터에 눈을 돌리게 한 것은 물론 응용 프로그램을 빌드하는 방법에도 막대한 영향을 미쳤습니다. DataReader에 익숙해지자(RecordSet과 유사하게 작동) 얼마 지나지 않아 DataAdapter, DataSet, DataTable 및 DataView에도 과감히 달려들어 살펴보게 되었습니다. 이는 개발 방식에 변화를 주는 새 개체를 활용하는 능력이 향상된 것으로 볼 수 있습니다. 연결이 끊어진 데이터를 사용하면 새로운 캐싱 기법을 활용할 수 있어 응용 프로그램의 성능이 크게 향상됩니다. 게다가 이러한 클래스 기능을 통해 보다 세련되고 강력한 함수를 작성할 수 있게 된 동시에 때로는 일반적인 작업에 필요한 코드의 양을 눈에 띌 만큼 줄이게 되었습니다. DataSet가 특히 적합한 상황은 프로토타입, 소형 시스템 및 지원 유틸리티를 비롯하여 다양합니다. 하지만 출시 시간보다 유지 관리의 편의성이 중요한 엔터프라이즈 시스템에 사용하면 최상의 효과를 발휘하지 못할 수도 있습니다. 이 가이드의 목표는 이러한 작업 유형을 위해 조정된 DataSet를 대신할 사용자 지정 엔터티 및 컬렉션이라는 대안을 살펴보는 것입니다. 다른 대안도 있기는 하지만 기능이 동일하지 않거나 지원 수준이 떨어집니다. 가장 먼저 할 일은 DataSet의 단점을 확인하고 해결할 문제를 이해하는 것입니다. 모든 솔루션은 저마다 장단점이 있으므로 사용자 지정 엔터티의 단점(이후 설명 참조)보다 DataSet의 단점에 더 친숙해질 수 있습니다. 따라서 여러분과 팀 구성원은 해당 프로젝트에 보다 적합한 솔루션을 결정해야만 합니다. 또한 변경할 요구 사항의 특성 및 실제 코드 개발보다 생산 후에 더 많은 시간이 소요될 가능성을 비롯하여 총 솔루션 비용을 반드시 고려해야 합니다. 마지막으로, 여기서 언급하는 DataSet는 형식화되지 않은 DataSet의 일부 단점을 해결한 형식화된 DataSet를 말하는 것이 아님을 유의하십시오. DataSet의 문제추상화의 부재DataSet의 대안을 고려해야 할 첫 번째이자 가장 확실한 이유는 코드와 데이터베이스 구조를 분리할 수 없다는 점에 있습니다. DataAdapter는 기본 데이터베이스 공급업체(Microsoft, Oracle, IBM 등) 종류에 관계 없이 코드를 작성하는 데는 효과적이지만 테이블, 열 및 관계 같은 핵심 데이터베이스 구성 요소를 추상화하지는 못합니다. 이러한 핵심 데이터베이스 구성 요소는 DataSet의 핵심 구성 요소이기도 합니다. DataSet와 데이터베이스는 일반적인 구성 요소 이상의 것을 공유하며 아쉽게도 스키마까지 공유합니다. 다음과 같은 Select 문이 있다고 가정합시다. SELECT UserId, FirstName, LastNameFROM Users 다들 알겠지만 값은 DataSet 내의 UserId, FirstName 및 LastName DataColumn에 있습니다. 이것이 어째서 문제가 되는 것일까요? 기본적인 일반 예제를 살펴봅시다. 먼저 다음과 같이 간단한 DAL 함수를 만듭니다. 'Visual Basic .NETPublic 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를 사용합니다. 어떠한 이유로든(예: 성능 향상을 위한 비정규화, 명확도 향상을 위한 정규화, 요구 사항의 변화) 데이터베이스 스키마가 변경되면 변경 내용은 항상 "FirstName" 열 이름을 사용하는 ASPX 즉, Databinder.Eval 줄로 전달됩니다. 이렇게 되면 즉시 '데이터베이스 스키마의 변경 내용이 항상 ASPX 코드로 전달될까?'와 같은 위험한 의문이 머리 속에 떠오르게 됩니다. N 계층의 장점이 무색해지는 대목입니다. 해야 할 작업이 간단한 열 이름 바꾸기 뿐이라면 이 예제에서의 변경 작업은 간단하게 이루어집니다. 그러나 GetAllUsers를 수많은 위치에 사용하거나 설상가상으로 웹 서비스로 노출하여 수없이 많은 소비자에게 공급한다면 어떻게 될까요? 얼마나 쉽게 또는 안전하게 변경 내용을 전파할 수 있을까요? 이 기본 예제에서는 저장 프로시저가 추상화 계층 역할을 수행하는 것으로 충분하지만 가장 기본적인 보호의 용도 이외에 모든 부분에서 저장 프로시저에 의존하면 향후 더 큰 문제가 발생하게 됩니다. 그러면 이러한 형태를 하드 코딩이라고 가정해 봅시다. 본질적으로 DataSet를 사용하면 데이터베이스 스키마(열 이름을 사용하든 순서를 사용하든 관계없이)와 응용 프로그램/비즈니스 계층 사이에 긴밀한 연결을 만들게 됩니다. 이전의 경험(또는 논리)을 통해 하드 코딩이 유지 관리 및 향후 개발에 미치는 악영향을 알고 있을 것입니다. DataSet가 적절한 추상화를 제공하지 못하는 또 다른 이유는 개발자가 기본 스키마를 알고 있어야 하기 때문입니다. 여기서 말하는 스키마란 기본 지식을 의미하는 것이 아니라 열 이름, 형식 및 관계에 대한 전체 지식을 의미하는 것입니다. 이러한 요구 사항을 없애면 위에서처럼 코드가 잘못될 위험이 줄어들 뿐 아니라 작성 및 유지 관리도 용이해집니다. 간단히 나타내면 다음과 같습니다. Convert.ToInt32(ds.Tables[0].Rows[i]["userId"]);위의 코드는 읽기 어려울 뿐만 아니라 열 이름 및 해당 형식에 대해 자세히 알고 있어야 합니다. 이상적인 경우라면 비즈니스 계층에서는 기본 데이터베이스, 데이터베이스 스키마 또는 SQL에 대해 전혀 알 필요가 없습니다. DataSet를 이전 코드 문자열에 나타난 대로 사용하면(CodeBehind를 사용해도 효과가 없음) 비즈니스 계층이 매우 얇아질 수 있습니다. 약한 형식DataSet는 오류가 자주 발생하여 개발 노력에 영향을 줄 수 있는 약한 형식입니다. 다시 말해 DataSet에서 값을 검색할 때마다 System.Object 형식으로 반환되므로 이를 변환해야 합니다. 여기서 직면하는 위험은 변환에 실패하는 상황입니다. 안타깝게도 이러한 실패 상황은 컴파일 타임이 아닌 런타임에 발생합니다. 또한 Microsoft VS.NET(Visual Studio.NET) 같은 도구는 개발자가 약한 형식의 개체를 작업하는 데 있어 그다지 많은 도움이 되지 못합니다. 바로 이러한 이유 때문에 앞에서 스키마에 대해 풍부한 지식을 갖추고 있어야 한다고 언급한 것입니다. 다음은 매우 일반적인 예제입니다. 'Visual Basic.NETDim 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를 비활성화했을 것이며 이 경우 문제는 훨씬 심각해집니다). 아쉽게도 위의 각 코드 줄은 다음과 같은 수많은 런타임 오류를 발생시킬 수 있습니다.
null/nothing에 대한 확인과 try/catch를 변환 과정에 추가하는 방식으로 코드를 수정하여 좀 더 방어적으로 작성할 수 있더라도 개발자에게는 도움이 되지 않습니다. 가장 나쁜 상황은 앞에서 언급했듯이 추상화되지 않는다는 점입니다. 이렇게 되면 DataSet에서 userId를 제거할 때마다 앞에서 언급한 위험을 겪게 되거나 동일한 방어 단계를 다시 프로그래밍해야 합니다(이 문제를 완화하는 데는 유틸리티 함수가 도움이 됨). 약한 형식의 개체는 오류를 항상 자동으로 발견하여 손쉽게 수정하는 디자인 타임이나 컴파일 타임에서 위험이 생산 단계에 노출되어 잡아내기가 어려운 런타임으로 옮깁니다. 비개체 지향DataSet가 개체이고 C# 및 Visual Basic .NET이 OO(개체 지향) 언어라고 해서 이를 사용할 때 개체 지향이 자동으로 이루어지는 것은 아닙니다. OO 프로그래밍의 "Hello World"는 일반적으로 Person 클래스의 하위 클래스인 Employee 클래스 입니다. 그러나 DataSet는 이러한 상속 유형이나 대부분의 다른 OO 기법을 가능한(최소한 자연스럽게/직관적으로) 만들지 않습니다. 클래스 엔터티의 열렬한 지지자인 Scott Hanselman은 이를 다음과 같이 잘 설명하고 있습니다. "DataSet는 물론 개체입니다. 그러나 도메인 개체도 아니고 'Apple' 또는 'Orange'도 아닌 'DataSet' 형식의 개체입니다. DataSet는 일종의 그릇입니다(백업 데이터 저장소에 대한 정보가 있는). DataSet는 행과 열을 저장하는 방법을 알고 있는 개체이기도 합니다. 또한 이 개체는 데이터베이스에 대해서도 많은 부분을 알고 있습니다. 그러나 저는 그릇은 반환하고 싶지 않으며 'Apples' 같은 도메인 개체를 반환하고 싶습니다."1 DataSet는 데이터를 관계 형식으로 유지하므로 강력한 특성을 나타내고 관계형 데이터베이스와 함께 사용하기 편리합니다. 하지만 아쉽게도 이렇게 되면 OO의 이점을 놓치게 됩니다. DataSet는 도메인 개체 역할을 할 수 없으므로 기능을 추가할 수 없습니다. 일반적으로 개체에는 클래스 인스턴스에 대해 동작하는 필드, 속성 및 메서드가 있습니다. 예를 들어 Promote 또는 CalcuateOvertimePay 함수가 someUser.Promote() 또는 someUser.CalculateOverTimePay()를 통해 명확하게 호출할 수 있는 User 개체와 연결되어 있을 수 있습니다. DataSet에 메서드를 추가할 수 없으므로 유틸리티 함수를 사용하고 약한 형식의 개체를 처리하며 하드 코딩된 값의 인스턴스를 코드 전체에 추가로 분배해야 합니다. 또한 기본적으로 절차 코드로 마무리하여 DataSet에서 계속 데이터를 제거하거나 이를 로컬 변수에 저장하여 전달합니다. 두 메서드 모두 단점은 있지만 어느 쪽도 이점은 없습니다. DataSet 사례데이터 액세스 계층이 DataSet를 반환하기 위한 것이라는 생각을 가지고 있으면 몇 가지 중요한 이점을 놓칠 수 있습니다. 한 가지 이유는 특히 추상화 능력을 제한하는 얇거나 존재하지 않는 비즈니스 계층을 사용할 수 있기 때문입니다. 또한 미리 빌드된 일반적인 솔루션을 사용하기 때문에 OO 기법을 사용하기가 어렵습니다. 마지막으로 Visual Studio.NET 같은 도구는 DataSet 같은 약한 형식의 개체를 사용하는 개발자의 능률을 손쉽게 끌어올리지 못해 생산성을 떨어뜨리고 버그의 발생 가능성을 높이게 됩니다. 이러한 모든 요소가 이런 저런 방식으로 코드의 관리 용이성에 직접적으로 영향을 미칩니다. 추상화를 수행하지 않으면 기능 변경 및 버그 수정의 복잡성과 위험이 높아집니다. 또한 코드 재사용이나 OO에서 제공하는 향상된 가독성을 완전히 활용할 수 없게 됩니다. 게다가 개발자는 비즈니스 논리를 작업하든 프레젠테이션 논리를 작업하든 간에 기본 데이터 구조에 대해 자세히 알고 있어야 합니다. 사용자 지정 엔터티 클래스DataSet와 관련된 대부분의 문제는 효율적으로 정의된 비즈니스 계층 내에 OO 프로그래밍의 풍부한 기능을 활용하여 해결할 수 있습니다. 일단 기본적으로 필요한 것은 관계에 따라 구성된 데이터(데이터베이스)를 얻어 개체(코드)에서 사용하는 것입니다. 개념적인 측면에서 보면 자동차에 대한 정보를 저장하는 DataTable을 가지는 대신에 실제로 자동차 개체(사용자 지정 엔터티 또는 도메인 개체라고 함)를 가지는 것입니다. 사용자 지정 엔터티를 살펴보기 전에 먼저 당면한 과제를 짚고 넘어가겠습니다. 가장 분명하게 드러나는 부분은 필요한 코드의 양입니다. 데이터를 가져와 DataSet를 자동으로 채우는 대신 데이터를 가져와 먼저 만들어야 하는 사용자 지정 엔터티에 수동으로 매핑합니다. 이렇게 되면 반복 작업을 수행하게 되므로 코드 생성 도구 또는 O/R 매퍼를 사용하여 이를 줄여야 합니다. 이에 대해서는 나중에 자세히 다룰 것입니다. 보다 큰 문제는 데이터를 관계 영역에서 개체 영역으로 매핑하는 실제 프로세스입니다. 단순한 시스템에서는 매핑이 가장 간단한 작업이지만 시스템이 복잡해지면 두 영역 간의 차이가 벌어져 문제가 발생할 수 있습니다. 예를 들어 개체 영역에서 코드 재사용 및 관리 용이성에 도움이 되는 주요 기법에는 상속이 있습니다. 하지만 아쉽게도 상속은 관계형 데이터베이스에서 낯선 개념입니다. 이러한 차이점의 또 다른 예는 개체 영역은 개별 개체에 대한 참조를 관리하고, 관계 영역은 외래 키를 사용한다는 점입니다. 이렇게 하면 마치 이 접근 방식이 코드의 양이 많고 관계형 데이터와 개체 간의 불일치로 인해 복잡한 시스템에는 적합하지 않는 것처럼 들리지만 실제로는 정반대입니다. 복잡한 시스템은 단일 계층에서 격리하는 데(매핑 프로세스) 어려움이 있으므로 이 접근 방식이 도움이 됩니다(자동화 가능). 또한 이 접근 방식은 이미 상당히 널리 사용되고 있으므로 추가되는 복잡성을 명확하게 처리할 수 있는 다양한 디자인 패턴이 나와 있습니다. 앞에서 복잡한 시스템의 단점과 함께 다룬 DataSet의 단점을 좀 더 자세히 살펴보면 결국 시스템을 빌드하는 데 따르는 어려움은 변경 불가능한 특성만 뛰어넘는 수준으로 마무리 될 것입니다. 사용자 지정 엔터티의 정의사용자 지정 엔터티는 비즈니스 도메인을 나타내는 개체로 비즈니스 계층의 기초가 됩니다. 사용자 인증 구성 요소(이 가이드 전체에서 사용할 예제)가 있다면 아마도 User 및 Role 개체가 있을 것입니다. 또한 전자 상거래 시스템이라면 Supplier와 Merchandise 개체가, 부동산 회사에는 Houses, Rooms 및 Addresses가 있을 수 있습니다. 사용자 지정 엔터티는 코드 내에서 단순한 클래스입니다(엔터티와 클래스는 OO 프로그래밍에 사용될 때 상당히 밀접한 상관 관계를 가짐). 일반적인 User 클래스는 다음과 같습니다. 'Visual Basic .NETPublic 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 } 이점 세부 사항사용자 지정 엔터티를 통해 얻게 되는 중요한 이점은 컨트롤에서는 완전히 개체라는 단순한 사실에서 비롯됩니다. 즉, 사용자 지정 엔터티를 사용하면 다음을 수행할 수 있습니다.
예를 들어 User 클래스는 클래스에 UpdatePassword 함수를 추가하여 효과적으로 사용할 수 있습니다(외부/유틸리티 함수를 사용하면 DataSet로도 가능하지만 가독성과 관리 용이성이 희생됨). 또한 강력한 형식이므로 IntelliSense가 지원됩니다. 그림 1. User 클래스의 IntelliSense 마지막으로 사용자 지정 엔터티는 강력한 형식이므로 다음과 같이 오류에 취약한 캐스트가 덜 필요합니다. Dim userId As Integer = user.UserId'vs Dim userId As Integer = Convert.ToInt32(ds.Tables("users").Rows(0)("UserId")) 개체 관련 매핑앞에서 언급했듯이 이 접근 방식의 한 가지 큰 난제는 관계형 데이터와 개체 간의 차이를 처리하는 것입니다. 관계형 데이터베이스에는 데이터가 영구적으로 저장되기 때문에 두 영역을 연결하는 것 외에 다른 선택은 없습니다. 앞의 User 예제에서 예상되는 데이터베이스의 사용자 테이블 모양은 다음과 같습니다. 그림 2. User의 데이터 뷰 이 관계형 스키마에서 사용자 지정 엔터티로 매핑하는 작업은 다음과 같이 매우 간단하게 이루어집니다. 'Visual Basic .NETPublic 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(); } } connection 및 command 개체는 여전히 평소와 마찬가지로 설정하지만 User 클래스의 새로운 인스턴스를 만들고 DataReader에서 이를 채웁니다. 또한 이 함수 내에서 계속해서 DataSet를 사용하여 이를 사용자 지정 엔터티에 매핑할 수 있지만 DataReader에 대한 DataSet의 주된 이점은 연결이 끊어진 데이터 뷰를 제공한다는 것입니다. 이 경우 User 인스턴스는 이처럼 연결이 끊어진 뷰를 제공하여 DataReader의 속도를 활용할 수 있게 해줍니다. 잠깐, 아직 아무 것도 해결되지 않았습니다!주의 깊은 독자라면 DataSet에 대해 지적한 문제 중 한 가지가 강력한 형식이 아닌 관계로 생산성이 떨어지고 런타임 오류 발생 가능성이 높은 점이라는 것을 알 수 있습니다. 또한 개발자들은 기본 데이터 구조에 대해 세부적인 지식을 갖추고 있어야 합니다. 앞의 코드를 보면 이와 똑같은 함정이 숨어 있음을 알 수 있을 것입니다. 그러나 이러한 문제들은 완전히 격리된 코드 영역에 캡슐화되어 있으므로 클래스 엔터티(웹 인터페이스, 웹 서비스 소비자 및 Windows Form)의 소비자는 이러한 문제를 완전히 알 수 없다는 점을 고려해야 합니다. 이와 반대로 DataSet를 사용하면 코드 전체에 이러한 문제가 확산됩니다. 향상 부분앞의 코드는 매핑 개념을 설명하기 위한 것으로, 두 가지 주요 부분을 향상시켜 이를 개선할 수 있습니다. 첫째, 채우기 코드를 자체 함수로 끌어내어 재사용이 쉽도록 합니다. 'Visual Basic .NETPublic Function PopulateUser(ByVal dr As IDataRecord) As User Dim user As New User user.UserId = Convert.ToInt32(dr("UserId")) '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"]); //NULL 검사 예제 if (dr["UserName"] != DBNull.Value){ user.UserName = Convert.ToString(dr["UserName"]); } user.Password = Convert.ToString(dr["Password"]); return user; } (참고: 프로그래머 코멘트는 샘플 프로그램 파일에는 영문으로 제공되며 기사에는 설명을 위해 번역문으로 제공됩니다.) 두 번째로 확인할 사항은 매핑 함수에 SqlDataReader를 사용하는 대신 IDataRecord를 사용했다는 점입니다. 이것은 모든 DataReader가 구현하는 인터페이스입니다. IDataRecord를 사용하면 매핑 프로세스를 공급업체와 무관하게 실행할 수 있습니다. 즉, 앞의 함수가 OleDbDataReader를 사용하더라도 이를 통해 Access 데이터베이스에서 User를 매핑할 수 있습니다. 이러한 특정 접근 방식과 Provider Model Design Pattern(링크 1, 링크 2)을 조합하면 서로 다른 데이터베이스 공급업체에 대해 손쉽게 사용할 수 있는 코드를 얻게 됩니다. 마지막으로 위의 코드는 캡슐화가 얼마나 강력한지를 보여 줍니다. DataSet의 NULL을 처리하기가 쉽지 않은 이유는 값을 추출할 때마다 NULL인지 확인해야 하기 때문입니다. 우리는 위의 채우기 메서드를 사용해 이를 단일 위치에서 편리하게 관리하여 소비자가 이를 직접 처리해야 하는 수고를 덜어주었습니다. 매핑 위치이러한 데이터 액세스 및 매핑 함수가 개별 클래스의 일부인지 아니면 해당 사용자 엔터티의 일부인지에 대한 논란이 일부에서 제기되고 있습니다. 모든 사용자 관련 작업(데이터 얻기, 업데이트 및 매핑)을 User 사용자 지정 엔터티의 일부로 사용하면 확실한 이점을 얻을 수 있습니다. 이러한 특성은 데이터베이스 스키마가 사용자 지정 엔터티와 매우 비슷한 경우에 확실한 효과를 나타냅니다(이 예제에서처럼). 시스템의 복잡성이 늘어나면서 두 영역 간의 차이가 드러나기 시작함에 따라 데이터 계층과 비즈니스 계층을 명확하게 구분하면 유지 관리를 단순화하는 데 큰 도움이 될 수 있습니다(이를 데이터 액세스 계층이라고 함). 자체 계층인 DAL 내에 액세스 및 매핑 코드를 두었을 때 얻어지는 부수적인 효과는 다음과 같은 명확한 계층 분리를 위한 훌륭한 규칙을 제공한다는 점입니다. "System.Data에서 클래스를 반환하거나 DAL에서 자식 네임스페이스를 반환해서는 안 됩니다." 사용자 지정 컬렉션지금까지는 개별 엔터티를 처리하는 부분만 살펴보았지만 단일 개체를 둘 이상 처리해야 하는 경우도 비일비재할 것입니다. 이를 위한 단순한 솔루션은 Arraylist 같은 일반적인 컬렉션 내에 여러 값을 저장하는 것입니다. 하지만 DataSet에 대해 겪었던 다음과 같은 몇 가지 문제를 다시 유발하므로 이상적인 솔루션이라고 하기에는 부족합니다.
여기서의 요구에 가장 적합한 솔루션은 사용자 지정 컬렉션을 만드는 것입니다. 다행히도 Microsoft .NET Framework는 다음과 같이 이를 위해 상속하도록 설계된 클래스인 CollectionBase를 제공합니다. CollectionBase는 전용 Arraylists 내에 모든 개체 형식을 저장하지만 User 개체 같은 특정 형식만 사용하는 메서드를 통해 이들 전용 컬렉션에 대한 액세스를 노출하는 방식으로 작동합니다. 즉, 약한 형식의 코드가 강력한 형식의 API 내에 캡슐화되는 것입니다. 사용자 지정 컬렉션은 코드가 많은 것처럼 보이지만 대부분은 코드 생성 또는 잘라내기 및 붙여넣기를 쉽게 수행할 수 있으며 찾아서 바꾸기는 한 번만 수행하면 되는 경우가 많습니다. 다음과 같이 User 클래스의 사용자 지정 컬렉션을 구성하는 다양한 부분을 살펴보겠습니다. 'Visual Basic .NETPublic 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 .NETPublic 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 .NETPublic 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 .NETPublic 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; } DataSet를 사용하면 DataTable.Select로도 동일한 방법을 수행할 수 있습니다. 자신의 기능을 만들면 코드를 완전히 제어할 수 있으며 Select 메서드는 매우 편리하고 자유로운 코딩 방식으로 이 기능을 제공합니다. 한편 Select는 강력한 형식이 아니므로 이를 사용하려면 개발자가 기본 데이터베이스에 대해 알고 있어야 합니다. 사용자 지정 컬렉션 바인딩우리가 살펴본 첫 번째 예제는 DataSet를 ASP.NET 컨트롤에 바인딩한 것이었습니다. 이 작업이 상당히 자주 이루어진다는 점을 고려한다면 사용자 지정 컬렉션이 그만큼 쉽게 바인딩된다는 사실에 반가움을 느낄 것입니다(이는 CollectionBase가 바인딩에 사용되는 Ilist를 구현하기 때문임). 다음과 같이 사용자 지정 컬렉션은 이를 노출하는 모든 컨트롤에 대해 DataSource 역할을 수행할 수 있으며 DataBinder.Eval은 DataSet에서처럼 사용할 수 있습니다. 'Visual Basic .NETDim 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에서 처리를 수행하는 경우 e.Item.DataItem을 DataRowView로 캐스팅할 수 있습니다. 다음과 같이 사용자 지정 컬렉션에 바인딩하는 경우 e.Item.DataItem은 대신 사용자 지정 엔터티로 캐스팅하며 이 예제에서는 User 클래스입니다. 'Visual Basic .NETProtected 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 .NETPublic 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;} } } 또는 다음과 같은 Role의 컬렉션일 수도 있습니다. 'Visual Basic .NETPublic 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; } } } 위의 두 예제에 사용된 Role 클래스 또는 RoleCollection 클래스는 가상의 것으로, 이는 User 및 UserCollection 클래스와 같이 사용자 지정 엔터티 또는 컬렉션 클래스의 하나 입니다. 관계 매핑실질적인 문제는 관계를 매핑하는 방법에 있습니다. 간단한 예제를 살펴보고 역할과 함께 userId를 기반으로 사용자를 검색하겠습니다. 먼저, 다음과 같은 관계형 모델을 살펴봅니다. 그림 3. Users 및 Roles 간의 관계 이제 Users 테이블과 Roles 테이블 모두 간단한 방식으로 사용자 지정 엔터티에 매핑할 수 있는지 확인해 보겠습니다. 여기에는 Users와 Roles 사이에 다대다 관계를 나타내는 UserRoleJoin 테이블도 있습니다. 그런 다음 아래와 같이 저장 프로시저를 사용하여 두 개의 개별 결과를 가져오는데 다음과 같이 첫 번째는 User용이고 두 번째는 사용자의 Role을 위한 것입니다. 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 .NETPublic 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(); } } User 인스턴스가 만들어져 채워지면 다음 결과로 이동하고 선택 및 순환하여 Roles를 채우고 이를 User 클래스의 RolesCollection 속성에 추가합니다. 중급 단계이 가이드의 목적은 사용자 지정 엔터티 및 컬렉션의 개념과 사용 방법을 소개하는 것입니다. 사용자 지정 엔터티의 사용은 업계에서 널리 사용되는 방식이며 그로 인해 다양한 시나리오를 처리하는 수많은 패턴이 문서화되어 있습니다. 디자인 패턴이 유용한 이유는 다양합니다. 첫째, 특정 상황을 처리하는 데 있어 아마 주어진 문제를 처음 겪지는 않을 것입니다. 디자인 패턴을 사용하면 이미 시도된, 그리고 테스트된 솔루션을 주어진 문제에 다시 사용할 수 있습니다(설계 패턴을 완전히 잘라내어 붙여넣을 수는 없지만 대개 솔루션을 위한 훌륭한 기초가 됩니다). 또한 널리 사용되는 접근 방식이고 체계적으로 문서화되어 있으므로 시스템을 복잡성의 정도에 따라 확장할 수 있다는 안정감을 느끼게 해줍니다. 디자인 패턴은 또한 일반적인 어휘를 제공하여 정보의 전달 및 교육이 매우 용이하게 이뤄질 수 있습니다. 물론 디자인 패턴은 사용자 지정 엔터티에만 적용되는 것이 아니며 실제로 다양한 분야에 사용됩니다. 하지만 사용자 지정 엔터티와 매핑 프로세스에 적용할 수 있는 문서화된 패턴이 얼마나 되는지 확인하면 깜짝 놀라게 될 것입니다. 이 마지막 섹션은 보다 크고 복잡한 시스템을 실행할 수 있는 일부 고급 시나리오를 설명하기 위한 것입니다. 대부분의 항목은 개별 가이드만으로 충분할 수 있지만 여기서는 최소한 몇 가지 시작 리소스를 제공할 예정입니다. 처음에 활용하기 좋은 자료로는 Martin Fowler의 Patterns of Enterprise Application Architecture가 있는데 일반적인 디자인 패턴을 위한 효과적인 참조(자세한 설명과 많은 샘플 코드가 있는) 역할 밖에 못하지만 처음 100페이지를 잘 읽어보면 전체적인 개념을 이해하는 데 많은 도움이 됩니다. 또한 Fowler의 온라인 catalog of patterns는 이미 개념에 친숙하지만 간단한 참조가 필요한 사람에게 유용합니다. 동시성앞에서 소개한 예제들은 모두 데이터베이스에서 데이터를 가져오고 이 데이터에서 개체를 만드는 부분을 다루고 있습니다. 또한 대부분의 경우 데이터의 업데이트, 삭제 및 삽입이 간단히 이루어집니다. 여기서 소개한 비즈니스 계층은 개체를 만들고 이를 데이터 액세스 계층으로 전달하며 관계 영역에 대한 매핑을 처리합니다. 예를 들면 다음과 같습니다. 'Visual Basic .NETPublic sub UpdateUser(ByVal user As User) Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("UpdateUser", connection) '역방향 맵핑을 위한 재사용 함수도 만들 수 있음 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); //역방향 맵핑을 위한 재사용 함수도 만들 수 있음 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(); } } 그러나 동시성을 처리하는 경우는 간단하지 않습니다. 즉, 두 명의 사용자가 동시에 동일한 데이터를 업데이트하면 어떤 일이 발생할까요? 기본 동작(아무 것도 하지 않는 경우)은 데이터를 마지막으로 커밋한 사람이 이전의 모든 작업을 덮어쓰는 것입니다. 이러한 동작은 사용자 한 명의 작업을 자동으로 덮어쓰게 되므로 이상적이지는 않을 수 있습니다. 충돌을 완전히 피하는 한 가지 방법은 비관적 동시성을 사용하는 것이지만 이 방식을 사용하려면 확장 가능한 방식으로 구현하기 힘든 특정 유형의 잠금 메커니즘을 사용해야 합니다. 이에 대한 대안은 낙관적 동시성 기법을 사용하는 것입니다. 첫 번째 커밋에 우선 순위를 부여하고 이후의 사용자에게 알리는 것은 일반적으로 보다 순조롭고 사용자 친화적인 접근 방식입니다. 이를 위해 타임스탬프 같은 특정 유형의 행 버전 관리를 사용합니다. 참고 자료
성능적절한 유연성 및 성능에 대한 염려와는 달리 사소한 성능 차이에 대해 걱정하는 경우가 너무나도 많습니다. 성능은 물론 중요하지만 가장 간단한 솔루션을 제외한 모든 부분에 대해 일반화된 지침을 제공하기란 어렵기 마련입니다. 사용자 지정 컬렉션과 DataSet를 예로 들어봅시다. 어느 쪽이 더 빠를까요? 사용자 지정 컬렉션을 사용하면 DataReader를 많이 사용하여 데이터베이스에서 데이터를 보다 신속하게 가져올 수 있습니다. 하지만 여기서 주의할 점은 이를 어떤 데이터 형식과 함께 어떻게 사용하느냐에 따라 해답이 달라지므로 포괄적인 설명은 아무런 소용이 없다는 것입니다. 보다 중요한 사항은 절감할 수 있는 처리 시간이 어느 정도이든 관계없이 관리 용이성과의 차이에 비해 그다지 많지 않을 것이라는 사실입니다. 물론 관리하기 용이한 고성능 솔루션을 가질 수 없다는 말은 아닙니다. 이를 사용하는 방법에 크게 좌우된다고 다시 말하지만 여기에는 성능을 극대화할 수 있는 몇 가지 패턴이 있습니다. 먼저 사용자 지정 엔터티 및 컬렉션 캐시와 DataSet가 HttpCache 같은 동일한 메커니즘을 사용할 수 있다는 점을 알아야 합니다. DataSet의 한 가지 이점은 Select 문을 작성하여 필요한 정보만 포함시킬 수 있는 기능에 있습니다. 사용자 지정 엔터티를 사용하면 전체 엔터티와 자식 엔터티까지 모두 채워야 한다는 느낌을 받는 경우가 많습니다. 예를 들어 DataSet를 사용하여 Organization 목록을 표시하려면 OganizationId, Name 및 Address를 가져와 이를 반복기에 바인딩할 것입니다. 필자의 경우 사용자 지정 엔터티를 사용할 때 다른 모든 Organization 정보를 가져와야 할 것 같은 느낌까지 듭니다. 또한 이러한 정보에는 ISO 인증 여부, 모든 직원 컬렉션, 추가 연락처 정보 등이 포함될 수 있습니다. 다른 사람은 이러한 고민거리를 공유하지 않을 수도 있지만 다행히도 우리는 원하는 경우 사용자 지정 엔터티를 세부적으로 제어할 수 있습니다. 가장 일반적인 접근 방식은 처음 필요할 때만 정보를 가져오는 레이지 로드(lazy-load) 패턴 형식을 사용하는 것입니다(속성에 효과적으로 캡슐화할 수 있음). 개별 속성을 이런 방식으로 제어하면 다른 방식으로는 얻기 힘든 엄청난 유연성을 발휘하게 됩니다(DataColumn 수준에서 유사한 작업을 수행한다고 가정해 보십시오). 참고 자료
정렬 및 필터링DataView의 기본 정렬 및 필터링 지원은 SQL 및 기본 데이터 구조에 대해 알아야 한다는 단점은 있지만 편리한 기능이며 사용자 지정 컬렉션에는 없는 기능이기도 합니다. 정렬 및 필터링은 계속 수행할 수 있지만 이렇게 하려면 기능을 작성해야 합니다. 고급 기법이라고 할 수는 없지만 전체 데모 코드는 이 섹션의 범위를 벗어납니다. 하지만 필터 클래스로 필터링하거나 비교 클래스로 정렬하는 것 같은 대부분의 기법은 전과 상당히 비슷하며 분명 방법이 있습니다. 다음 리소스를 참조하십시오. 코드 생성개념적인 문제를 지나쳤다면 사용자 지정 엔터티 및 컬렉션의 중요한 단점은 이러한 모든 유연성, 추상화 및 낮은 유지 관리 비용을 제공하는 추가 코드의 양을 들 수 있습니다. 실제로 지금까지 언급한 줄어든 유지 관리 비용과 버그보다 추가 코드가 더 부담스러울 수도 있습니다. 어떤 솔루션도 완벽하지는 않으므로 이것은 분명히 올바른 지적이지만 디자인 패턴 및 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 릴리스는 이 가이드에서 전체적으로 살펴본 몇 가지 구현 세부 사항이 변경될 예정입니다. 이러한 변화로 인해 사용자 지정 엔터티를 지원하는 데 필요한 코드의 양이 줄어드는 것은 물론 매핑 문제를 처리하는 데도 도움이 될 것으로 보입니다. Genericsgenerics에 대해 자주 언급하는 한 가지 중요한 이유는 개발자에게 강력한 형식의 컬렉션을 제공하기 때문입니다. Arraylists 같은 기존 컬렉션은 특성상 약한 형식으로 인해 외면 당했습니다. Generics는 현재의 컬렉션과 동일한 수준의 편의성을 강력한 형식으로 제공합니다. 이를 위해 선언 시에 형식을 지정해야 합니다. 예를 들어 다음과 같이 코드를 추가하지 않고 UserCollection을 대체하며 List<T> generic의 새 인스턴스를 만들고 User 클래스를 지정할 수 있습니다. 'Visual Basic .NETDim users as new IList(of User) //C# IList<User> users = new IList<user>(); 선언된 users 컬렉션은 User 형식의 개체만 처리할 수 있어 컴파일 타임 검사 및 최적화의 모든 이점을 제공합니다. 참고 자료 Nullable 형식Nullable 형식은 실제로 앞에서 나열된 내용과 다른 이유로 사용되는 generics입니다. 데이터베이스를 처리할 때 직면하는 한 가지 난제는 NULL을 지원하는 열을 적절히 일관된 방식으로 처리하는 것입니다. 문자열 및 다른 클래스(참조 형식이라고 함)를 처리할 때는 다음과 같이 nothing/null을 코드의 변수에 할당할 수 있습니다. 'Visual Basic .NETif dr("UserName") Is DBNull.Value Then user.UserName = nothing End If //C# if (dr["UserName"] == DBNull.Value){ user.UserName = null; } 아니면 그냥 아무 작업도 수행하지 않을 수 있습니다(참조 형식은 기본적으로 nothing/null임). 하지만 integer, boolean, decimal 등과 같은 값 형식에 대해서는 이 방식이 적용되지 않습니다. nothing/null을 이러한 값에 할당할 수는 있지만 이렇게 하면 기본값이 할당됩니다. 정수를 선언하거나 정수에 nothing/null을 할당하는 경우 변수는 실제로 값 0을 저장하게 됩니다. 이로 인해 데이터베이스에 다시 매핑하기가 어려워지는데 이는 값이 0인지 null인지 확인이 필요하기 때문입니다. nullable 형식은 값 형식에 실제 값이나 null을 저장할 수 있도록 하여 이 문제를 해결합니다. 예를 들어 userId 열에 null 값을 지원하려는 경우(현실적이지 않지만) 먼저 다음과 같이 userId 필드와 해당하는 속성을 nullable 형식으로 선언합니다. 'Visual Basic .NETPrivate _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 속성을 사용하여 nothing/null의 할당 여부를 결정합니다. 'Visual Basic .NETIf UserId.HasValue Then Return UserId.Value Else Return DBNull.Value End If //C# if (UserId.HasValue) { return UserId.Value; }else { return DBNull.Value; } 참고 자료 Iterator지금껏 살펴본 UserCollection 예제는 사용자 지정 컬렉션에 필요할 수 있는 기본 기능만 나타내고 있습니다. 제공된 구현으로 수행할 수 없는 일 중 하나로 foreach 루프를 사용하여 컬렉션을 순환하는 기능을 들 수 있습니다. 이를 수행하려면 사용자 지정 컬렉션에 IEnumerable 인터페이스를 구현하는 열거자 지원 클래스가 있어야 합니다. 이는 매우 간단하고 반복적인 프로세스지만 훨씬 많은 코드가 필요합니다. C# 2.0에는 이러한 인터페이스의 구현 세부 사항을 자동으로 처리하는 새로운 yield 키워드가 도입되었습니다. 현재 Visual Basic .NET에는 새로운 yield 키워드와 동일한 항목이 없습니다. 침고 자료: 결론사용자 지정 엔터티 및 컬렉션으로 전환하는 결정을 가볍게 내려서는 안 되며 수많은 요소들을 고려해야 합니다. 예를 들어 OO 개념에 대한 이해, 이 새로운 접근 방식을 활용하는 데 걸리는 시간은 물론 배포를 염두에 두고 있는 환경 등이 여기에 속합니다. 일반적으로는 이점이 상당하지만 특정 상황에 해당되지 않을 수도 있습니다. 또한 자신의 사례에 적합하더라도 단점으로 인해 효력을 발휘하지 못할 수 있습니다. 아울러 수많은 대안 솔루션이 있음을 염두에 둬야 합니다. Jimmy Nilsson은 직접 저술한 5부 시리즈인 Choosing Data Containers for .NET(1, 2, 3, 4 및 5부)을 통해 이러한 몇 가지 대안을 간략히 설명하고 있습니다. 사용자 지정 엔터티는 개체 지향 프로그래밍의 풍부한 기능을 제공하는 것은 물론 견고하고 관리가 용이한 N 계층 아키텍처를 위한 토대를 설치하는 데 도움을 줍니다. 이 가이드의 목표 중 한 가지는 시스템을 일반적인 DataSet 및 DataTable 대신 이를 구성하고 있는 비즈니스 엔터티의 관점에서 생각하도록 만드는 것입니다. 또한 선택한 방법, 디자인 패턴, 개체 및 관계 영역 간의 차이(자세한 정보) 및 N 계층 아키텍처에 관계없이 알아두어야 할 몇 가지 주요 사항을 짚어 보았습니다. 지금까지 쏟은 시간은 시스템 수명 전체에 걸쳐 수많은 시간을 보상해 주는 방법이 될 것입니다. 관련 서적
1http://www.hanselman.com/blog/PermaLink.aspx?guid=d88f7539-10d8-4697-8c6e-1badb08bb3f5 |
'ASP.NET' 카테고리의 다른 글
Transferring page values to another page (0) | 2005.07.26 |
---|---|
숫자형식을 문자 형식으로 바꾸는 소스 (0) | 2005.06.23 |
asx 리스트 만들기 | (0) | 2005.05.21 |
GIF 투명 이미지를 만들려 하는데 (0) | 2005.05.13 |
자바스크립트 asp.net 에서 좋은것.. (0) | 2005.05.02 |