'이것이 C#이다 개정판'의 chapter 17을 정리한 글입니다.
dynamic
dynamic 형식은 C#의 정적 형식 시스템에서 예외적으로 컴파일 타임에 형식 검사를 건너뛰고, 형식 확인 및 멤버 바인딩을 프로그램 실행 시점(런타임)으로 연기할 수 있도록 해주는 특별한 형식입니다.
dyanimc 핵심 특징
- 런타임 바인딩
- dynamic 형식의 변수에 대해 어떤 연산(메서드 호출, 속성 접근, 필드 접근, 인덱서 사용, 연산자 사용 등)을 수행하더라도, 컴파일러는 해당 연산이 유효한지 검사하지 않습니다.
대신, 해당 연산에 대한 정보와 대상 객체를 함께 '동적 언어 런타임(DLR, Dynamic Language Runtime)'에 넘깁니다.
실제 연산이 성공할지 실패할지는 프로그램이 실행될 때 해당 객체의 실제 런타임 형식에 따라 결정됩니다.
- dynamic 형식의 변수에 대해 어떤 연산(메서드 호출, 속성 접근, 필드 접근, 인덱서 사용, 연산자 사용 등)을 수행하더라도, 컴파일러는 해당 연산이 유효한지 검사하지 않습니다.
- 정적 형식 선언
- dynamic 자체는 컴파일 타임에 존재하는 형식 키워드입니다. dynamic d = ...;과 같이 변수 선언이 가능합니다.
하지만 이 변수가 어떤 형식의 값을 담고 있는지는 컴파일러가 알지 못하거나 무시합니다.
- dynamic 자체는 컴파일 타임에 존재하는 형식 키워드입니다. dynamic d = ...;과 같이 변수 선언이 가능합니다.
- 어떤 형식의 값도 가능
- object 형식과 유사하게 dynamic 변수는 값 타입이든 참조 타입이든 모든 형식의 객체를 담을 수 있습니다.
var 형식 및 object 형식과의 차이점
dynamic 형식은 var나 object와 혼동될 수 있지만, 근본적으로 다릅니다.
- var
- 컴파일러가 초기화 식을 보고 변수의 형식을 추론합니다.
- 추론된 형식은 정적 형식이며, 이후 변수에 대한 모든 연산은 컴파일 타임에 엄격하게 검사됩니다.
- object
- 어떤 형식의 객체든 담을 수 있는 정적 형식입니다.
- object 형식 변수에 대해 object 클래스에 정의되지 않은 멤버에 접근하려면 명시적인 형식 캐스팅이 필요합니다.
- 캐스팅 없이는 컴파일 타임에 해당 멤버에 접근할 수 없습니다.
- dynamic
- 어떤 형식의 객체든 담을 수 있는 정적 형식이지만, 이 형식 변수에 대한 연산은 컴파일러가 검사하지 않고 런타임으로 넘깁니다.
- 명시적인 캐스팅 없이도 어떤 멤버든 호출/접근하는 코드를 작성할 수 있으며, 성공 여부는 실행 시점에 결정됩니다.
dynamic이 필요한 이유
[ 덕 타이핑(Duck Typig)의 관점 ]
덕 타이핑은 객체의 형식보다는 해당 객체가 가진 메서드나 속성 등 '행동(Behavior)'에 기반하여 호환성을 판단하는 프로그래밍 스타일입니다.
즉, 어떤 객체가 필요한 메서드나 속성을 가지고 있다면, 그 객체의 실제 형식이 무엇이든 상관없이 사용가능하다고 판단하는 방식입니다.
C#은 기본적으로 강력한 정적 타이핑 언어입니다. 컴파일 시점에 모든 변수의 형식이 결정되고, 해당 형식에 정의된 멤버만 호출할 수 있는지 엄격하게 검사합니다.
기본적으로 덕 타이핑은 C#의 기본 철학과 상반되지만, C#에서 덕 타이핑이 가능한 dynamic이 필요한 이유는 정적 타이핑의 제약을 우회하고 유연성을 확보하기 위함입니다.
덕 타이핑은 상속 관계를 이용하지 않기 때문에 프로그램의 동작에 관여하는 부분만 확인하기 때문에 가능합니다.
[예제]
using System;
namespace DuckTyping
{
class Duck
{
public void Walk()
{ Console.WriteLine(this.GetType() + ".Walk"); }
public void Swim()
{ Console.WriteLine(this.GetType() + ".Swim"); }
public void Quack()
{ Console.WriteLine(this.GetType() + ".Quack"); }
}
class Mallard : Duck
{ }
class Robot
{
public void Walk()
{ Console.WriteLine("Robot.Walk"); }
public void Swim()
{ Console.WriteLine("Robot.Swim"); }
public void Quack()
{ Console.WriteLine("Robot.Quack"); }
}
class MainApp
{
static void Main(string[] args)
{
dynamic[] arr = {new Duck(), new Mallard(), new Robot()};
foreach(dynamic duck in arr)
{
Console.WriteLine(duck.GetType());
duck.Walk();
duck.Swim();
duck.Quack();
Console.WriteLine();
}
}
}
}

