Dynamic

Dynamic

Reflection
Published

May 17, 2025

Abstract

dynamic은 컴파일 타임 형식 검사를 생략하고 런타임에 형식 및 멤버 접근의 유효성을 확인하는 특별한 형식입니다. 이를 통해 정적 형식 언어인 C#에서도 동적 형식 지정의 유연성을 활용할 수 있으며, 특히 COM 상호 운용성이나 동적 언어와의 연동 시 유용합니다.

dynamic 형식 소개

C#은 전통적으로 강력한 형식의 정적 형식 언어입니다. 변수를 선언할 때 특정 형식을 지정해야 하며, 컴파일러는 해당 형식에 정의되지 않은 멤버(메소드, 속성 등)에 접근하려고 하면 컴파일 오류를 발생시킵니다. dynamic 키워드는 이러한 컴파일 타임 형식 검사를 우회하도록 지시합니다. dynamic으로 선언된 변수는 기본적으로 object 형식처럼 모든 형식을 담을 수 있지만, 해당 변수를 통해 멤버에 접근할 때 컴파일러는 유효성 검사를 하지 않습니다. 대신, 실제 멤버 호출이나 접근은 프로그램 실행 중(런타임)에 이루어지며, 이때 해당 멤버가 존재하지 않으면 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException 예외가 발생합니다.

using System;

public class DynamicIntro
{
    public static void Main(string[] args)
    {
        // 정적 형식: 컴파일 타임에 형식이 결정되고 검사됨
        string staticStr = "Hello";
        
        // 컴파일 오류! string에 NonExistentMethod 없음
        // Console.WriteLine(staticStr.NonExistentMethod()); 

        // dynamic 형식: 컴파일 타임 검사 생략
        dynamic dynamicVar = "Hello";
        Console.WriteLine(dynamicVar.ToUpper()); // 런타임에 string의 ToUpper() 호출 -> 성공
        Console.WriteLine(dynamicVar.Length);    // 런타임에 string의 Length 속성 접근 -> 성공

        try
        {
            Console.WriteLine(dynamicVar.NonExistentMethod()); // 컴파일 시점에는 오류 없음
        }
        catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex)
        {
            Console.WriteLine($"\n런타임 오류 발생: {ex.Message}");
        }

        // dynamic 변수에 다른 형식의 값 할당 가능
        dynamicVar = 123;
        Console.WriteLine(dynamicVar + 1); // 런타임에 int + int 연산 수행 -> 124

        dynamicVar = new { Name = "Alice", Age = 30 }; // 익명 형식 할당
        Console.WriteLine(dynamicVar.Name); // 런타임에 익명 형식의 Name 속성 접근 -> Alice
    }
}

dynamicobject와 유사해 보이지만 중요한 차이가 있습니다. object 형식의 변수에 담긴 값의 멤버를 사용하려면 명시적으로 형변환(Casting)해야 하지만, dynamic은 형변환 없이 바로 멤버에 접근할 수 있습니다. 단, 그 책임은 런타임으로 미뤄집니다.

덕 타이핑 (Duck Typing)

덕 타이핑은 객체의 실제 형식이 무엇인지보다는, 객체가 특정 메소드나 속성을 가지고 있는지(즉, ‘오리처럼 걷고, 오리처럼 꽥꽥거리는지’)에만 관심을 두는 프로그래밍 스타일입니다. dynamic 형식은 C#에서 덕 타이핑을 구현하는 주요 수단입니다. 정적 형식 시스템에서는 특정 인터페이스를 구현하거나 특정 기본 클래스를 상속해야만 해당 타입으로 취급하고 관련 메소드를 호출할 수 있습니다. 하지만 dynamic을 사용하면 객체가 특정 인터페이스나 기본 클래스를 따르지 않더라도, 런타임에 필요한 멤버(메소드, 속성)를 가지고 있기만 하면 해당 멤버를 호출할 수 있습니다.

using System;

public class Duck
{
    public void Quack() { Console.WriteLine("Duck: Quack!"); }
    public void Walk() { Console.WriteLine("Duck: Walking like a duck."); }
}

public class Person
{
    public void Quack() { Console.WriteLine("Person: Trying to quack like a duck!"); }
    public void Walk() { Console.WriteLine("Person: Walking normally."); }
}

public class Robot
{
    // Quack 메소드가 없음
    public void Walk() { Console.WriteLine("Robot: Rolling forward."); }
}

