C# 4.0 새기능들(2008년10월CTP버젼)

개발자 분들을 위한 내용들을 적는 블로그임에도 불구하고 소식들과 가십들을 위주로 적다보니 정작 코드를 적는 것은 굉장히 오랜만인 것 같습니다.^^ 드디어 조금 적을 기회가 왔습니다. C# 4.0에 관한 이야기를 적으면서 말이죠…

C# 3.0의 주요 테마 중 한가지는 LINQ였습니다. 물론 각 기능들은 독립적인 유용성이 있었지만, LINQ를 구현하는 연장선상에서 여러가지 새 기능들이 설명될 수 있었고 그에 준하여 추가되었습니다. 이번 C# 4.0이 진행되는 동안 (물론 3.0을 개발하는 것과 동시에 시작되었죠) 재미난 영향들이 있었습니다.

C#의 설계자들은 알려진대로 9년전부터 매주 몇번씩 C# 디자인 미팅을 가집니다(참고로 VB도 마찬가지지요). 마치 원탁의 기사들처럼 이 미팅에는 다양한 사람들이 참가하여 새기능과 기존 철학의 밸런스를 조절합니다. 근래의 주요 참가자들을 보면 당연히 C#의 Anders Hejlsberg와 Mads Torgersen가 있고, JScript등의 스크립팅의 Eric Lippert, VB의 Paul Vick 그리고 C# 3.0 이후로 Jim Hugunin이 참가를 하는데, Jim은 DLR(Dynamic Language Runtime) 전문이죠.

Jim을 빼고는 오랫동안 이 멤버들이 참가를 했었지만, Jim의 DLR의 등장으로 인해 4.0이 되어서 한 테마로 떠오른 것이 기술들의 행복한(?) 조화였습니다. (물론 커뮤니티로부터의 커다란 피드백도 많은 영향을 미쳤습니다.) 동적(dynamic)인 요소는 이들이 잘 지내는데에 필수적인 요소였기 때문이랄까요? 아마도 이런 이유로 4.0의 새기능들 중에서 다음의 내용들이 주를 이룹니다:

  • dynamic이라는 키워드 –> DLR(IronPython/IronRuby등), COM과의 Interop
  • Optional/Named 파라미터, No PIA –> 좀 더 편해진 COM Interop, VB 기능과의 싱크

모두가 다른 기술과 잘 지내보려는 개방의 노력이라고 할 수 있겠습니다. 다르게 이야기하면 다른 언어나 기술에서 제공하던 방식으로 상호연동(Interop)하기 위해서는 C#에서 제공하지 않는 것들은 포기해야하거나 혹은 C#의 방식대로 불편하게 사용해야하는 상황을 개선하고자 했다고 이야기할 수 있겠습니다.

- dynamic 키워드

다음의 C# 3.0 코드를 보시죠:

var calc = new Calculator();

int sum = calc.Add(10, 20);

이 경우에는 var이라는 형식(type)이 존재하는 것이 아니고, 유추(infer)에 의해 Calculator라는 클래스임을 판단하고 calc의 형식을 정의하게 됩니다. 따라서 기존의 정적인 방식과 다름없이 컴파일시에 형검사가 이뤄지게 됩니다.

반면 4.0에서 소개되는 dynamic이라는 형식(type)은 (여러가지 규칙이 있지만) 일반적으로 evaluation을 실행시간(runtime)으로 미루겠다는 표시입니다. Anders의 슬라이드에서 빌리자면:

dynamic calc = GetCalculator();

int sum = calc.Add(10, 20);

calc는 정적(static) 언어의 의미로는 dynamic이라는 형식(type)을 가지는 변수입니다. 따라서 여기서 Add라는 메서드의 호출문은 컴파일러에 의한 형검사를 통하지 않고 실행시간(runtime)에 바인딩되어 동적으로 호출됩니다. (내부적으로는 호출 이전에 dynamic이라는 형식을 필요한 형식으로 대체를 하는 방식을사용하게 되며, 이로써 정적 언어에서 동적인 속성을 사용할 수 있는 방안을 제공하게 되는 것입니다.)

dynamic 키워드가 없다면 위의 코드를 다음과 같은 방식으로 동적인 속성을 코딩해야할 것입니다:

object calc = GetCalculator();

Type calcType = calc.GetType();

object res = calcType.InvokeMember("Add", BindingFlags.InvokeMethod, null, new object[] { 10, 20 });

