Silverlight를 설치하려면 여기를 클릭합니다.*
Korea 대한민국변경|Microsoft 전체 사이트
MSDN
|개발자 센터
MSDN Home   MSDN Home

데이터 지향 응용 프로그램을 Visual Basic 6에서 Visual Basic 2005로 변환(2부)

Ken Getz
Paul D. Sheriff

2005년 10월

요약: 이 연재 기사의 2부에서는 ADO 코드를 ADO.NET 코드로 변환하는 데이터 기능에 대해 자세히 설명합니다. ADO.NET에서는 보다 간단한 코드를 사용하여 작업할 수 있으며 COM Interop 기능을 대체함으로써 응용 프로그램의 효율성을 높일 수 있습니다(32페이지/인쇄 페이지 기준).

목차

소개
필요한 ADO.NET 클래스
Utility 모듈 업데이트
CustomerHandler 클래스 재정비
결론

소개

3부로 나뉘어 연재되는 이 기사의 1부에서는 기존 Visual Basic 6 데이터 지향 응용 프로그램을 Visual Basic 2005 응용 프로그램으로 정상적으로 작동하도록 변환하는 과정을 살펴보았습니다. 이렇게 변환된 새 응용 프로그램은 Visual Studio 2005에서 실행되며 Windows용 .NET 응용 프로그램을 만드는 데 사용할 수도 있지만 아직 많은 과정이 남아 있습니다. 자세한 내용은 연재의 첫 번째 기사 데이터 지향 응용 프로그램을 Visual Basic 6에서 Visual Basic 2005로 변환(1부)을 참조하십시오. 1부에서 필자는 Windows Forms 기능과 ADO.NET으로 코드를 변환하는 방법에 대해서는 2부에서 자세하게 설명하기로 약속했었습니다. 그러나 ADO에서 ADO.NET으로 아무런 문제 없이 변환 작업을 수행하는 데는 예상했던 것보다 많은 작업이 필요한 것으로 확인됨에 따라 2부에서는 폼은 변경하지 않고 데이터 기능만 중점적으로 살펴보기로 결정을 내렸습니다. 3부에서는 Windows Forms 데이터 바인딩 기능을 활용하여 이전보다 훨씬 간단한 코드로 응용 프로그램 전체를 다시 작성하는 방법을 알아보겠습니다.

현재 상태의 응용 프로그램에서는 ADO와의 통신에 COM Interop 기능을 사용하여 Visual Basic 6 응용 프로그램에서 사용한 코드와 아주 유사한 코드를 사용하여 작업할 수 있습니다. 이제 다음 목표는 기존 ADO 코드를 모두 ADO.NET을 사용하는 코드로 바꾸는 것이 될 수 있겠습니다. 이 두 번째 연재 기사에서 그 과정을 자세히 살펴보겠습니다. 다른 특별한 요인이 없다면 COM Interop 요구 사항을 대체함으로써 응용 프로그램의 효율성을 높일 수 있습니다. 또한 ADO.NET에서는 비슷한 작업을 더 간단한 코드로 실행할 수 있는 경우가 많으므로 응용 프로그램을 일정 수준 간소화할 수 있습니다. 이 기사의 단계를 시작하는 데 필요한 프로젝트는 연결된 다운로드 파일에 들어 있습니다. 이전 기사의 단계를 직접 수행하는 데 문제가 있었다면 제공된 프로젝트를 사용하여 시작하시기 바랍니다.

필요한 ADO.NET 클래스

기존 응용 프로그램에서 중요한 부분을 제거하고 ADO를 ADO.NET 및 다양한 개체로 바꾸기 전에 사용할 개체에 대해 익숙해져야 합니다. 여러 가지 ADO.NET 클래스와 각각의 속성, 메서드, 이벤트는 이 기사의 내용에 적합하지 않고 공간도 부족하므로 이 문서에서 자세히 설명하지 않지만 이러한 설명이 없더라도 ADO.NET의 기본 개념을 쉽게 이해할 수 있습니다. 또한 ADO를 사용한 경험이 있다면 ADO.NET을 보다 쉽게 이해할 수 있습니다. 데이터를 검색하고 저장하며 관련 작업을 수행하는 것이 ADO.NET의 전체 기능입니다. ADO.NET에 대한 일반적인 내용은 Overview of ADO.NET (영문)을 참조하십시오.

ADO.NET에서는 작업에 사용할 수 있는 여러 가지 클래스를 제공하지만 이 샘플 응용 프로그램에서는 System.Data.DataTable 클래스 및 OLEDB 데이터 공급자와 직접 작업하는 System.Data.OleDbConnection, OleDbCommandBuilder, OleDbDataAdapter 등을 비롯한 몇 가지 클래스만 이해하면 됩니다. 이 기사의 설명을 참조하면 충분히 이러한 클래스를 사용할 수 있습니다.

System.Data 네임스페이스에는 연결이 끊어진 데이터에 대한 작업에 사용되는 클래스가 포함되어 있습니다. 따라서 필요한 데이터만 검색한 후에는 System.Data 네임스페이스에 있는 클래스를 사용하여 해당 데이터를 조작합니다. System.Data.DataTable 클래스는 행 및 열 집합의 메모리 내 표현을 제공합니다. 대부분의 사용자에게는 데이터베이스의 테이블과 비슷하게 느껴질 것입니다. 실제로 DataTable 클래스는 스키마에 대한 정보를 제공하는 열(System.Data.DataColumn 인스턴스로 표시)과 실제 데이터가 들어 있는 행(System.Data.DataRow 인스턴스로 표시)으로 이루어진 데이터 행과 열의 컨테이너를 제공합니다. 응용 프로그램의 새 버전에서는 ADO Recordset 인스턴스 대신 단일 DataTable 인스턴스를 사용합니다. 한편 다른 응용 프로그램에서는 여러 테이블을 동시에 사용해야 하는 경우가 많은데 DataSet 클래스에서 바로 이러한 기능을 제공합니다. 하나의 DataSet에 여러 DataTable 인스턴스를 채움으로써 DataRelation 인스턴스를 사용하여 테이블의 관계를 설정하고 부모/자식 관계를 쉽게 제공할 수 있습니다. DataTable 인스턴스는 DataTable을 채울 당시의 원래 데이터뿐 아니라 현재 오프라인 데이터까지 추적합니다. 이러한 방법으로 변경 내용을 저장할 때 각 행의 변경 여부를 알 수 있으며 해당 열에 있는 현재 값을 업데이트를 수행하기 전에 데이터 원본에 있던 값과 비교할 수 있습니다.

DataTable로 데이터를 가져올 때는 한 번에 한 행씩 데이터를 추가하거나 데이터베이스 테이블의 전체 데이터로 DataTable을 채우거나 또는 한꺼번에 표시할 수 있습니다. DataTable과 DataSet 클래스는 데이터 원본과 직접 통신할 수 없으므로 연결 정보를 포함하는 System.Data.OleDb.OleDbDataAdapter와 같은 클래스가 필요합니다. 샘플 응용 프로그램에서는 데이터를 한 번에 검색하기 위해 OleDbDataAdapter 인스턴스와 여기에 속한 Fill 메서드를 사용합니다. OleDbDataAdapter와 이의 사촌뻘에 해당하는 System.Data.SqlClient.SqlDataAdapter, System.Data.Odbc.OdbcDataAdapter 등으로 DataTable을 채울 수 있습니다. 또한 사용자가 DataTable의 데이터를 변경할 수 있도록 하는 경우에도 OleDbDataAdapter 인스턴스를 사용하고 여기에 속한 Update 메서드를 호출하여 데이터 원본의 데이터를 업데이트할 수 있습니다. 이 문서에서는 OleDbDataAdapter를 비롯하여 System.Data.OleDb 네임스페이스에 포함된 클래스만 언급하고 있지만 System.Data.SqlClient, System.Data.Odbc 등의 네임스페이스에 포함된 클래스에도 동일한 개념이 적용됩니다. System.Data.SqlClient 네임스페이스는 SQL Server와 직접 상호 작용하는 클래스를 제공하며 System.Data.Odbc 네임스페이스는 ODBC 데이터 엔진과 상호 작용하는 클래스를 제공합니다.

OleDbDataAdapter 클래스를 사용하여 DataTable에 데이터를 채우려면 원하는 데이터에 대한 정보(예: SELECT SQL 문, 저장 프로시저 이름, 쿼리, 뷰 등)와 데이터를 가져올 위치(연결 문자열)에 대한 정보가 필요합니다. 검색할 데이터에 관한 정보가 포함된 OleDbCommand 개체와 데이터를 가져올 위치에 대한 정보가 포함된 OleDbConnection 개체를 OleDbDataAdapter에 제공하여 이러한 문제를 해결할 수 있습니다. ADO.NET은 매우 유연하므로 원하는 경우 OleDbDataAdapter를 만들 때 SQL 및 연결 문자열을 제공하여 데이터 원본에 연결하고 데이터를 가져오는 작업의 모든 세부적인 처리를 데이터 어댑터가 자동으로 수행하도록 할 수 있습니다. 또한 직접 OleDbConnection 인스턴스와 OleDbCommand 인스턴스를 만들고 OleDbDataAdapter에 제공할 수도 있습니다. 이 경우 데이터 원본에 연결할 시점을 정확하게 제어할 수 있습니다. 가장 중요한 규칙은 OleDbDataAdapter에서 여기에 속한 Fill 메서드를 호출할 때의 연결 상태를 유지한다는 것입니다. 연결이 닫힌 경우 Fill 메서드를 호출하면 연결을 열고 데이터를 가져온 후에 다시 닫습니다. 연결되어 있는 상태라면 Fill 메서드에서 데이터를 가져온 후에도 계속 열린 상태가 유지됩니다.

