본문 바로가기

IT 살이/04. 기술 - 프로그래밍

개발 프레임워크 만들기 대장정 31 - Spring.NET의 데이터 액세스 III

지난 포스트에서 말한대로 이번에는 Spring.NET에서 지원하는 OR매핑( Object Relational Mapping) 기능에대해서 알아본다. AdoTemplate의 Execute류의  메소드를 이용하면 CRUD 모두가 가능하다. 그러나 조회의 경우 Spring.NET의 데이터 접근 모듈에서는 좀 더 특별한 API를 제공한다. 지금까지의 개발 방식에서는 보통 조회를 하면 DataSet으로 넘어오고 이것을 그대로 Biz 레이어, UI 레이어로 넘겨서 레코드별로 루프를 돌면서 필요한 데이터를 꺼내서 작업을 했었다. 그러나 Spring.NET에서는 조회된 각 레코드를 사용자 정의 객체와 매핑시킬 수 있는 기회를 제공하고 있다. 예를 들어 여러 건의 사용자 정보 레코드가 조회되었을 경우 하나의 레코드는 하나의 UserInfo 객체로 변환된다. 그래서 Biz 레이어나 UI 레이어로 반환될때는 전체 레코드는 UserInfo 컬렉션으로 변환되어 반환된다.

개발자는 도메인 객체 UserInfo를 정의해야 한다. 도메인 객체는 애플리케이션의 서버측과 클라이언트측 모두에서 참조되어 사용될 수 있다. 따라서 별도의 어셈블리로 분리하여 개발하는 것이 보통이다. 개발자는 또한 레코드의 컬럼과 UserInfo의 속성을 연결시켜주는 매핑 정보를 제공해야 한다.  이런 작업을 OR 매핑( Object Relational Mapping )작업이라고 한다. OR매핑 작업을 좀 더 편하게 할 수 있는 전문적인 OR매핑툴도 있다. 대표적인 것으로 NHibernate라는 것이 있는데 이것은 뒤에 별도로 살펴볼 것이다.

Spring.NET에서는 QueryWith로 시작하는 데이터 접근용 메소드가 많은데 이런 메소드를 이용하면 개발자가 OR 매핑 작업을 할 수 있는 기회를 제공받을 수 있게 된다. OR매핑 작업이 수행되는 구조는 앞에서의 AdoTemplate의 Execute류 메소드를 이용하는 콜백 구조와 유사한 구조를 갖는다.


▶  OR매핑 작업 구조



AdoTemplate객체의 Execute를 사용할때는 DB에 접근하는 작업을 콜백객체의 콜백함수에서 개발자가 직업 수행했었다. 게릿? QueryWith류의 메소드를 사용하면 DB 접근해서 조회하는 작업은 AdoTemplate에서 수행하게 된다. 그리고 Execute 메소드를 사용할때도 직접 콜백함수에서 조회된 테이블의 각 레코드를 사용자 정의 객체로 변환해서 반환하면 된다. 그러나 QueryWith류의 메소드를 이용하면 그런 작업을 좀 더 편하게 할 수 있다. 콜백 함수에서 파라미터로 받는 것은 IDataReader 객체이다. 어떤 타입의 콜백 객체를 사용하느냐에 따라서 IDataReader 객체는 전체 조회결과를 받는냐 아니면 한 레코드씩을 받느냐가 결정된다. 콜백 객체의 타입은 다음 3 종류가 있다.