public class DuckTypingExample
{
    // dynamic 매개변수를 사용하여 덕 타이핑 구현
    public static void MakeItQuackAndWalk(dynamic creature)
    {
        try
        {
            Console.WriteLine($"\n--- {creature.GetType().Name} 테스트 ---");
            creature.Quack(); // 런타임에 Quack 메소드 호출 시도
            creature.Walk();  // 런타임에 Walk 메소드 호출 시도
        }
        catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex)
        {
            Console.WriteLine($"오류: {ex.Message}"); // 해당 멤버가 없을 경우 예외 처리
        }
    }

    public static void Main(string[] args)
    {
        Duck duck = new Duck();
        Person person = new Person();
        Robot robot = new Robot();

        MakeItQuackAndWalk(duck);   // Duck 객체 전달 (Quack, Walk 모두 있음)
        MakeItQuackAndWalk(person); // Person 객체 전달 (Quack, Walk 모두 있음)
        MakeItQuackAndWalk(robot);  // Robot 객체 전달 (Walk는 있지만 Quack 없음 -> 런타임 오류 발생)
    }
}

이 예제에서 MakeItQuackAndWalk 메소드는 전달받은 객체의 실제 형식이 Duck인지, Person인지, Robot인지 컴파일 시점에는 신경 쓰지 않습니다. 단지 런타임에 Quack()Walk() 메소드를 가지고 있는지만 확인하고 호출을 시도합니다. 이것이 dynamic을 통한 덕 타이핑의 핵심입니다.

COM과 .NET 사이의 상호 운용성을 위한 dynamic 형식

COM(Component Object Model)은 Microsoft가 개발한 기술로, 다른 프로그래밍 언어로 작성된 소프트웨어 컴포넌트들이 서로 상호 작용할 수 있게 합니다. .NET 이전부터 많이 사용되었으며, 대표적으로 Microsoft Office 애플리케이션(Excel, Word 등)의 자동화(Automation)에 COM이 사용됩니다.

.NET에서 COM 컴포넌트를 사용하는 것은 가능했지만, dynamic이 도입되기 전에는 다소 번거로웠습니다.

  • PIA(주 Interop 어셈블리): COM 라이브러리에 대한 .NET 래퍼 어셈블리가 필요했습니다.
  • 형변환: COM 메소드가 종종 object 형식을 반환하므로, 반환된 객체의 멤버에 접근하려면 명시적인 형변환이 자주 필요했습니다.
  • 선택적 매개변수: COM 메소드는 선택적 매개변수를 많이 사용하는데, C#에서는 이를 처리하기 위해 System.Reflection.Missing.Value를 명시적으로 전달해야 했습니다.

dynamic 형식은 이러한 COM 상호 운용성의 복잡성을 크게 줄여줍니다. COM 객체를 dynamic 변수에 할당하면, 마치 네이티브 .NET 객체처럼 형변환 없이 직접 멤버에 접근할 수 있습니다. 또한, 컴파일러가 dynamic과 함께 사용될 때 COM 호출에 필요한 처리를 자동으로 해주므로 Missing.Value 등을 직접 사용할 필요가 없어집니다.

아래 코드는 dynamic을 사용하여 Excel COM 객체를 다루는 방식을 보여주는 개념적인 예제입니다. 실제 실행하려면 Excel이 설치되어 있어야 하고, COM 참조 설정을 해야 할 수 있습니다.

using System;
using System.Reflection; // Missing.Value 사용 비교용

public class ComInteropExample
{
    public static void Main(string[] args)
    {       
        dynamic excelAppDyn = null;
        try
        {
            // Excel.Application 객체를 동적으로 생성 (Type.GetTypeFromProgID 사용)
            Type excelType = Type.GetTypeFromProgID("Excel.Application");
            if (excelType == null)
            {
                Console.WriteLine("Excel이 설치되어 있지 않거나 COM 등록에 문제가 있습니다.");
                return;
            }
            excelAppDyn = Activator.CreateInstance(excelType);

            excelAppDyn.Visible = true; // 형변환 없이 속성 접근
            dynamic workbook = excelAppDyn.Workbooks.Add(); // 선택적 매개변수 자동 처리, 형변환 불필요
            dynamic worksheet = workbook.Worksheets[1];    // 형변환 불필요
            dynamic range = worksheet.Cells[1, 1];         // 형변환 불필요
            range.Value = "Hello COM with dynamic";      // 형변환 없이 속성 접근 (Value 또는 Value2 사용 가능)

            Console.WriteLine($"셀 (1,1)의 값: {range.Value}");

            // 잠시 대기 (Excel 창 확인용)
            Console.WriteLine("Excel을 확인 후 Enter 키를 누르면 종료합니다...");
            Console.ReadLine();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"오류 발생: {ex.Message}");
            Console.WriteLine("팁: Excel 자동화가 제대로 동작하려면 Excel이 설치되어 있어야 하고, 경우에 따라 권한 설정이 필요할 수 있습니다.");
        }
        finally
        {
            // 리소스 해제 시도
            if (excelAppDyn != null)
            {
                excelAppDyn.Quit(); // 종료
                // COM 객체 해제는 좀 더 복잡한 과정이 필요할 수 있음 (Marshal.ReleaseComObject 등)
            }
        }
    }
}

