본문 바로가기

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

LINQ 시리즈 07 - 람다 표현식(lamda expressions)

람다 표현식. 참 이름도 신기하다. 이 녀석을 뭔지 미리 말하면 이렇다. 델리게이트가 사용될 자리에서 매우 심플한 표현으로 대신할 수 있는 녀석이다. 

Customer[] customers = GetCustomers();

//LINQ 쿼리문

var query =

    from c in customers

    where c.Discount > 3

    orderby c.Discount

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

// C# 표현

var query = customers

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

   .OrderBy( c=>c.Discount )

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

첫번째와 같은 LINQ 쿼리문을 만나면 C#은 두번째 표현으로 해석하게 된다. LINQ쿼리문의 "c.Discount > 3"이 두번째의 "c=>c.Discount > 3"에 해당한다. "=>"이 들어간 표현을 바로 람다 표현식이라고 한다. 이 람다 표현식은 익명 메소드가 더 간단해진 표현이다. 이 표현식을 함 풀어서 말하면 이렇다. "파라미터 c를 받아서 그 c의 속성 Discount값이 3보다 큰지를 확인해서 그 불린값을 반환한다"이다. 이런 의미를 표현하기 위해서 어떻게 표현이 진화하게 되었는지 이제 제대로 알아보자.

다음은 이어질 설명에서 계속 사용될 메소드 Aggregate에 대한 정의이다.

public delegate T Func<T>(T a, T b);

public class AggDelegate

{

    public List<int> Values;

    public T Aggregate<T>(List<T> l, Func<T> f)

    {

        T result = default(T);

        bool firstLoop = true;

        foreach (T value in l)

        {

            if (firstLoop)

            {

                result = value;

                firstLoop = false;

            }

            else

            {

                result = f(result, value);

            }

        }

    }

}

제네릭 메소드 Aggregate()에는 두개의 파라미터가 정의되어 있다. 첫번째 인자는 컬렉션을 받아들이고 두번째 인자는 델리게이트 Func<T> 타입의 인자 f를 받아들인다. Func<T> 정의를 보면 T 타입의 인자를 두개 받아들이는 메소드를 가리키는 포인터를 받아들인다는 것을 알 수 있다. Aggregate의 본문에 있는 forech문 내부에서는 데이터 소스를 순환하면서 각 요소값에 대해서 f를 호출하고 또 다시 그 결과값과 다음 요소의 값을 델리게이트 f에 넘겨서 연산을 반복적으로 수행한다.

여기서 예제로 정의한 Aggregate() 메소드이기는 하지만, 이렇게 컬렉션을 데이터 소스로 하고 그 데이터 소스에 대해서 루프를 돌면서 외부에서 정의한 연산 로직을 수행하는 형태에 대한 구조를 기억하고 있을 필요가 있다. 외부 연산 로직이 어떻게 구현될지는 Aggregate 메소드 내부에서는 모른다. 외부의 코드에서 즉 개발자가 원하는 대로 정의하면 된다. 다음은 Aggregate()를 호출하때 그 연산 로직을 제공하는 샘플 코드이다.

 public static void Demo()

{

    AggDelegate l = new AggDelegate();

    int sum;

    sum = l.Aggregate(l.Values, delegate(int a, int b) { return a + b; });

}

Aggreate()의 첫번째 인자 즉 데이터 소스로 l.Values가 전달되고 있는데, 이것은 List<int> 타입의 컬렉션이이다. 반드시 이 컬렉션의 위치가 AggDelegate에 멤버로 정의될 필요는 없다. 데이터 소스로서 컬렉션이 주어졌다는 것이 중요하다. 그리고 Aggregate()를 호출할때 두번째 파라미터를 보면 delegate 키워드를 통해서 익명 메소드를 하나 정의하고 있는데, 이것이 바로 외부에서 제공하는 연산로직이다.