[ COM과 .NET 사이의 상호 운용성의 관점 ]
COM은 Component Object Model의 약자로, 마이크로소프트의 소프트웨어 컴포넌트 규겨을 말합니다.
서로 다른 프로그래밍 언어로 작성되었거나 다른 프로세스에 실행되는 소프트웨어 컴포넌트들이 서로 상호작용하고 기능을 공유할 수 있도록 하는 이진 표준입니다.
기능을 제공하는 컴포넌트와 그 기능을 사용하는 애플리케이션을 분리하여, 한 번 만들어진 컴포넌트를 다양한 애플리케이션에서 재사용할 수 있도록 하고, 컴포넌트가 어떤 프로그래밍 언어로 구현되었듯 상관없이 COM 표준을 따른다면 다른 언어로 작성된 코드에서도 해당 컴포넌트를 호출하고 사용할 수 있습니다.
C#을 비롯한 .NET 언어들은 RCW(Runtime Callable Wrapper)를 통해서 COM 컴포넌트를 사용할 수 있습니다.
RCW는 .NET이 제공하는 Type Library Importer(tlbimp.exe)를 이용해서 만들 수 있는데, 비주얼 스튜디오를 사용해서 COM 객체를 프로젝트 참조에 추가하면 IDE가 자동으로 tlbimp.exe를 호출해 RCW를 만들어줍니다.
또한, COM에 대한 프록시 역할을 함으로써 C# 코드에서 .NET 클래스 라이브를 사용하듯 COM API를 사용할 수 있게 해줍니다.
※ RCW는 .NET 런타임(CLR)이 관리되지 않는 COM 객체를 관리되는 .NET 코드에서 사용할 수 있도록 포장(Wrap)해주는 역할을 하는 개체입니다.
하지만 C#은 RCW가 있어도 여전히 COM과 서먹한 부분이 있었는데, 이 원인은 다음 두 가지입니다.
- COM은 메서드가 결과를 반환할 때 실제 형식이 아닌 object 형식으로 반환합니다. 이 때문에 C# 코드에서는 이 결과를 실제 형식으로 변환해줘야 하는 번거로움이 있었습니다.
- COM은 오버로딩을 지원하지 않습니다. 그 대신 메서드의 선택적 인수와 기본값을 지원합니다.
C#은 4.0 버전으로 업그레이드되기 전까지는 선택적 인수와 기본값을 지원하지 못했습니다.
그렇기에 C# 코드에서 COM API를 호출할 때 사용하지도 않을 인수를 수없이 입력해야 하는 번거로움이 있었습니다.
이 원인을 해결한 것이 C# 4.0버전의 dynamic 형식의 도입과, 메서드의 선택적 인수 기본값 도입 기능입니다.
이를 통해 비로소 C#도 VB처럼 COM 친화적인 언어가 될 수 있었습니다.
마이크로소프는 워드를 비롯해서 파워포인트, 엑셀 등 오피스 제품들의 기능을 코드에서 이용할 수 있도록 이 소프트웨어들을 COM 컴포넌트로 구성해놨습니다.
해당 예제는 엑셀의 COM 컴포넌트를 이용해서 문서를 생성하고 그 안에 데이터를 넣은 후 저장까지 하는 코드를 다음표에 작성했습니다.
[예제 전 준비사항]
1. 솔루션 탐색기에서 '종속성' 항목에 대해 [COM 참조 추가] 항목을 선택합니다.

2. 참조 관리자 창에서, [COM] - [형식 라이브러리] 항목의 구성 요소 목록 중 'Microsoft Excel 16.0 Object Library'를 선택한 후
<확인>합니다.
※ 엑셀 2019 이전 버전이라면 15.0을 선택합니다.

3. 솔루션 탐색기의 '종속성' 항목의 'COM' - 'Interop.Microsoft.Office.Interop.Excel' 항목을 선택 후,
속성을 열어 [Interop 형식 포함] 항목 값을 "예"로 변경합니다.


