ASP.NET 엔티티 프레임웍과 놀기 #1 ASP.NET

만남

2008년 초에 Linq to SQL로 프로젝트를 하나 진행한 이후로는 닷넷에서 ORM을 전혀 쓰지 못했다. 한 가지 이유는 Linq to SQL이 MS로부터 사실상 버려지다시피 한 것이고, 다른 하나는 엔티티 프레임웍이나 NHibernate 등 다른 ORM을 고려하려고 해도 (자바에 비하면 상대적으로) 주변에 ORM을 쓰는 것을 찾기 힘들다 보니 굳이 나서서 쓰겠다고 주장하기가 어려웠다. 그러던 중 최근 진행한 프로젝트에서 이런저런 사정으로 노가다를 아주 단단히 경험하고는 ORM이 없으면 안되겠다고 생각하게 됬는데, 마침내 이번에 개인적으로 맡은 일에서 엔티티 프레임웍Entity Framework을 처음으로 사용해보게 되었다.

첫 구현

엔티티 프레임웍을 쓰면서 가장 먼저 한 일은 오브젝트 데이터 소스ObjectDataSource의 셀렉트 메서드SelectMethod인 메서드 한 개를 Linq to Entities 기반으로 변경한 것이다. 보통 이런 메서드는 호출되면 데이터베이스 연결을 맺고, 주어진 키 값에 따른 항목을 데이터베이스에서 가져와 도메인 객체 혹은 데이터셋 형태로 구성한 뒤 결과로 반환한다. 이중 골치아픈 것은 SQL문의 Where 절을 구성하는 일인데, 엔티티 프레임웍을 사용하면 아래처럼 쉽게 쿼리와 메서드를 구성할 수 있다.

public object GetOrders(string processState, string orderNum, int startRowIndex, int maximumRows)
        {
            using (siteEntities entity = new siteEntities())
            {
                //Order 테이블과 Member 테이블을 Left outer join으로 연결한다.
                var items = from o in entity.Order
                            let m = (from tmp in entity.Member
                                     where tmp.idx == o.Member.idx
                                     select new { tmp.idx, tmp.user_name, tmp.user_email }).FirstOrDefault()
                            select new
                            {
                                o.idx,
                                o.order_name,
                                o.order_num,
                                o.reg_date,
                                o.process_state,
                                o.comment,
                                user_idx = (int?)m.idx,
                                user_name = m.user_name,
                                user_email = m.user_email
                            };

                //where 절을 구성한다.
                if (!string.IsNullOrEmpty(processState))
                    items = items.Where(s => s.process_state.Equals(processState));

                if (!string.IsNullOrEmpty(orderNum))
                    items = items.Where(s => s.order_num.Equals(orderNum));

                //order by 절을 구성한다.
                items = items.OrderByDescending(o => o.idx);

                // 페이징
                if (startRowIndex > 0) items = items.Skip(startRowIndex);
                if (maximumRows > 0) items = items.Take(maximumRows);

                // 쿼리를 실행한 뒤 결과를 반환한다.
                return items.ToList();
            }
        }

얼핏 보면 다소 복잡하게 보이지만 사실 문자열 조합으로 범벅이 된 SQL 실행문의 방대한 양에 비하면 훨씬 단순하다. 물론 대개는 SQL문을 직접 작성하지 않고 이를 자동으로 구성해주는 유틸리티 클래스를 만들어 사용하겠지만, Linq 구문 정도의 유연함과 대체성을 가지는 클래스를 만들기는 쉽지 않다. 다만 Left/right outer join문을 구성한다거나 할 때는 우선적으로 두 테이블간 관계 설정이 되어 있어야 한다거나 코드에 보여지듯이 다소 난해한 코드를 써야 한다는 단점이 있는데, 이에 관한 내용은 타 블로그 - http://geekswithblogs.net/SudheersBlog/archive/2009/06/11/132758.aspx - 에 자세히 수록되어 있다.

보완

위의 메서드에서 특기할만한 점 하나는 38행의 ToList() 호출이다. 6행의 items에 이미 쿼리가 실행되어 결과가 담겨있을 것이라고 추측하기 쉽지만, 사실 이 때까지는 쿼리를 실행할 수 있는 IQueryable 객체일 뿐이다. 그래서 이후 36행까지 실컷 이런저런 조건을 추가해도 조건을 추가할 때 마다 쿼리가 실행된다던지 하는 따위의 문제가 없다.