데이터를 검색할 때에는 분명 SELECT 명령을 사용할 것입니다. 그렇다면 데이터를 업데이트, 삭제 또는 삽입할 때는 어떻게 하겠습니까? OleDbDataAdapter 클래스는 OleDbCommand 개체가 포함된 속성 4개를 제공합니다. 이러한 속성으로 각각 특정 데이터 작업을 수행할 수 있습니다. 즉, OleDbDataAdapter에서 제공하는 SelectCommand, InsertCommand, UpdateCommandDeleteCommand 속성을 사용하여 필요한 작업을 수행하면 됩니다. OleDbDataAdapter 인스턴스를 만들 때는 일반적으로 사용자가 데이터를 검색할 SQL 문자열을 인스턴스에 전달하면 ADO.NET이 해당 문자열을 포함하는 OleDbCommand 개체를 OleDbDataAdapter의 SelectCommand 속성에 추가합니다. 이때 해당하는 DELETE, UPDATE 및 INSERT 문을 직접 제공하거나 도우미 개체인 OleDbCommandBuilder를 사용하여 작업을 수행할 수도 있습니다. 이 클래스에서는 필요한 DELETE, UPDATE 및 INSERT SQL 문을 자동으로 생성할 수 있으며 샘플 응용 프로그램에서 이러한 기능을 활용합니다. 완벽주의자라면 직접 SQL 문을 작성하고자 할 수 있지만 그러한 단계는 불필요합니다.

사용자 입력 작업을 줄이기 위해 기본적으로 Visual Basic 2005에서는 System.Data 네임스페이스에 대해 프로젝트 차원의 Imports 문을 자동으로 프로젝트에 추가합니다. 샘플 프로젝트에서는 이 기능도 활용합니다. 다른 네임스페이스에 대한 Imports 문은 직접 추가해야 합니다. 샘플 프로젝트에서 다른 System.Data.OleDb 네임스페이스에 대한 Imports 문을 추가하는 것도 확인할 수 있습니다.

프로젝트 설정 수정

이 기사의 남은 부분의 목적은 실행되는 Visual Basic 2005 응용 프로그램을 ADO 대신 ADO.NET을 사용하도록 수정하는 것입니다. 여기에 제시되는 단계에 따라 필요한 사항을 변경할 수 있습니다. 방대한 내용 때문에 이러한 변환을 수행하기 위해 많은 부분을 변경해야 하는 것으로 느껴질 수 있지만 실제로는 ADO.NET 개체의 동작 원리와 프로시저를 작성하여 해당 개체를 활용하는 방법을 이해하는 것이 전부입니다. 여기에 소개되는 코드 양의 방대함 때문에 부담스럽게 생각할 필요는 없습니다. 각 프로시저를 이해하는 데 시간을 투자하면 직접 ADO.NET 응용 프로그램을 만드는 데 필요한 기초 지식을 얻을 수 있습니다.

프로젝트 수정을 시작하기 전에 먼저 Visual Studio 2005를 적절하게 구성하는 과정을 살펴보겠습니다. 다음 단계에 따라 설정을 변경하십시오.

  1. Visual Studio 2005를 실행하고 샘플 프로젝트(ADOForm.sln)를 로드합니다.
  2. 속성 메뉴에서 프로젝트 | ADOForm을 선택합니다.
  3. 프로젝트 속성 왼쪽에서 컴파일 탭을 선택합니다. Option Explicit 설정과 Option Strict 설정이 각각 On으로 구성되어 있고 Option Compare 설정이 Binary로 구성되어 있는지 확인합니다. 아래에 있는 표에서 그림 1과 같이 되도록 설정을 수정합니다.
  4. 프로젝트 설정을 닫고 빌드 | 솔루션 다시 빌드를 선택합니다. 그러면 한 개나 두 개의 경고가 표시되면서 샘플이 빌드될 것입니다. 앞으로 변경 사항을 적용하는 동안 더 많은 경고가 표시될 수 있습니다. 그러나 작업을 끝내는 시점이 되면 이러한 경고가 모두 해결됩니다.

    그림 1. 프로젝트 설정

DataLayer 클래스 수정

ADO.NET을 활용하기 위해서는 데이터 계층을 많이 변경해야 합니다. 먼저 Visual Studio 2005 코드 편집기에서 DataLayer.vb를 엽니다. 파일의 맨 위에 다음과 같은 코드 줄이 있습니다.

Option Strict Off
Option Explicit On

Friend Class DataLayer

변환 유틸리티에서 Option Strict Off 설정을 추가했습니다. 이 설정은 Visual Basic 2005 코드에서 COM Interop을 사용하여 ADO를 호출할 수 있도록 합니다. COM 개체는 강력한 형식의 참조보다는 개체 참조를 사용하는 경우가 많으므로 변환 유틸리티에서는 이러한 느슨한 참조가 작동할 수 있도록 간단히 Option Strict 설정을 해제하여 사용자가 직접 모든 문제를 수정할 필요가 없도록 했습니다. 이 클래스에서 모든 코드를 수정하고 프로젝트 차원에서 Option Strict/Option Explicit 설정을 지정할 것이므로 더 이상 이 클래스의 맨 처음에 이러한 Option 문을 사용할 필요가 없습니다. 따라서 이 두 개의 문을 지금 삭제합니다.

System.Data.OleDb 네임스페이스의 개체를 사용할 것이므로 파일 맨 처음에 Imports 문을 추가하면 코드 입력 작업을 다소 줄일 수 있습니다. 파일의 코드 맨 위에 다음 문을 추가합니다.

Imports System.Data.OleDb

이 코드를 다른 프로젝트에서도 사용할 수 있도록 나중에는 DataLayer.vb 클래스를 별도의 어셈블리로 옮기는 작업을 생각해 볼 수 있을 것입니다. 이렇게 되면 이 클래스의 선언을 Friend에서 Public으로 수정해야 합니다. 선언을 수정함으로써 외부 어셈블리에서 해당 코드에 액세스할 수 있습니다.

' 변경할 코드:
Friend Class DataLayer
' 변경된 코드:
Public Class DataLayer
(참고: 프로그래머 코멘트는 샘플 프로그램 파일에는 영문으로 제공되며 기사에는 설명을 위해 번역문으로 제공됩니다.)

원래 프로젝트에서는 ADO Recordset 개체를 사용하여 데이터를 이동했었지만 새 프로젝트에서는 DataTable 인스턴스를 사용하게 됩니다. 따라서 몇 가지 이름을 수정해야 합니다. 표준 찾기/바꾸기 메커니즘을 사용할 수도 있지만 Visual Basic 2005에서는 사용자가 메서드의 이름을 바꾸면 해당 메서드에 대한 모든 참조를 수정하는 강력한 이름 바꾸기 리팩터링 기능을 제공합니다.

나중에 새로 추가된 행의 CustomerID를 검색해야 하므로 몇 가지 사항을 특별히 고려해야 합니다. 조금 복잡한 내용이기는 하지만 Access 데이터베이스를 사용하여 새로 추가된 행에서 일련 번호 값을 검색하는 방법이 Retrieving Identity or Autonumber Values (영문)에 잘 설명되어 있습니다. 이 문서에는 Access와 SQL Server 데이터베이스 간의 서로 다른 문제에 대한 내용도 모두 설명되어 있습니다. 따라서 여기에서는 개념 활용에 대해서만 간단히 소개하고 자세한 내용은 다루지 않겠습니다. 새 일련 번호 값을 검색하려면 OleDbDataAdapter RowUpdated 이벤트를 처리해야 하며 이 이벤트 처리기에서 새 값을 검색하는 SQL 쿼리 SELECT @@IDENTITY를 사용해야 한다는 것만 기억하면 됩니다. 이러한 동작을 지원하기 위해 기존 프로시저 외부의 DataLayer 클래스에 다음 코드를 추가합니다.

' OleDbDataAdapter.RowUpdated 이벤트 처리기에는
' 연결이 필요합니다.
Private Shared mcnn As OleDbConnection

DataLayer 클래스에 다음 프로시저를 추가합니다. 이 프로시저는 OleDbDataAdapter RowUpdated 이벤트를 처리하고 새 CustomerID 값을 검색합니다.

Private Shared Sub OnRowUpdated( _
 ByVal sender As Object, ByVal eventArgs As OleDbRowUpdatedEventArgs)

  ' 새 CustomerID 값을 검색합니다.
  ' 이 기법을 사용하려면 동일한 연결을 통해
  ' 업데이트를 수행해야 하므로 mcnn은 클래스 수준
  ' 변수여야 합니다. 
  Using cmd As New OleDbCommand("SELECT @@IDENTITY", mcnn)
    If eventArgs.StatementType = StatementType.Insert Then
      ' ID 값을 검색하여
      ' CustomerID 열에 저장합니다.
      eventArgs.Row("CustomerID") = CInt(cmd.ExecuteScalar())
    End If
  End Using
End Sub