이것을 말하기 전에 우선 타입 유추에 대해서 알아보자. 앞에서 정의한 Aggregate() 메소드는 제네릭 타입 <T>를 갖는 제네릭 메소드이지만 호출하는 코드에서는 <T>가 없다. 즉 T가  어떤 타입으로 유추되었는지 알아보자는 것이다. 인자로 주어진 l객체의 Values 값이 첫번째 파라미터 타입 List<T> 에 해당하는 객체이다. l.Values는 AggDelegate 타입에 정의되어 있는  List<int> 타입이다. 즉  List<T>는 List<int>라는 것을 알 수 있고 결국 T는 int 라는 것을 알 수 있다. 또는 두 번째 인자 익명 메소드의 파라미터( int a, int b)를 통해서도 타입 T를 int임을 유추할 수 있겠다. 타입 유추를 하는 프로세스가 MSDN에 나와있기는 한데 사실 아직 필자도 다 외지 못하고 있다. 다음에 기회가 되면 정리해 보도록 하겠다. 물론 생각나면 -_-;;

여튼 이제 타입 T가 결정되었다. 두번째 인자를 알아보자. int 타입의 두 인자를 받아들여서 그 합을 반환하는 로직을 구현하고 있다. 이제 이 표현이 좀 더 간단한 모습으로 진화한 결과를 보겠다.

sum = l.Aggregate(l.Values, (int a, int b) => { return a + b; });

파라미터 목록 앞에 있는 delegate 키워드를 없앴다. 대신에 "=>" 표시를 파라미터 목록과 메소드 본문 사이에 두었다. 이제 람다 표현식이 드러나기 시작한다.  "int 형 인자 a,b를 받아들여서, a와 b의 합인 a+b를 반환한다"로 읽을 수 있다.  머릿속에 항상 염두에 두고 있어야 할 것은 Aggregate 메소드의 두번째 인자로 코드가 넘어가는 것이 아니다. 그 코드를 가리키고 있는 포인터 즉 델리게이트 인스턴스가 넘어간다는 기억하고 있어야 한다. 이 표현이 컴파일되면 delegate를 사용한 익명 메소드와 동일한 결과가 된다. 이 람다 표현이 진화의 끝은 아니다. 좀 더 간단한 표현으로 될 수 있다.

sum = l.Aggregate(l.Values, (a, b) => { return a + b; });

익명 메소드의 파라미터의 타입이 생략되었다. 그렇다고 T를 모르는 것은 아니다. 앞에서 본 것처럼 l.Values 첫번째 인자를 추적해가다보면 T의 타입이 결정된다. 이 표현이 끝은 아니다. 다시 진화할 수 있다. 만약 {}블럭내에 return 문 하나만 있다면 return과 {}블럭도 생략될 수 있다.

sum = l.Aggregate(l.Values, (a, b) => a + b );

람다 표현식의 파라미터가 하나뿐이라면 또 표현이 간단해 질 수 있다( 이 예제에서 AggregateSingle()의 두번째 파라미터는 FuncSingle<T>(T)와 유사한 델리게이트 타입의 인스턴스가 될 것이다).

int sum = 0;

sum = AggregateSingle(l.Values, x=>sum+=x );

인자 목록을 감싸고 있는 괄호가 없어졌다.  간단해졌다. 이전에도 자주 봐 왔던 LINQ의 C# 표현이 이제 되어 가고 있다. 그러나 람다 표현에서 인자가 없게 되면 다시 => 앞에 괄호가 나타난다. 다음의 마지막 표현이다. 람다 표현식의 진화 과정을 정리하면 다음과 같다.

delegate(int a, int b) { return a + b; }

(int a, int b)=>{ return a + b; }

( a,  b)=>{ return a + b; }

( a,  b)=>a+b;

(x) => sum += x;

x => sum+=x;

()=>sum +1;

람다식을 사용하면 표현이 아주 간단해질 수 있다.  C#3.0부터서는 LINQ 표현을 읽기 쉽고, 간단하게 해주는 기능이 또 하나 있는데, 기존 타입의 메소드를 확장할 수 있는 방법을 제공한다는 것이다. 이미 클래스 타입을 정의했고, 그 소스 코드에는 접근할 수가 없다. 예를 들어 String 타입에 사용자 정의 Display()같은 메소드를 추가할 수 있다는 것이다. JavaScript의 property 속성을 떠올리는 사람이 있다면 바람직한 연상을 하고 있는 것이다. 그와 비슷한 기능을 C#에서 제공하고 있다는 것이다. 이것은 다음 포스트에서.