이 객체는 쿼리의 결과에 대한 호출이 일어났을 때 비로소 실행되는데, 여기서는 오브젝트 데이터 소스를 사용하는 ASPX 페이지가 바로 그 장소가 될 것이다. 문제는, 그 지점이 메서드의 using() 문을 벗어난다는 데 있다. ASPX 페이지에서 값을 사용하기 위해 쿼리를 실행할 때는 이미 siteEntities 객체가 닫혀버린 뒤이므로, 다음과 같은 에러 메시지만 보게 된다.

The ObjectContext instance has been disposed and can no longer be used for operations that require a connection.

따라서 이 쿼리를 미리 실행해 줄 방법이 필요한데, ToList()를 실행하면 쿼리의 결과를 리스트 형태로 만들어 반환해야 하므로 비로소 쿼리가 실행되는 것이다. 이렇게 생성된 리스트 객체는 using문 밖에서 실행되어도 아무런 문제가 없다.

다른 문제는, 현재 코드에서 그렇게 반환된 리스트 객체를 이루고 있는 객체는 익명 타입Anonymous Type이라는 것이다. 이 문제는 전적으로, (DB와 주고받는 데이터의 양을 줄이기 위해) 10행에서 이후 필요한 몇 개의 컬럼만을 골라 익명 타입을 만들도록 지정한 데서 기인한다. 문제의 해결을 위해 우선 (엔티티 프레임웍이 DB 스키마로부터 생성한) 엔티티 클래스들을 직접 만들어 반환하는 방법을 고려해볼 수 있다. 하지만 안타깝게도 엔티티 프레임웍은 위와 같은 상황에서 아래처럼 엔티티 클래스를 직접 만드는 것을 허용하지 않는다.

// 이렇게 만들면 ToList()의 결과로 List<Order>가 반환되겠지만,
// 아쉽게도 에러를 내며 실행되지 않는다.
select new Order
{
    idx = o.idx,
    order_name = o.order_name,
    order_num = o.order_num,
    //...
    Member = m.idx == null ? null : new Member
    {
        idx = m.idx,
        user_name = m.user_name
        //...
    }
}

그래서 위 코드에서는 우선 익명 타입을 그대로 사용하기로 하고, 메서드의 반환 형식을 object 타입으로 정했다. 이렇게 하면 당장 좀 찝찝하기는 하지만, <%# Eval("order_name") %> 같은 데이터 바인딩 식에서 별 탈 없이 쓸 수 있다. 그러나 이 방식은 약한 타입Weak Typing이 가져오는 다른 문제들을 차치하더라도 치명적인 약점을 하나 가지고 있는데, 바로 데이터 바인딩 식을 제외한 메서드 외부에서 도무지 사용할 방법이 없다는 것이다. (캐스팅을 못 하니 당연히...) 대표적인 것이 ObjectDataSource.Select()를 호출할 때나 ListView의 DataItem 객체에 접근할 때이다.

다른 방법으론 반환을 위한 객체를 별도로 만드는 것이 있다.

public class OrderResultItem
{
    public int Idx { get; set; }
    
    public string OrderName { get; set; }

    // ...    
}

//...

select new OrderItem
{
    Idx = o.idx,
    OrderName = o.order_name,
    OrderNum = o.order_num,
    //...
    Member = m.idx == null ? null : new MemberItem
    {
        idx = m.idx,
        user_name = m.user_name
        //...
    }
}

이 방식은 비교적 잘 동작하지만, 매번 결과 반환을 위한 클래스를 새로 만들어줘야 한다는 단점이 있다. 필요한 컬럼이 추가되면 클래스도 변경되야 하므로 수고가 만만치 않다. 때문에 다른 방법을 찾아야 한다.

오브젝트 데이터 소스를 포기하고 엔티티 데이터 소스EntityDataSource를 사용하는 방법도 있다. 그러나 이 글에서는 3-Tier 기반에서 DAL을 사용중인 것을 가정하고 있으므로 엔티티 데이터 소스는 사용하지 않는다.

[#2에서 계속]

Tag :
, ,

Leave Comments