[예제]
using System;
using Excel = Microsoft.Office.Interop.Excel;
namespace COMInterop
{
class MainApp
{
// C# 4.0 이전 버전 코드, '메서드의 선택적 인수 기본값 도입 기능'이 없을 때
public static void OldWay(string[, ] data, string savePath)
{
Excel.Application excelApp = new Excel.Application();
excelApp.Workbooks.Add(Type.Missing);
Excel.Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet;
for(int i = 0; i < data.GetLength(0); i++)
{
((Excel.Range)workSheet.Cells[i + 1, 1]).Value2 = data[i, 0];
((Excel.Range)workSheet.Cells[i + 1, 2]).Value2 = data[i, 1];
}
workSheet.SaveAs2(savePath + "\\shpark-book-old.xlsx",
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing,
Type.Missing);
excelApp.Quit();
}
// C# 4.0 이상 버전 코드, '메서드의 선택적 인수 기본값 도임 기능'이 있을 때
public static void NewWay(string[,] data, string savePath)
{
Excel.Application excelApp = new Excel.Application();
excelApp.Workbooks.Add();
Excel.Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet;
for (int i = 0; i < data.GetLength(0); i++)
{
workSheet.Cells[i + 1, 1] = data[i, 0];
workSheet.Cells[i + 1, 2] = data[i, 1];
}
workSheet.SaveAs2(savePath + "\\shpark-book-dynamic.xlsx");
excelApp.Quit();
}
static void Main(string[] args)
{
string savePath = System.IO.Directory.GetCurrentDirectory();
string[,] array = new string[,]
{
{ "좋은날", "2010" },
{ "밤편지", "2017" },
{ "팔레트", "2017" },
{ "Blueming", "2019" },
{ "에잇", "2020" },
{ "드라마", "2021" },
{ "라일락", "2021" },
{ "홀씨", "2024" },
{ "Love wins all", "2024" }
};
Console.WriteLine("Creating Excel document in old way...");
OldWay(array, savePath);
Console.WriteLine("Creating Excel document in new way...");
NewWay(array, savePath);
}
}
}



