전문가칼럼

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

닷넷 데이터 프로바이더의 구현

전문가칼럼
DBMS별 분류
DB2
작성자
dataonair
작성일
2002-04-01 00:00
조회
11961





닷넷 데이터 프로바이더의 구현

유경상
ksyn@tysystems.com

필자가 이번 컬럼에서 제시하고자 하는 내용은 ADO.NET 에서 데이터를 데이터 소스(data source)에서 읽고 그 내용을 데이터 셋(DataSet) 객체에 채워주는 닷넷 데이터 프로바이 더(.NET Data Provider)를 작성하는 방법에 대한 것이다. 앞서 제시한 TP 모니터의 경우, 이 TP 모니터를 위한 데이터 프로바이더를 작성하면 된다. Managed Data Provider를 작 성하는 것을 닷넷 이전 환경에 비유하면 ADO에서 사용하기 위한 OLEDB Provider를 작성하는 것과 같다. OLEDB Provider는 구현하기가 매우 어려웠다. 복잡한 스펙은 물론이 요, C++로나 겨우 가능한 OLEDB Provider는 개발자에게 태산처럼 우뚝 서 있었던 것이다. 이제 닷넷 환경에서 그 산은 동네 뒷산 정도로 낮아졌으며 만만해 보일 정도다. 이전에 비 해 크게 간소화된 인터페이스뿐만 아니라 기본적인 기능은 이 미 구현돼 있으므로 이들 클래스를 상속받을 수도 있게 된 것 이다.

넷 환경에서 데이 터를 액세스하고자 한다. 무엇이 가장 먼저 떠오르는 가 물론 ADO.NET이 떠오를 것이다. ADO.NET은 전신인 ADO의 역할을 충분히 감당하고 있으며 ADO보다 더 많은 일을 해내고 있다. ADO.NET과 ADO의 차이점은 그다지 많지 않 다. ADO에 비해 ADO.NET이 데이터 액세스 및 데이터 컨테이 너로서의 역할을 보다 충실히, 그 리고 개발자들에게 보다 편리한 기능을 제공한다는 점이 다소 다 른 점이다. ADO를 통해 접근이 가능했던 데이터는 거의 모두 ADO.NET을 통해 접근할 수 있 다. ADO.NET이 OLEDB .NET Data Provider를 제공하기 때문 이다.

만약 독자가 액세스해야 하는 데이터가 ADO.NET을 통해 읽 을 수 없다면 어떻게 해야 할까 설마 그런 경우가 발생할까 하고 생각하는 독자들이 있을 것 같아 필자가 최근에 부딪혔던 문제를 제시해 보겠다. ASP.NET을 사 용하는 웹 프로젝트가 있었다. 이 프로젝트는 기존 미들웨어인 B 사의 TP 모니터를 호출하고 그 결과를 웹 페이지에 표시해야만 한다. 이 TP 모니터의 서비스를 호출한 결과를 데이터 그리드를 통해 표시하고자 한다. 쉽고 좋은 방법이 떠오르는가

자, 이제 닷넷 데이터 프로바이더라는 산을 향해 올라가 보 자. 혼동을 막기 위해 단순히 데이터 프로바이더라고 함은 닷 넷 데이터 프로바이더를 지칭하는 것이다. 기존의 OLEDB 프로바이더는‘OLEDB’접두어를 사용할 것이므로 독자들이 혼동하지 말기를 바란다. 또한 필자는 독자들이 ADO.NET이 무엇이고 어떻게 사용하는가에 대해서 잘 알고 있다고 가정할 것이다.


ADQNET의 구조

ADO.NET의 구조를 설명한 자료는 매우 많다. 마이크로소프 트의 자료들은 모두 일관된 모양과 내용으로 ADO.NET을 설 명하고 있다. 이들 자료들과 약간 다른 각도에서 ADO.NET 의 구조를 살펴보자. ADO.NET은 크게 두 부분으로 나눌 수 있다. 한 부분은 ADO.NET를 통해 액세스하고자 하는 데이 터 소스에 의존적이며, 나머지 한 부분은 데이터 소스에 독립 적이다(<그림 1>).

g4-1b%20.jpg

데이터 소스 독립적인 부분은 System.Data 네임스페이스 에 존재하며 DataSet 클래스, DataTable 클래스, DataView 클래스 등이 이 부류에 속한다. 이들 데이터 소스 독립적인 클 래스들은 데이터 소스에 무관하게 동일한 기능을 제공하며 데 이터가 어디서부터 유래됐는지 전혀 개의치 않는다. 즉, 데이 터가 관계형 데이터베이스에서 유래됐건 액티브 디렉토리나 XML과 같은 비 관계형 데이터 소스에서 왔건 상관하지 않는 다. 예를 들어 SQL 서버에서 한 테이블을 읽어 DataSet 객 체에 넣고 다른 테이블을 XML 파일에서 읽고, 마지막으로 또 다른 테이블을 엑셀 워크시트에서 읽어올 수 있다. 그리고 DataSet 객체 내부에서 이들 테이블의 관계(relation)도 맺 어줄 수 있다. 이렇게 ADO.NET의 데이터 소스 독립적인 클 래스들은 일단 메모리에 로드되면 데이터가 어떠한 데이터 소 스에서 유래됐는지와 관계없이 동일한 테이블로서 액세스할 수 있다.

ADO.NET의 또 다른 부분은 데이터 소스에 의존적인 부분 으로서 특정 데이터 소스에서 데이터를 읽어오는 방법을 제공 한다. SQL 서버에서 데이터를 읽기 위해서는 SqlConnection 객체와 SqlCommand 객체를 사용하여 SQL 쿼리 문장을 수 행하고 수행 결과를 SqlDataReader 객체를 통해 읽게 된다. 반면 데이터 소스가 오라클이나 DB2라면 OleDbConnection 객체, OleDbCommand 객체, OleDbDataReader 객체를 사 용하게 된다. 이렇게 데이터 소스가 어떤 것이냐에 따라 적절 한 객체들을 사용해야 하며 이들 데이터 소스 의존적인 클래 스를 묶어서 닷넷 데이터 프로바이더(.NET Data Provider) 라 부른다. 닷넷 데이터 프로바이더는 ADO/OLEDB에서 OLEDB 데이터 프로바이더와 비슷한 역할을 수행한다. 이번 컬럼에서 다루는 내용이 이 데이터 소스 의존적인 부분으로서 의 닷넷 데이터 프로바이더를 어떻게 구현하는가에 대한 내용 이다. 즉, 독자들이 독자만의 커스텀 데이터 프로바이더를 작 성하고자 할 때 어떻게 시작해야 하는가에 대한 답을 이 컬럼 에서 소개하고자 한다.

