본문 바로가기

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

LINQ 시리즈 08 - 메소드 확장( Extension Methods)

객체 지향을 지원하는 언어에서 타입을 확장하는 방법하면 제일 먼저 떠오르는 것은 바로 상속(inheritance)에 의한 메소드의 오버라이딩 또는 오버로딩 또는 하이딩(hinding)이다. 혹시 이 세가지 개념이 구분이 잘 가지 않는다면 구글링을 한번 해 보자. 여튼 타입 확장 하면 상속이라는 것이 제일 먼저 떠오르는 것은 당연하다.

근데 C#3.0부터 새로운 확장 방법을 제공하고 있으니 "메소드를 확장"할 수 있다는 것이다. 즉 사용자 정의 메소드를 마치 원래의 그 타입의 메소드에서 정의한 것처럼 호출해서 사용할 수 있다는 것이다. 클래스에 Sealed로 해서 상속을 허락하지 않는 타입에서도 이런 메소드를 확장하는 방법이 가능하다. C#의 이런 능력은 LINQ문이 좀 더 읽기 쉽고 코딩하기 쉽게 해준다는 것을 알게 될 것이다.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace BulogTestConsole

{

    static class Program

    {

        public static void Display<T>(T[] names, Func<T, bool> filter)

        {

            foreach (T s in names)

            {

                if (filter(s))

                {

                    Console.WriteLine(s);

                }

            }

        }

        static void Main(string[] args)

        {

            string[] names = { "Marco", "Paolo", "Tom", "John" };

            Display(names, s => s.Length > 4);


            Console.Read();

        }

    }

}

public, static으로 된 메소드 Display<>()가 정의되어 있다.  컬렉션과 이 컬렉션의 요소를 필터링할 델리게이트 인자를 받는 메소드이다. 필터링을 통과하는 값을 컨솔에 출력한다.  이 메소드를 호출하는 부분이 Main()에 있다. 첫번째 인자는 문자열 배열이다. 그리고 두번째 인자는 익명 메소드의 델리게이트 인스턴스가 넘어간다. 이 델리케이트 타입을 보면 Func<T, bool> 타입이다. 근데, 이 델리게이트 타입이 정의된 곳이 코드에는 없다. 이 정의는 System 네임스페이스에 정의되어 있다.

public delegate TResult Func<TResult>()

public delegate TResult Func<T, TResult>(T arg)

public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2)

public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3)

public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)

TResult 타입은 반환값의 타입과 동일하고, T1~Tn은 파라미터 타입들과 동일하다. 타입 유추에서 이것은 중요하다. 타입인자 T1~Tn의 타입이 결정되면 파라미터의 타입이 결정되고 TResult 타입이 결정되면 반환값의 타입도 밣혀질 수 있다. 다음 포스트는 타입 유추 프로세스에 대해서 정리를 하려고 한다. 이번 포스트는 메소드 확장에 대한 것이니 여기까지만.

앞의 코드에서 제너릭 메소드 Display를 호출하는 부분은 "이름 배열중에서 길이가 4 이상인 이름을 출력하라"는 표현을 하고 있다. 만약 호출하는 코드를 다음처럼 할 수 있다면 더 직관적일 것이다.

names.Display( s=>s.Length > 4);

  마치 names 즉 string[] 타입에 Display()라는 메소드가 노출되어 있는양. C#3.0의 확장 메소드 기능을 사용하면 이것이 가능하다는 것이다. 이렇게 호출하려면 다음과 같이 메소드의 정의에 하나의 변화만 주면 된다. 다른점을 찾자.

public static void Display<T>(this T[] names, Func<T, bool> filter)

{

...

}

this 키워드를 첫번째 파라미터 names 앞에 붙이고 있다. 이렇게 파라미터 타입 앞에 키워드를 추가하면 그 타입에 마치 현재 정의하고 있는 메소드가 정의되어 있는 것처럼 호출할 수 있다는 것이다. 이것을 "타입의 메소드를 확장한다"고 표현하는 것이다. 만약 그 파라미터 타입이 제네릭 타입이라면 타입 유추를 하고 나서 결정되는 타입의 메소드가 확장된다. 앞에서처럼 T[]앞에 this가 붙어 있으므로 일단 T가 결정되고 난 후의 타입 즉 string[]의 타입에 Display 메소드가 확장되는 것이다.

그러나 타입의 메소드를 확장하는 규칙이 있다. 

확장 메소드는 static 클래스에 정의되어야 한다.

확장 메소드는 static, public 이어야 한다.

this 키워드는 확장될 타입의 파라미터의 타입 앞에 붙어야 한다.

확장 타입은 반드시 첫번째 파라미터 타입이어야 한다.

다음은 decimal 타입에 Double 메소드를 확장하는 샘플 코드이다.

static class ExtensionMethods

{

    public static decimal Double( this decimal d )

    {

        return d+d;

    }

}

decimal d = decimal.Double( 4)와 같은 호출이 가능하다는 것이다.