int sum = Convert.ToInt32(res);

물론 이는 calc가 다른 외부의 객체 모델(OM, Object Model)을 대표하는 것이 아니라 .NET 객체를 사용한 경우의 예이고 다른 경우에는 다른 방식을 사용하겠죠.

이런 Reflection을 사용하지 않는 구현을 위하여 IDynamicObject라는 인터페이스를 제공하며, 이는 정확히는 DLR에서 제공하는 인터페이스입니다. Anders의 PDC 세션에서는 HTML DOM도 AsDynamic()이라는 Extension 메서드를 통해 IDynamicObject로 래핑해서 dynamic으로 사용하는 것을 보여줍니다.

이를 통하여 COM Interop에 몇가지 더 편리함을 제공하게 됩니다. 예를들어, Variant를 위해 사용하는 object 형식을 다시 해당하는 형식으로 cast하는 오버헤드가 없어집니다. C# 4.0 새기능 문서의 예제를 빌리면:

((Excel.Range)excel.Cells[1, 1]).Value2 = "Hello";

excel.Cells[1, 1].Value = "Hello";

이렇게 사용할 수 있습니다.

- Optional / Named 인자

이는 이전부터 구현의 이슈라기 보다는 언어의 철학에 관한 이슈였습니다. .NET Framework 디자인에 있어서 인자는 명백히 선언되어 호출시 명기해야하는 경우에 더 나은 코딩 방식으로 생각되어져 왔지만, 그동안 수많은 반대 피드백들이 있었습니다. 특히 COM Interop시의 Missing값 이슈는 VB에서는 제공하던 Optional 인자와 비교해서 코딩에 해를 주는 요소였습니다.

이에 C# 4.0부터는 다음과 같이 정의시에 인자의 기본값을 지정하고, 호출시에 원하는 인자의 값을 지정할 수 있게 되었습니다(Anders의 강의에서 환호를 받았죠.^^):

public StreamReader OpenTextFile( string path, Encoding encoding = null, bool detectEncoding = true, int bufferSize = 1024); // 뒤의 3 인자에 기본값 지정

OpenTextFile("foo.txt", Encoding.UTF8); // 뒤의 2 인자 생략

OpenTextFile("foo.txt", Encoding.UTF8, bufferSize: 4096); // 뒤의 2 인자 중 bufferSize만 지정

OpenTextFile( bufferSize: 4096, path: "foo.txt", detectEncoding: false); // 각 인자를 순서에 관게없이 지정

- Variance

행복한 조화 분류에 속하지는 않지만, 중요한 결정 중 하나는 Variance입니다. Variance를 설명하는 것은 마치 입문자에게 포인터를 설명하는 것처럼 잘못 설명하면 헷갈리기 쉬운 것이라 조심스럽습니다만, 이번 4.0에서는 인터페이스와 delegate에 한하여 지원하므로 해결하고자 하는 문제에 한해 Anders의 설명방식으로 설명해봅니다.

.NET 배열은 co-variant하기 때문에 어떤 객체에 더 구체적인 객체를 대입할 수 있습니다. 즉 object[]를 요하는 곳에 string[]를 사용할 수 있습니다. 쉽게 “아무 동물을 각 방에 넣어줘봐” 하면 닭들을 채워주는 것과 같은 것입니다. 반면, string[]을 대입했더라도 object[]인 것에는 변함이 없기 때문에 컴파일시에는 여기에 string이 아닌 다른 값을 대입해도 object에 대입한 것으로 간주하여 오류를 내지 않습니다. 대신 실행시간에 런타임오류가 발생하죠:

object[] oa;
oa = new string[10];
oa[1] = new int(); // 컴파일되지만 런타임오류가 발생

이런 문제에 type safety를 제공하기 위해서 Generics라는 훌륭한 기능이 들어옵니다만, 문제는 Generics는 invariant하다는 것입니다. 즉 List<object>에 List<string>을 대입할 수는 없습니다.

List<object> ls;
ls = new List<string>(10);

위의 코드는 VS에디터에서 “Cannot implicitly convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>'라고 항변합니다. “object 리스트인데, 왜 string을 각 요소로 사용할 수 없는거지?”하는 불만이 생기게 되는 것입니다. 이 두가지 이슈의 완충 작용을 위해서 C# 4.0에서는 제한적으로 인터페이스/delegate에 covariance와 contravariance를 지정하는 방식을 도입합니다. 다음의 코드를 보시죠(아직 C# 4.0의 프로토타입 단계라 아래의 코드가 동작하는지는 모르겠습니다만 컨셉만 생각해보세요):