현재 닷넷 프레임워크에서 제공되는 데이터 프로바이더는 크게 세 종류다. .NET SQL Client Data Provider는 SQL 서버 7.0 및 SQL 서버 2000을 데이터 소스로 하는 데이터 프로바이더로서 OLEDB를 사용하는 것보다 훨씬 더 빠르게 SQL 서버를 액세스할 수 있도록 해준다. .NET OLEDB Data Provider는 기존의 OLEDB를 통해 액세스 가능했던 데이터 소스들을 ADO.NET을 통해 액세스하도록 해준다.

OLEDB Data Provider를 통해 액세스 가능한 데이터 소스는 오라클, DB2, 사이베이스 등의 RDBMS들과 DBF액세 스VSAM 류의 ISAM 파일 데이터, 액티브 디렉토리인덱 스 서버익스체인지 메일 등 비 관계형 데이터다. 마지막으 로 ODBC Data Provider는 기존에 ODBC를 통해 액세스 가 능했던 데이터 소스들을 액세스할 수 있도록 해준다. 예전처 럼 OLEDB를 통해 ODBC 데이터 소스를 액세스하는 것이 가능하지만 이제는 ODBC Data Provider를 사용하는 것이 보다 유리할 것이다. ODBC Data Provider는 닷넷 프레임워 크에 포함돼 있지 않으므로 별도로 MSDN 온라인 사이트에 서 내려받아야 함에 유의하자.

데이터 프로바이더의 클래스는 프로바이더의 종류에 따라 적절한 접두사가 붙는다. SQLClient 데이터 프로바이더는 Sql이라는 접두사가, OLEDB 데이터 프로바이더는 OleDb 가, ODBC 데이터 프로바이더는 Odbc가 붙는다. 접두사를 사용하지 않는다고 해서 컴파일이 안 되거나 오류가 발생하는 것은 아니지만 플랫폼에서 요구하는 규칙이나 권고사항은 따 르는 것이 좋은 프로그래밍 습관이자 읽기 좋은 코드를 작성 하는 지름길임을 명심하자.

ADQNET의 엔진 "닷컴데이터 프로바이더"

t4-1.jpg

닷넷 데이터 프로바이더는 ADO.NET에서 요구하는 일련의 인터페이스를 구현하는 클래스 집합을 말한다. <표 1>은 데이 터 프로바이더에서 요구하는 인터페이스와 이들의 용도에 대 해 설명하고 있다. 닷넷 데이터 프로바이더를 구현한다는 말 은 곧 <표 1>에 나열된 인터페이스의 프로퍼티 및 메쏘드를 구현한다는 말과 상통한다. 이들 인터페이스는 메쏘드와 프로퍼티로 구성돼 있다. 커스 텀 데이터 프로바이더를 작성하다 보면 인터페이스에는 정의되어 있지만 실제 데이터 프로바이더에서는 불필요하거나 의 미 없는 메쏘드/프로퍼티들이 존재할 수 있다. 이러한 경우에 는 NotSupportedException 예외를 생성시켜 주는 것이 일반 적이다. 예를 들어 어떤 데이터 프로바이더는 트랜잭션을 사 용하지 않으며 지원하지도 않는다. 그럼에도 불구하고 IDb Connection 인터페이스는 BeginTransaction 메쏘드를 정의 하고 있다. 이 때 BeginTransaction 메쏘드의 구현은 다음과 같이 하면 된다는 얘기다. public IDbTransaction BeginTransaction(IsolationLevel il) { throw new NotSupportedException( “BeginTransaction 메쏘드는 지원하지 않습니다.”); } 필자의 경험상 이렇게 지원하지 않는 메쏘드/프로퍼티는 데이터 프로바이더 인터페이스에서 많이 등장하므로 아예 클 래스마다 NotSupported() 메쏘드를 정의하고 예외를 발생하 도록 하면 편리하다. 이제 <표 1>에 나열된 인터페이스를 좀 더 상세히 살펴보자. 각 인터페이스의 모든 메쏘드/프로퍼티를 모두 설명할 수 없으므로 일부 중요한 것들 위주로 살펴볼 것이다.

r4-1.jpg

Connection 객체 구연

데이터 프로바이더의 Connection 객체는 IDbConnection 인 터페이스를 구현하는 클래스의 인스턴스이다. IDbCon nection 인터페이스는 데이터 소스에 대한 커넥션을 열거나 (open) 닫고(close), 커넥션에 대한 각종 설정을 읽거나 설정 할 수 있도록 해준다. IDbConnection 인터페이스는 Con nection 객체들(SqlConnection, OleDbConnection, Od bcConnection 등)이 구현하고 있으며 커스텀 데이터 프로바 이더를 작성하고자 한다면 Connection 개체는 이 인터페이스 를 구현해야 한다.

IDbConnection 인터페이스는 네 개의 프로퍼티와 다섯 개 의 메쏘드로 구성돼 있다. 따라서 커스텀 데이터 프로바이더 의 Connection 클래스는 최소한 9개의 public 멤버를 구현해 야 한다. 이외에도 IDbConnection 인터페이스가 IDispo sable 인터페이스에서 파생됐으므로 Dispose() 메쏘드를 구 현해야 한다. 이들 메쏘드 중 중요한 것은 역시 Open 메쏘드 와 Close 메쏘드다. Open() 메쏘드는 데이터 소스에 대한 커 넥션을 맺고 Close() 메쏘드는 커넥션을 닫는다. 두 메쏘드에 서 수행해야 할 중요한 작업은 State 프로퍼티의 값을 갱신해 주는 것이다. Connection 객체의 초기 State 프로퍼티 값은 ConnectionState.Closed이다. Open() 메쏘드가 호출되고 연결이 맺어짐에 따라서 State 프로퍼티의 값은 Connection State 열거자 타입에서 정의한 값에 의해 바뀌어야 한다. 마 찬가지로 Close() 메쏘드에서는 커넥션을 닫고 State를 Closed로 바꿔야 할 것이다.