[ 동적 언어와의 상호 운용성의 관점 ]
CLR은 본래 IL로 컴파일할 수 있는 언어들은 지원하지만, 파이썬이나 루비처럼 실행할 때 코드를 해석해서 실행하는 방식의 동적 언어는 지원할 수 없습니다.
그래서 마이크로소프트는 동적 언어를 실행할 수 있도록 하는 플랫폼인 DLR(Dynamic Language Runtime)을 선보였습니다.
DLR은 CLR 위에서 동작하며, 파이썬이나 루비와 같은 동적 언어를 실행할 수 있습니다.
IronPython, IronRuby 등 .NET 위에서 실행되는 동적 언어로 작성된 객체를 C#에서 다루는 것이 가능하고, C# 코드가 동적 언어 객체에 대해 컴파일 시점(정적 분석 단계)에는 그 유효성을 확신하거나 검증할 수 없는 코드의 형태에도 접근하는 것이 가능합니다.
즉, C# 코드에서 직접 파이썬이나 루비 코드를 실행하고 그 결과를 받아 볼 수 있으며, CLR 입장에서 보면 DLR API를 기반으로 구현된 동적 언어라도 호스팅할 수 있다는 것입니다.
그리고 C#의 dynamic 키워드는 바로 이러한 DLR의 기능을 C# 언어 차원에서 활용할 수 있도록 해줍니다.
CLR과 DLR 사이의 상호 운용성 문제를, 미리 형식 검사를 할 수 없는 동적 형식 언어에서 만들어진 객체를 C#의 dynamic 형식이 받아주는 방식으로 해결한 것입니다.
C# 컴파일러는 dynamic으로 선언된 변수에 대한 연산을 만나면, 이를 DLR이 처리할 수 있는 형태로 변환하고, 런타임에 DLR을 통해 실제 호출이 이루어지게 합니다.
[DLR이 제공하는 클래스]
C# 코드에서 동적 언어를 호스팅하기 위해서는 해당 클래스들의 도움을 받아야 합니다.
| 클래스 | 설명 |
| ScriptRuntime | 동적 언어를 호스팅하는 시작점입니다. ScriptRuntime 클래스는 참조된 어셈블리나 전역 객체 같은 전역 상태를 나타내며 하나의 .NET AppDomain 안에 여러 개의 ScriptRuntime 인스턴스를 만들 수 있습니다. |
| ScriptScope | 기본적으로 네임스페이스를 나타냅니다. 호스트(즉, C# 코드)는 ScriptScope 객체 안의 동적 언어 코드에서 사용하는 변수에 값을 대입하거나 읽을 수 있습니다. |
| ScriptEnigne | 스크립트 엔진은 언어의 구문을 나타내는 일꾼입니다. 스크립트 엔진은 코드를 실행하고 ScriptScope와 ScriptSource를 생성하는 다양한 방법을 제공합니다. |
| ScriptSource | 이 클래스는 소스 코드를 읽어들이는 여러 메서드와 읽어들인 소스 코드를 다양한 방법으로 실행하는 메서드들을 제공합니다. |
| CompiledCode | 이 클래스는 컴파일된 코드를 나타냅니다. 한 번 컴파일해놓고 여러 번 반복해서 실행하는 코드를 나타내는 데 사용합니다. |
- ScriptRuntime 객체는 소스 코드 "파일"의 경로를 넘겨받아 실행할 수 있습니다.
- 프로그램을 실행할 때 생성한 문자열에 담긴 동적 언어 코드를 실행하기 위해서는 ScriptEngine, ScriptScope, ScriptSource 클래스를 이용하면 됩니다.
[예제 전 준비사항]
1. 비주얼 스튜디오 메뉴에서 [도구] → [NuGet 패키지 관리자] → [패키지 관리자 콘솔]을 엽니다.

2. 패키지 관리자 콘솔에서 'Install-Package IronPyhon' 명령어를 입력합니다.
NuGet 패키지가 인터넷에 접속해서 자동으로 DLR과 IronPython을 내려받고, 필요한 클래스 라이브러리 참조를 프로젝트에 추가해줍니다.

[예제]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;
using IronPython.Hosting;
namespace WithPython
{
class MainApp
{
public static void Main(string[] args)
{
ScriptEngine engine = Python.CreateEngine();
ScriptScope scope = engine.CreateScope();
scope.SetVariable("n", "홍길동");
scope.SetVariable("p", "010-123-4567");
// 파이썬 코드에서 클래스 선언
ScriptSource source = engine.CreateScriptSourceFromString(
@"
class NameCard :
name = ''
phone = ''
def __init__(self, name, phone) :
self.name = name
self.phone = phone
def printNameCard(self) :
print (self.name + ', ' + self.phone)
NameCard(n, p)
");
// 파이썬 코드를 실행하여 그 결과를 반환합니다.
// NameCard() 생성자를 호출했으니 NameCard 객체가 생성되어 반환됩니다.
dynamic result = source.Execute(scope);
// result 객체의 메서드를 호출할 수도 있고, 필드에도 접근하는 것이 가능합니다.
result.printNameCard();
Console.WriteLine("{0}, {1}", result.name, result.phone);
}
}
}

dynamic 사용의 단점
- 컴파일 타임 오류 검출 불가
- 가장 큰 문제점으로, 존재하지 않는 멤버를 호출하거나, 잘못된 수/형식의 매개변수로 메서드를 호출하는 등의 오류가 컴파일 시점에 잡히지 않고 프로그램 실행 중에야 발견됩니다.
- 컴파일 시점에 오류 검출이 불가해 디버깅을 어렵게 만들고 프로그램의 안정성을 저해합니다.
- 성능 저하
- 런타임에 DLR을 통해 멤버를 확인하고 바인딩하는 과정은 정적으로 바인딩된 코드 실행보다 오버헤드가 큽니다.
- 성능에 민감한 부분에서는 dynamic 사용을 피해야 합니다.
- 개발 도구 지원 제한
- 컴파일러가 형식을 모르므로 IntelliSense, 자동완성, 리팩토리 기능 등의 지원이 제한적입니다.
- 코드 가독성 저하
- 변수의 실제 형식을 파악하기 어렵고, 어떤 연산이 가능한지 코드를 읽는 시점에는 알기 어렵기 때문에 코드의 가독성과 유지보수성이 떨어질 수 있습니다.
'C#' 카테고리의 다른 글
| [C#] 스레드 (Thread) (0) | 2025.05.23 |
|---|---|
| [C#] 파일과 디렉터리 다루기, 스트림, 객체 직렬화 (0) | 2025.05.23 |
| [C#] 리플렉션과 애트리뷰트 (0) | 2025.05.13 |
| [C#] LINQ (0) | 2025.05.12 |
| [C#] 람다식, 식 트리 (0) | 2025.05.12 |