DataLayer 클래스에서 GetRecordset 메서드를 찾습니다. 이 프로시저에서 다음 5가지 변경 사항을 적용합니다.

  1. GetRecordset 메서드 이름을 마우스 오른쪽 단추로 클릭하고 상황에 맞는 메뉴에서 이름 바꾸기를 선택합니다. 새 이름으로 GetDataTable을 입력하고 확인을 클릭합니다. Visual Studio에서 자동으로 메서드의 정의를 변경하며 해당 메서드를 호출하는 모든 코드를 수정합니다.
  2. 메서드에서 ADODB.Recordset 대신 DataTable을 반환하도록 반환 형식을 수정합니다.
  3. ByRefByVal로 변경하여 모든 매개 변수를 수정합니다. Visual Basic 6에서는 사용자가 별도로 지정하지 않는 한 모든 매개 변수를 참조로 전달하지만 Visual Basic 2005에서는 기본적으로 매개 변수를 값으로 전달합니다. 일반적으로 이러한 동작이 보다 효율적이기 때문에 다른 언어에서도 마찬가지로 적용됩니다. Visual Studio 2005에서는 사용자가 작성한 새 코드에 ByVal이 자동으로 추가되지만 지금은 직접 추가해 보겠습니다. 수정을 마친 프로시저 선언은 다음과 같습니다.
    Public Function GetDataTable( _
     ByVal ConnectionString As String, _
     ByVal SQL As String) As DataTable
  4. DataLayer 클래스의 인스턴스를 여러 개 만들 이유는 없으므로 호출자가 DataLayer 클래스의 인스턴스를 만들고 이에 속한 GetDataTable 메서드를 호출하도록 하는 것은 과잉 처리입니다. 대신 프로시저에 Shared 키워드를 추가하면 호출자가 메서드를 호출할 때 Visual Basic에서 자동으로 단일 인스턴스를 만들도록 할 수 있어 호출자의 처리가 훨씬 간단해집니다. 다음 코드에서 그 차이를 확인할 수 있습니다.
    ' 다음 코드 대신
    Dim data As New DataLayer
    Dim dt As DataTable = data.GetDataTable(ConnectionString, SQL)
    
    ' 다음 코드를 사용할 수 있습니다.
    Dim dt As DataTable = DataLayer.GetDataTable(ConnectionString, SQL)
  5. 호출자의 관점에서는 클래스에 공유 메서드를 사용하는 것과 표준 Visual Basic 모듈에서 메서드를 호출하는 것에 차이가 없습니다. 모듈은 컴파일러에 의해 모든 멤버가 Shared로 선언된 클래스라 할 수 있습니다. Shared 키워드를 사용하면 클래스의 인스턴스를 만들지 않고도 호출 가능한 메서드를 제어할 수 있습니다. 즉, Shared 키워드를 추가하여 다음과 같이 프로시저 선언을 작성합니다.
    Public Shared Function GetDataTable( _
     ByVal ConnectionString As String, _
     ByVal SQL As String) As DataTable
    마지막으로 프로시저의 내부를 바꾸어야 합니다. 다음과 같이 프로시저를
    수정합니다.
    Public Shared Function GetDataTable( _
     ByVal ConnectionString As String, _
     ByVal SQL As String) As DataTable
    
      ' 호출자에게 예외가 전달되도록 합니다.
      Dim dt As New DataTable
      Dim da As New OleDbDataAdapter(SQL, ConnectionString)
    
      da.Fill(dt)
    
      Return dt
    End Function

코드에서는 데이터를 보유할 수 있는 새 DataTable 인스턴스를 만듭니다. 그런 다음 새 OleDbDataAdapter를 만들고 이에 속한 생성자에 SQL SELECT 문자열과 연결 문자열을 전달합니다. 그리고 OleDbDataAdapter.Fill 메서드를 호출하여 데이터베이스에서 데이터를 검색하고 이 데이터를 DataTable(dt)에 넣습니다. 마지막으로 메서드는 데이터로 채워진 DataTable을 반환합니다.

이제 DataLayer 클래스의 다른 두 메서드(CloseRecordsetOpenConnection)를 삭제해도 됩니다. 데이터 작업에 사용되는 DataTable은 항상 연결이 끊겨 있으므로 연결을 닫을 필요가 없습니다. 또한 데이터 어댑터를 채우면 연결 열기/닫기 작업이 처리되므로 이 작업을 처리하는 별도의 프로시저도 필요 없습니다. 따라서 이 과정을 마치면 DataLayer 클래스에 GetDataTable 메서드만 남게 됩니다.

DataLayer 클래스의 새 버전에는 기존 메서드 이외에 추가 프로시저가 필요합니다. 새로 추가되는 메서드는 DataTable 인스턴스의 데이터 변경 내용을 다시 데이터 원본에 저장하기 위한 것입니다. 이러한 기능을 구현하는 방법은 여러 가지가 있지만 가장 간단한 방법은 OleDbCommandBuilder 인스턴스에서 적절한 SQL UPDATE 문을 생성하고 OleDbDataAdapter의 Update 메서드를 호출하도록 하는 것입니다.

DataLayer 클래스에 다음 프로시저를 삽입합니다.

Public Shared Sub UpdateDataTable( _
 ByVal dt As DataTable, _
 ByVal ConnectionString As String, ByVal SQL As String)

  ' 호출자에게 예외를 전달합니다.

  ' 데이터 어댑터 및 관련
  ' 명령 작성기를 만듭니다.
  Try
    ' RowUpdated 이벤트 처리기에서는 업데이트 수행을 위해
    ' 동일한 연결을 사용해야 하므로
    ' 연결을 만든 후 이를 계속 유지합니다.
    mcnn = New OleDbConnection(My.Settings.ConnectString)
    mcnn.Open()

    Dim da As New OleDbDataAdapter(SQL, mcnn)
    Dim bld As New OleDbCommandBuilder(da)

    AddHandler da.RowUpdated, AddressOf OnRowUpdated

    ' Update 메서드에서는 명령 작성기에서 생성한 INSERT, UPDATE 및 DELETE
    ' 문을 사용합니다.
    da.Update(dt)
  Finally
    ' 코드가 완료되면
    ' 항상 연결을 닫습니다.
    mcnn.Close()
  End Try
End Sub

이 메서드는 GetDataTable 메서드와 마찬가지로 OleDbDataAdapter를 만듭니다. UpdateDataTable 메서드를 호출할 때는 변경된 행을 검색할 DataTable과 데이터 검색에 사용될 연결 문자열 및 SQL SELECT 문자열을 전달해야 합니다.

UpdateDataTable 메서드에서는 AddHandler 문을 사용하여 OleDbDataAdapter에 RowUpdated 이벤트 처리기를 연결합니다. .NET에 새로 추가된 이 문을 사용하면 개체의 이벤트에 대한 응답으로 호출할 프로시저를 동적으로 Visual Basic에 알릴 수 있습니다. 이 코드 예의 경우 RowUpdate 이벤트가 발생하면 같은 클래스에 있는 OnRowUpdated 프로시저를 호출할 것을 .NET 런타임에 알립니다.

업데이트를 수행하려는 경우에도 SELECT 문을 전달하는 이유가 궁금할 수 있습니다. OleDbCommandBuilder 클래스는 데이터 어댑터에 연결된 SELECT 문을 가져오고 이에 해당하는 INSERT, UPDATE 및 DELETE 문을 만들 수 있습니다. 이 프로시저에서는 명령 작성기가 작업을 수행하면 코드에서 데이터 어댑터의 Update 메서드를 호출하여 변경 내용을 데이터 원본에 저장합니다.

응용 프로그램을 실행하는 동안 OleDbDataAdapter의 UpdateCommand, InsertCommand 및 DeleteCommand 속성을 살펴보면 상당히 유용할 것입니다. 이 기사에서 설명한 단계를 완료한 후 UpdateDataTable 프로시저에 중단점을 설정하고 코드 실행 시에 중단점에 도달하면 Visual Studio 2005의 디버깅 도구로 이러한 속성을 검토해 보십시오.

Utility 모듈 업데이트

다음은 ADO.NET 기능을 활용할 수 있도록 Utility.vb 파일을 업데이트할 차례입니다. 코드 편집기에서 이 파일을 열어 보면 표준 Visual Basic 모듈임을 알 수 있습니다. Visual Basic 2005의 관점에서 볼 때 프로젝트에는 몇 가지 공유 메서드가 있는 basUtility라는 클래스가 포함되어 있습니다. 이 예에서는 그렇게 하지 않았지만 새 프로젝트에서는 모듈이 아닌 표준 클래스를 만들고 명시적으로 Shared 키워드를 사용하여 호스트 클래스의 인스턴스를 만들지 않고도 호출할 수 있는 프로시저를 나타내도록 할 수 있습니다.

다음 단계에 따라 Utility.vb를 수정하십시오.

  1. 이전 클래스와 마찬가지로 변환 유틸리티에서 코드 맨 위에 추가한 문 두 개를 제거합니다.
    Option Strict Off
    Option Explicit On
  2. 찾기 및 바꾸기 작업을 수행하여 모든 ByRef 문을 ByVal 문으로 변경합니다.
  3. 첫 번째 메서드인 HandleUnexpectedError는 이 응용 프로그램에 더 이상 필요하지 않으므로 제거합니다.

이제 다음 몇 개의 섹션에서 제시하는 내용에 따라 각 메서드를 수정합니다.

AddQuotes 메서드

