본문 바로가기

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

LINQ 시리즈 10 - 쿼리 표현식(Query expression) 이해

이제 앞에서부터 계속 사용해온 쿼리 표현식을 이해해보자.

Customer[] customers = GetCustomers();

//Query expression

var query =

    from c in customers

    where c.Discount > 3

    orderby c.Discount

    select new { c.Name, Perc = c.Discount / 100 };

C# 컴파일러는 쿼리 표현식을 만나면 C# 의 클래스와 인터페이스를 사용하는 표현으로 전환한다.

// C# 표현

var query = customers

   .Where ( c => c.Discount > 3 )

   .OrderBy( c=>c.Discount )

   .Select ( c=> new { c.Name, Perc = c.Discount /100 } );

결국에 customers라는 데이터 소스에 대해서 Where, OrderBy, Select 메소드를 계속 호출하는 것이 된다. Where, OrderBy, Select 메소드는 이미 C# 라이브러리에 정의되어 있다.  그러나 모든 C# 표현이 쿼리 표현으로 바뀔 수 있는 것은 아니다. C#의 어떤 메소드는 쿼리 표현에서 지원해주지 않는다. 그래서 LINQ 표현에서는 두 표현을 혼합해서 사용할 수 있다.

야튼 이처럼 각 쿼리 표현식은 제너릭 메소드를 이용한 표현으로 바뀌는데, 이때 어떤 제네릭 메소드로 변환되어야 하는지 결정하는 과정에 확장 메소드에 적용된 규칙이 그대로 적용된다. 가릿? System.Linq 네임스페이스안의 static 클래스 Enumerable를 보면 이런 메소드들이 정의되어 있는 것을 볼 수 있다.

public static class Enumerable

{

    // Methods