콜백객체 타입 설명
IResultSetExtrator/ResultSetExtractorDelegate - AdoTemplate으로 부터 조회 결과를 전부 넘겨 받는다. 즉 커서가 처음 위치에 있는 IDataReader 객체를 넘겨받는다.
- 클라이언트 코드로 넘겨줄 사용자 정의 객체를 모두 구성해서 반환해준다. 
- 클라이언트 코드에서는  QueryWith 메소드이 반환값으로 사용자 정의 객체 컬렉션을 받을 수 있다.
IRowCallback / RowCallbackDelegate - AdoTemplate으로 부터 레코드 하나씩을 건네받는다. 즉  커서가 현재 위치로 이동한 상태의 IDataReader 객체를 건네받는다.
- 레코드별 사용자 정의 객체를 콜백 객체에 쌓아둔다.
- 클라이언트 코드에서 나중에 콜백 객체에서 구성해둔 사용자 정의 컬렉션을 가져간다.
IRowMapper / RowMapperDelegate - AdoTemplate으로 부터 레코드 하나씩을 건네받는 것은 이전 로우 콜백 타입과 같다.
- 레코드별 사용자 정의 객체 컬렉션을 AdoTemplate쪽에서 담당한다.
- 클라이언트 코드에서는 QueryWith 메소드이 반환값으로 사용자 정의 객체 컬렉션을 받을 수 있다.

어떤 타입의 콜백 객체를 사용하느냐에 따라서 QueryWith 메소드, 그리고 QueryQith 메소드내에서 호출되는 콜백 함수가 달라진다. 그러나 콜백함수의 인자로 넘어가는 것은 언제나 IDataReader 객체이다. 이 객체도 다시 어떤 타입의 콜백객체를 사용하느냐에 따라서 콜백 객체에서 받는 IDataReader객체의 현재  상태가 달라질 수 있다.

IDataReader 객체는 forward-only 속성이 있다. 즉 테이블의 레코드를 가리키는 현재 커서는 항상 앞으로만 움직일 수 있다. 콜백 객체의 타입은 선정은 이 커서의 위치에 영향을 줄 수 있다. 만약 IResultSetExtractor / ResultSetExtractorDelegate를 선택했다면 현재 커서가 움직이지 않은 상태의 원래의 IDataReader 객체를 넘겨받는다. 콜백 함수에서는 하나씩 앞으로 커서를 움직이면서 OR매핑 작업을 구현해야 한다.  그러나 Row로 시작하는 나머지 두 타입은 콜백 객체에서는 커서가 진행된 IDataReader 객체를 받는다. 레코드 루핑은 AdoTemplate쪽에서 일어난다. 이 경우 콜백 함수에서는 사용자 정의 객체를 하나씩만 생성하면 된다.

그러나 이 경우에도 두 타입에는 차이가 있다. 사용자 정의 객체의 집합을 어디에서 관리하느냐 하는 문제를 다르게 두 타입별로 해결하고 있다. IRowCallback / RowCallbackDelegate 타입은 사용자 정의 객체의 집합을 콜백 객체에서 간직하고 있다. 그래서 클라이언트 코드는 그 집합을 나중에 직접 콜백 객체에서 가져가야 한다. 그러나 IRowMapper / RowMapperDelegate를 사용하면 사용자 정의 객체의 결과 집합을 AdoTemplate에서 간진한다. 콜백 함수에서는 레코드를 받아서 하나씩 사용자 정보 객체를 생성해서 AdoTemplate으로 반환해준다. AdoTemplate에서는 콜백 함수에서 받을 객체를 차곡차곡 쌓아두었다가 QueryWith 메소드가 종료될때 클라이언트 코드로 객체 집합을 반환해준다.

이제 몇가지 콜백 객체 유형별로 개발 샘플 코드를 보도록 하자.


▶ ResultSetExtractor 타입의 콜백 객체 사용