AddQuotes 메서드를 .NET 기능을 활용하는 다음 메서드로 대체합니다. 이 코드에서는 다른 Visual Basic 2005 코드와 일관성을 위해 Return 문을 사용하여 값을 반환합니다. 또한 매개 변수의 대체가 가능한 템플릿을 만들 수 있도록 프로시저에서 String.Format 메서드를 사용합니다. 이 예에서 템플릿 내의 {0} 값은 제공된 문자열에 대해 Replace 메서드를 호출하여 반환된 값으로 바뀌며 작은따옴표는 큰따옴표로 바뀝니다.

Public Function AddQuotes(ByVal strValue As String) As String
  Return String.Format("'{0}'", strValue.Replace("'", "''"))
End Function

FindString 메서드

FindString 메서드는 사용자가 전달하는 컨트롤의 형식을 확인하기 위해 Visual Basic의 If TypeOf 문을 사용합니다. 그러나 이 문은 Visual Basic 2005에서 구조체 형식과 함께 사용되는 경우 Visual Basic 6에서와는 다르게 동작합니다. 예에서는 구조체를 사용하지 않으므로 코드에 표시되는 경고가 문제가 되지는 않습니다. 그래도 경고를 해결하고 싶다면 프로시저를 다음과 같이 대체하면 됩니다. 굳이 대체할 필요는 없지만 이렇게 하면 경고를 해결할 수 있습니다.

Public Sub FindString(ByVal ctl As ListControl, _
 ByVal strFind As String)
  Dim intPos As Integer = -1

  ' 컨트롤 형식을 확인합니다. ListBox 및 ComboBox가
  ' FindStringExact 메서드를 제공하는 기본 컨트롤 형식에서 상속되면
  ' 코드가 훨씬 간단하겠지만 실제로는 그렇지 않습니다.
  ' 따라서 두 가지를 별도로 취급해야 합니다.
  If ctl.GetType() Is GetType(ListBox) Then
    ' 컨트롤의 형식을 변환하고 문자열을 찾습니다.
    intPos = CType(ctl, ListBox).FindStringExact(strFind)
  ElseIf ctl.GetType() Is GetType(ComboBox) Then
    ' 컨트롤의 형식을 변환하고 문자열을 찾습니다.
    intPos = CType(ctl, ComboBox).FindStringExact(strFind)
  End If

  ' 선택한 인덱스를 설정합니다.
  ctl.SelectedIndex = intPos
End Sub

사실 GetType에 대한 호출은 상당히 복잡해 보이므로 원래 If TypeOf 구조를 사용하기를 원할 수도 있습니다. 이 경우 개체 형식을 비교하는 데 If TypeOf를 사용하면서도 Visual Basic 2005 기능을 활용하려면 프로시저를 다음 버전으로 대체할 수 있습니다.

Public Sub FindString(ByVal ctl As ListControl, _
 ByVal strFind As String)
  Dim intPos As Integer = -1

  ' 컨트롤 형식을 확인합니다. ListBox 및 ComboBox가
  ' FindStringExact 메서드를 제공하는 기본 컨트롤 형식에서 상속되면
  ' 코드가 훨씬 간단하겠지만 실제로는 그렇지 않습니다.
  ' 따라서 두 가지를 별도로 취급해야 합니다.
  If TypeOf ctl Is ListBox Then
    ' 컨트롤의 형식을 변환하고 문자열을 찾습니다.
    intPos = CType(ctl, ListBox).FindStringExact(strFind)
  ElseIf TypeOf ctl Is ComboBox Then
    ' 컨트롤의 형식을 변환하고 문자열을 찾습니다.
    intPos = CType(ctl, ComboBox).FindStringExact(strFind)
  End If

  ' 선택한 인덱스를 설정합니다.
  ctl.SelectedIndex = intPos
End Sub

FixBoolean 메서드

FixBoolean 프로시저는 ADO Field 개체를 받고 필드 내용의 참 값과 일치하도록 확인란의 값을 설정합니다. 이 프로시저를 수정하려면 입력 값을 Object 형식으로 변경하고 CBool 함수에 직접 매개 변수를 전달해야 합니다. 수정을 마친 프로시저는 다음과 같습니다.

Public Function FixBoolean(ByVal fld As Object) As CheckState
  ' 예/아니요/Null 필드 값을
  ' 확인란 값으로 변환합니다.
  If CBool(fld) Then
    Return CheckState.Checked
  Else
    Return CheckState.Unchecked
  End If
End Function

NullToText 메서드

NullToText 메서드는 Visual Basic 6에서 Nothing 값을 비롯한 어떤 Variant 형식이라도 빈 문자열에 연결하여 문자열을 만들 수 있다는 점을 활용했었습니다. 그러나 ADO.NET은 null 값을 특수 Nothing 값과는 별개로 처리하기 때문에 이 동작을 더 이상 활용할 수 없습니다. 대신 null 데이터베이스 값을 별도로 확인하여 해당하는 값에 대해서는 빈 문자열을 반환하도록 코드를 작성해야 합니다. 따라서 다음과 같이 NullToText를 수정합니다.

Public Function NullToText(ByVal fld As Object) As String
  If IsDBNull(fld) Then
    Return String.Empty
  Else
    Return CStr(fld)
  End If
End Function

TextToNull 메서드

TextToNull 메서드에서는 심각해 보이는 경고가 많이 발생하지만 실제로 심각한 영향은 없으므로 메서드를 변경할 필요는 없습니다. 그러나 깔끔한 Visual Basic 2005 코드를 작성하기 위해서는 들어오는 문자열 값을 확인하여 System.DBNull.Value나 문자열 자체를 반환하도록 이 메서드를 수정해야 합니다. 이를 위해 프로시저를 다음과 같이 대체합니다.

Public Function TextToNull(ByVal strValue As String) As Object
  If String.IsNullOrEmpty(strValue) Then
    Return System.DBNull.Value
  Else
    Return strValue
  End If
End Function

지금까지 간단한 클래스 두 개를 처리했습니다. 앞으로 살펴볼 CustomerHandler 클래스와 ADO 폼은 다루기가 좀 더 까다롭습니다. 이 변환 과정을 꼼꼼히 살펴보는 데 충분한 시간을 투자하시기를 바랍니다. 또한 단계를 진행하는 동안 몇 가지 Visual Basic 2005 팁과 ADO.NET에 대한 자세한 내용을 배울 수 있습니다.

CustomerHandler 클래스 재정비

다음은 CustomerHandler 클래스를 살펴볼 차례입니다. 코드 편집기에서 Customer.vb를 열고 다음 단계에 따라 업데이트를 수행하십시오.

  1. 앞에서 수정한 다른 두 파일과 마찬가지로 파일 맨 위에 있는 Option 문 두 개를 제거합니다.
    Option Strict Off
    Option Explicit On
  2. CustomerHandler 클래스를 다시 사용할 수 있도록 나중에 다른 어셈블리로 옮길 수도 있으므로 Friend 키워드를 Public으로 변경합니다.
    Public Class CustomerHandler
  3. 더 이상 필요 없는 CloseRecordset 메서드를 제거합니다.
  4. 고객 DataTable과 DataTable의 현재 행을 추적하는 데 클래스 수준 변수가 필요하므로 클래스 내부이면서 다른 프로시저 외부 위치에 다음 두 변수를 추가합니다.
    Private mdt As DataTable
    Private mintCurrentRow As Integer

연결 문자열 작업

현재 연결 문자열은 CustomerHandler 클래스의 GetConnection 문자열 메서드에 하드코드되어 있습니다. 간단한 응용 프로그램이라면 문제가 없겠지만 일반적으로 실제 응용 프로그램에서는 이러한 코딩 방식이 적합하지 않습니다. 이러한 정보는 응용 프로그램에서 하드코드하기보다는 손쉽게 수정할 수 있는 다른 위치, 말하자면 구성 파일에 넣는 것이 좋습니다. Visual Basic 2005에서는 구성 파일에 데이터를 저장하고 검색하는 강력한 기능을 지원할 뿐만 아니라 필요한 경우 .NET Framework를 통해 손쉽게 이 정보의 보안을 유지할 수 있습니다.

우선 연결 문자열을 구성 파일로 이동하고 Visual Basic 2005의 My.Settings 클래스를 사용하여 검색합니다. 다음 단계에 따라 연결 문자열을 이동합니다.

  1. 속성 메뉴에서 프로젝트 | ADOForm을 선택합니다.
  2. 속성 창 왼쪽에서 설정 탭을 클릭합니다.
  3. 새 설정 파일을 만들지 여부를 묻는 링크를 클릭합니다.
  4. 설정 표에서 이름을 ConnectString으로 설정하고 다음 문자열을 값으로 입력합니다. 이때 문자열에 있는 경로는 사용자 컴퓨터에서 Visual Basic 6Demo.MDB 파일을 저장한 위치로 변경합니다.
    Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\Samples\ADOFormNET\VB6Demo.MDB

    설정을 마친 표는 그림 2와 같습니다.

    그림 2. 연결 문자열 설정