    public static TSource Aggregate<TSource>(this IEnumerable<TSource> source, Expression<Func<TSo

    public static bool All<TSource>(this IEnumerable<TSource> source, Expression<Func<TSource, boo

    public static bool Any<TSource>(this IEnumerable<TSource> source);

    public static IEnumerable AsQueryable(this IEnumerablesource);

    public static double Average<TSource>(this IEnumerable<TSource> source, Expression<Func<TSourc

    public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource item, IEquality

    public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(this IQueryable<TSo

    public static TSource Max<TSource>(this IEnumerable<TSource> source);

    public static TSource Min<TSource>(this IEnumerable<TSource> source);

    public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, E

    public static IEnumerable<TResult> SelectMany<TSource, TResult>(this IEnumerable<TSource> sourc

    public static IEnumerable<TSource> ThenByDescending<TSource, TKey>(this IOrderedQueryab

    public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Expression<

    //...

}

정의된 메소드들의 첫번째 인자의 타입이 IQueryable<T>이고 그 앞에 this가 붙어있다. 즉 클래스 Queryable에 정의된 대부분의 메소드는 IEnumerable<T> 인터페이스를 구현한 타입의 객체를 확장하는데 사용된다. IEnumerable<T>를 구현한 객체들에 Where 라는 메소드가 없으면 이곳에 정의된 Where가 호출된다. 분명 Customer[] 타입의 객체 customers에는 Where라는 메소드는 없다. 배열은 IEnumberable<T>를 구현하게 되는데, 따라서 Enumerable에 정의된 IEnumberable<T>의 확장 메소드 Where 메소드를 호출하게 된다( 맞나?  복잡하다. 쓰으... )

만약 메소드를 확장할 수 있는 능력이 없다면 앞의 C# 표현은 다음과 같이 될 것이다.

var query = Select( OrderBy( Where( customers, c=>c.Discount > 3), c=>c.Discount ), c=>new {c.Name, Perc = c.Discount/100} );

뭔소린지 복잡하다. 지금까지 배운 개념들이 쿼리문에서 어떻게 사용되는지를 정리해야 겠다. 

확장 메소드가 있어서 메소드가 호출되는 순서대로 차례로 표현할 수 있다. 그리고 람다 표현식이 있어서 where, orderby같은 키워드에 해당하는 메소드의 로직을 간단히 표현할 수 있다. 또한 익명 타입과 object initializers가 있어서 앞에서 호출한 메소드의 결과를 다음 호출될 메소드의 인자로 넘겨줄 수 있다. 이 모든 것들의 사이 사이에 타입 유추(type inference) 기능이 제 역할을 하고 있다.

무슨 말인지 모르겠더라도 그냥 정리됐다고 넘어가자. 증말 배고파 죽겠다. 헉헉

필자가 현재로서는 이해할 수 없는 것이 있다.

C#2.0부터 Array 타입은 IEnumerable<T>를 구현하기는 한데, 런타임시에 구현이 제공된다고 한다. 다음은 MSDN의 Array 타입 설명에 나와 있는 내용의 일부이다.


".NET Framework 버전 2.0에서 Array 클래스는 System.Collections.Generic..::.IList<(Of <(T>)>), System.Collections.Generic..::.ICollection<(Of <(T>)>) 및 System.Collections.Generic..::.IEnumerable<(Of <(T>)>) 제네릭 인터페이스를 구현합니다. 이 구현은 런타임에 배열에 제공되므로 설명서 빌드 도구에서는 볼 수 없습니다. 따라서 제네릭 인터페이스는 Array 클래스의 선언 구문에 표시되지 않으며, 배열을 제네릭 인터페이스 형식으로 캐스팅(명시적 인터페이스 구현)해야만 액세스할 수 있는 인터페이스 멤버에 대한 참조 항목은 없습니다."


즉 컴파일시에는 IEnumerable<T> 인터페이스를 구현하지 않고 있다는 것이다. 그런데 확장 메소드의 결정은 컴파일 타임에 수행된다. 즉 앞의 Cutomers[]에 대한 확장 메소드 Where를 결정하는 것은 컴파일시에 일어난다는 것이다. 그러나 컴파일시에는 Customers[]가 IEnumerable<T>를 구현하고 있다는 것을 알 수 없다. 음...뭐가 어떻게 된기야. 그렇지만 샘플 코드를 만들어서 돌려보면 돌아간다.

class Program

{

    static void Main(string[] args)

    {

      Customer[] customers = new[] { new Customer("달봉이"), new Customer("봉달이") };

        var query = from c in customers

                    where c.Name == "달봉이"

                    select new { c.Name };

        foreach (var c in query)

        {

            Console.WriteLine(c.Name);

        }

        Console.Read();

    }

}

public class Customer 

{

    public string Name = "";

    public Customer(string name)

    {

        this.Name = name;

    }

}

이게 뭔 시츄에이션인지 이해가 되는 분이 있다면, 연락 좀 오네가이~~배고파서 더 이상 구글링도 못하겠다.

내부적으로는 이런 변환이 수행될지라도 개발자가 쿼리 표현을 이해하기 위해서 모두 이렇게 변환을 수행해보는 것은 번거롭다. 쿼리 표현의 키워드를 이해해서 쿼리 표현식에서 바로 이해하는 것이 효과적, 능률적일게다.

쿼리 표현식은 LINQ to Objects, LINQ to SQL, LINQ to XML 또는 다른 사용자 정의 LINQ 프로바이더중 어떤 것을 사용하든지간에 LINQ 쿼리를 만들때 핵심 표현이다. 쿼리 표현식을 이해하지 못하고서는 안된다는 얘기다.

SQL 쿼리문을 공부할때 from, where, select, orderby 같은 키워드를 먼저 공부했었다. 이제 다음 포스트에서는 이런 쿼리 표현식의 키워드들과 IEnumerable<T> 타입의 확장용 제네릭 메소드들을 알아본다. SQL문과 유사한 것들도 많지만 생소한 것도 많다.

근데. 아...이젠 쪼금 지겨워지려고 한다. 이런 기본적인 키워드들과 메소드에 대한 설명은 잘 설명된 다른 블로그의 포스트에 대한 링크로 대신하고 어쩌면 다음 포스트는 프레임워크관련 주제로 다시 돌아갈 지도 모르겠다. 내 맘이다~~~울랄라.