[42] IEnumerable<T> 데이터 소스와 IQueryable<T> 데이터 소스를 구분하라

다음 본문은 도서 이펙티브 C# (빌 와그너)에서 나오는 주제를 다룹니다.

 

 

IEnumerable<T>와 IQueryable<T>는 거의 동일한 API정의를 가진다.

대부분의 경우 두 인터페이스는 상호 교환이 가능하다. 하지만 사실 이 둘은 동작 방식도 다르고 성능도 크게 차이 난다.

// IQueryable<T>
var q = from c in dbContext.Customers
        where c.City == "London"
        select c;
        
var finalAnswer = from c in q
                    orderby c.Name
                    select c;
// IEnumerable<T>    
var q = (from c in dbContext.Customers
         where c.City == "London"
         select c).AsEnumerable();
     
var finalAnswer = from c in q
                  orderby c.Name
                  select c;

위 코드의 결과는 동일하지만 동작 방식은 상이하다. 

첫 번째 예는 일반적인 LINQ to SQL 쿼리이며 IQueryable<T>의 기능을 사용한다. LINQ to SQL 라이브러리가 모든 쿼리문을 결합하여 단번에 SQL 결과를 생성한다. 단 한차례 데이터베이스를 호출한다.

 

두 번째 예는 데이터베이스 객체를 IEnumerable<T> 시퀀스로 변경하기 때문에 데이터베이스가 아니라 로컬에서 더 많은 작업을 수행하게 된다. 쿼리문이 IEnumerable<T> 시퀀스를 반환하므로 그다음 작업은 LINQ to Objects 구현체와 델리게이트를 이용하여 수행된다. 첫 번째 쿼리문이 수행되면 데이터베이스에 쿼리를 전달하여 City값이 London인 모든 레코드를 가져온다. 이후 로컬 머신에서 Name 필드에 따라 정렬한다.

 

대부분의 경우 쿼리 작업을 수행할 때 IEnumerable<T>보다는 IQueryable<T>를 사용하는 편이 훨씬 효율적이다.

 

Enumerable<T>

쿼리식 내의 람다 표현식과 함수 매개변수를 나타내기 위해 델리게이트를 사용한다.

모든 메서드가 로컬 머신에서 수행된다. 따라서 모든 데이터를 메모리로 가져와야 한다.

 

Queryable<T>

표현식 트리를 이용하여 이를 처리한다.

데이터가 실제 위치하고 있는 컴퓨터에서 수행한다.

코드로 표현할 수 있는 쿼리 표현식이 IEnumerable<T>에 비해 제한적이다.

 

다른 메서드를 호출

IQueryable<T>는 각각의 메서드를 분석하지 않는다. 따라서 쿼리 표현식이 다른 메서드를 호출한다면 Enumerable구현체를 사용하도록 변경해야 한다.

private bool isValidProduct(Product p) =>
    p.ProductName.LastIndexOf('C') == 0;
    
// 다음 코드는 정상 작동한다.
var q1 = from p in dbContext.Products.AsEnumerable()
         where isValidProduct(p)
         select p;
         
// 다음 코드는 반환된 컬렉션을 순회할 때 예외를 유발한다.
var q2 = from p in dbContext.Products
         where isValidProduct(p)
         select p;

따라서 성능보다 안정성을 원한다면 쿼리 결과를 IEnumerable<T>로 변환하여 예외를 회피할 수 있다.

 

AsQueryable() 

시퀀스의 런타임 타입을 확인한다. 만약 런타입 타입이 IQueryable이라면 IQueryable을 반환하고, 반대로 IEnumerable타입이면 LINQ to Objects를 사용하여 IQueryable을 구현한 래퍼를 생성하여 반환한다.

public static IEnumerable<Product> ValidProducts(
    this IEnumerable<Product> products) =>
    from p in products.AsQueryable()
    where p.ProductName.LastIndexOf('C') == 0
    select p;

즉, IQueryable을 구현하고 있는 경우 그 구현체를 사용하고, IEnumerable만을 구현하고 있는 경우에도 문제없이 동작한다.

 

결론

 IQueryable<T>와 IEnumerable<T>는 동일한 기능을 제공하는 것처럼 보이지만 사실 동작 방식이 매우 다르기 때문에 데이터 원본이 어떤 인터페이스를 제공하느냐에 따라 쿼리를 구성하도록 하자.

 

 

댓글

Designed by JB FACTORY