이 응용 프로그램은 데이터 저장소로 JET MDB 파일을 사용하기 때문에 다소 복잡합니다. JET 데이터베이스에 대한 연결 정보에는 전체 경로가 필요하므로 데이터베이스를 이동하면 더 이상 연결되지 않습니다. 따라서 장기적으로 볼 때 SQL Server, MSDE 또는 SQL Express에 데이터를 저장하도록 응용 프로그램을 변환하는 것이 더 쉽고 유리하며 단기적으로도 유익합니다. 응용 프로그램에 필요한 변경 사항은 많지 않지만 연결 문자열은 변경해야 합니다.
  1. 설정 창을 닫고 메시지가 표시되면 파일을 저장합니다.
  2. 솔루션 탐색기 창에서 app.config 파일을 두 번 클릭하여 변경 내용을 확인합니다. 파일 아래쪽에 다음과 같은 데이터를 확인할 수 있을 것입니다. 데이터를 살펴본 후에 창을 닫습니다.
    applicationSettings>
      <ADOForm.Settings>
        <setting name="ConnectString" serializeAs="String">
          <value>Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\Samples\ADOFormNET\VB6Demo.MDB</value>
        </setting>
      </ADOForm.Settings>
    </applicationSettings>
    참고 app.config 파일에 설정을 추가하면 Visual Basic 2005에서 My 네임스페이스에 각 설정에 해당하는 속성이 지정된 Settings라는 클래스를 생성합니다. 코드에서는 간단히 My.Settings.ConnectString을 참조하면 Visual Basic 2005에서 자동으로 구성 파일의 값을 검색합니다.
  3. GetConnectionString 메서드를 적합한 위치에 두기 위해 Customer.vb 파일에서 Utility.vb 파일로 옮기고 다음과 같이 코드를 수정합니다.
    Public Function GetConnectionString() As String
      ' My.Settings를 사용하여 구성 파일에서 연결 문자열을
      ' 검색합니다.
      Return My.Settings.ConnectString
    End Function

GetCustomers 메서드

GetCustomers 메서드는 데이터를 검색하고 현재 행을 설정한 다음 저장된 DataTable을 반환해야 합니다. 이를 위해 프로시저의 내용을 다음 코드로 대체합니다.

Public Function GetCustomers() As DataTable
  mdt = DataLayer.GetDataTable(GetConnectionString(), _
         "SELECT * FROM tblCustomer ORDER BY LastName")
  mintCurrentRow = 0
  Return mdt
End Function
DataLayer.GetDataTable 메서드를 작성할 때 Shared 키워드를 사용했으므로 메서드를 호출하기 위해 DataLayer 클래스의 인스턴스를 만들 필요는 없습니다. 다음 섹션에서 사용할 DataLayer.GetStates 메서드 역시 마찬가지입니다.

GetStates 메서드

GetStates 메서드는 GetCustomers 메서드와 마찬가지로 DataLayer 클래스를 활용할 수 있습니다. 다음과 같이 GetStates 프로시저를 수정합니다.

Public Function GetStates() As DataTable
  Return DataLayer.GetDataTable(GetConnectionString(), _
   "SELECT State FROM tblStates")
End Function

ADO.NET을 활용하도록 이 응용 프로그램을 변환하는 과정에서 CustomerHandler 클래스가 직접 탐색을 수행하도록 하는 "실무적인 결정"을 내렸습니다. 즉, CustomerHandler 클래스에는 데이터 탐색을 처리하는 메서드가 필요합니다. 이 섹션에서는 이러한 새 메서드에 대해 설명하고 CustomerHandler 클래스에 해당 메서드를 추가하는 과정을 안내합니다.

Location 속성 추가

DataTable 클래스는 현재 행에 대한 정보를 관리하지 않고 그래서도 안 되며 이는 DataTable을 사용하는 코드가 해야 할 작업이므로 CustomerHandler 클래스에는 현재 행 번호, 행 개수, 그리고 현재 행이 행 집합의 시작 행인지 또는 끝 행인지에 대한 정보를 반환하는 속성이 필요합니다. 이러한 기능을 지원하기 위해 다음 읽기 전용 속성을 CustomerHandler 클래스에 추가합니다.

Public ReadOnly Property RowCount() As Integer
  Get
    Return mdt.Rows.Count
  End Get
End Property

Public ReadOnly Property CurrentRow() As DataRow
  Get
    Return mdt.Rows(mintCurrentRow)
  End Get
End Property

Public ReadOnly Property BOF() As Boolean
  Get
    Return (mintCurrentRow = 0)
  End Get
End Property

Public ReadOnly Property EOF() As Boolean
  Get
    Return (mintCurrentRow = (mdt.Rows.Count - 1))
  End Get
End Property

Navigation 메서드 추가

데모 폼에는 CustomerHandler 클래스 데이터 내부를 탐색할 수 있도록 하는 단추가 있습니다. 이제 CustomerHandler 클래스에서 자체적으로 탐색을 처리해야 하므로 소비자가 데이터 내부를 탐색할 수 있도록 하는 메서드를 추가해야 합니다. 이를 위해 CustomerHandler 클래스에 다음 프로시저를 추가합니다.

Public Sub MoveFirst()
  mintCurrentRow = 0
End Sub

Public Sub MoveLast()
  ' 여러 행이 있으면 마지막 행으로 이동합니다.
  If mdt.Rows.Count > 0 Then
    mintCurrentRow = mdt.Rows.Count - 1
  End If
End Sub

Public Sub MoveNext()
  If mintCurrentRow < mdt.Rows.Count Then
    mintCurrentRow += 1
  End If
End Sub

Public Sub MovePrevious()
  If mintCurrentRow > 0 Then
    mintCurrentRow -= 1
  End If
End Sub

검색 기능 추가

데모 폼에서 사용자는 LastName 필드를 기준으로 행을 검색할 수 있습니다. SQL WHERE 절에서 WHERE를 제외한 나머지 부분을 Find 메서드에 전달하는 방법으로 DataTable에서 하나 이상의 행을 찾을 수 있습니다. Find 메서드는 DataRow 개체의 배열을 반환하며 이 예에서는 결과 배열에 여러 행이 있는 경우 첫 번째 일치 항목을 탐색합니다.

CustomerHandler 클래스의 Find 메서드에서는 사용자가 전달한 검색 조건을 사용하며 일치하는 항목이 있는 경우 내부 현재 행 값을 첫 번째 일치하는 행으로 설정합니다. 일치하는 항목이 없는 경우 현재 행 값을 그대로 둡니다. 또한 메서드가 검색에 성공하면 True를 반환하고 실패하면 False를 반환합니다.

CustomerHandler 클래스에 다음 프로시저를 추가합니다.

Public Function Find( _
 ByVal SearchCriteria As String) As Boolean
  Dim retval As Boolean = False

  Dim adr As DataRow() = mdt.Select(SearchCriteria)
  If adr.Length > 0 Then
    ' 행이 검색되면
    ' 검색된 배열의 첫 번째 행(행 0)을
    ' 현재 행으로 설정합니다.
    retval = True
    mintCurrentRow = mdt.Rows.IndexOf(adr(0))
  End If

  Return retval
End Function

Find 메서드와 이에 해당하는 DataTable.Select 메서드에는 다음과 같은 형식의 문자열을 전달할 수 있습니다.

LastName = 'Jones'
' 또는
CompanyName LIKE 'A%'

변경 내용을 저장하는 코드 추가

CustomerHandler 클래스의 DataTable 인스턴스에서 데이터를 변경한 후에는 이 데이터를 기본 데이터 원본에 저장하기를 원할 것입니다. DataLayer 클래스에서는 UpdateDataTable 메서드를 호출하여 저장할 DataTable, 연결 문자열, 원본 SELECT 문(기본 코드에서 적절한 UPDATE, DELETE 및 INSERT 문을 생성하기 위해 필요)을 전달할 수 있습니다.

전체 고객 DataTable을 UpdateDataTable 메서드에 전달할 수도 있지만 나중에 DataLayer 클래스를 별도의 어셈블리로 옮기기로 결정한다면 변경된 행만 전달하여 효율성을 어느 정도 개선할 수 있습니다. 변경된 행만 필요하므로 모든 행을 전달할 이유가 없습니다. DataTable 클래스의 DataTable.GetChanges 메서드는 행을 필터링하여 변경된 행만 "표시"하므로 이를 활용해 간단히 변경된 행을 검색할 수 있습니다. 즉, UpdateDataTable에 데이터를 전달하기 전에 이 메서드를 호출하면 간단하고 효과적으로 작업을 개선할 수 있습니다.

CustomerHandler 클래스에 다음 프로시저를 추가합니다.

Public Sub SaveToDatabase()
  ' 변경된 행만 UpdateDataTable 메서드에 전달합니다.
  DataLayer.UpdateDataTable(mdt.GetChanges(), _
   GetConnectionString(), _
   "SELECT * FROM tblCustomer ORDER BY LastName")
End Sub

변경 내용을 업데이트하는 코드 추가

CustomerHandler 클래스에서 행을 업데이트하려면 몇 가지 단계를 거쳐야 합니다. 먼저 기본 데이터베이스에 변경 내용을 저장한 다음 DataTable에 "원래 행" 정보를 비우도록 지시하여 더 이상 데이터 원본에서 해당 행을 수정할 필요가 있는 것으로 인식하지 않도록 해야 합니다.

CustomerHandler 클래스에 추가할 Update 메서드가 이러한 작업을 수행합니다. 코드는 SaveToDatabase 메서드를 호출하여 변경 내용을 저장하고 DataTable의 AcceptChanges 메서드를 호출하여 행이 변경되었던 사실을 "잊도록" 합니다.

CustomerHandler 클래스에 다음 메서드를 추가합니다.

Public Sub Update()
  SaveToDatabase()

  ' 보류 중인 변경 내용이 없음을
  ' DataTable에 알립니다.
  mcust.AcceptChanges()