다음은 ResultSetExtrator 타입의 콜백 객체를 사용하는 구조에서의 DAO 객체의 정의 일부이다. ResultSetExtractorDao.cs 페이지에 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    /// <summary>

    /// A simple DAO that uses Generic.AdoTemplate ResultSetExtractor functionality

    /// </summary>

    public class ResultSetExtractorDao : AdoDaoSupport

    {

        ...

        private string customerByCountryAndCityCommandText =

                @"select ContactName from Customers where City = @City and Country = @Country";

        public virtual IList<string> GetCustomerNameByCountryAndCity(string country, string city)

        {

            // note no need to use parameter prefix.


            // This allows the SQL to be changed via external configuration but the parameter setting code

            // can remain the same if no provider specific DbType enumerations are used.


            IDbParameters parameters = CreateDbParameters();

            parameters.AddWithValue("Country", country).DbType = DbType.String;

            parameters.Add("City", DbType.String).Value = city;


            return AdoTemplate.QueryWithResultSetExtractor(CommandType.Text,

                                                           customerByCountryAndCityCommandText,

                                                           new CustomerNameResultSetExtractor<List<string>>(),

                                                           parameters);

        }

        ...

GetCustomerNameByCountryAndCity 메소드는 country와 city 파라미터값을 받아서 해당하는 고객 집합을 IList<string> 타입으로 반환하는 DAO 객체의 메소드이다. 이 메소드 내부에서는 넘겨받은 country, city값을 DB 파라미터로 변환해서 AdoTemplate에 넘겨줄 준비를 하고 있다. DbParameter를 구성하는 코드는 어렵지 않으니 눈치껏 이해하기 바란다. 이제 적절한 QueryWith 메소드를 선택해야 한다.  코드에서는 ResultSetExtractor 타입의 객체를 콜백객체로 받을 수 있는 메소드로서 QueryWithResultSetExtrator 메소드를 사용하고 있다. 넘겨주는 구체적인 콜백 객체는 CustomerNameResultSetExtrator<List<string>> 타입의 객체이다.

콜백 객체는 개발자가 정의해야 하는 타입으로서 샘플에서는 그 구현을 다음처럼 하고 있다. 이 콜백 객체도 같은 페이지에 internal로 정의되어 있다.

internal class CustomerNameResultSetExtractor<T> : IResultSetExtractor<T> where T : IList<string>, new()

{

    /// <summary>

    /// Implementations must implement this method to process all

    /// result set and rows in the IDataReader.

    /// </summary>

    /// <param name="reader">The IDataReader to extract data from.

    /// Implementations should not close this: it will be closed

    /// by the AdoTemplate.</param>

    /// <returns>An arbitrary result object or null if none.  The

    /// extractor will typically be stateful in the latter case.</returns>

    public T ExtractData(IDataReader reader)

    {

        T customerList = new T();

        while (reader.Read())

        {

            string contactName = reader.GetString(0);

            customerList.Add(contactName);

        }

        return customerList;

    }

}

실제 사용될 구체적인 콜백객체는 IResultSetExtractor<T>를 구현하고 있다. 그리고 그 인페이스에서 정의하고 있는 콜백 함수 ExtractData()를 구현하고 있다. 이 메소드는 AdoTemplate에서 파라미터로 IDataReader 타입의 객체 reader를 받고 있는데 이 객체는 커서가 움직이지 않은 초기상태로 넘어온다. 클라이언트 코드 즉 DAO 객체로 넘겨줄 최종 값은 reader를 이용해서 개발자가 이곳에서 모두 구성해야 한다. 코드에서도 while 문을 돌면서 필요한 반환값을 구성한 다음 반환하고 있다.

이 코드에서는 반활될 값으로 특별히 사용자 정의의 객체를 사용하고 있지는 않다. 그래서 OR 매핑 작업은 구현하고 있지 않다. 그러나 만약 반환값이 IList<string>이 아니라 IList<사용자정의타입>으로 되었다고 하면 while 문을 돌면서 사용자 정의 객체를 생성해서 리스트에 추가하면 된다.

while문에서 통해서 구성된 최종 반환값이 반환되면 이 값이 결국 DAO객체에서 호출을 시작한 메소드의 반환값으로 된다는 것을 알 수 있다. 그래서 Biz 레이어 객체에게로 넘어갈 것이다.


▶ RowCallback 타입의 콜백 객체 사용


이제 RowCallback 타입의 콜백 객체를 사용해서 OR 매핑을 구현하는 구조를 알아보자. 이 타입의 콜백 객체를 사용하면 DAO 객체로 반환될 최종 객체 집합이 사용자 정의의 콜백 객체에 있는 구조가 된다고 했다.  코드를 보자. 우선 DAO객체의 호출 메소드이다. RowCallbackDao.cs 페이지에 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    public class RowCallbackDao : AdoDaoSupport

    {

        private string cmdText = "select ContactName, PostalCode from Customers";


        public virtual IDictionary<string, IList<string>> GetPostalCodeCustomerMapping()

        {

            PostalCodeRowCallback statefullCallback = new PostalCodeRowCallback();

            AdoTemplate.QueryWithRowCallback(CommandType.Text, cmdText,

                                            statefullCallback);


            // Do something with results in stateful callback...

            return statefullCallback.PostalCodeMultimap;

        }

    }

    ...

RowCallbackDao라는 타입의 DAO객체를 정의하고 있다. 이 객체에 GetPostalCodeCustomerMapping() 메소드에서 DB 데이터에 액세스하고 그 결과를 조작하려는 작업을 하고 있다. 우선 RowCallback을 사용하는 OR 매핑 구조에서는 AdoTemplate의 QueryWithRowCallback 메소드를 호출하고 있다. 앞의 코드에서는 DB 접근에 필요한 값이 없어서 DB 파라미터를 구성하는 코드는 없다. QueryWithRowCallback에는 콜백 객체로서 PostalCodeRowCallback 타입의 객체 statefulCallback이 넘겨지고 있다.

이 객체의 구현은 다음과 같다. 같은 페이지에 정의되어 있다.

internal class PostalCodeRowCallback : IRowCallback

{

    private IDictionary<string, IList<string>> postalCodeMultimap =

        new Dictionary<string, IList<string>>();


    public IDictionary<string, IList<string>> PostalCodeMultimap

    {

        get { return postalCodeMultimap; }

    }


    /// <summary>

    /// Implementations must implement this method to process each row of data

    /// in the data reader.

    /// </summary>

    /// <remarks>

    /// This method should not advance the cursor by calling Read()

    /// on IDataReader but only extract the current values.  The

    /// caller does not need to care about closing the reader, command, connection, or

    /// about handling transactions:  this will all be handled by

    /// Spring's AdoTemplate

    /// </remarks>

    /// <param name="reader">An active IDataReader instance</param>

    /// <returns>The result object</returns>

    public void ProcessRow(IDataReader reader)

    {

        string contactName = reader.GetString(0);

        string postalCode = reader.GetString(1);

        IList<string> contactNameList;

        if (postalCodeMultimap.ContainsKey(postalCode))

        {

            contactNameList = postalCodeMultimap[postalCode];

        }

        else

        {

            postalCodeMultimap.Add(postalCode, contactNameList = new List<string>());

        }

        contactNameList.Add(contactName);

    }

}

IRowCallback 인터페이스는 ProcessRow라는 메소드 하나만을 정의하고 있다. 이 메소드가 AdoTemplate의 QueryWithCallback에서 호출하는 콜백함수이다.

이 QueryWithCallback 메소드에서는 DB 조회 결과를 가지고 있으면서 IDataReader 객체의 루프를 돈다. 그래서 커서가 하나씩 앞으로 움직인 상태의 reader 객체를 콜백 함수로 넘겨주는 것이다. 콜백 함수가 AdoTemplate 객체로부터 콜백될때 넘겨받는 IDataReader 타입의 객체 reader의 커서는 이미 필요한 만큼 움직인 상태이다. 따라서 콜백 함수에서는 현재 readerd 객체에서 필요한 필드의 값을 뽑아 사용하면 된다. 이 콜백 메소드는 반환값이 없다. 구성된 값을 반환하는 대신에 로컬 변수인 postalCodeMultimap에 Add 시키고 있다.

즉 결과값을 콜백 객체에서 자체적으로 관리하고 있다. 그런 다음 최종 구성값을 외부에서 접근할 수 있도록 public 속성 PostalCodeMultimap을 통해서 노출시키고 있다. DAO 객체의 GetPostalCodeCustomerMapping() 메소드의 return문을 보면 이곳에서 콜백 객체의 공개 속성에 접근하고 있는 것을 볼 수 있다.

이 구조에서 AdoTemplate는 조회 결과의 루프만 돌면서 콜백 함수에 각 레코드를 건네주기만 하면 된다. 각 레코드의 정보로 반환될 값을 구성하고 관리하는 작업은 모두 콜백 객체에서 담당해야 한다. 이런 구조는 DB에서 조회된 결과에 레코드별로 추가할 가공 작업이 많은 경우 편리할 것이다.


▶ RowMapper 타입의 콜백 객체 사용


다음은 AdoTemplate에서 DB조회 결과에 대한 루핑 작업뿐만 아니라 콜백 객체에서 생성된 결과 집합도 관리하는 구조이다.  콜백 객체에서는 레코드의 값을 이용해서 필요한 작업을 한 후 그 결과값을 AdoTemplate로 반환만 해 주면 된다. RowMapperDao.cs 페이지에 샘플 코드가 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    public class RowMapperDao : AdoDaoSupport

    {

        private string cmdText = "select Address, City, CompanyName, ContactName, " +

                            "ContactTitle, Country, Fax, CustomerID, Phone, PostalCode, " +

                            "Region from Customers";



        public virtual IList<Customer> GetCustomersWithDelegate()

        {

            return AdoTemplate.QueryWithRowMapperDelegate<Customer>(CommandType.Text, cmdText,

                        delegate(IDataReader dataReader, int rowNum)

                            {

                                Customer customer = new Customer();

                                customer.Address = dataReader.GetString(0);

                                customer.City = dataReader.GetString(1);

                                customer.CompanyName = dataReader.GetString(2);

                                customer.ContactName = dataReader.GetString(3);

                                customer.ContactTitle = dataReader.GetString(4);   

                                customer.Country = dataReader.GetString(5);

                                customer.Fax = dataReader.GetString(6);

                                customer.Id = dataReader.GetString(7);

                                customer.Phone = dataReader.GetString(8);

                                customer.PostalCode = dataReader.GetString(9);

                                customer.Region = dataReader.GetString(10);

                                return customer;

                            });

        }

    }

}