이외에 Connection 객체를 구현할 때 주의할 사항은 Dispose() 메쏘드와 관련된 사항이다. 닷넷 CLR은 가비지 컬렉션(GC, garbage collection)에 의해 객체들이 메모리에 서 해제된다. 많은 객체들이 생성자(constructor)에서 자원들 을 할당하고 이들 자원들은 객체가 해제됨에 따라 역시 해제 되기 마련이다. 주의할 것은 객체가 해제되는 시점을 프로그 래머가 예측할 수 없으며 또한 강제적으로 해당 객체를 해제 할 마땅한 방법 역시 없다는 점이다. 막연히 GC가 수행되어 자원이 해제되기를 기다릴 수도 있지만 데이터베이스 커넥션 과 같은 민감하고 비싼 자원들은 커넥션이 더 이상 사용되고 있지 않음에도 불구하고 GC가 수행될 때까지 커넥션을 맺고 있도록 둘 수 없다. 따라서 대부분의 데이터 프로바이더는 Close() 메쏘드를 통해 개발자가 명시적으로 커넥션을 닫을것을 요구하고 있다.

개발자가 Close() 메쏘드를 호출해 커넥션을 닫았다면 다 행이지만 그렇지 않았다면 어떻게 해야 할까 어찌할 수 없이 소멸자(좀 더 정확하게 Finalizer라고 한다)에서 커넥션을 닫 아야 할 것이다. 이와 같은 표준적인 자원 해제 방법을 정의하 고 있는 것이 IDisposable 인터페이스다. Connection 객체의 IDisposable.Dispose() 메쏘드는 단순히 Close()를 호출함으 로써 커넥션을 닫을 수 있으며, 소멸자가 Dispose()를 호출함 으로써 열린 커넥션이 반드시 닫히도록 강제할 수 있는 것이 다. <리스트 2>는 Connection 객체의 상태 및 Open/Close/ Dispose 메쏘드의 구현을 보여준다.

Connection 클래스를 구현할 때 마지막으로 고려해야 할 사항은 이 클래스의 생성자다. IDbConnection은 인터페이스 이기 때문에 생성자를 정의할 수 없다. 그러나 일반적으로 닷 넷 데이터 프로바이더의 Connection 객체는 두 개 이상의 생 성자를 구현해야 한다. 매개변수 없는 디폴트 생성자와 커넥 션 스트링을 매개변수로 하는 생성자가 그것이다. 생성자를 이렇게 구현하지 않는다고 해서 오류가 발생하는 것은 아니지 만 SqlClient, OleDb, Odbc 데이터 프로바이더가 이와 같은 생성자를 제공하므로 일관성 유지 측면에서 이러한 생성자를 제공하는 것이 좋다. 이외에 데이터 프로바이더 고유의 생성 자를 제공하는 것은 아무런 문제가 없다.

지금까지 설명한 메쏘드/프로퍼티 외에도 커넥션 스트링 (ConnectionString), 타임아웃 시간(ConnectionTimeout), 데이터베이스 이름(Database)을 액세스하는 프로퍼티와 커 맨드 객체를 생성(CreateCommand)하거나 데이터베이스를 변경(ChangeDatabase)하는 메쏘드가 IDbConnection 인터 페이스에 정의돼 있다. ConnectionString 프로퍼티를 제외하 고 이들은 지원하지 않아도 되므로 자세한 설명은 마치겠다. 상세한 내용은 MSDN 라이브러리를 참고하기 바란다. MSDN 라이브러리의“Accessing Data with ADO.NET”항 목을 보면 하위 항목으로서“Implementing a .NET Data Provider”라는 항목이 제공된다. 이 항목에서 커스텀 데이터 프로바이더를 구현할 때 고려해야 하는 사항과 예제가 제공된 다. 예제는 실제로 작동하는 데이터 프로바이더는 아니며 커 스텀 데이터 프로바이더를 작성할 때 사용할 수 있는 템플릿 정도다. 처음부터 데이터 프로바이더를 작성할 때 상당히 유 용한 템플릿이므로 이 템플릿 코드로부터 코드를 작성하는 것 이 좋을 것이다.

Command 객체 구현

r4-2.jpg

Command 객체는 IDbCommand 인터페이스를 구현하는 클 래스의 인스턴스를 말한다. IDbCommand 인터페이스는 데 이터 소스에 대한 커맨드를 설정하고 수행하기 위한 일련의 메쏘드와 프로퍼티로 구성되어 있다. Command 객체에서 핵 심적인 메쏘드 및 프로퍼티로는 ExecuteReader/Execute NonQuery 메쏘드와 Connection/CommandText/Com mandType 프로퍼티를 들 수 있다.

Connection 프로퍼티는 Command 객체가 명령을 수행할 데이터 소스와의 연결을 설정하는 데 사용된다. 예상할 수 있 듯이 IDbCommand 인터페이스가 정의하는 Connection 프 로퍼티의 타입은 IDbConnection 타입이다. 하지만 IDbConnection 타입을 사용하기는 곤란할 것이다. 만일 독 자가 작성한 커스텀 데이터 프로바이더가 XxxConnection 객 체를 사용한다면 Connection 프로퍼티 타입은 IDbCon nection보다는 XxxConnection 타입이 돼야 할 것이다. C# 스펙에 의하면 인터페이스를 구현할 때 명시적으로 인터페이 스 멤버를 구현할 수 있다. 명시적 인터페이스 멤버 구현은 다 음과 같이 public/protected/private 등의 한정자를 사용하지 않고 인터페이스 이름을 직접 사용하면 된다. 그리고 인터페 이스를 구현하는 클래스가 자신만의 인터페이스 구현을 public 한정자를 이용해 구현할 수 있다.