End Sub
경고 이 샘플 코드에서는 동시성 오류나 변경 내용을 저장할 때 잠긴 행으로 인해 발생하는 문제를 특별히 고려하지 않았습니다. 이러한 문제는 이 기사의 범위에서 벗어난 내용이지만 관련 문제와 해결 방법에 대해 알아 두는 것이 좋습니다. 자세한 내용은 Concurrency Control in ADO.NET (영문)을 참조하십시오.

고객을 삭제하는 코드 추가

CustomerHandler 클래스에서 행을 삭제하려면 업데이트 작업 이전에 특정 행을 삭제하는 단계를 하나 더 수행하면 됩니다. 이를 위해 CustomerHandler 클래스에 다음 프로시저를 추가하면 나중에 고객을 삭제할 수 있습니다.

Public Sub Delete()
  ' 현재 행을 삭제합니다.
  mdt.Rows(mintCurrentRow).Delete()

  ' 변경 내용을 저장합니다.
  Update()
End Sub

새 고객 추가 허용

마지막으로 CustomerHandler 클래스에서 새 행을 구성할 수 있도록 해야 합니다. 폼에서는 실제로 새 행을 저장하기 전까지는 Customer.AddNew 메서드를 호출하지 않을 것입니다. 즉, 사용자가 새 고객을 만들도록 지시하면 폼 자체에서는 컨트롤을 지울 뿐이며 변경 내용을 커밋하기 전까지는 Customer.AddNew 메서드를 호출하지 않습니다.

CustomerHandler 클래스에 다음 프로시저를 추가합니다. 이 프로시저는 빈 행을 새로 만들어 DataTable에 추가하고 현재 행을 새 인덱스로 설정합니다.

Public Sub AddNew()
  Dim row As DataRow

  ' 새 행을 만듭니다.
  row = mdt.NewRow
  ' 새로 만든 빈 행을 DataTable에 추가합니다.
  mdt.Rows.Add(row)

  ' 현재 행을 이 새 행으로 설정합니다.
  mintCurrentRow = mdt.Rows.Count - 1
End Sub

샘플 폼 수정

이제 지원 프로시저를 모두 완성했으므로 다음은 사용자 인터페이스를 수정할 차례입니다. 응용 프로그램의 기초가 되는 지원 프로시저를 크게 변경했기 때문에 분명히 폼에도 많은 변경이 필요합니다. 대개는 불필요한 코드만 삭제하면 되지만 Visual Basic 2005 기능을 보다 잘 활용하도록 코드를 변경해야 하는 경우도 있습니다. 다음 단계에 따라 폼을 수정하십시오.

  1. 솔루션 탐색기 창에서 ADO.vb 파일을 마우스 오른쪽 단추로 클릭하고 상황에 맞는 메뉴에서 코드 보기를 선택합니다.
  2. 파일 맨 위로 스크롤하여 다른 클래스와 마찬가지로 다음 두 지시문을 삭제합니다.
    Option Strict Off
    Option Explicit On
  3. 다음 선언을 찾아 삭제합니다.
    Private customerRecordset As ADODB.Recordset
    CloseRecordset 메서드를 호출하는 모든 인스턴스를 찾아
    코드에서 이와 관련된 줄을 삭제합니다.
    모든 ByRef 항목을 찾아 ByVal로 바꿉니다. 
    InitForm 프로시저를 제거합니다.
    frmADO_FormClosed 프로시저를 제거합니다.
    txtData_TextChanged 프로시저를 제거합니다.
    chkActive_CheckStateChanged 프로시저를 제거합니다.

ShowData 메서드

ShowData 메서드는 DataTable에서 현재 데이터 행을 가져와 폼에 표시합니다. 데이터 저장소가 여러 가지로 변경되었기 때문에 이 프로시저도 완전히 바꾸어야 합니다. 다음과 같이 ShowData 메서드를 수정합니다.

Private Sub ShowData()
  Dim row As DataRow

  ' 이 플래그를 설정하여 이 프로세스를 처리하는 동안
  ' 이벤트 프로시저가 아무 작업도 수행하지 않도록 합니다.
  currentDataState = DataState.Loading

  Try
    If customer.RowCount = 0 Then
      AddNewRow()
    Else
      ' 필요한 코드 양을 줄일 수 있도록
      ' 로컬 변수에 현재 행을 저장합니다.
      row = customer.CurrentRow

      ' VB6에서 VB 2005로 변환하는 경우
      ' 변환 프로세스에서는 사용자 지정 컨트롤을 사용하여
      ' 컨트롤 배열의 동작을 에뮬레이트합니다. txtData는
      ' 실제 컨트롤의 이름이 아니며 폼에서
      ' 컨트롤의 이름은 _txtData_1, _txtData_2 등으로
      ' 지정됩니다. 새 폼에는 이 기능을 사용하기 보다는
      ' 각 필드별로 별도의 컨트롤을
      ' 만듭니다. 사용자 지정 컨트롤에서는 이 모두를 사용자에게 숨깁니다.
      txtData(0).Text = NullToText(row("CustomerID"))
      txtData(1).Text = NullToText(row("FirstName"))
      txtData(2).Text = NullToText(row("LastName"))
      txtData(3).Text = NullToText(row("Address"))
      txtData(4).Text = NullToText(row("City"))
      txtData(5).Text = NullToText(row("ZipCode"))

      FindString(cboState, NullToText(row("State")))
      chkActive.CheckState = FixBoolean(row("Active"))

      HandleButtonState()
      currentDataState = DataState.Normal
    End If
    GotoFirstControl()

  Catch ex As Exception
    MessageBox.Show(ex.Message)

  End Try
End Sub

이 코드의 목적은 원본 Visual Basic 6 코드와 크게 다르지 않습니다. 가장 큰 차이점은 물론 전체 Recordset가 아닌 하나의 DataRow 개체를 사용하여 작업을 수행한다는 점입니다.

응용 프로그램을 Visual Basic 6에서 Visual Basic 2005로 변환한 경우 변환 프로세스에서는 입력란 컨트롤 배열을 변경하지 않고 Visual Basic 2005에서 컨트롤 배열을 에뮬레이트하는 사용자 지정 컨트롤을 사용합니다. 일반적으로 Visual Basic 2005에서는 컨트롤 배열을 사용하기 보다는 이벤트 처리기를 공유할 수 있는 개별 컨트롤을 사용합니다. 이 문제는 다음 기사에서 다른 방식으로 처리할 예정이므로 여기에서는 다루지 않겠습니다.

탐색 메서드 수정

폼에 있던 탐색 코드를 CustomerHandler 클래스로 옮겼으므로 데이터에 액세스하는 데 사용되는 모든 단추의 Click 이벤트 처리기를 수정해야 합니다. 다음 프로시저에서 필요한 내용을 변경하고 업데이트합니다.

Private Sub cmdFirst_Click( _
 ByVal eventSender As System.Object, _
 ByVal eventArgs As System.EventArgs) _
 Handles cmdFirst.Click
  customer.MoveFirst()
  ShowData()
End Sub

Private Sub cmdLast_Click( _
 ByVal eventSender As System.Object, _
 ByVal eventArgs As System.EventArgs) _
 Handles cmdLast.Click
  customer.MoveLast()
  ShowData()
End Sub

Private Sub cmdNext_Click( _
 ByVal eventSender As System.Object, _
 ByVal eventArgs As System.EventArgs) _
 Handles cmdNext.Click
  customer.MoveNext()
  ShowData()
End Sub

Private Sub cmdPrevious_Click( _
 ByVal eventSender As System.Object, _
 ByVal eventArgs As System.EventArgs) _
 Handles cmdPrevious.Click
  customer.MovePrevious()
  ShowData()
End Sub

HandleNavButtons 메서드 수정

ShowData 메서드에서는 작업을 처리하는 과정 중에 HandleButtonState 메서드를 호출합니다. HandleButtonState 메서드 자체는 변경할 필요가 없지만 이 메서드에서는 현재 DataTable 행에 따라 탐색 단추를 적절하게 활성화/비활성화하기 위해 HandleNavButtons 메서드를 호출합니다. EOF와 BOF를 계산하는 논리는 모두 CustomerHandler 클래스 자체에 포함되어 있으므로 이 코드를 상당히 간소화할 수 있습니다. 다음과 같이 HandleNavButtons 프로시저를 수정합니다.

Private Sub HandleNavButtons()
  ' 탐색 단추의 활성화/비활성화를
  ' 처리합니다.

  Select Case currentDataState
    Case DataState.Adding, DataState.Editing
      cmdFirst.Enabled = False
      cmdPrevious.Enabled = False
      cmdNext.Enabled = False
      cmdLast.Enabled = False

    Case Else
      cmdFirst.Enabled = Not customer.BOF
      cmdPrevious.Enabled = Not customer.BOF

      cmdNext.Enabled = Not customer.EOF
      cmdLast.Enabled = Not customer.EOF
  End Select
End Sub

폼 로드

폼의 Load 이벤트 처리기에서는 데이터와 데이터 표시 방법을 설정합니다. 기초가 되는 코드가 변경되었으므로 이러한 Load 이벤트 처리기에도 역시 수정이 필요합니다. 이 코드는 CustomerHandler 클래스의 인스턴스를 만들고 새 인스턴스의 GetCustomers 메서드를 호출하여 데이터를 표시합니다. 다음과 같이 Load 이벤트 처리기를 수정합니다.