RowMapperDao라는 DAO 객체를 정의하고 있고 GetCustomersWithDelegate()라는 업무 메소드를 정의하고 있다. 이 메소드에서 DB 작업을 위해서 AdoTemplate을 사용하고 있는데, 조회된 결과를 조작하기 위해서 QueryWithRowMapperDelegate() 메소드를 호출하고 있다. 이 메소드의 인자로 앞 포스트에서 본 것과 유사한 익명 델리게이트 객체를 넘겨주고 있다. 콜백 객체와 콜백 함수 등 구조를 좀 더 명확히 하고 싶다면 익명 델리게이트 대신에 표준 구조로 변환해보길 권한다. 이 작업은 앞 포스트를 참조한다.

RowMapper를 사용하는 콜백 구조에서의 콜백 함수에서는 한 레코드에 대한 정보를 인자로 넘겨진 IDataReader 객체로부터 얻어서 반환에 필요한 값을 구성하고 구성된 값을 자신에게 남겨둘 필요없이 바로 반환해주면 된다. AdoTemplate에서는 반환된 값을 모두 차곡차곡 모아두었다가 클라이언트 코드( DAO 객체)넘겨준다. 


이제 알겠지만, Spring.NET에서 제공하는 방법은 전문적인 OR 매핑 방법은 아니다. 단지 Spring.NET에서 제공하는 방법을 사용하면 개발자가 수동으로 OR 매핑을 할 수 있는 기회를 제공받을 수 있다는 것이다. 개발자가 수동으로 해야 한다는 것은 불편한 일이다. 이렇든 저렇든 Spring.NET이 제공하는 이 3가지 OR 매핑 방법을 사용했을 경우, 혹시라도 DB 테이블의 컬럼이 변경되거나 사용자 정의 객체의 구조가 변경되면 소스 코드를 다시 빌드해야 하는 것은 피할 수 없다. 그러나 이런 불편은 또 프레임워크에서 질색을 하는 단점중의 하나이다.  좀 더 전문적인 OR매핑툴 NHibernate을 사용하면 좀 더 발전된 매핑 작업을 할 수 있지 않을까 기대해본다.  그럼 다음 포스트에서. 아니 모르겠다. 트랜잭션을 먼저 해야 할지. 이것을 먼저 해야 할지. 먼저 준비되는 것부터 하기로 한다.