List<string> strings = new List<string>(10);
IEnumerable<object> objects = strings;

objects에 strings가 대입되었지만, IEnumerable에 의해서 objects에 값을 지정할 수 있는 방법이 없게 됩니다. 즉, co-variance를 허용한 대신 값에 쓰는 방법을 제공하지 않는 것으로 safety를 제공하게 됩니다. 이는 다음과 같은 방법으로 정의함으로써 구현됩니다:

public interface IEnumerable<out T> {
  IEnumerable<T> GetEnumerabor();
  T Current { get; }
  …
}

위의 코드에서는 out을 T에 붙임으로써 co-variant임을 지정합니다. 위의 코드에서 밑줄친 것과 같이 out을 붙임으로써 T의 사용은 output에 제한하게 됩니다. 즉, IEnumerable<T>는 co-variant하게 대입이 가능함과 동시에 이로 인해 발생할 수 있는 safety를 보장하도록 하는 장치를 가지게 됩니다.

반대로:

public interfact IComparer<in T> {
  int Compare(T x, T y);
}

에서처럼 T에 in을 붙임으로써 contra-variance를 지정하고 input에만 사용할 수 있는 제약을 둡니다(그렇지 않으면 컴파일 에러를 내겠죠). 다시 쉬운 예를 들면, 100원짜리가 들어갈 수 있는 저금통을 요구하는 사람에게 500원짜리가 더 큰 것이기에 500원짜리가 들어갈 수 있는 저금통을 줘도 괜찮은 것과 같을 수 있겠습니다 – 반대로 500원짜리만 넣겠다는 사람에게 100원만 들어가는 구멍의 저금통을 주면 contra-variant하기 때문에 안되겠죠. 즉, 여기서는 IComparer<object>를 IComparer<string>에 대입할 수 있게 되는 것이고, 그 safety가 컴파일시에 보장되는 것이죠. 인자로 string을 받아 비교하는 클래스이기 때문에 그 구현이 object라고 하더라도 명백히 호환이 되는 것이죠.

물론 in과 out을 섞어서 활용할 수도 있겠습니다. Eric Lipper의 Co-variance와 Contra-variance에 관한 씨리즈를 참고하시면 좋을 것 같습니다. Wikipedia에도 이에 관해 잘 나와있습니다.

- 정리

C# 4.0의 새기능 문서를 보면 이외에도 VB의 기능과의 parity에 관한 이야기가 언급됩니다. C#의 dynamic과 VB의 Late Binding은 속성이 비슷하기 때문에 VB에서 DLR을 사용하도록 조절될 수 있고, Optional/Named 인자는 이미 VB에서 오랫동안 사용되었기 때문에 이를 바탕으로 C#의 기능을 설계하였고, 또한 NoPIA와 variance는 C# 뿐만 아니라 VB에도 추가되는 기능이라고 이야기합니다.

이렇듯, C#의 방향성은 다른 기술과 잘 지내려고 하는 방향으로 흐르고 있고, 애초의 태생도 다양한 언어와 기술의 기반이 될 수 있는 CLR(.NET Framework)을 지원하는 언어로서의 역할을 위한 것이었기 때문에 이런 방향이 잘 설명될 수 있습니다. (반면 JavaVM은 Java를 위해서만 만들어졌기 때문에 이에 충실한 방향이었고, 이에 맞는 방향성과 동적언어등으로 인한 그 방향성의 변화도 설명될 수 있습니다.)

위에서 설명한 DLR이나 COM Interop 혹은 스크립팅언어등 이외에도 CLR위에 F#과 같은 새로운 함수형 언어의 공식 지원으로 인한 변화와 병렬(Parellel) 환경 지원을 위한 CLR의 변화등도 함께 C#의 방향성에 영향을 주고 있으며, 이미 SQL과 같은 절차형이 아닌 선언형식의 언어적 요소를 위한 LINQ의 수용도 3.0이후에 이뤄지고 있습니다. 앞으로 계속되는 “정적(static) 언어”인 C#의 이런 요소들의 실용적인 섭렵을 계속 기대해봅니다.