이처럼 dynamic을 사용하면 COM 객체 멤버 접근이 훨씬 직관적이고 간결해집니다.

동적 언어와의 상호 운용성을 위한 dynamic 형식

.NET 플랫폼 위에는 C#, VB.NET과 같은 정적 언어뿐만 아니라, IronPython, IronRuby와 같은 동적 언어도 실행될 수 있습니다. 이러한 동적 언어는 DLR(Dynamic Language Runtime) 위에서 동작합니다. DLR은 CLR(Common Language Runtime) 위에 구축되어 동적 언어 실행을 위한 서비스를 제공합니다.

dynamic 키워드는 C# 코드가 DLR 위에서 실행되는 동적 언어의 객체와 상호 작용하는 것을 매우 쉽게 만들어 줍니다. C#에서 동적 언어 라이브러리를 사용하거나, 동적 언어로 작성된 스크립트를 호스팅하고 그 결과를 받아 처리할 때, dynamic을 사용하면 동적 객체의 멤버를 마치 C# 객체처럼 자연스럽게 접근할 수 있습니다.

아래는 IronPython 스크립트를 실행하고 그 결과를 dynamic으로 받아 처리하는 개념적인 예제입니다. 실제 실행을 위해서는 IronPython NuGet 패키지를 설치해야 합니다.

// 이 예제는 개념 설명용이며, 실제 실행을 위해서는 IronPython 등의 DLR 관련 패키지 설치가 필요합니다.
// 예: NuGet에서 IronPython 설치

using System;
using IronPython.Hosting; // IronPython 패키지 필요
using Microsoft.Scripting.Hosting; // ScriptRuntime 등 DLR 핵심 클래스

public class DynamicLanguageInterop
{
    public static void Main(string[] args)
    {
        try
        {
            // DLR 호스팅 엔진 생성
            ScriptEngine engine = Python.CreateEngine();
            ScriptScope scope = engine.CreateScope();

            // Python 스크립트 정의
            string pythonScript = @"
class Calculator:
    def add(self, x, y):
        return x + y
    def multiply(self, x, y):
        return x * y

calculator = Calculator() # Python 클래스 인스턴스 생성
result_add = calculator.add(10, 5)
result_multiply = calculator.multiply(10, 5)
message = 'Hello from Python!'
";

            // Python 스크립트 실행
            engine.Execute(pythonScript, scope);

            // 스크립트에서 정의된 변수들을 dynamic으로 가져오기
            dynamic calculatorObj = scope.GetVariable("calculator");
            dynamic resultAdd = scope.GetVariable("result_add");
            dynamic resultMultiply = scope.GetVariable("result_multiply");
            dynamic message = scope.GetVariable("message");

            // dynamic 변수를 사용하여 Python 객체 멤버 접근 및 값 출력
            Console.WriteLine($"Python 메시지: {message.ToUpper()}"); // Python 문자열의 메소드 호출 시도 (성공 시)

            // calculator 객체의 메소드 호출 (C#에서 dynamic 통해)
            dynamic sum = calculatorObj.add(100, 50);
            dynamic product = calculatorObj.multiply(100, 50);

            Console.WriteLine($"Python 계산기 (add): {resultAdd} (타입: {resultAdd.GetType()})");
            Console.WriteLine($"Python 계산기 (multiply): {resultMultiply} (타입: {resultMultiply.GetType()})");
            Console.WriteLine($"C#에서 호출 (add): {sum}");
            Console.WriteLine($"C#에서 호출 (multiply): {product}");

            // 존재하지 않는 멤버 접근 시도
             try {
                 var nonExistent = calculatorObj.subtract(10, 5);
             } catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex) {
                 Console.WriteLine($"\n예상된 오류 (subtract 없음): {ex.Message}");
             }

        }
        catch (Exception ex)
        {
            Console.WriteLine($"DLR/IronPython 연동 오류: {ex.Message}");
            Console.WriteLine("팁: IronPython NuGet 패키지가 프로젝트에 설치되어 있는지 확인하세요.");
        }
    }
}

dynamic은 C#과 동적 언어 사이의 경계를 허물어, 두 세계의 장점을 조합하여 개발할 때 강력한 도구가 됩니다.