아놔... 간만에 포스트를 올립니다... T_T 최근 올린 포스트라고는 마소지에 기고한 글을 HTML로 바꾸어 포스트
한 것이 전부였기에... 뭐 저도 평범한 사람입니다. 가끔은 아니 자주 삶의 고단함과 게으름에 지쳐 술쳐마시고 놀기도 하고
다른 무엇인가에 빠져 살기도 하지요... 혹시나 새로운 글을 기다린 분이 계시다면 죄송합니다...
 |
닷넷 프레임워크 2.0에서는 다양하고 많은 기능들이 추가되었음은 독자들도 잘 알고 있을 것이다. 이러한
새로운 기능들 중 리플렉션에 대한 기능들도 많이 향상되었고 기능 추가 역시 상당부분 이루어졌다. 성능적인
향상을 위해 리플렉션 내부적인 옵티마이징을 비롯하여 새로이 추가된 Generics 지원을 위한 속성들 등등
여기서 모두 나열하면 필자만 피곤해 지는 관계로 대충 넘어가도록 하겠다. (사실 잘 모른다는 거......)
이번 포스트에서는 리플렉션을 이용한 메쏘드 호출이 갖는 단점을 어느 정도 커버해 주는 Dynamic
Method에 대해 썰을 풀어 볼까 한다.
동적 메쏘드 정도로 해석할 수 있는 Dynamic Method는 동적으로 코드를 생성할 수 있는 방법
중 하나이다. 먼저 '동적'(Dynamic)이란 말을 잘 음미해 보자. 동적이란 말은 컴파일 타임이 아닌
런타임에 무엇인가를 한다는 말로 사용하곤 한다. 그렇담
"동적 메쏘드"란 얘기는 런타임에 메쏘드의 코드를
생성하고 이를 호출할 수 있다는 말이 되겠다. 물론 닷넷 프레임워크 2.0 이전의 과거에도 동적으로 메모리
상에 어셈블리를 생성하고 수행할 수 있는 방법이 있긴 있었다. 하지만 동적 메쏘드는 동적 어셈블리에 비해
보다 적은 오버헤드만을 가지며, 어셈블리가 일단 로드 되면 어플리케이션 도메인(AppDomain)이 종료될
때까지 메모리에서 해제되지 않는다는 단점을 동적 메쏘드는 갖지 않는다. 다르게 말하면 필요하면 동적 메쏘드에
의해 점유되는 메모리가 해제될 수 있다는 말도 되겠다.
동적 메쏘드를 응용 할 수 있는 분야는 생각보다 다양하다. 첫 번째로 생각해 볼 수 있는 것은
리플렉션의
InvokeMember 혹은
Invoke 메쏘드를 통해 런타임에 바인딩 하는 것 처럼 동적
메쏘드를 통해 메쏘드 호출을 수행할 수 있다. 이렇게 동적 바인딩은 컴파일 타임에 어떤 메쏘드를 호출할 수
있는지 알 수 없는 경우 자주 사용되곤 한다. 예를 들어 호출할 메쏘드의 이름을 사용자로부터 입력을 받는
경우가 되겠다.
|
요렇게만 말하면 이해를 못하는 독자들이 많을 것 같아서 전형적인 예를 들어 보겠다. ASP.NET 웹
서비스(ASMX)를 만들고 작성한 .asmx 파일을 브라우저로 브라우징 해 보면 웹 서비스에 포함된 웹 메쏘드들이
나열된다. 그리고 이 웹 메쏘드를 클릭하면 테스트를 위한 호출을 수행할 수 있게 된다. 만약 독자들이 이러한 웹 페이지를
만든다고 가정해 보면 어떤 방식으로 웹 페이지를 만들겠는가? 페이지는 어떤 메쏘드를 어떤 매개변수로 호출해야 하는지 전혀
알 수 없는 상황이기 때문에, 리플렉션을 통해 주어진 웹 서비스의 클래스로부터 메쏘드 이름, 매개변수를 알아내고 이
리플렉션 정보를 이용하여 동적으로 메쏘드를 호출해야 할 것이다. 이 때 리플렉션의 InvokeMember 혹은 Invoke
메쏘드를 사용해야만 한다.
리플렉션의 Invoke 씨리즈 메쏘드들은 적절한
CAS(Code Access Security)
권한만 있다면 private 멤버까지도 호출할 수 있는 능력을 가지고 있지만, 일반 메쏘드 호출에 비해 매우 느리다는
단점을 가지고 있다. 어떤 클래스의 메쏘드를 그냥 호출하는 것과 리플렉션의 Invoke 씨리즈를 통해 호출하는 것은 최대
1,000배 까지도 성능적인 차이를 나타낼 수 있다. (메쏘드의 내용이 어떠냐에 따라 성능 비교는 크게 달라진다. 메쏘드
내용이 오랜 시간을 소요한다면 리플렉션에 의한 오버헤드는 전체에서 극히 일부분이 될 수 있지만 메쏘드의 내용이 간단하다면
리플렉션의 오버헤드는 크게 작용될 수도 있음에 유의하자.)
동적 메쏘드는 리플렉션의 Invoke 씨리즈에 비해 훨씬 작은
호출 오버헤드만을 가진다. 그도 그럴 것이 Invoke 씨리즈 메쏘드들은 호출을 할 때마다
해당 타입에 메쏘드가 있는지 확인해야 하고 매개변수가 맞는지도 확인을 떠야 한다. 그리고 메쏘드에서 반환 값도 잘 추출해
내야지... 예외 발생되면 그에 따라 처리도 따로 해줘야지... 신경 써야할 일이 조낸 많기도 하다. 반면 동적 메쏘드는
타입과 메쏘드가 존재하는지에 대한 검사나 매개변수의 검사를 필요로 하지 않기 때문에 오버헤드를 크게 줄일 수 있다. 왜
동적 메쏘드가 그러한 검사가 필요하지 않는지는 이제 구체적인 예를 보면서 살펴보도록 하자.
동적 메쏘드에 대해 설명하기 전에 리플렉션을 사용하여 메쏘드 호출을 하는 예제부터 살펴 보도록 하자. 다음 코드는
리플렉션을 이용하여 String 클래스의 internal 멤버인 FillStringChecked 정적(static)
메쏘드를 호출하는 것을 보여준다. FillStringChecked 메쏘드는 주어진 문자열의 내용을 바꾸는 메쏘드로써 닷넷
프레임워크 내부에서만 사용된다. (닷넷 프레임워크 내부에서는 문자열 객체가 immutable 이란 말은 구라가 되겠다.
조낸 쉽게 바꿀 수 있다...)
Type
stringType = typeof(string);
MethodInfo
mi = stringType.GetMethod("FillStringChecked",
BindingFlags.NonPublic |
BindingFlags.Static);
string
s = "xxx: The Original String";
mi.Invoke(null,
new
object[] { s, 0, "yyy: The
Changed " });
Console.WriteLine(s);
위 코드를 수행하면 문자열 s 의 내용이 바뀌게 된다. 어찌 되었건 리플렉션을 통해 메쏘드에 대한 정보를 MethodInfo 클래스에 담아
올 수 있다. 그리고 MethodInfo 클래스의 Invoke 메쏘드를 통해 FillStringChecked 를 호출하는 것이다. 참고로
FillStringChecked 메쏘드는 3개의 매개변수를 취하며 첫 번째 매개변수는 바꾸고자 하는 문자열 객체를, 두 번째 매개변수는 문자열
내에서 바꿀 위치 인덱스를, 마지막으로 세 번째 메쏘드는 바꾸고자 하는 새로운 문자열이 되겠다. 따라서 위 코드의 결과는 "yyy: The
Changed String" 가 된다. FillStringChecked 메쏘드가 문자열을 통째로 바꾸는 것이 아니라 기존 문자열에
문자들을 복사해 넣는 것이므로 뒤 꽁무니에 String 이란 문자열이 남아 있음에 유의하자.
동적 메쏘드를 이용하여 위에서 보인 코드와 동등한 코드를 작성해 보도록 하자. 이 예제를 통해 어떻게 동적 메쏘드를
사용하는지 알 수 있을 것이다. 백문이 불여일견이라 했다. 코드부터 까보자.
1 delegate
void
FillStringDelegate(string
dest, int destPos,
string src);
2
3 class
Program
4 {
5
static
void Main(string[]
args)
6
{
7
Type stringType =
typeof(string);
8
MethodInfo mi =
stringType.GetMethod("FillStringChecked",
BindingFlags.NonPublic |
BindingFlags.Static);
9
DynamicMethod dm =
new
DynamicMethod(
10
"FillString",
11
typeof(void),
12
new
Type[] {
typeof(string),
typeof(int),
typeof(string)
},
13
typeof(Program).Module,
14
true);
15
ILGenerator iLGenerator =
dm.GetILGenerator();
16
iLGenerator.Emit(OpCodes.Ldarg_0);
17
iLGenerator.Emit(OpCodes.Ldarg_1);
18
iLGenerator.Emit(OpCodes.Ldarg_2);
19
iLGenerator.Emit(OpCodes.Call,
mi);
20
iLGenerator.Emit(OpCodes.Ret);
21
22
string s =
"xxx: The Original String";
23
FillStringDelegate
FillString = (FillStringDelegate)dm.CreateDelegate(typeof(FillStringDelegate));
24
FillString(s, 0, "vvv: Is Changed
?");
25
Console.WriteLine(s);
26
}
27 }
먼저 리플렉션을 통해 String 클래스의 정적 메쏘드인 FillStringChecked 메쏘드의 MethodInfo 객체를 알아내는 것은
동일하다(라인 7~8). 이제 동적 메쏘드를 생성하기 위해 System.Reflection.Emit 네임스페이스의
DynamicMethod 클래스의 인스턴스를 생성한다. 이 클래스의 생성자의 상세한 내용은 MSDN을 디비보기 바란다. 이 예제에서
사용된 생성자만을 설명하자면, 첫 번째 매개변수는 동적 메쏘드의 이름으로 사용된다. 두 번째 매개변수는 동적 메쏘드의 반환형(return
type)을 나타내고, 세 번째 매개변수는 Type에 대한 배열로서 동적 메쏘드의 매개변수들의 타입이 되겠다. 호출 하고자하는
String.FillStringChecked 메쏘드는 세 개의 매개변수를 가지며 각각의 타입이 문자열, 정수형, 그리고 문자열 이므로 라인
12와 같은 매개변수를 생성했다. 네 번째 매개변수는 이 동적 메쏘드를 어떤 모듈의 일부로서 간주할 것인가를 나타낸다. 이렇게 모듈에 연결된
동적 메쏘드는 해당 모듈에 선언된 public 멤버들과 internal 멤버들을 호출할 수 있게 된다. 즉, 특정 모듈에 연결된 메쏘드는 해당
모듈에 선언된 메쏘드와 동등한 자격을 갖게 된다는 말이 되겠다. 생성자의 마지막 매개변수는 JIT 컴파일러의 가시성(visibility) 검사를
스킵 할 것인가를 나타낸다. 이 매개변수가 true 이면 JIT는 가시성 검사를 수행하지 않으므로 클래스의 private 멤버나
protected 멤버를 호출하는 코드를 동적 메쏘드 내에 포함시킬 수 있게 된다.
이런 맥락에서 라인 9~14까지의 코드는 String 클래스의 internal 멤버인 FillStringChecked
메쏘드를 호출하기 위해 다음 코드와 비스므레한 동적 메쏘드 선언을 수행한 것이 되겠다.
static
void FillString(string
arg1, int arg2,
string arg3)
{
// 메쏘드의 body는 아직 정의되지 않음
}
이제 동적 메쏘드의 본문(body)을 '작성' 해야 할 차례이다. C# 코드를 명시할 수 있었으면 얼마나 좋으련만 현실은 그렇지 않다.
IL 코드를 생성해야만 한다. 이를 위해 DyanmicMehtod 클래스의
GetILGenerator() 메쏘드를 호출하여
ILGenerator 객체를 얻어 내고 이 객체의
Emit() 메쏘드를 반복적으로 호출하여 메쏘드 본문을 말 그대로 생성해 내야 한다. 이 과정을 라인 15~20까지에서 보여주고 있다.
MSIL 코드를 모른다면 어쩔 수 없다. 대략 DynamicMethod 클래스와 Emit 메쏘드 호출을 통해 생성된 동적 메쏘드 코드를 C#으로
표현해 보자면 다음 정도가 될 것이다.
static
void FillString(string
arg1, int arg2,
string arg3)
{
String.FillStringChecked(arg1,
arg2, arg3);
}
비록 FillStringChecked 메쏘드가 internal 멤버이지만 가시성(visibility) 검사를 수행하지 않도록 동적 메쏘드를
생성했으므로 호출이 가능한 것이다. 그리고 호출할 메쏘드의 정보는 리플렉션을 통해 구해 놓은 MethodInfo 객체가 사용되었음(라인
19)에도 유념하도록 하자.
이제 동적 메쏘드가 정의되었으므로 이를 호출하기만 하면 된다. 하지만 동적 메쏘드는 런타임에 생성된 메쏘드이므로 C#
코드 상에서 이를 정적으로 참조할 방법이 없다. 따라서 메쏘드에 대한 포인터 정도가 되는 delegate를 사용하면 된다.
DynamicMethod 클래스의
CreateDelegate 메쏘드는 정확하게 바로 이런 용도로 사용된다. CreateDelegate 메쏘드를
호출하여 생성된 동적 메쏘드에 대한 delegate를 구할 수 있고 이 delegate를 이용하여 메쏘드 호출이 가능한
것이다. 라인 23-24 는 이러한 코드를 보여주고 있으며 필요한 delegate의 선언 역시 라인 1에서 찾을 수 있을
것이다.
동적 메쏘드의 사용법은 대충 알겠지만 이것이 정말로 동적 호출에 사용될 수 있을지는 잘 이해가 안 될 것이다. 위
코드에서 메쏘드의 정보를 구하는 코드(라인 8)가 특정 타입의 특정 메쏘드가 아닌 사용자가 입력하는 클래스의 메쏘드라고
가정을 해보자. 이 경우 프로그램은 사용자가 어떤 메쏘드를 입력할 것인가 알 수 없기 때문에 정적인 코드를 작성할 수
없다. 대신 동적 메쏘드를 생성하는 코드만을 작성해 놓고 이 코드가 사용자가 입력한 메쏘드의 MethodInfo로 부터
호출하는 동적 메쏘드를 작성할 수 있을 것이다.
동적 메쏘드는 동적 호출 뿐만 아니라 사용자의 입력
조건에 따라 메쏘드의 본문이 바뀌는 코드를 런타임에 '생성'하는 용도로 사용될 수 있다.
실제로 MSDN에서 동적 메쏘드를 사용할 수 있는 다양한 시나리오를 제시하고 있다(Reflection
Emit Dynamic Method Scenarios 문서 참조). 하지만 동적 메쏘드를 독자 여러분이 사용할
기회는 많지 않으리라고 본다. 필자 역시 실무 프로젝트에서 닷넷 프레임워크 클래스의 private 멤버를 호출하는 용도로
두 번밖에 사용해 본적이 없다.
동적 메쏘드가 많이 사용되기 어려운 또 다른 이유는
MSIL 코드를 '생성' 해야 한다는 점이다. 그렇다. MSIL 수준으로 메쏘드의 본문을
작성하기란 쉽지 않다. 아니 조낸 어렵다. 하지만 MSIL을 이용하여 메쏘드 본문을 자유로이 생성할 수 있는 능력이 있는
독자라면 동적 메쏘드를 이용하여 다양한 문제 해결에 응용할 수 있을 것이다. 시간이 뎀비는 독자라면 MSIL 도 별도로
공부를 해보는 것이 좋을 것이다.
뭐네 뭐네 해도 리플렉션의 Invoke 씨리즈를
이용하는 것보다는 동적 메쏘드가 수십 배 빠르므로 성능적인 고려를 해야 하는 상황이라면 동적
메쏘드를 사용하는 것이 보다 나은 선택이 될 수 있다는 것 정도는 알아 두도록 하자.
Epilog
조낸 어려운 토픽을 쓴 감이 없지 않다. 잘 이해가 안 갈지도 모르겠지만 언젠가 이 글의 내용에 무릎을 탁 칠 날이
온다면 스스로의 내공이 상당히 향상되었음을 자부해도 된다. 아놔... 간만에 쓴 포스트가 영... T_T