names.Display()로 호출하니 이 Display 메소드가 어디에 정의되어 있는지 그 메소드를 검색하는 절차가 필요하다. 현재 네임스페이스와 using문을 사용해서 포함시킨 모든 네임스페이스에 있는 모든 static 클래스에서 static, public Display 메소드를 검색한다. 만약 두개 이상의 타입에서 동일한 확장 메소드를 가지고 있다면 컴파일러는 에러를 발생시킨다.

만약 인스턴스 메소드가 이미 정의되어 있는 경우 즉 동일한 이름과 시그너쳐를 갖는 메소드가 이미 타입에 정의되어 있다면, 인스턴스 메소드를 먼저 호출할까 아니면 확장 메소드를 먼저 호출할까? 인스턴스 메소드 승! 동일한 확장 메소드와 가상 메소드가 같은 타입에 정의되어 있다면 ? 가상 메소드가 승!

다음은 호출한 메소드를 검색하는 로직이다. 가상 메소드를 결정하는 기존의 로직은 동일한다.

▶먼저 현재 호출하는 객체의 타입을 확인하고 그 타입의 정의로 이동한다.

▶그곳에 호출하는 메소드가 있는지 확인한다.

▶해당 메소드가 인스턴스 메소드인지, 가상 메소드인지 확인한다( abstract, virtual, override가 붙어있으면 가상 메소드가 된다).

▶인스턴스 메소드라면 해당 메소드를 바로 호출한다.

▶가상 메소드라면 현재 객체의 실제 타입(concrete type)이 뭔지를 확인한다.

▶실제 타입의 정의도 다시 이동한다.

▶실제 타입에서 실제 구현하고 있는 해당 메소드를 호출한다.

가상 메소드와 인스턴스 메소드가 없다면 그제서야 확장 메소드를 검색한다.

추가

확장 메소드를 검색할때도 현재 호출하는 객체의 타입에 확장 메소드가 없다면 부모 타입에 대해서 확장 메소드가 정의되었는지를 확인한다. 가릿? 다음처럼 Object 타입에 대해서 Display라는 메소드가 확장되어 있다고 하자.

static class Displayer

{

    public static void Display( this object o)

    {

        string s = o.ToString();

        Console.WriteLine( s );

    }

}

이런 상황에서 다음처럼 사용자 정의 타입 Customer의 객체에 대해서 Display 메소드를 호출했다고 하자.

Customer c = new Customer();

c.Name = "달봉이";

c.Display();

물론 Customer 타입에는 Display 메소드가 없다고 하자. 그럼 Cutomer의 부모 타입인 object에 확장되어 있는 Display 메소드가 호출된다는 것이다. 만약 동일한 메소드가 Customer에 대해서 확장되어 있다면?

static class Displayer

{

    public static void Display( this object o)

    {

        string s = o.ToString();

        Console.WriteLine( s );

    }

    public static void Display( this Customer c)

    {

        string s= String.Format( "Name={0}", c.Name );

        Console.WriteLine( s );

    }

}

이런 경우는 Customer 타입의 확장 메소드를 사용하게 된다.

사실 필자도 확장 메소드를 아직 사용해보지는 않았다. 필자도 처음에는 가상 메소드의 호출하는 메커니즘과 비슷해서 어떻게 받아들여야 할지 몰랐다. 근데 차이가 있었다. 확장 메소드의 결정은 "컴파일타임"에 일어나고 가상 메소드의 결정은 "런타임"에 수행된다는 것이다.

즉 어떤 객체에 대해서 메소드를 호출했을때 이 메소드가 확장 메소드인지 여부는 컴파일타임에 결정된다. c.Display()를 호출했을때 Customer 타입에는 Display가 정의되어 있지 않다는 것을 알고 확장 메소드인지 여부를 확인하는 절차를 따라가게 된다. 그래서 사용하고 있는 네임스페이스에 있는 모든 static 클래스에 정의된 public, static 메소드들을 확인하게 된다. 그래서 Customer 또는 그 베이스 클래스에 Display가 정의되어 있는지를 확인한다. 그래서 Customer 타입에 대해서 확장된 Display 메소드가 확장되었다. 여기까지가 컴파일 타임시에 일어난다.

이제 런타임시 c.Display() 메소드를 호출하는 코드에 다다르면 컴파일 타임에 결정된 그 Display()가 호출된다. 이 메소드는 public static이다.  static 메소드는 가상 메소드일수가 없다. 즉 public static인 Display의 가상 메소드 버전은 있을 수 없다는 것이다. 런타임시에도 그대로 컴파일 타임에 결정된 그 메소드가 호출될 수 있다.

마아...여기까진데. 쩜 복잡한가 싶다.

근데 문서를 보면 확장 메소드를 배울수록, 강력한 타입의 특성을 유지하면서도 언어가 유연해질 수 있다는 것을 알게 된단다. 가상 메소드의 강력함을 안다면 타입의 확장 메소드의 위력도 어느 정도는 이해할 수 있겠다 싶지 않은가. 나만 그런가. 야튼 좋텐다.  다음 포스트에서는 타입 유추에 대해서 좀 더 정리한다.