1. 소개
C#은 강한 타입 언어야. 즉 컴파일 단계에서 변수는 특정 타입을 가져야 하고 클래스의 멤버에 접근할 때 그 타입에 맞게 접근해야 해. 이건 안전하고 편리하며 IDE와 컴파일러에 친절하지.
하지만 가끔 컴파일 시점에 구조를 모르는 객체에 접근해야 할 때가 있어 — 예를 들면:
- .NET의 다른 언어로 작성된 코드(IronPython나 IronRuby 같은)처럼 타입이 동적인 경우.
- COM 객체(예: Interop을 통한 Microsoft Office)와 작업할 때.
- 비교적 구조가 약한 데이터 구조와 상호작용할 때: 예컨대 포맷이 일정치 않은 JSON 객체.
- 상투적인 코드를 잔뜩 쓰기 싫고, 그냥 객체를 믿고 필요한 메서드를 이름으로 호출해보고 싶을 때.
C# 4.0에 dynamic 키워드와 DLR이 도입되면서 "Python처럼 동작하는 C#" 코드를 쓸 수 있게 됐어: 검사는 컴파일 타임이 아니라 런타임에 이뤄져. 이 마법 같은 키워드를 통해 컴파일러에게 좀 덜 엄격해지라고 지시하고, 실제 프로그램이 그 지점에 도달했을 때 메서드/프로퍼티가 있는지 확인하게 만들 수 있어.
DLR이 뭐야?
Dynamic Language Runtime(DLR)은 CLR(Common Language Runtime)에 더해진 인프라 확장으로, 동적 언어의 지원(그리고 정적 언어에서의 동적 동작)을 구현할 수 있게 해줘. DLR은 컴파일러가 미리 검사할 수 없는 연산들—예를 들어 타입이 런타임에만 알려져 메서드를 이름으로 호출해야 하는 경우—을 처리해. DLR 덕분에 C#은 "동적 모드"를 갖게 되는 거지.
2. 키워드 dynamic: 어떻게 동작하는가
object와의 차이
초보자 중에는 dynamic이 단지 더 멋진 방식의 object이고 매번 캐스팅만 한다고 생각하는 경우가 있어. 그건 아니야.
- object 타입 변수는 메서드를 호출하려면 항상 구체 타입으로 명시적 캐스팅이 필요하고(컴파일러가 그걸 검사함).
- dynamic 타입 변수는 컴파일러에게 "내가 알아서 할게 — 런타임에 처리해!"라고 지시하는 거야. 만약 메서드가 없다면 실행 중에 예외가 발생해.
예제:
// object 예제
object obj = "안녕, C#!";
// obj.ToUpper(); // 컴파일 오류! 컴파일러는 obj가 문자열인지 모름
string s = ((string)obj).ToUpper(); // 이렇게 해야만 함
// dynamic 예제
dynamic dyn = "안녕, C#!";
string upper = dyn.ToUpper(); // 컴파일러가 반대하지 않음, 검사는 런타임에 일어남
dynamic로는 존재하지 않는 프로퍼티나 메서드에 접근할 수 있어. 만약 없으면 런타임에서 RuntimeBinderException이 발생할 거야.
예제 애플리케이션에서의 사용
우리 미니 앱에 구조가 런타임에 바뀔 수 있는 JSON 데이터를 다루는 블록이 있다고 해보자. dynamic이면 프로퍼티가 항상 있는 것처럼 접근할 수 있어:
dynamic user = new System.Dynamic.ExpandoObject();
user.Name = "바샤";
user.Age = 35;
Console.WriteLine($"이름: {user.Name}, 나이: {user.Age}");
3. DLR 작동 원리: 내부에서 무슨 일이 일어나는가
변수가 dynamic으로 선언되면, C# 컴파일러는 해당 객체에 대한 메서드, 프로퍼티, 인덱서, 연산자 등의 모든 접근을 미리 검사할 수 없다는 것을 기억해 둬.
대신 모든 동적 호출 자리에는 시스템의 "자유 디스패처"에 대한 호출 코드가 삽입돼. DLR은 "우리가 낙관적으로 접근한 그 메서드나 프로퍼티가 실제로 존재하나?"를 체크해. 존재하면 호출을 실행하고, 없으면 예외를 던져.
시각적 도식
+------------------------+
| dynamic 코드 |
+----------+-------------+
|
v
+----------+-------------+
| C# 컴파일러가 |
| "DLR 호출" 삽입 |
+----------+-------------+
|
v
+----------+-------------+
| 런타임에서 DLR이 |
| 메서드/프로퍼티 검색 |
+----------+-------------+
|
/---------\
| |
발견 미발견
| |
v v
메서드 예외 발생
호출 (RuntimeBinderException)
DLR은 객체 내의 메서드와 프로퍼티를 찾는 일을 담당하고, 자주 쓰이는 동적 호출은 캐시해 주기도 해!
약간의 역사
DLR은 원래 동적인 성격을 가진 언어들(IronPython, IronRuby 등)을 지원하기 위해 만들어졌고, C#에서 동적 객체들과 통합을 쉽게 해 주기 위해 도입됐어. 지금은 ExpandoObject, DynamicObject, COM, 동적으로 직렬화된 데이터 다루기에 널리 쓰이고 있어.
4. 비교: static vs dynamic
정적과 동적 패러다임에서의 프로퍼티/메서드 호출이 어떻게 다른지 비교해 보자:
| 방법 | 선언 | 컴파일 시간 검사 | 오류 발생 시 동작 |
|---|---|---|---|
|
|
호출하려면 타입으로 캐스팅 필요 | 컴파일 오류 또는 InvalidCastException |
|
|
아니요, 모두 런타임에서 | RuntimeBinderException, 메서드가 없으면 |
| 정적 타입 | |
컴파일러가 검사함 | 컴파일 오류 |
눈에 보이는 예:
// 정적 타이핑
MyUser u = new MyUser();
u.PrintInfo(); // 컴파일러가 검사함
// dynamic
dynamic du = new MyUser();
du.PrintInfo(); // 컴파일러가 검사하지 않음, 런타임에 메서드가 있는지 찾음
// object
object ou = new MyUser();
// ou.PrintInfo(); // 컴파일 오류
((MyUser)ou).PrintInfo(); // 타입으로 명시적 캐스팅 필요
정적 메서드와 dynamic
dynamic d = "Hello";
Console.WriteLine(d.Length); // 5 — 잘 동작
Console.WriteLine(d.FakeMethod()); // 실행 중에 RuntimeBinderException 발생
여기서 조심해야 할 점! 오타를 내거나 메서드 이름이 존재하지 않으면 애플리케이션 실행 중에만 드러나. 전형적인 "프로덕션에서 터짐" 상황이 될 수 있어.
5. 실전 예제: 현실 문제에서의 동적 사용
ExpandoObject와의 작업
using System.Dynamic;
dynamic person = new ExpandoObject();
person.Name = "세르게이";
person.Greet = (Action)(() => Console.WriteLine($"안녕, {person.Name}!"));
person.Greet(); // 출력: 안녕, 세르게이!
여기서는 런타임에 프로퍼티 Name을 추가하고 delegate인 Greet도 붙였어. JavaScript 객체 다루는 느낌과 비슷하지.
JSON 구조 바인딩
using System.Dynamic;
using Newtonsoft.Json;
string json = @"{""name"": ""안드레이"", ""age"": 30}";
dynamic obj = JsonConvert.DeserializeObject
(json); Console.WriteLine(obj.name); // 안드레이 Console.WriteLine(obj.age); // 30
주의: obj.surname처럼 존재하지 않는 프로퍼티에 접근하면 실행 중에 에러가 남.
COM-Interop (Office Automation)
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true; // 컴파일러는 타입을 모르는 상태에서도 Visible 프로퍼티에 접근 가능
6. 유용한 팁
DLR, dynamic과 다른 .NET 언어와의 사용
- IronPython/IronRuby: 이 언어들의 객체를 인스턴스화하고 C#에서 dynamic을 통해 메서드를 호출할 수 있어.
- COM 객체: dynamic을 통해 메서드와 프로퍼티에 대한 자동 래핑이 이뤄짐.
- 커스텀 동적 객체: IDynamicMetaObjectProvider를 구현하면 자체 클래스에 동적 호출을 구현할 수 있어.
IronPython 통합 예제
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
ScriptEngine engine = Python.CreateEngine();
dynamic py = engine.Execute("class Test: pass\nTest()");
py.x = 42;
Console.WriteLine(py.x); // 42
자체 동적 타입을 구현하는 방법
보통은 DynamicObject를 상속하는 게 제일 쉬워.
using System.Dynamic;
class MyDynamic : DynamicObject
{
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "Ping")
{
result = "Pong!";
return true;
}
result = null;
return false;
}
}
dynamic d = new MyDynamic();
Console.WriteLine(d.Ping); // Pong!
Console.WriteLine(d.Miss); // null (예외를 던지지 않음)
dynamic이 해로운 경우
- 성능 저하(동적 호출은 정적 호출보다 느림).
- 디버깅과 유지보수 복잡도 증가.
- IDE의 많은 기능 상실: 자동 완성, 리팩터링, 사용처 검색 등.
- 사용자나 서버에서만 발견되는 버그를 유발할 수 있음.
정말 dynamic을 써야 할 때
- 랩을 만들기 어렵거나 비경제적인 외부 라이브러리와 통합할 때(COM, Python, JavaScript 등).
- 약하게 타입된 구조(JSON/XML/ExpandoObject)를 조작할 때.
- 프레임워크 작성, 메타프로그래밍, 동적이어야 하는 유연한 API를 만들 때.
그 외 대부분의 경우엔 정적 타입을 쓰고 컴파일러의 도움을 누리는 게 좋아 :-)
7. dynamic과 DLR 사용 시 흔한 실수
실수 #1: 존재하지 않는 멤버에 접근하기.
dynamic에 대해 존재하지 않는 메서드나 프로퍼티를 호출하면 런타임에 RuntimeBinderException이 발생해서 개발 단계에서 잡기 어렵다.
실수 #2: 타입 검사 무시하기.
예컨대 if (obj is IDictionary<string, object>) 같은 검사 없이 호출하면 런타임에서 예기치 않은 오류가 날 수 있어.
실수 #3: public API에 dynamic 남용하기.
동적 타입은 코드 유지보수를 어렵게 해. IDE와 분석기가 올바른 멤버를 알려주지 못하니 가독성이 떨어져.
실수 #4: 성능 민감한 코드에서 dynamic 남발하기.
동적 호출은 정적보다 느리고 오버헤드가 커. 꼭 필요할 때만 dynamic을 사용하자.
GO TO FULL VERSION