확장 메소드(extension method)

인터페이스(혹은 API) 설계 철학에는 크게 두 가지 사조가 있습니다.

각각은 장단점이 있습니다. 미니얼 인터페이스는 배워야 할 API 숫자가 적기 때문에 학습이 용이합니다. 필요하지도 않은 수많은 API사이를 헤매면서 내가 필요한 메소드를 찾아 헤맬 필요도 없습니다. 하지만 내가 필요한 기능이 API로 제공되지 않으면 저수준의 API만을 사용하여 내가 필요한 API를 만들어내야 하는 부담이 있습니다. 반대로 인간적 인터페이스는 배워야 할 API가 많기 때문에 학습에 시간이 더 걸립니다. 필요한 메소드를 찾기 위해 수백 개의 메소드 사이를 찾아 헤매야 합니다. 하지만 일단 내가 필요한 메소드를 찾으면 추가적인 노력 없이 편리하게 사용할 수 있습니다.

마틴 파울러(Martin Fowler)의 인간적 인터페이스 설명에도 나오지만, 두 철학의 차이가 나타난 대표적인 예로 Java의 java.util.List 클래스, Ruby의 Array 클래스가 있습니다. 두 인터페이스의 의미는 크게 다르지 않음에도 불구하고, Java는 25개의 메소드, Ruby는 78개의 메소드를 제공합니다. Java의 꼭 필요한 메소드만 최소한으로 제공하고 있고, Ruby는 자주 사용될 것으로 예상되는 API를 모두 제공하는 방식을 택했습니다.

예를 들어, 리스트의 가장 마지막 원소를 얻어 오는 방법이 Java에는 없습니다. 대신 리스트의 size를 얻어와서 1을 빼주면 마지막 원소의 인덱스가 나오므로 get 메소드를 통해 마지막 원소를 얻을 수 있습니다.

aList.get(aList.size -1)

반대로 Ruby는 마지막 원소를 얻어오는 API를 제공합니다.

anArray.last

어느 한 쪽이 반드시 더 좋다고는 할 수 없기 때문에 프로그래밍 언어나 라이브러리 설계자들은 철학에 따라 미니멀 인터페이스를 채택하기도 하고, 인간적 인터페이스를 채택하기도 합니다. 보통 Java나 C# 같은 정적 타이핑하는 언어들은 미니멀 인터페이스를 선호하는 경향이 있고, Python이나 Ruby 같은 동적 타이핑하는 스크립트 언어는 인간적 인터페이스를 선호하는 경향이 있습니다.

C#도 기본적으로는 Java와 마찬가지로 미니멀 인터페이스를 제공합니다. 그런데, C# List 클래스 문서를 보면 100개 이상의 메소드가 존재하고, 마지막 원소를 얻어올 수 있는 Last 메소드가 존재함을 발견할 수 있습니다. 문서만 보면 미니멀 인터페이스가 아니라 인간적 인터페이스에 가깝다고 느끼실 겁니다. C#은 어떻게 두 마리 토끼를 다 잡았을까요?

문서를 좀 더 자세히 보면 힌트를 발견할 수 있습니다. Java의 java.util.List 클래스에 해당하는 메소드들은 단순히 “메소드”로 분류되어 있고, Last와 같은 편의 메소드들은 “확장 메소드”로 분류되어 있습니다. 여기서확장 메소드(extension method)는 C# 3.0에 추가된 언어 기능으로, 기존 클래스를 수정하거나 새로 컴파일하지 않고도 메소드를 추가할 수 있는 방법을 제공합니다.

예를 들어, string 클래스에 단어 숫자를 세는 WordCount라는 메소드를 추가하고 싶다고 하면 다음과 같이 작성할 수 있습니다.

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this String str)
        {
            return str.Split(new char[] { ' ', '.', '?' }, 
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }   
}

얼핏 보기에는 string 클래스가 아니라 MyExtensions라는 클래스에 단순히 정적 메소드를 하나 추가하는 것처럼 보입니다. 하지만 WordCount 메소드의 첫 번째 인자인 strthis라고 표시되어 있기 때문에 이 메소드는 일반적인 정적 메소드가 아니라 확장 메소드로 분류됩니다. 확장 메소드는 일반적인 정적 메소드와 할 수 있는 일에 차이가 없지만, 다음과 같은 특별한 문법을 제공합니다.

using ExtensionMethods;

string s = "Hello Extension Methods";
int i = s.WordCount();

위 코드를 보면, string 타입에 마치 WordCount라는 메소드가 존재하는 것처럼 호출이 가능한 것을 확인할 수 있습니다. 즉, 실제로 string 클래스를 고쳐서 WordCount를 수정하거나 새로 컴파일하지 않았음에도 불구하고 마치 string 클래스를 수정하여 새로운 메소드를 추가한 것과 같은 효과를 얻을 수 있습니다. 확장 메소드의 경우, 일반 메소드와 마찬가지로 IntelliSense가 자동 완성까지 제공하므로 사용하는 입장에서는 전혀 차이를 느낄 수가 없습니다.

또 하나의 장점은 using ExtensionMethods에 있습니다. 위와 같이 using 문을 사용해 ExtensionMethods 네임스페이스를 임포트해주지 않으면, string 클래스에 WordCount라는 메소드는 존재하지 않게 됩니다. 즉, 인간적 인터페이스와 달리 확장 메소드를 이용해 추가한 메소드들은 필요한 경우에만 임포트해 사용할 수 있으므로, 불필요하게 많은 메소드를 제공해 인터페이스를 복잡하게 만드는 문제도 어느 정도 해결됩니다.

앞서 미니멀 인터페이스와 인간적 인터페이스에 대한 설명으로 글을 시작한 이유는, 확장 메소드를 단순히 프로그래밍 언어의 기능으로만 생각하면 단순한 편의 문법 제공 그 이상도 이하도 아니기 때문입니다. 확장 메소드를 이해하는 올바른 방법은 미니멀 인터페이스와 인간적 인터페이스의 장점을 고루 취하는 해결책으로 생각하는 것입니다. 꼭 필요한 최소한의 메소드는 일반적인 메소드로 제공하고, 경우에 따라 필요할 수도 있는 수많은 편의 메소드들은 분류에 따라 확장 메소드로 제공하면 각 인터페이스의 장점을 모두 취할 수 있게 됩니다.

LINQ는 철저히 이런 생각에 따라 만들어졌습니다. 앞서 살펴본 Last 메소드를 포함하여 LINQ가 제공하는 대부분의 메소드는 기존 collection 타입에 추가된 것이 아니라 IEnumerable 인터페이스에 추가된 확장 메소드입니다. 일반적인 사용자와 달리 기존 소스 코드를 얼마든지 수정할 수 있었던 마이크로소프트가 기존 collection 타입에 메소드를 추가하지 않고, 굳이 확장 메소드를 사용하여 LINQ의 메소드들을 제공한 이유는 (여러 가지가 있지만) API 관점에서 보면 API를 “필수”와 “편의”로 구분하기 위함입니다.

One thought on “확장 메소드(extension method)

  1. Pingback: 확장 메소드(extension method) | 서광열의 C# 스쿨

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s