본문 바로가기

TechLog

LINQ : .NET Language-Integrated Query - 2

이 포스트의 내용은 지난 2007.10.6 ASP 뉴스그룹 오프라인 세미나에서 발표했던 내용을 간추린 것입니다.
어디까지나 LINQ의 개요를 살펴보기 위한 내용이므로, 여기에 나온 내용이 LINQ의 전부라고 생각하시면 곤란합니다.

포스트가 조금 길어져서, 둘로 나누어 올립니다.

링크 : LINQ : .NET Language-Integrated Query - 1 


- 작성자 : 이수겸, 올랩컨설팅 컨설턴트, keniallee_at_gmail.com


3. LINQ-SQL, LINQ-XML

- LINQ to SQL: SQL Integration

자 그럼 SQL과 LINQ의 통합은 어떻게 이루어지는지 한 번 살펴보자. 다음과 같은 테이블이 있다고 가정해보자 :

관계형 테이블을 많이 다뤄 본 사람이면 그냥 딱 통박으로도 'people과 orders를 조인하겠군...'이라는 느낌이 들 것이다. 훌륭하다. 계속 가 보자.

다음은 위 테이블에 대한 클래스 정의이다(그냥 테이블을 개체로 맵핑하기 위한 클래스라고 생각하면 된다) :

[Table(Name = "People")]
    public class Person
    {
        [Column(DbType = "nvarchar(32) not null", IsPrimaryKey = true)]
        public string Name;

        [Column]
        public int Age;

        [Column]
        public bool CanCode;
    }

    [Table(Name = "Orders")]
    public class Order
    {
        [Column(DbType = "nvarchar(32) not null", IsPrimaryKey = true)]
        public string OrderID;

        [Column(DbType = "nvarchar(32) not null")]
        public string Customer;

        [Column]
        public int Amount;
    }

그런 다음의 실제 프로그램는 다음과 같다 :

static void Main(string[] args)
{
    // establish a query context over ADO.NET sql connection
    DataContext context = new DataContext(
         "Initial Catalog=LINQ_Test;Integrated Security=sspi");

    // grab variables that represent the remote tables that
    // correspond to the Person and Order CLR types
    Table<Person> custs = context.GetTable<Person>();
    Table<Order> orders = context.GetTable<Order>();

    // build the query
    var query = from c in custs
                from o in orders
                where o.Customer == c.Name
                select new
                {
                    c.Name,
                    o.OrderID,
                    o.Amount,
                    c.Age
                };

    // execute the query
    foreach (var item in query)
        Console.WriteLine("{0} {1} {2} {3}",
                          item.Name, item.OrderID,
                          item.Amount, item.Age);

}

... 갑자기 조금 복잡해져서 설명할 것이 많다.

먼저 DataContext 개체를 볼 수 있는데, 이는 LINQ와 ADO.NET을 연결해 주는 일종의 중간 개체이다. ADO.NET의 시각에서 보자면, 최종적으로 DataSet을 얻게 해 주는 중간 커넥션이나 어댑터를 떠올려도 대충 맞을 것이다. 하지만 LINQ에서는 이마저도 대폭 간소화되어, 단순히 데이터베이스에 대한 연결 문자열(Connection String)만 인자로 넘겨주면 중간 DataContext 개체를 얻을 수 있다. 그런 다음, Person, Order 형식의 개체인 custs, orders에 맵핑하는 것을 볼 수 있을 것이다. 하지만 실제 데이터베이스에 존재하는 테이블 이름이나 컬럼 이름도 모르는데 어떻게 맵핑이 이루어지느냐고? 아까 클래스 선언할 때 다 있었잖은가. 클래스 선언부의 []로 둘러싸인 부분이 테이블 및 컬럼에 대한 정보를 Attribute 형태로 정의해두는 것이다.

그런 다음 쿼리 식 부분은 거의 다를 것이 없지만(조인을 수행하고 있긴 하지만 where 절에서 컬럼을 비교해서 조인하는 것은 많이들 보았을테니 넘어가겠다), 마지막 select new {}라는 부분이 조금 생소하게 느껴질 것이다. 이건 대체 뭘 의미하는 것일까? 이것을 이해하기 위해서는 먼저 암시적으로 형식이 지정된 지역 변수(implicitly typed local variables)라는 것을 이해해야 한다.