public class XxxCommand {
private XxxConnection m_Connection = null;
// 인터페이스 멤버의 명시적 구현(public 한정자가 없음에 유의한다.)
// 외부에서 XxxCommand 클래스 타입을 통해 이 구현을 호출할 수 없다.
// IDbCommand 인터페이스 타입을 통해서만 이 구현을 호출할 수 있다.
IDbConnection IDbCommand.Connection {
get { return m_Connection; }
set { m_Connection = (XxxConnection)value; }
}
// 인터페이스 멤버를 재정의(프로퍼티 타입이 달라졌음에 유의한다.)
public XxxConnection Connection {
get { return m_Connection; }
set { m_Connection = value; }
}

위 예의 경우, 프로퍼티이기 때문에 리턴 타입만 바뀌었지 만 메쏘드의 경우라면 매개변수 타입을 바꾸거나 매개변수를 추가/삭제할 수도 있다. SqlCommand 객체의 Connection 프로퍼티의 타입이 왜 IDbConnection이 아닌 SqlConne ction이 되는가를 이해할 수 있을 것이다.

CommandText 프로퍼티는 Command 객체에서 중요한 역할을 하지만 이 프로퍼티 자체를 구현하는 데는 그다지 어 렵지 않을 것이다. 이 프로퍼티에 설정된 커맨드를 private 필 드에 기록해 두었다가 ExecuteXXX 메쏘드에서 설정된 커맨 드를 수행하면 될 것이기 때문이다. CommandType 역시 비 슷하게 중요한 역할을 하지만 구현 자체는 어렵지 않다. 다만 CommandType 프로퍼티가 CommandType 열거자 타입으 로서 명령의 종류가 Text, StoredProcedure, TableDirect 세 종류뿐이라는 점이다. 이것도 큰 문제가 되지 않는 것이 앞서 Connection 프로퍼티의 경우와 마찬가지로 구현할 커맨드 객 체가 다루는 별도의 명령 종류를 XxxCommandType 열거자 로 정의하고 CommandType 프로퍼티를 이 열거자 타입으로 정의하면 될 것이다.

Command 객체의 ExecuteXXX 메쏘드는 설정된 명령을 수행하고 그 결과를 받아 오는 메쏘드다. 명령 수행 결과가 일 련의 레코드 셋이라면 ExecuteReader 메쏘드를 사용하고, 단일 값을 반환한다면 ExecuteScalar 메쏘드를, 결과를 전혀 반환하지 않거나 결과에 관심이 없다면 ExecuteNonQuery 메쏘드를 사용한다. IDbConnection 인터페이스와 마찬가지 로 작성할 커스텀 데이터 프로바이더에서 지원하지 않는ExecuteXXX 메쏘드는 NotSupportedException을 생성하 는 것이 좋다. 예를 들어, ExectueNonQuery 메쏘드나 Exe cuteScalar 메쏘드는 ExecuteReader 메쏘드를 통해 대체가 가능하므로 구현하지 않는 것도 좋은 방법이라 할 수 있다.

Command 객체 역시 생성자를 제공해야 한다. 앞서 언급 했지만 IDbCommand가 인터페이스이기 때문에 생성자를 정 의하고 있지 않다. MSDN에 따르면 Command 객체는 네 개 의 생성자를 구현하도록 권유하고 있다. 물론 이러한 생성자 를 모두 구현하지 않아도 되지만 가능하다면 모두 구현하는 것이 좋을 것이다. 구현해야 할 생성자에 대한 내용은 MSDN 의 닷넷 프레임워크 레퍼런스에서 IDbCommand를 찾아보기 바란다.

IDbCommand 인터페이스는 앞서 열거한 프로퍼티/메쏘 드 외에도 CommandTimeout, Parameters, Transaction, UpdatedRowSource 프로퍼티와 Cancel, CreatePara meter, Prepare 메쏘드를 갖고 있다. UpdatedRowSource는 Command 객체가 데이터 소스를 변경하는 명령을 수행할 때 수행된 결과로서 DataTable 객체의 DataRow를 갱신하는 방법을 정의한다. UpdatedRowSource 프로퍼티 자체의 구현 은 어렵지 않지만 이 프로퍼티를 통한 설정은 ExecuteXXX 메쏘드의 구현에 영향을 준다. 나머지 프로퍼티들은 직관적이 므로 설명을 생략한다. 상세한 내용은 MSDN 라이브러리를 참고하기 바란다.


DataReader객체 구현

DataReader 객체는 IDataReader 인터페이스를 구현하는 클래스의 인스턴스를 말한다. IDataReader 인터페이스는 데 이터 소스로부터 데이터를 전진 전용(forward-only) 및 읽기 전용(read-only) 커서를 이용해 데이터를 읽기 위한 일련의 메쏘드와 프로퍼티를 제공한다. 경우에 따라서, 아니 많은 경 우에 데이터 소스가 레코드 단위의 패치를 지원하지 않는 경 우가 많을 것이다. 대부분의 RDBMS는 레코드 단위 패치를 지원하겠지만 독자들이 작성할 데이터 프로바이더의 데이터 소스는 원격 컴퓨터의 메모리나 원격 파일 등일 가능성이 높 고 이것으로부터 레코드 단위의 데이터를 읽어오는 경우는 상 당히 드물다고 할 수 있다. 이 경우 DataReader 객체가 필요 없을 것처럼 보이지만 전혀 그렇지 않다. DataAdapter에서 설명하겠지만 DataAdapter가 커맨드를 수행하고 그 수행 결 과를 DataSet이나 DataTable에 기록한다면 DataAdapter 객체는 Command 객체의 ExecuteReader 메쏘드를 호출하 고 이 메쏘드가 반환하는 DataReader 객체를 통해 DataTable의 컬럼 정보와 레코드를 설정한다. 따라서 Data Reader 객체는 필수 구현 사항이 돼 버린다(항상 필수는 아 니다! DataAdapter 객체에서 이유를 설명하겠다). 비록 데이터 소스가 커서 메커니즘에 의해 한 레코드씩 데 이터를 돌려주지 않고 한꺼번에 여러 데이터를 넘겨주더라도 DataReader는 여러 건의 레코드에서 한 건씩(실제 필드에서 레코드 하나를 한‘건’이라고 표현한다) 데이터를 읽어 제공 하는 메커니즘을 제공해야 함을 주의하자.

IDataReader 인터페이스는 세 개의 프로퍼티와 네 개의 메쏘드를 제공하는데, 여기서 중요한 것은 RecordsAffected 프로퍼티와 Read/Close 메쏘드라 할 수 있다. Records Affected 프로퍼티를 구현하는 것은 그다지 어렵지 않지만 주 의할 사항은 이 프로퍼티가 어떠한 쿼리의 수행 결과로서 반 환되는 레코드 개수를 의미하지 않는다는 점이다. Data Reader 객체가 전진 전용 커서를 내포하고 있기 때문에 조회 된 레코드 개수를 알아낼 방법이 없다. 반환된 레코드 개수를 알기 위해서는 모든 레코드를 읽어야 하지만 레코드를 읽어 레코드 개수를 알아낸 후에 정작 레코드 데이터를 읽을 수가 없게 돼 버린다(전진 전용 커서이므로 커서를 되돌릴 수 없 다). 그렇다면 이 프로퍼티가 의미하는 것은 데이터 프로바 이더가 어떤 데이터 소스를 액세스하는가에 따라 이 프로퍼티 의 의미는 약간씩 다르겠지만 SqlClient 데이터 프로바이더를 예로 들면, 액션 쿼리(INSERT, UPDATE, DELETE 등)를 수행했을 때 이 액션 쿼리의 영향을 받는 레코드 개수를 Rec ordsAffected 프로퍼티가 나타내게 된다. 이러한 의미를 잘 이해하고 이 프로퍼티를 구현해야 할 것이다.

Read 메쏘드는 데이터 소스에서 레코드를 패치해 오는 역 할을 담당한다. ADO의 레코드 셋과는 달리 Command 객체 가 수행돼 그 결과로서 DataReader 객체가 만들어지면 DataReader를 통해 곧바로 첫 번째 레코드를 읽을 수 없다. 즉, Read 메쏘드가 최소한 한번은 호출돼야만 레코드를 읽 을 수 있다는 점이다. DataReader 객체를 구현하다 보면 이 객체의 내부에 커서의 현재 위치를 갖는 필드를 두는 것 이 일반적이다. 그리고 Read 메쏘드는 이 위치 값을 1씩 증 가시키면서 해당 데이터를 읽어온다. 더 이상 읽을 레코드가 없다면 Read 메쏘드는 false를 반환한다. 다음의 예제처럼 Read 메쏘드의 구현은 그다지 복잡하지 않다. 실제 중요한 부분은 데이터 소스로부터 한 레코드를 패치해 오는 메쏘드 일 것이다.

private int m_CurrentPosition = -1;
private object m_RecordData[];
public bool Read() {
bool isRead = false;
// 데이터 소스에서 레코드 한건을 읽어 DataReader의 버퍼에 채운다.
isRead = FetchDataFromSource(this.m_CurrentPosition,
out m_RecordData);
if (isRead)
m_CurrentPosition++; return isRead;
}

I DataReader 인터페이스의 Close 메쏘드는 DataReader 의 커서를 닫는 개념으로 사용된다. 물론 이 메쏘드는 Ex ecuteReader 메쏘드의 CommandBehavior 매개변수가 CloseConnection일 때는 Connection 객체의 Close 메쏘드를 호출하기도 하지만, 근본적인 Close 메쏘드의 의미는 커서를 닫는 것이다. 따라서 Close 메쏘드가 호출된 후에 Read 메쏘 드를 호출하는 것은 오류를 유발한다.

r4-3%20copy.jpg

DataReader의 구현이 간단해 보이지만 DataReader 객체 는 IDataReader 인터페이스 외에도 IDataRecord 인터페이 스를 구현해야 한다. IDataReader 인터페이스가 IData Record 인터페이스에서 파생(derived)됐기 때문이다. 필자의 경험상 DataReader 객체의 구현은 그다지 어렵지 않으나, 코 드 상으로 가장 구현해야 할 것이 많은데 그 이유가 IData Record 인터페이스 때문이였다. 이 인터페이스는 IData Reader.Read 메쏘드를 통해 읽은 레코드의 구체적인 컬럼값 을 액세스하는 데 사용되는 일련의 프로퍼티와 메쏘드를 정의 하고 있다. IDataRecord의 두 프로퍼티는 필드(컬럼) 개수 (FieldCount 프로퍼티)와 각 컬럼을 C# 인덱서(indexer)를 통해 액세스(Item 프로퍼티)하도록 해준다.

IDataRecord 인터페이스의 메쏘드는 필드의 데이터 타입 (GetFieldType, GetDataTypeName), 필드 이름(Get Name), 필드 값이 NULL인가 확인(IsDBNull)하는 몇몇 메 쏘드와 GetXXX 형태의 형식화된 데이터 읽기(typed data read) 메쏘드를 정의하고 있다. GetXXX 메쏘드는 필드 인덱 스를 매개변수로 취하고 XXX 타입의 필드 값을 반환하도록 돼 있다. 예를 들어 GetInt32 메쏘드는 필드 값을 32비트 정 수로 읽는 메쏘드이며, GetDouble 메쏘드는 배정도 실수를 읽는 메쏘드다. 구현할 때 주의할 점은 이들 GetXXX 메쏘드 가 형 변환을 의미하지 않는다는 점이다. 예를 들어 어떤 필드 (컬럼) 값이 string(데이터베이스에서는 varchar 정도가 될 것이다)이지만 이 필드를 읽는데 GetInt32 메쏘드를 호출했 다면 Int32 타입으로 변환된 값이 반환되는 것이 아니라 InvalidCastException이 발생한다는 의미다. <리스트 3>에서 보인 것처럼 단순한 타입 캐스팅(casting)으로 GetXXX 메쏘 드를 구현하면 된다.

DataReader 객체는 Command 객체와 더불어 데이터 프 로바이더의 중요한 역할을 하며 구현하는 데도 가장 시간이 많이 소요되는 객체다. 일반적으로 Command 객체가 데이터 소스를 액세스해 필요한 커서 작업이나 데이터 버퍼링 작업을 수행하고 이 수행 결과를 DataReader 객체를 통해 외부(데 이터 프로바이더 외부)에 제공하는 것이 일반적인 구현 방법 이다. DataAdapter 객체가 DataSet이나 DataTable 객체에 데이터를 채워 넣을 때도 DataReader 객체를 사용하며 Get FieldType, GetName, GetValues 등의 일련의 DataReader 의 메쏘드를 호출해 DataTable을 생성하고 레코드 값들을 채 워 넣는다.

마지막으로 DataReader 객체에서 다뤄야 할 부분은 역시 생성자에 관련된 부분이다. DataReader 객체는 new 키워드를 통해 인스턴스를 생성할 수 없다. SqlDataReader도 그렇 고 OleDbDataReader도 예외는 아니다. 하지만 Command 객체의 ExecuteReader 메쏘드는 DataReader 객체의 인스 턴스를 만들어야만 한다. 어떻게 구현해야 할까 간단하다. DataReader 객체의 생성자에 internal 한정자를 사용하면 된다. 이렇게 하면 Command 객체에서는 DataReader 객체 를 생성할 수 있지만 이 데이터 프로바이더를 사용하는 코드 에서는 DataReader의 인스턴스를 생성할 수 없다. 물론 Command 객체의 구현과 DataReader 객체의 구현이 동일 한 어셈블리에 존재한다는 가정하에서 말이다.


DataAdapter객체 구현

DataAdapter 객체는 IDataAdapter 인터페이스 혹은 IDb DataAdapter 인터페이스를 구현하는 클래스의 인스턴스를 말한다. “혹은”이라니 둘 다 구현했으면 했지 그 중 하나만 구현해도 된다는 얘기인가

DataAdapter 객체는 데이터 프로바이더의 종류에 따라서 두 종류로 나눌 수 있다. 데이터 프로바이더는 Connection, Command, DataReader, DataAdapter를 모두 구현하는 풀 셋 데이터 프로바이더와 오직 DataAdapter만을 구현하는 심 플 데이터 프로바이더로 나눌 수 있기 때문이다.

먼저 간단한 심플 데이터 프로바이더의 DataAdapter에 대 해 살펴보자. 심플 데이터 프로바이더는 Connection이나 Command, DataReader와 같은 객체를 제공하지 않고 DataAdapter만을 제공하는 데이터 프로바이더다. 이러한 데 이터 프로바이더가 필요()한 이유는 데이터 소스에 따라 Connection이나 Command, DataReader와 같은 객체가 불 필요하거나 의미 없는 경우가 있기 때문이다. 예를 들어, 텍스 트 파일이 데이터 소스인 데이터 프로바이더의 경우 Conne ction이나 Command는 불필요하며 읽기 전용 커서 방식 역 시 무의미한 작업일 뿐이다. DataAdapter 객체는 텍스트 파 일로부터 데이터를 읽어 DataSet에 데이터를 채우면 될 것이 다. 이러한 경우에 심플 데이터 프로바이더를 사용할 수 있으 며 많은 데이터 소스를 액세스하는 데 심플 데이터 프로바이 더를 적용할 수 있다.

심플 데이터 프로바이더의 DataAdapter 객체는 IData Adapter 인터페이스를 구현하는 클래스의 인스턴스를 말한 다. 이 객체는 데이터 소스로부터 데이터를 DataSet 혹은 DataTable에 채워 넣는 역할을 수행한다. 그리고 이 객체는 Connection, Command 및 DataReader 객체와 상호작용을 정의하고 있지 않다. IDataAdapter 인터페이스의 내용을 살 펴보면, 세 개의 프로퍼티와 네 개의 메쏘드로 구성돼 있다. 한 된다는 점 이다. 얼핏 보기에 Connection, Command, DataReader 객 체들에 비해 DataAdapter 객체가 구현하기에 훨씬 복잡해 보이지만 실제는 전혀 그렇지 않다. 그 이유는 기본적인 구현 이 이미 다 돼 있기 때문이다. IDataAdapter 인터페이스를 구현하는 클래스는 DataAdapter 클래스로서 이 클래스는 IDataAdapter 인터페이스의 많은 내용을 이미 구현하고 있 다. 따라서 커스텀 (심플) 데이터 프로바이더를 작성하려면 DataAdapter 클래스에서 파생된 클래스를 정의하고 몇몇 메 쏘드 및 인터페이스를 직접 구현하면 된다.

DataAdapter 클래스는 기본적인 구현을 대부분 제공하지 만 모든 것에 대한 구현을 제공하지 않는다. DataAdapter 클 래스를 상속한 파생 클래스는 네 개의 메쏘드에 대한 구현을 제공해야 한다. 실제로 DataAdapter 클래스는 abstract 클래 스로서 인스턴스를 만들 수 없는 클래스이며 Fill, FillSche ma, GetFillParameters, Update 네 개의 메쏘드가 abstract 메쏘드로서 파생 클래스에서 구현을 제공해야 하는 메쏘드 다. Fill 메쏘드는 말 그대로 매개변수로 주어진 데이터 셋에 데이터를 채워 넣는 역할을 수행한다. 심플 데이터 프로바이 더이기 때문에 DataReader 객체로부터 테이블 정보(컬럼 개수, 컬럼 이름, 컬럼 타입 등)를 얻어낼 수 없다. 따라서 Fill 메쏘드는 직접 DataTable을 만들어 DataSet 객체에 추가해야 하며 이 테이블에 DataRow 역시 삽입해야 한다.

<리스트 4>는 심플 데이터 프로바이더 예제다. 이 데이터 프로바이더는 텍스트 파일로부터 데이터를 읽어 구분자 (delimiter) 문자에 의해 컬럼을 구분하고 데이터 행은 라인 단위로 구분하도록 돼 있다. 구현 내용을 약간 살펴보자. TextDataAdapter 클래스는 DataAdapter 클래스로부터 파 생되고 앞서 언급한 Fill, FillSchema, GetFillParameters, Update 메쏘드를 구현하고 있다. 나머지 프로퍼티/메쏘드는 DataAdapter가 제공하는 것을 그대로 상속받아 사용한다.

FillSchema, GetFillParameters, Update 메쏘드는 Not SupportedException을 발생해 지원하지 않음을 명시하고 있 으며 오직 Fill 메쏘드만을 구현하고 있다. Fill 메쏘드는 새로 운 테이블을 만들고 텍스트 파일로부터 읽은 데이터를 Data Set 내의 DataTable에 삽입하고 있다.

이 데이터 프로바이더를 사용하는 방법은 매우 간단하다. 다음과 같이 TextDataAdapter 클래스의 인스턴스를 만들고 Fill 메쏘드에 DataSet 객체를 매개변수로 넘겨주면 된다.

r4-4.jpg

img1.gif

using System.Data.Text;
// 생략…
TextDataAdapter adapter =
new TextDataAdapter(“..\\..\\Sample.txt”, ‘,’);
DataSet ds = new DataSet(“Sample”);
adapter.Fill(ds, “Accounts”);
// 데이터 그리드에 바인딩한다.
dgResult.DataSource = ds.Tables[0];

앞 코드와 같이 데이터 컨슈머(consumer)를 작성하고 다 음과 같은 예제 데이터에 대해 수행시키면 <화면 1>과 같은 결과를 얻을 수 있다. 심플 데이터 프로바이더는 DataAda pter 객체만을 구현하면 되므로 적은 양의 코드로 이렇게 많 은 효과를 얻을 수 있다는 장점이 있다. 텍스트 파일과 같은 간단한 데이터 소스에 대해서는 좋은 선택 사항이라고 말할 수 있을 것이다.

ACCOUNT_ID,NAME,ADDRESS,PHONE,BALANCE 10005,Smith,456 Grafton Ct.Oakland,555-3452,1244 10006,John,8552 Main Ave. San Francisco, 555-1242,1342 이제 풀셋 데이터 프로바이더에 대해 이야기해보자. 풀셋 데이터 프로바이더는 Connection, Command, DataReader, DataAdapter를 모두 구현하는 데이터 프로바이더다. 풀셋 데이터 프로바이더의 DataAdapter 객체는 IDbData Adapter 인터페이스를 구현하는 클래스의 인스턴스를 말한 다. IDbDataAdapter 인터페이스는 IDataAdapter 인터페이 스로부터 파생된 인터페이스로서 상속된 프로퍼티/메쏘드를 제외하고 네 개의 프로퍼티만을 갖고 있다. 이 프로퍼티들은 SelectCommand, InsertCommand, UpdateCommand, DeleteCommand 프로퍼티이다. 이들 프로퍼티의 용도는 독 자들도 잘 알고 있으리라 생각하고 설명을 생략하겠다. 풀셋 데이터 프로바이더의 DataAdapter 객체의 구현은 상 상 외로 쉽다. 심플 데이터 프로바이더의 DataAdapter보다 구현이 더 쉬운데, 그 이유는 풀셋 DataAdapter 역시 베이스 클래스인 DbDataAdapter가 대부분의 구현을 제공하고 게다 가 이 클래스는 DataReader 객체를 호출해 DataTable을 만 들거나 컬럼을 자동으로 설정해주기 때문이다. 게다가 Fill 메 쏘드의 구현이 이미 DbDataAdapter 클래스에서 제공된다.

g4-2.jpg

DbDataAdapter 클래스의 Fill 메쏘드는 SelectCommand 에 설정된 커맨드 객체의 ExecuteReader 메쏘드를 호출한 다. 그리고 이 메쏘드가 반환한 DataReader 객체의 Field Count, 프로퍼티와 GetFieldType, GetName 메쏘드를 이 용해 테이블이 어떠한 컬럼을 가지며 이 컬럼 타입 등을 알아 낸다. 이들 컬럼 정보를 이용해 DataTable 객체를 만들 수 있 으며, 이 테이블의 데이터 행의 값은 DataReader 객체의 GetValues를 통해 얻어낸다. 이러한 Fill 메쏘드의 구현이 모 두 DbDataAdpater 클래스에서 제공되므로 커스텀 데이터 프로바이더를 작성하고자 한다면 DbDataAdapter 클래스에 서 파생된 클래스를 정의하고 이 클래스에 IDbDataAdapter 인터페이스의 프로퍼티와 DbDataAdapter 클래스의 몇몇 abstract 메쏘드만을 구현하면 된다. 풀셋 데이터 프로바이더 의 DataAdapter 객체는 Connection, Command, Data Reader 객체 없이는 작동하지 않으므로 예제를 보일 수 없음 을 독자들에게 양해를 구하는 바이다.

DataAdapter 객체는 Connection, Command 객체와 비슷 하게 일련의 생성자를 제공해야 한다. 이들 생성자는 네 종류 로서 각각에 대한 상세 내용은 MSDN을 참고하자. 풀셋 데이 터 프로바이더의 경우 네 개의 생성자를 모두 지원하는 것이 좋지만 Command 객체나 Connection 객체를 지원하지 않는 심플 데이터 프로바이더의 경우 MSDN에서 권고하는 생성자 는 의미가 없다. 따라서 IDataAdapter 인터페이스만을 지원 하는 DataAdapter 객체는 <리스트 4>에서 구현한 것처럼 해 당 DataAdapter가 필요로 하는 생성자를 구현하면 된다.


그밖의 객체들

지금까지 데이터 프로바이더의 핵심적인 객체인 Connection, Command, DataReader, DataAdapter 객체에 대해 설명했 다. 이외에도 닷넷 데이터 프로바이더는 Transaction 객체, Parameter 객체, ParameterCollection 객체 등이 존재한다. 풀셋 데이터 프로바이더라면 최소한 Connection, Comm and, DataReader, DataAdapter 객체를 구현해야만 한다.

나머지 객체에 대한 구현은 선택 사항이다. 반면 심플 데이터 프로바이더라면 DataAdapter 객체만을 구현해도 된다.Transaction 객체는 ITransaction 인터페이스를 구현하는 클래스의 인스턴스를 말하며, 데이터 소스가 트랜잭션을 지원 하는 경우 데이터 프로바이더가 제공해야 할 객체이다. 트랜 잭션을 지원하는 데이터 소스는 그다지 많지 않으므로 이 객 체의 지원 여부는 선택 사항이다. Parameter 객체는 Comm and 객체가 커맨드를 수행할 때 매개변수를 설정할 수 있는 경우 Command 객체를 통해 지원하는 객체를 말한다.

Parameter 객체는 IDataParamter 인터페이스와 IDbData Parameter 인터페이스를 구현하는 클래스의 인스턴스이며, ParamterCollection 객체는 IDataParameterCollection 인 터페이스를 구현하는 클래스의 인스턴스이다. 이름에서 알 수 있듯이 ParameterCollection 객체는 Parameter에 대한 컬렉 션을 유지하는 객체이다. Parameter 객체와 Parameter Collection 객체 역시 데이터 프로바이더가 선택적으로 지원 가능한 객체로서 Command 객체가 매개변수를 사용할 때에 만 구현하면 된다.


데이터 프로바이더가 필요한 이유

지금까지 닷넷 커스텀 데이터 프로바이더를 작성하는 데 필요 한 내용을 다뤘다. ADO의 OLEDB Provider만큼 복잡하거 나 작업량이 많지는 않지만 여전히 많은 인터페이스와 메쏘드 를 구현해야 한다. 필자가 작성한 풀셋 데이터 프로바이더는 약 6000여 라인(주석 포함. XML 주석을 사용해 주석이 상당 히 많은 편이다) 정도였다. 많다면 많고 적다면 적은 분량이 다. 사소한 수준은 아니다. 이 정도의 노력을 투자해 커스텀 데이터 프로바이더를 만들 필요가 있을까 단순히 데이터를 데이터 셋을 통해 액세스하고자 하는 것이 목적이라면 코드를 통해 DataColumn 객체를 만들고 이것을 DataTable에 추가 한 다음, 이 DataTable을 다시 DataSet에 추가하면 되지 않 을까 왜 사서 고생을 해야 하지 이 질문에 대한 답은 독자 스스로가 해야 한다. 독자가 데이터 프로바이더를 제공했을 때와 그러지 않았을 때의 득실을 따져보고 판단을 내려야 한 다. 독자의 판단을 돕기 위해 데이터 프로바이더를 제공하는 경우와 그렇지 않은 경우 두 시나리오를 들어보겠다.

여러분이 일련의 데이터를 가지고 있다. 이 데이터를 다른 개발자가 사용할 수 있도록 해야 할 때 독자들은 어떤 방법이 떠오르는가 얼핏 떠오르는 방법은 데이터를 액세스하는 API를 만들고 개발자들이 이 API를 사용하도록 하는 것이다. 하지만 개발자들은 이 API에 익숙하지 않을 것이다. 더욱 문 제되는 것은 이 데이터를 ASP.NET 페이지나 Win Form에 표시하고 싶을 때는 더더욱 불편을 감소해야 한다. 데이터 그 리드 컨트롤과 같은 많은 데이터 바운드 컨트롤은 DataSet 객체 혹은 DataTable 객체를 요구한다.

반면 데이터를 액세스하는 방법을 데이터 프로바이더로서 제공한다면 어떠한 장점이 있는가 먼저 이 데이터를 액세스 하고자 하는 개발자는 익숙한 ADO.NET 코딩 패턴을 통해 데이터를 액세스할 수 있다. 즉, Connection 객체와 Comm and 객체를 만들고 Open 메쏘드를 통해 데이터 소스와 연결 한다. 그리고 DataAdapter 객체를 통해 DataSet에 데이터 를 채워 넣을 것이다. 개발자가 새로 배워야 할 것은 매우 미 미하다. 게다가 수행 결과가 곧바로 DataSet이나 DataTable 에 기록되므로 데이터의 표시나 가공은 더 이상 어려운 일이 아니게 된다. 하지만 데이터 프로바이더를 작성하는 오버헤드 가 발생하게 됨은 물론이다.

커스텀 데이터 프로바이더 예제

이제 사실을 고백해야 할 때가 온 것 같다. 원래 이번 컬럼은 닷넷 COM+ 컴포넌트나 원격 객체와 데이터를 주고받을 때 어떤 방식(매개변수, 클래스, 데이터 셋)을 사용하는 것이 효율 적이며 성능에 도움이 되는가에 대해 다루려고 했다. 필자의 게으름과 성능 테스트에 시간이 필요하다는 것이 맞물려 이것 에 대한 내용을 다음 컬럼으로 미루게 되었다. 급하게 토픽을 바꾸다보니 실제 예제를 작성할 시간이 부족했다. 물론 필자가 이미 작성해 놓은 풀셋 데이터 프로바이더가 있지만 이것은 필 자가 일하는 회사와 관계돼 공개가 불가능했다. 궁여지책으로 내놓은 것은 MSDN에 포함된 닷넷 데이터 프로바이더 예제와 참고문헌에서 제시하는 예제다. 특히 Bob Beauchemin이 MSDN 매거진 12월호에 기고한 내용은 완전히 작동하는 풀셋 데이터 프로바이더 예제로서 파일 시스템의 디렉토리 내에 존 재하는 파일과 하위 디렉토리의 정보를 ADO.NET을 통해 액 세스하는 예제다. 이 예제는 Connection, Command, Data Reader, DataAdapter 객체를 모두 구현하고 있으며 그 내용 도 그다지 어렵지 않다. 데이터 프로바이더를 직접 구현해보고 자 하는 독자들은 Bob이 쓴 글과 예제를 참고하면 많은 도움이 되리라 믿는다. 이 컬럼에서는 구체적인 예제를 작성해 보지 않았지만 다수의 독자들이 원한다면 직접 구현하는 예제를 다 음 컬럼에서 다뤄볼 수도 있을 것이다.



제공 : DB포탈사이트 DBguide.net