Tag Archives: .NET

.NET 4.0 (C#): dynamic 키워드의 이해와, 그 성능

이번 글에서는 .NET Framework 4.0에서 새롭게 추가된 ‘dynamic’이라는 키워드와 해당 키워드를 사용한 메소드 호출에 대하여 알아본다.

1. DLR (Dynamic Language Runtime)

MS는 .NET Framework 4.0을 발표하며 DLR(Dynamic Language Runtime)이라는 새로운 프로그래밍 모델을 선보였다(MSDN: Dynamic Language Runtime Overview). 이전의 버전에서는 리플렉션(MSDN: Reflection) 기능을 활용하여 런타임에 동적으로 결정되어야 하는 사항들을 관리할 수 있었으나, 이러한 방법은 복잡하고 성능의 제약이 많았다. DLR은 CLR을 바탕으로 동적 프로그래밍을 지원하는 다양한 기능들이 포함된 라이브러리의 집합이다. 이제 프로그래머들은 .NET Framework 4.0와 함께 등장한 DLR을 활용하여 보다 편리하고 다양하며 최적화된 동적 기능들을 사용할 수 있게 되었으며, 또한 .NET 기반으로 Python이나 Ruby와 같은 동적 언어들을 사용하기가 보다 용이(MSDN: CLR Inside Out – IronPython and the Dynamic Language Runtime)해졌다.


2. ‘dynamic’ Keyword 

이러한 DLR의 새로운 기능들 중에 오늘 살펴볼 내용은 ‘dynamic’이라는 키워드를 사용한 메소드 호출의 방법과 성능이다. 이전에는 런타임까지 특정할 수 없는 객체에 대한 동작에 대해 보통 리플렉션을 사용하여 관리하여왔다. 하지만 리플렉션은 코드의 복잡도를 증가시키며 시스템의 성능을 크게 감소시킬 여지가 있다. .NET 4.0 이후부터는 ‘런타임에 결정되는 클래스 타입에 대하여 동작을 수행하고자 하는 경우’에 대하여 dynamic 키워드를 사용하여 이러한 리플랙션의 단점을 보완할 수 있게 되었다. dynamic 키워드에 대한 간단한 사용 예제는 다음과 같다.

일반적인 경우에는 컴파일 시점에서 변수의 타입이 결정되어야만 한다. 그렇기 때문에 새롭게 생성된 객체에 대한 참조는 그에 대응되는(객체 스스로의 클래스 타입이나, 객체가 상속받은 클래스의 타입) 변수에 저장되어야 컴파일 시에 오류가 발생하지 않는다. 하지만 dynamic으로 선언된 변수는 그에 할당된 정보의 타입에 상관없이 컴파일되며, 오직 런타임 시에 dynamic 변수가 수행하여야 할 동작이 올바르게 수행(Dynamic Dispatch)될 수 있는지의 여부가 검사된다.

위의 예에서 보면, ClassA와 ClassB는 모두 TEST라는 동일한 서명을 가진 메소드를 갖고 있지만 각각 대응되는 변수에 저장되어야만 컴파일 시에 오류가 발생하지 않는다. 하지만 dynamic 키워드를 사용하여 선언된 변수는 ClassA와 ClassB 모두의 경우를 참조할 수 있으며, PRINT()라는 동일한 이름의 메소드를 손쉽게 호출할 수 있음을 알 수 있다. 이는 런타임시에 PRINT()라는 메소드를 찾아서 실행시키기 때문이며, dynamic 키워드로 선언된 변수는 IntelliSence 기능이 지원되지 않는다.

이처럼 dynamic 키워드는 런타임 시에 타입이 확정되어 동작을 수행하는 특징을 지원하며, 위의 예제와 같이 간략한 코드만으로 이러한 기능을 사용할 수 있다. 코드로 작성된 동작을 수행할 수 없다면 그에 해당되는 예외(Exception)가 런타임 시에 발생한다. 또한 dynamic 키워드는 .NET Framework 4.0 이상에서 기본으로 지원되며, 이를 사용하기 위해서는 별다른 추가 using 구문이 필요하지 않다.


3. CallSite and dynamic Keyword

그렇다면 dynamic 키워드를 사용한 메소드 호출이 동작할 수 있는 원리는 무엇일까. DLR은 CLR의 제네릭(generic)과 대리자(delegate) 기능을 활용하여 이를 구현하고 있는 것으로 보인다. 앞서 살펴본 예제 코드에서 dynamic으로 선언한 D 변수에 ClassA의 참조를 할당하는 과정을 ILDasm을 사용하여 IL(Intermediate Language) 코드에서 살펴보자.

앞서 언급한 예제를 컴파일하여, 컴파일된 EXE 파일을 열어보면 위와 같은 내용을 확인할 수 있다. dynamic 키워드를 사용하지 않은 코드를 염두에 두고 이 내용을 살펴보면 정적(static)으로 선언된 두 개의 CallSite 객체(System.Runtime.CompilerServices.CallSite 클래스)를 확인할 수 있다. 이는 예제에서 dynamic 키워드를 선언하여 선언한 변수 D에 대하여 두 번의 호출동작이 발생하였기 때문이며, 두 정적변수는 각각의 호출에 대응하여 선언된 변수이다.

MSDN은 CallSite 클래스에 대하여, “동적 호출 사이트의 기본 클래스입니다. 이 형식은 동적 사이트 대상에 대한 매개 변수 형식으로 사용됩니다.” 라고 설명하고 있다(MSDN: CallSite Class). 일반 사용자들에게는 CallSite의 클래스의 세부적인 정의나 동작은 필요하지 않을 것이다. dynamic 키워드를 이해하고 사용하는 수준에서는 CallSite 클래스의 역할을 개념적으로 이해하면 충분할 것이며, 이러한 이해는 “Call Site”라는 용어의 뜻을 살펴봄을 통해 얻을 수 있다.

In programming, a call site of a function (subroutine) is a line in the code which calls (or may call, through dynamic dispatch) a function. A call site passes zero or more arguments to the function, and receives zero or more return values.(Wikipedia: Call Site)

결국 dynamic으로 선언된 변수의 호출을 위한 ‘매개변수를 비롯한 호출 정보’가 CallSite 객체에 저장된다. 좀 더 자세히 ILDasm의 정보를 분석해 본다면, 이러한 CallSite 클래스는 제네릭을 사용하여 매개변수화 되어 있으며 ILDasm의 정적변수 선언 가장 마지막에 포함된 ‘string’이라는 정보는 예제의 함수 호출 시에 포함된 매개변수 타입이라는 점을 알 수 있다. 또한 ‘string’ 선언 앞에 위치한 ‘object’는 dynamic 변수에 담겨있는 내용이 될 것이다. 즉, dynamic으로 선언된 변수는 일종의 범용 Object 클래스와 유사하게 이해될 수 있으며, 그 호출에 대해서는 컴파일러의 도움을 받아 동적 지원이 가능해진다는 점을 알 수 있다.


4. Performance on Dynamic Method Calls

하지만 변수에 담길 타입을 정적으로 결정할 수 없고 런타임까지 지연시킨다는 것은, 필연적으로 런타임 시에 추가적인 비용이 소비됨을 의미한다. 컴파일된 코드가 실행되는 입장에서, 개발자가 코드 상에 기록해 둔 메소드 호출의 서명과 일치하는 실제 메소드를 수행시키는 작업은 예측될 수 없고 최후까지 보류된다. 결국 dynamic 키워드를 올바로 사용하기 위해서는 dynamic 키워드가 기존에 제공되던 방법인 리플렉션이나 기타 다른 선택가능한 접근(컴파일 시점에 타입을 확정시키는 것을 포함한)과의 성능차를 검토하고 이해하여야 한다.

가장 우려스러운 시나리오는 dynamic 변수에 저장된 값이 변경되지 않았음에도 불구하고, 새로운 호출 요청이 발생할 때 마다 모든 정보를 다시 파악하고 검사하여 바인드하는 선제 루틴들이 동작되는 상황일 것이다. 상식적으로 이러한 상황에서는 앞선 요청에서 수행하였던 정보를 바탕으로 메소드의 호출 비용을 많이 줄일 수 있을 것으로 예상할 수 있으며, 당연하게도 DLR에서 역시 이러한 기능을 제공하고 있는 것으로 보인다.

앞서 살펴본 예제의 Main에 해당하는 IL코드를 살펴보면, 29번째 줄과 8C번째 줄에 위와 같은 내용이 있음을 알 수 있다. 여기서 brtrue.s(MSDN: Ildasm.exe Tutorial)라는 인스트럭션은 조건분기문으로써, 위의 두 행은 해당 호출에 따른 CallSite가 사전에 파악된 내용이 있다면 뒤에 나타난 코드위치(6b, d2)로 이동하라는 내용을 담고 있다. 물론 CallSite가 올바르게 파악되어있지 않다면 코드를 점프하지 못하고 CallSite를 초기화하는 부분을 수행하여야만 한다. 결국 반복되는 호출 요청에 대하여 불필요한 비용의 지출 없이 효과적으로 dynamic 키워드에 대한 메소드의 호출이 수행될 수 있음을 알 수 있다.


5. Conclusion

DLR은 개발자에게 자유도 높고 간결한 런타임 지원 기능을 포함하고 있으며, dynamic 키워드를 사용하여 컨파일 시점의 타입 확정여부에 구애받지 않을 수 있음을 알 수 있었다. 또한 (기존의 리플랙션 기능에 비하여) 자동으로 효과적인 구조를 가진 IL을 생성해줌을 확인할 수 있었다. 특히 상속구조 상에서 업캐스팅(Up-Casting, 혹은 Widening) 뿐만 아니라 다운캐스팅(Down-Casting, 혹은 Narrowing)을 동적 상황에서 훌륭하게 지원해줄 수 있음을 기억해두자. 물론 코드의 성능을 위해서는 동적 기능을 최소한으로 유지해야 하겠지만, 동적 기능이 사용되어야만 하는 시점에서 DLR의 추가는 큰 이점이 되지 않을까 생각해본다.

본 문서에서는 메소드의 호출에 대한 내용만을 살펴보았으나, 이는 일부분일 뿐이며 많은 유용한 기능들이 DLR에 포함되어 있다. 아래와 같은 유용한 참조 사이트들을 함께 기록해둔다.