* 암시적으로 형식이 지정된 지역 변수

jscript등 스크립트 언어를 다뤄본 개발자라면, var 키워드를 알고 있을 것이다. var a=0; 과 같은 형태로 사용할 수 있으며, a라는 변수를 특별한 형을 지정하지 않고서 선언하는 방법을 제공하는 것으로, 닷넷 프레임워크 3.5에서는 이와 비슷한 키워드가 암시적으로 형식이 지정된 지역 변수라는 이름으로 추가되었다. 하지만 닷넷 언어는 스크립트 언어가 아니다보니 얼마간의 제약이 있다.

예를 들면, 다음과 같은 코드는 에러가 발생한다 :

var integer = 1;
integer = "hello";

 이는 integer가 1로 할당된 상태에서, 이미 int 형식으로 정의되었기 때문이다.

또한, 이렇게 정의된 지역 변수는 메서드의 경계를 벗어날 수 없으며, 필드, 혹은 매개 변수로 사용할 수 없다. (이런 특성 때문에 '지역 변수'라는 명칭이 붙은 것 같다)


이런 지역 변수는 명시적인 형 정의가 없어도 생성할 수 있으며, 그에 따라서 위의 select 문에 정의된 것과 같은 개체도 생성할 수 있는 것이다. 위의 new {...} 구문은 Name, OrderID, Amount, Age를 멤버 변수로 갖고 있는 새로운 개체를 생성하게 된다. 그리하여, 아래의 foreach 문에서 각 item(이 새로운 형식의 개체!)의 데이터를 조회할 수 있는 것이다. 결론적으로는 다음의 쿼리문과 같다고 할 수 있다 :

SELECT [t0].[Age], [t1].[Amount], [t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]

위 코드의 결과는 다음과 같다 :

lee chocolate 5 32
kenial cola 2 29
lee cookie 10 32
kenial cup noodle 2 29
kim kimchi 10 22



- LINQ to XML: XML Integration

사실 이 부분은 윗 부분의 코드를 이해하였다면, XML을 다루는 것과 관련된 문제만 알고 있으면 될 것이므로 그리 어려운 부분은 없다.
백문이 불여일타. 코드를 보자 :

public class Person
{
    public string Name;
    public int Age;
    public bool CanCode;
}