Private Sub frmADO_Load( _
 ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles MyBase.Load
  ' 새 Customer 개체를 만듭니다.
  customer = New CustomerHandler

  Try
    customer.GetCustomers()
    If customer.RowCount > 0 Then
      LoadStates()

      ' 폼을 표시합니다.
      ShowData()
    Else
      MessageBox.Show("No rows to load. Exiting the application.")
      Me.Close()
    End If

  Catch ex As Exception
    MessageBox.Show(ex.Message)
  End Try
End Sub

상태를 포함한 ComboBox 로드

LoadStates 메서드는 Load 이벤트 처리기에서 호출되며 상태 목록을 포함하는 콤보 상자를 설정하는 역할을 합니다. ComboBox 컨트롤을 사용하면 목록과 값을 손쉽게 DataTable에 바인딩할 수 있으며 바로 LoadStates 프로시저가 이러한 작업을 처리합니다. 다음 예와 같이 LoadStates 프로시저를 수정합니다.

Private Sub LoadStates()
  ' tblStates에서 cboStates를 로드합니다.

  Try
    ' ComboBox 컨트롤의 Sorted 속성이
    ' True로 설정되어 있으면 데이터 바인딩이
    ' 증상 없이 실패하므로
    ' 주의해야 합니다. 이 예에서는 VB6에서 변환할 때
    ' ComboBox 컨트롤의 Sorted 속성이 True로 설정되므로
    ' 코드가 작동하지 않습니다.
    ' 이 문제를 해결하려면 Sorted 속성을
    ' False로 설정해야 합니다.
    Dim states As DataTable = customer.GetStates()

    cboState.Sorted = False
    cboState.DisplayMember = "State"
    cboState.ValueMember = "State"
    cboState.DataSource = states

  Catch ex As Exception
    MessageBox.Show(ex.Message)

  End Try
End Sub
참고 이 코드를 처음 작성했을 때는 콤보 상자가 빈 채로 표시되었으며 콤보 상자를 삭제하고 처음부터 다시 작성하자 올바르게 처리되었습니다. 이는 일부 속성이 잘못 설정되었기 때문에 발생한 문제였습니다. 원인을 조사한 결과 Sorted 속성이 True로 설정되어 있었기 때문에 데이터 바인딩이 작동하지 않은 것이었습니다. 데이터 바인딩이 처리되도록 하려면 Sorted 속성을 False로 설정해야 합니다. 그렇지 않으면 항목이 없는 콤보 상자가 표시됩니다.

필드 값 변경 처리

원본 응용 프로그램에서 txtData.TextChanged 이벤트 처리기와 chkActive.CheckedChanged 이벤트 처리기는 마지막에 HandleChange 프로시저를 호출했었습니다. 그러나 HandleChange 프로시저가 이 두 이벤트 자체를 처리하도록 하는 것이 더 간단합니다. 이를 위해 HandleChange 프로시저 선언을 다음과 같이 수정합니다.

Private Sub HandleChange( _
 ByVal sender As Object, ByVal e As EventArgs) _
 Handles txtData.TextChanged, chkActive.CheckedChanged

  ' 입력란 또는 확인란의
  ' 변경 내용을 처리합니다.
  ' 행이 없으면
  ' 편집하도록 합니다.
  ' 데이터를 확인하는 중에도
  ' 편집하도록 합니다.
  ' 다른 경우 빠져나갑니다.
  Select Case currentDataState
    Case DataState.NoRows
      currentDataState = DataState.Adding
    Case DataState.Normal
      currentDataState = DataState.Editing
    Case Else
      Exit Sub
  End Select

  ' 단추를 설정합니다.
  HandleButtonState()
End Sub

상태 SelectedIndexChanged 이벤트 프로시저

상태 필드 ComboBox의 SelectedIndexChanged 이벤트를 처리하는 코드에서는 폼의 현재 상태를 추적하고 변경 작업을 올바르게 처리해야 합니다. 문제는 사용자가 콤보 상자의 목록에서 항목을 선택하면 값을 변경하지 않더라도 SelectedIndexChanged 이벤트 처리기가 호출된다는 점입니다. 이 경우 편집 모드로 전환되어서는 안 됩니다. SelectedIndexChanged 이벤트 처리기의 코드에서는 새 값을 원래 값과 비교하여 값이 변경된 경우에만 편집 모드로 전환해야 합니다. 이를 위해 다음 코드와 같이 SelectedIndexChanged 이벤트 처리기를 수정합니다.

Private Sub cboState_SelectedIndexChanged( _
 ByVal eventSender As System.Object, _
 ByVal eventArgs As System.EventArgs) _
 Handles cboState.SelectedIndexChanged

  ' 상태가 변경되면 현재 DataState를 수정합니다.
  Select Case currentDataState
    Case DataState.NoRows
      currentDataState = DataState.Adding
    Case DataState.Normal
      ' 동일한 상태가 선택되었는지 확인합니다.
      ' 이 경우 작업을 수행하지 않습니다.
      If cboState.Text = NullToText( _
       customer.CurrentRow("State")) Then
        Exit Sub
      End If
      currentDataState = DataState.Editing
    Case DataState.Editing
      ' 작업을 수행하지 않습니다.
    Case Else
      Exit Sub
  End Select

  HandleButtonState()
End Sub
참고 이 기사를 준비하면서 원래 SelectedIndexChanged 이벤트 처리기에서 오류를 발견했으며 새 코드에서는 이를 수정했습니다. 따라서 코드가 이전 버전과 약간 변경되었습니다.

KeyDown 이벤트 프로시저 수정

마우스 대신 키 입력을 통해 샘플 폼의 행 사이를 이동할 수 있습니다. 이 기능을 구현하기 위해 샘플에서는 폼의 KeyPreview 속성을 True로 설정했으며 KeyDown 이벤트 처리기에서 키 입력을 처리하는 코드를 포함하고 있었습니다. 변환 프로세스의 일부로 KeyDown 이벤트 처리기는 여러 단추의 상태를 확인하여 활성화된 경우 Click 이벤트 처리기를 호출하는 코드를 많이 포함하고 있었습니다. 다행스럽게도 이제 이러한 코드는 모두 필요 없게 되었습니다. Button 클래스에서는 단추가 활성화된 경우에만 Click 이벤트 처리기를 호출하는 PerformClick 메서드를 제공합니다. 따라서 다음 코드만 포함하도록 KeyDown 이벤트 처리기를 간소화할 수 있습니다.

Private Sub frmADO_KeyDown(ByVal eventSender As System.Object, _
 ByVal eventArgs As System.Windows.Forms.KeyEventArgs) _
 Handles MyBase.KeyDown
  ' PageUp 키, PageDown 키,
  ' Ctrl+PageUp, Ctrl+PageDown,
  ' Ctrl+Home, Ctrl+End 입력에 대한 지원 기능을 추가합니다.
  Select Case eventArgs.KeyCode
    Case Keys.PageDown
      ' Ctrl+PgDn의 경우 마지막 행으로 이동합니다.
      ' PgDn 키의 경우 다음 행으로 이동합니다.
      If eventArgs.Control Then
        cmdLast.PerformClick()
      Else
        ' PerformClick 메서드는 매우 유용합니다. 단추가
        ' 활성화되어 있지 않으면 메서드를
        ' 호출하지 않습니다. 단추의 활성화 여부를
        ' 확인할 필요가 없습니다.
        cmdNext.PerformClick()
      End If
    Case Keys.PageUp
      ' Ctrl+PgUp의 경우 첫째 행으로 이동합니다.
      ' PgUp 키의 경우 이전 행으로 이동합니다.
      If eventArgs.Control Then
        cmdFirst.PerformClick()
      Else
        cmdPrevious.PerformClick()
      End If
    Case Keys.Home
      ' Ctrl+Home의 경우 첫째 행으로 이동합니다.
      If eventArgs.Control Then
        cmdFirst.PerformClick()
      End If
    Case Keys.End
      ' Ctrl+End의 경우 마지막 행으로 이동합니다.
      If eventArgs.Control Then
        cmdLast.PerformClick()
      End If
  End Select
End Sub
참고 KeyDown 이벤트 처리기는 두 번째 매개 변수를 활용하여 사용자가 다른 키와 함께 Ctrl 키를 눌렀는지 확인합니다. 사용자가 Ctrl 키를 누른 경우 eventArgs.Control 속성이 True로 설정됩니다. eventArgs 매개 변수에는 이외에도 이벤트가 발생했을 때 키보드의 상태를 확인할 수 있는 유용한 속성이 많이 있습니다.

폼 컨트롤 모두 지우기

ClearForm 메서드는 폼의 모든 컨트롤에 대해 반복 실행하여 각 컨트롤을 지우고 알려진 상태로 만듭니다. 앞서 살펴보았던 FindString 코드에서와 마찬가지로 Control.GetType 메서드를 사용하는 긴 코드를 작성하거나 계속 If TypeOf 구조를 사용하여 변환 경고 문제를 해결할 수 있습니다. 여기에서는 후자의 방법을 사용하기로 결정했으므로 코드를 다음과 같이 바꿉니다.

Private Sub ClearForm()
  ' 컨트롤을 모두 지웁니다.
  ' 이 폼에는 입력란, 확인란 및
  ' 콤보 상자밖에 없습니다.

  ' 실제 작업에서는 폼에 다른 컨트롤이 있을 수 있습니다.

  ' 재귀적 이벤트에 주의를 기울여야 합니다. 예를 들어 입력란의
  ' 텍스트를 ""로 설정하면 Change 이벤트가
  ' 트리거됩니다. 때문에 이 코드에서는
  ' currentDataState를 Loading으로 설정합니다. 이 상태 값을 사용하는
  ' 코드는 현재 없지만 최소한
  ' 작업 진행 중임을 나타낼 수 있습니다.

  Try
    currentDataState = DataState.Loading

    For Each ctl As Control In Me.Controls
      If TypeOf ctl Is TextBox Then
        ctl.Text = ""
      ElseIf TypeOf ctl Is CheckBox Then
        CType(ctl, CheckBox).CheckState = CheckState.Checked
      ElseIf TypeOf ctl Is ComboBox Then
        CType(ctl, ComboBox).SelectedIndex = -1
        ' TODO: 필요한 경우 컨트롤 형식을 추가합니다.
      End If
    Next ctl

    If customer.RowCount = 0 Then
      currentDataState = DataState.NoRows
    Else
      currentDataState = DataState.Adding
    End If

  Catch ex As Exception
    MessageBox.Show(ex.Message)

  End Try
End Sub

데이터 저장

Save 단추는 currentDataState 내부 값이 DataState.Adding 또는 DataState.Editing으로 설정된 경우에만 활성화됩니다. Save 단추를 클릭하면 상태 확인 및 적절한 작업이 수행됩니다. 이제는 Recordset의 메서드 대신 CustomerHandler 클래스의 메서드를 호출하므로 이 프로시저를 크게 변경할 필요는 없습니다. 또한 오류가 발생한 경우에도 더 이상 현재 책갈피를 추적할 필요가 없습니다. Visual Basic 6 버전과 마찬가지로 SaveData 프로시저에는 새 기본 키 값을 검색하고 폼에 표시하는 코드가 포함되어 있습니다. SaveData 프로시저를 다음 코드로 대체합니다.

Private Function SaveData() As Boolean
  Try
    If currentDataState = DataState.Adding Then
      customer.AddNew()
    End If

    ' 추가 또는 편집에 관계없이
    ' 필드를 저장하고 레코드 집합을
    ' 업데이트해야 합니다.
    SaveFields()

    customer.Update()

  Catch ex As Exception
    MessageBox.Show(ex.Message)
  End Try

  If currentDataState = DataState.Adding Then
    ' 새로 추가된 키를 표시합니다.
    GetID()
  End If

  ' 단추를 다시 설정합니다.
  currentDataState = DataState.Normal
  HandleButtonState()
End Function

폼에서 테이블로 필드 값 이동

SaveFields 메서드는 크게 변경할 필요가 없으며 간단히 데이터를 폼에서 DataRow 인스턴스로 이동하면 됩니다. 이를 위해 SaveFields 메서드를 다음 코드로 대체합니다.

Private Sub SaveFields()
  Try
    Dim row As DataRow = customer.CurrentRow

    row.BeginEdit()
    row("FirstName") = TextToNull(txtData(1).Text)
    row("LastName") = TextToNull(txtData(2).Text)
    row("Address") = TextToNull(txtData(3).Text)
    row("City") = TextToNull(txtData(4).Text)
    row("ZipCode") = TextToNull(txtData(5).Text)
    row("State") = TextToNull((cboState.Text))
    row("Active") = CBool(chkActive.CheckState)
    row.EndEdit()

  Catch ex As Exception
    MessageBox.Show(ex.Message)

  End Try
End Sub

새 고객 ID 검색

새 고객 ID 값을 검색하는 작업은 전혀 어려워지지 않았지만 코드를 약간 변경해야 합니다. 새 버전에서는 RecordSet 대신 DataTable에서 값을 검색합니다. 이를 위해 GetID 프로시저를 다음 코드로 대체합니다.

Private Sub GetID()
  txtData(0).Text = customer.CurrentRow("CustomerID").ToString()
End Sub

고객 삭제

이 버전에서는 DeleteRow 메서드가 다소 간단해졌습니다. 즉, 더 이상 오류가 발생했을 때 현재 행으로 되돌리기 위해 현재 행을 나타내는 책갈피를 유지할 필요가 없습니다. 대신 CustomerHandler 클래스가 모든 세부 작업을 처리하므로 이 클래스의 Delete 메서드를 호출한 다음 ShowData 메서드를 호출하여 현재 행을 표시하기만 하면 됩니다. 또한 행 번호가 변경되지 않으므로 다음 행으로 이동할 필요도 없습니다.

이 프로시저는 MessageBox.Show 메서드를 사용하므로 이 메서드에서 제공하는 다양한 옵션을 활용할 수 있습니다. MsgBox 함수를 사용해도 되지만 이 예에서는 이에 해당하는 .NET Framework 구성 요소를 소개하기 위해 이 메서드를 사용했습니다.

기존 DeleteRow 메서드를 다음 코드로 대체합니다.

Private Sub DeleteRow()
  Try
    If MessageBox.Show("Delete the current Customer?", _
     "Delete", MessageBoxButtons.YesNoCancel, _
     MessageBoxIcon.Exclamation) = DialogResult.Yes Then
      ' DataTable에서 고객 레코드를 삭제합니다.
      customer.Delete()

      ' 데이터가 있는지 여부에 관계없이
      ' 행을 표시합니다.
      ShowData()
    End If

  Catch ex As Exception
    MessageBox.Show(ex.Message)

  End Try
End Sub

행 찾기

변경해야 할 마지막 프로시저인 FindRow 메서드는 이 버전에서 상당히 간소해졌습니다. 즉, 현재 책갈피를 추적하고 검색에 실패한 경우 그 값을 복원할 필요가 없어졌으며 대신 검색에 성공하면 CustomerHandler.Find 메서드에서 True 값을 반환하므로 코드가 훨씬 간단해집니다. 기존 FindRow 메서드를 다음 코드로 대체합니다.

Private Sub FindRow()
  Dim searchCriteria As String
  Dim nameToFind As String

  nameToFind = InputBox("Enter " & conFindField & _
   " value to find:", Me.Text)

  If Len(nameToFind) > 0 Then
    ' 와일드카드를 사용하고
    ' 사용자가 입력한 값을 검색 문자열의
    ' 시작 부분으로 하여 검색을 수행합니다.
    searchCriteria = conFindField & _
     " LIKE " & AddQuotes(nameToFind & "%")

    ' 레코드를 찾습니다.
    If customer.Find(searchCriteria) Then
      ShowData()
    Else
      MsgBox("No match found!", MsgBoxStyle.OKOnly, Me.Text)
    End If
  End If
End Sub

결론

지금까지 살펴본 바와 같이 기존 ADO 코드를 해당하는 ADO.NET 코드로 바꾸는 작업은 크게 어렵지 않지만 응용 프로그램에서 데이터와 통신하는 부분을 모두 수정해야 합니다. 물론 사용자 인터페이스와 데이터를 적절하게 구분한다면 변환이 훨씬 쉬워질 것입니다. 이 기사에서도 ADO를 ADO.NET으로 변환하는 과정에서 이 부분에 대한 수정까지 시도해 보았습니다. 기사에서 사용한 응용 프로그램 예는 실제 데이터 지향 응용 프로그램을 흉내 내는 수준에도 못 미치고 있지만 기본 개념을 이해하는 데는 충분하리라 생각합니다. 이제 이 기사에서 다룬 내용 외에 MSDN의 다른 기사에서 데이터 동시성, 오류 처리, ADO.NET의 고급 기능 사용과 같은 다른 문제도 살펴보시기 바랍니다. 자세한 내용은 Overview of ADO.NET (영문)을 참조하십시오.

실제로 직접 사용하기 위해 이 코드를 작성했다면 상속을 사용하여 Customer, Order, Employee 등과 같은 엔터티를 나타내는 기본 클래스를 만들고 모든 탐색 코드를 추가할 수 있습니다. 이렇게 하면 기본 클래스에서 각 개별 엔터티 유형이 상속되므로 개별 탐색 코드가 필요 없습니다. 이러한 추가 코드는 이 기사의 범위를 벗어난 것이고 공간도 제한되어 있으므로 다루지 않지만 알아보는 것이 좋습니다.

이 연재의 마지막 기사에서는 다시 전체 응용 프로그램을 살펴보고 Visual Studio 2005와 Visual Basic 2005에서 제공하는 새롭고 흥미로운 데이터 바인딩 기능을 활용하는 방법에 대해 알아봅니다. 이러한 기능을 활용하여 놀라울 만큼 많은 양의 코드를 간소화할 수 있습니다.

저자 소개

Ken Getz는 MCW Technologies의 선임 컨설턴트이며 ASP .NET Developers Jumpstart(Addison-Wesley, 2002), Access Developer's Handbook(Sybex, 2001) 및 VBA Developer's Handbook, 2nd Edition(Sybex, 2001)의 공동 저자입니다.

Paul D. Sheriff는 남부 캘리포니아에 위치한 Microsoft 파트너, PDSA, Inc.(www.pdsa.com)의 대표이사로서 남부 캘리포니아 지역의 Microsoft 지역 책임자로 활동하며 지역의 Microsoft 지사와 함께 매년 개발자의 날 등 여러 대규모 행사를 개최하고 있습니다. Paul은 .NET에 대한 서적 다섯 권을 집필했으며 SharePoint에 관한 그의 저서(전자 서적) 두 권을 PDSA (영문) 웹 사이트에서 구입할 수 있습니다.

페이지 맨 위로 페이지 맨 위로


Microsoft