static void Main(string[] args)
{
    var e = XElement.Parse(
@"<People>
<Person Age='11'>Allen Frances</Person>
<Person Age='59'>Connor Morgan</Person>
</People>");

    IEnumerable<Person> persons =
        from p in e.Descendants("Person")
        select new Person
        {
            Name = p.Value,
            Age = (int?)p.Attribute("Age") ?? 21
        };

    // execute the query
    foreach (var p in persons)
        Console.WriteLine("{0} {1}",
                          p.Name, p.Age);

    var query = from p in persons
                where !p.CanCode
                select new XElement("Person",
                                      new XAttribute("Age", p.Age),
                                      p.Name);
    // execute the query
    foreach (var p in query)
        Console.WriteLine("{0} {1}",
                          p.Value, p.Attributes("Age"));
}

XElement라는 새로운 형식이 등장한다. 이는 LINQ에서 사용하기 위해 System.Xml에 새로 추가된 개체 형식으로, 기존의 XmlElement와 비슷하게 XML 문서 내의 한 Element('<element>...</element>'와 같은 태그의 단위)를 표현한다. e는 위와 같은 XML 문서 내용을 갖고 있는 XElement 개체를 생성하고, persons 컬렉션에는 Person 형식의 데이터가 담기게 된다. 다만 여기에서는, XElement 개체에서 .Decendants() 메서드를 사용해 원하는 XML 요소를 추출하고 있다는 점에 주목하자.

select도 앞서 나온 코드들과 별 차이는 없으나, '(int?)' (오타가 아니다!)라는 구문이 보일 것이다. 이는 XML 문서 내에 해당 요소가 없을 경우를 대비한 것으로, 이렇게 클래스에 값을 할당할 수 없게 되면 ArgumentNullException이 발생하게 된다. 위의 구문은 Age라는 어트리뷰트가 존재하지 않을 경우, Age 멤버 변수에 21을 할당하라는 의미이다.

두번째 쿼리 식에서는 new XElement를 통해 XML 요소 개체를 직접 생성하는 것을 볼 수 있다. 이와 같은 방법으로 데이터 개체가 아닌, 관련 형식 개체를 직접 생성할 수도 있다.



4. 기타

- 그 외의 표준 쿼리 연산자

표준 쿼리 연산자가 select, from, where, orderby 정도만 등장해서 '어 저거 말고도 SQL 구문은 더 많잖아..'라고 생각한 사람도 있을 것이다. 물론, LINQ에서는 그 이외의 수많은 표준 쿼리 연산자를 제공하고 있다 :

Operator

Description

Where

Restriction operator based on predicate function

Select/SelectMany

Projection operators based on selector function

Take/Skip/ TakeWhile/SkipWhile

Partitioning operators based on position or predicate function

Join/GroupJoin

Join operators based on key selector functions

Concat

Concatenation operator

OrderBy/ThenBy/OrderByDescending/ThenByDescending

Sorting operators sorting in ascending or descending order based on optional key selector and comparer functions

Reverse

Sorting operator reversing the order of a sequence

GroupBy

Grouping operator based on optional key selector and comparer functions

Distinct

Set operator removing duplicates

Union/Intersect

Set operators returning set union or intersection

Except

Set operator returning set difference

AsEnumerable

Conversion operator to IEnumerable<T>

ToArray/ToList

Conversion operator to array or List<T>

ToDictionary/ToLookup

Conversion operators to Dictionary<K,T> or Lookup<K,T> (multi-dictionary) based on key selector function

OfType/Cast

Conversion operators to IEnumerable<T> based on filtering by or conversion to type argument

SequenceEqual

Equality operator checking pairwise element equality

First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault

Element operators returning initial/final/only element based on optional predicate function

ElementAt/ElementAtOrDefault

Element operators returning element based on position

DefaultIfEmpty

Element operator replacing empty sequence with default-valued singleton sequence

Range

Generation operator returning numbers in a range

Repeat

Generation operator returning multiple occurrences of a given value

Empty

Generation operator returning an empty sequence

Any/All

Quantifier checking for existential or universal satisfaction of predicate function

Contains

Quantifier checking for presence of a given element

Count/LongCount

Aggregate operators counting elements based on optional predicate function

Sum/Min/Max/Average

Aggregate operators based on optional selector functions

Aggregate

Aggregate operator accumulating multiple values based on accumulation function and optional seed


- LINQ의 현황

지금 이 포스트를 쓰고 있는 시점(2007.10.10)에서 LINQ에 대한 반응은 찬반이 엇갈린 상태인 것 같다. LINQ의 기본적인 방향에는 공감하고 있으나, XML 쪽에서의 데이터를 다루는 방식이라든가 전체적인 라이브러리의 구성이라든가... 여기저기 들쑤시며 둘러본 결과로는 지금도 계속해서 개선중이라고 한다. 해서, 실제로 Visual Studio 2008이 릴리즈될 즈음의 LINQ의 모습은 이것과 조금 다른 형태일 수는 있겠지만, 기본적인 개념은 동일할 것이므로, MS 개발 환경의 데이터 플랫폼을 미리 살펴보고, 변화의 조짐을 느껴보는 것도 나쁘지는 않을 것이다.


그럼 다음에 뵙자.




* 참고자료

Basic Instincts : 람다 식
http://msdn.microsoft.com/msdnmag/issues/07/09/BasicInstincts/Default.aspx?loc=ko

bkchung's WebLog : LINQ의 정체(정의)는?
http://blogs.msdn.com/bkchung/archive/2005/10/16/481569.aspx

LINQ: .NET Language-Integrated Query
http://msdn2.microsoft.com/ko-kr/library/bb308959.aspx

MSDN Magazine > October 2007 - Parallel LINQ ; 다중 코어 프로세서에서 쿼리 실행
http://msdn.microsoft.com/msdnmag/issues/07/10/PLINQ/Default.aspx?loc=ko

MSDN Magazine > June 2007 - C# 3.0 ; LINQ의 발전과 C# 설계에 미치는 영향
http://msdn.microsoft.com/msdnmag/issues/07/06/CSharp30/default.aspx?loc=ko