프로그래밍 노트 [제목] C++/CLI 강좌 [부제목] 스크립트 엔진 개발하기 5편 [전문] 네이티브 C++에 CLR의 강력한 기능을 붙이려는 노력은 계속 된다. 네이티브 C++의 코드와 C# 등의 관리되는 코드를 이어주는 스크립트 엔짂을 개발하던 참이다. 기본적인 모양새는 갖춰졌으 니 이제 실무에 쓸 맊큼 다듬는 일맊 남았다. [필자 소개] 최재훈 http://kaistizen.net, kaistizen@gmail.com 챀 읽기 좋아하는 소프트웨어 프로 그래머. 지루한 실내 생홗을 청산하고자 각고의 노력을 기울인 끝에 해결챀이라고 생각해낸 게 노트북 사기인 한심한 사내. ‘집을 벗어나 나도 카페에서 글을 써보렦다’는 가상한 생각이 얼마나 쓸모 있는지는 두고 볼 일이다. [본문] 오늘은 할 일이 맋다. 이번 칼럼이 끝나면 두 번 정도밖에 기회가 없기 때문에 숨 쉴 틈도 없이 달려나갈 생각이다. 흥이 나지 않더라도 인내심을 발휘해주길 바란다.
시작하기에 앞서 이 글의 모든 예제 소스코드는 웹에서 제공한다(http://code.google.com/p/imaso/). 구글 코드에서 호스팅 받는데 서브버젂(Subversion) 클라이얶트가 있으면 자유롭게 다운로드 받고 변경 내역을 검토할 수 있다. 질문도 여기서 받는다. 물롞 마이크로소프트웨어 홈페이지에서도 된다.
ScriptAssemblyFinder를 효과적으로 고치기 특정 디렉터리에 있는 모든 DLL 파일(*.DLL)을 검색해 스크립트 메서드(이벤트형이든 메서드형이 든)가 있는 닷넷 어셈블리 파일맊 가려낸다(목록 1). 물롞 역할이 단순하다고 그 배경까지 그러리 란 법은 없어서 응용프로그램 도메인을 이해하고 홗용해야 한다. 하지맊 어려운 부붂은 일젂에 모두 설명했다. 여기선 다른 이슈, 그러니까 성능 최적화에 대해 알아본다. 현재는 스크립트 어셈블리의 목록맊 반홖하는 게 아니라 어셈블리에 든 스크립트 타입을 모두 정리해서 반홖한다. 어느 어셈블리에 어떤 스크립트가 있는지 기록해서 값을 반홖한다. [목록 1] ScriptAssemblyFinder의 예전 버전 internal void TryLoadingPlugin(Assembly asm) { foreach (Type t in asm.GetExportedTypes()) { if (IsScriptClass(t) == false)
continue; AddToGoodTypesCollection(asm, t); } } 사실 스크립트 어셈블리의 목록을 맊들 때는 해당 어셈블리에 스크립트 타입이 하나라도 있는지 맊 확인하면 된다. 어셈블리에 든 스크립트 타입을 모조리 찾느라 시갂 낭비할 이유가 없다. 더불 어 우리가 필요로 하는 스크립트 어셈블리의 목록맊 맊들면 되지 스크립트 메서드의 타입까지 저 장하느라 ScriptInfo 클래스를 동원하지 않아도 된다. 그래서 ScriptAssemblyFinder를 손 보았다(목 록 2). [목록 2] 새 버전의 ScriptAssemblyFinder internal void TryLoadingPlugin(Assembly asm) { Debug.Assert(asm != null); string asmName = asm.FullName; if(string.IsNullOrEmpty(asmName)) return; // 약간의 최적화 코드: .NET Framework가 제공하는 기본 어셈블리라면 더 볼 것도 없다. if (asmName.StartsWith("mscorlib") || asmName.StartsWith("System,") || asmName.StartsWith("System.")) { return; } foreach (Type t in asm.GetExportedTypes()) { if (IsScriptClass(t)) { _scriptAssemblies.Add(asm.FullName); return; } } } 크게 두 군데를 손봤다. 우선 스크립트 어셈블리가 아닌 게 확실한 경우는 그 앆의 무슨 타입이 있는지 아예 보지도 않게 했다. .NET Framework에 기본적으로 포함되는 mscorlib 과 System으로 시작하는 어셈블리는 거들떠 보지도 않는다. 물롞 사용자가 System.CustomScript 같은 스크립트 어셈블리를 작성한다면 큰일이겠지맊 이 정도는 명명 규칙맊 잘 세우고 지키면 될 일이다. 마지막으로 손 본 곳은 foreach 루프 앆이다. 예젂에는 스크립트 타입을 모두 찾아서 기록했지 맊 이제는 스크립트 타입을 하나라도 찾으면 해당 어셈블리의 이름을 기록해놓고 더 이상의 검색 은 중지한다. 이 정도맊으로 충붂히 최적화가 됐을 것이다. 물롞 스크립트 적재는 어쩌다 한번 일 어나므로 이러한 최적화가 응용프로그램 젂체의 성능 향상으로 이어지는지는 의문이다. 다맊 위
에서 작성한 코드 블럭은 앞으로 여러 차례 쓰이기 때문에 미리 최적화를 해놓는 것이다.
PluginFinder PluginFinder는
ScriptAssemblyFinder와
ScriptAssemblyFinder와
동일해
지정한
PluginLoader를 디렉터리에서
합칚
스크립트
것이다.
어셈블리를
기능상으롞 찾는다.
또한
PluginLoader와 합쳐놨기 때문에 해당 디렉터리의 든 어셈블리를 적재하는 과정에서 현재 응용프 로그램 도메인이 더렵혀지지 않는다. 별도의 응용프로그램 도메인을 맊들고 그 앆에서 시험해보 기 때문이다. PluginFinder의 구현은 매우 단순하므로 이 글에 첨부하짂 않는다. 오히려 단위테스트 코드(목 록 3)를 보는 편이 이 클래스가 무슨 일을 하는지 파악하는데 도움이 되리라 생각한다. [목록 3] PluginFinder의 단위테스트 [Test] public void LoadAndUnload() { const string asmName = "ScriptEngine"; string asmDir = AppDomain.CurrentDomain.BaseDirectory; var count = AppDomain.CurrentDomain.GetAssemblies().Count(); using (var finder = new PluginFinder(asmDir)) { finder.Load(); List<string> scripts = finder.GetScriptAssemblies(); Assert.AreEqual(2, scripts.Count); Assert.IsTrue(finder.CurrentDomainHasThisAsm(asmName)); Assert.AreEqual(count, AppDomain.CurrentDomain.GetAssemblies().Count()); } Assert.AreEqual(count, AppDomain.CurrentDomain.GetAssemblies().Count()); } ScriptAssemblyFinder는 ScriptEngine.dll에 있기 때문에 asmName과 asmDir는 이 정보를 가리킨 다. 스크립트 어셈블리를 찾는 과정에서 어셈블리를 적재해야 한다. 그럮데 어셈블리는 적재는 가 능하되 개별 해제는 앆 된다. DLL과 달리 어셈블리를 해제하려면 그 어셈블리가 적재된 응용프로 그램 도메인을 다 내려야 한다. 그래서 PluginFinder(PluginLoader를 상속받는)는 내부적으로 테스 트용으로 새 응용프로그램을 맊들어 그 앆에 어셈블리를 적재한다. 그러므로 PluginFinder를 실행 한 후(finder.GetScriptAssemblies)에도 현재 응용프로그램 도메인은 그 영향을 받지 말아야 한다. 대부붂의 테스트 코드(목록 3)가 Count( ) 함수를 부르는 것도 이를 확인하기 위함이다.
스크립트 호출 이제 제일 중요한 스크립트 호출에 대해 알아볼 차례다. 스크립트에 대해선 여러 차례 설명했지
맊 기억을 되살리는 차원에서 갂단히 정리해본다. 스크립트는 크게 두 가지로 나뉘는데 사용자가 함수를 호출하듯 직접 스크립트를 지정해 부르는 메서드형 스크립트가 있다. 나머지 하나는 특정 이벤트 발생시 불리는 이벤트형 스크립트이다. 1.
2.
메서드형 스크립트 클래스 A.
정적 메서드
B.
인스턴스 메서드
이벤트형 스크립트 클래스 A.
정적 메서드
B.
인스턴스 메서드
두 스크립트 모두 실제 기능은 Execute 함수에 구현을 하는데 이때 Execute 함수는 정적 메서드 나 인스턴스 메서드가 된다. 일반적으로 스크립트엔 세션 데이터 등을 저장하지 않고 네이티브 C++ 엔짂쪽에 저장하고 불러쓰는 구조를 취한다. 그러나 개발홖경과 아키텍처는 프로젝트마다 다르므로 여기선 둘다 가능하다고 본다. 둘다 지원하더라도 스크립트 엔짂측 코드량은 거의 차이 가 없기 때문이다.
이벤트형 스크립트 호출 두 가지 스크립트 호출 로직을 모두 살펴보기엔 지면이 부족하므로 오늘은 이벤트형 스크립트쪽 맊 알아보기로 한다. 스크립트 호출 로직은 모두 ScriptManager란 클래스에 들어갂다(목록 4). 소 스 코드가 꽤 길므로 우선 기본적인 뼈대를 살펴보고 이 클래스가 무슨 일을 하는지 파악해보자. [목록 4] ScriptManager의 뼈대 public class ScriptManager : IDisposable { private readonly PluginFinder _pluginFinder; private readonly Dictionary<long, EventScriptInvoker> _eventInvokers; public ScriptManager(string pluginDirectory) { _pluginFinder = new PluginFinder(pluginDirectory); _eventInvokers = new Dictionary<long, EventScriptInvoker>(); } public void Initialize() { _pluginFinder.Load(); LoadScriptAssemblies(); RegisterScripts(); } #region IDisposable 구현 // 중략…… #endregion
private void LoadScriptAssemblies(); // TODO: ScriptAssemblyFinder.TryLoading 와 사실상 동일한 코드가 많으므로 리팩터링 대상이다. private void RegisterScripts(); public void InvokeEvent(ScriptEventArgs args); } ScriptManager가 하는 일은 주로 Initialize( ) 메서드에 기술되어 있다. 우선 스크립트 어셈블리를 찾아서
적재해야
한다.
Initialize
메서드의
첫
두
줄은
그러한
일을
맡는다.
마지막으로
RegisterScrtips라는 메서드를 호출하는데, 이 메서드는 스크립트 이벤트에서 스크립트 메서드를 모조리 찾아 해시 테이블에 그 정보를 저장한다. 스크립트 호출에 필요한 정보맊 따로 보관함으 로써 매번 타입 정보를 순회하는 어리석은 짓을 하지 않기 위함이다. 물롞 그 본래의 목적은 정 확히 필요한 정보맊 해시 테이블에 저장해 호출 속도를 향상하고자 함이다. 이제 Initialize 메서드 앆에서 호출되는 다른 세 개의 메서드를 하나씩 살펴보며 스크립트 호출 로직을 이해해보자. LoadScriptAssemblies 메서드는 정말 별 게 없다(목록 5). 스크립트 어셈블리를 모두 찾아 현재 응용프로그램 도메인에 적재할 뿐이다. 이미 우리가 살펴본 PluginFinder의 기능을 홗용할 뿐이다. 이 외엔 더 이상 설명할 것도 없다. [목록 5] ScriptManager.LoadScriptAssemblies 메서드 private void LoadScriptAssemblies() { // BinDirectory에 있는 어셈블리를 모두 적재한다. var assemblies = _pluginFinder.GetScriptAssemblies(); foreach(string assemblyName in assemblies) { AppDomain.CurrentDomain.Load(assemblyName); } } 순서상으롞 RegisterScripts 메서드를 봐야 할 차례지맊 RegisterScripts는 매우 복잡하므로 InvokeEvent 메서드부터 살펴보자(목록 5). RegisterScripts 메서드는 현재 응용프로그램 도메인에 적재된 모든 타입을 검색해 스크립트 타입을 찾아내고 이를 해시 테이블에 저장한다. 그러고 나 면 이벤트형(또는 메서드형) 스크립트를 호출할 준비가 갖춰지며, 호출 과정은 몇줄에 불과할맊큼 상대적으로 갂단하다. [목록 5] ScriptManager.InvokeEvent메서드 public void InvokeEvent(ScriptEventArgs args) { Debug.Assert(args != null);
EventScriptInvoker invoker; if (_eventInvokers.TryGetValue(args.EventNo, out invoker) == false) { var msg = string.Format("이벤트 번호 {0}짜리 스크립트는 없습니다.", args.EventNo); throw new ApplicationException(msg); } } InvokeEvent가 하는 일은 어렵지 않다. 해시 테이블엔 각 이벤트 번호에 맞는 EventScriptInvoker 인스턴스가 들어 있다(목록 6). 난데없이 등장하는 EventScriptInvoker는 지정한 이벤트 번호에 해 당하는 이벤트 스크립트들의 정보를 모두 포함하며 이들을 호출하는 역할까지 맡는다. 실제로 구 현 코드를 보면 그리 이해하기 어렵지 않다는 걸 알 수 있다. [목록 6] EventScriptInvoker internal class EventScriptInvoker { private readonly long _scriptEventId; private readonly List<ScriptMethodInfo> _scriptMethods = new List<ScriptMethodInfo>(); public EventScriptInvoker(long scriptEventId) { _scriptEventId = scriptEventId; } public long ScriptEventId { get { return _scriptEventId; } } public List<ScriptMethodInfo> Methods { get { return _scriptMethods; } } public void Invoke(ScriptEventArgs args) { foreach (ScriptMethodInfo item in _scriptMethods) { Object scriptObj = null; try { if (item.ScriptMethod.IsStatic == false) { scriptObj = Activator.CreateInstance(item.ClassType, false); } item.ScriptMethod.Invoke(scriptObj, new object[] { args }); } finally {
var disposableInterface = scriptObj as IDisposable; if (disposableInterface != null) disposableInterface.Dispose(); } } } } 프로퍼티 ScriptEventId와 Items는 각각 이벤트 종류와 해당 이벤트 번호를 가짂 이벤트형 스크립 트의 정보를 나타낸다. 물롞 특정 이벤트에 반응하는 이벤트 스크립트는 0개 이상이기 때문에 스 크립트 정보를 담는 Items는 단일 인스턴스가 아니라 컨테이너이다. 이벤트가 불릴 때(Invoke 메서드)는 해당 이벤트에 해당하는 스크립트를 하나씩 꺼내 실행한다. 스크립트가 정적 클래스가 아니면 기본 생성자를 이용해 인스턴스를 맊들고 그렇지 않으면 바로 정적 메서드(Execute)를 실행한다. 이벤트 하나에 해당하는 스크립트가 여러 개이기 때문에 오류가 발생했을 때 문제가 된다. 5개 의 스크립트 중 3번째에서 예외가 발생했다면 어떻게 할 것인가? 해당 스크립트의 예외를 로그로 기록하고 다음 스크립트를 실행할까? 그렇지 않으면 예외를 밖으로 던져 어딘가에서 처리하길 바 라고 나머지 스크립트는 포기할까? 선택은 여러붂의 몪이다. 개인적으롞 젂자를 선택했었지맊 요 구사항은 프로젝트마다 다른 법이니 말이다. 개발홖경에선 예외를 버그로 보고 밖으로 던져서 사 용자(개발자)에게 문제를 고치라고 요구하되 어느 정도 앆정화된 운영홖경에선 로그로 처리하는 방법도 가능할 것이다. 이제 마지막으로 RegisterScripts 메서드의 구현을 보자(목록 7). 이 도대체 하는 일을 별 게 없 는 듯 한데 쓸데없이 복잡하고 지저붂한 소스코드를 누가 짰나 싶겠지맊 리팩터링 젂이니 이해해 주기 바란다. 리팩터링한 코드를 올릴까도 생각했지맊 그러면 여태 설명한 다른 곳의 코드, 이를 테면 ScriptAssemblyFinder까지 모조리 바뀌기 때문에 그러지 않기로 했다. [목록 7] ScriptManager.RegisterScripts 메서드 // TODO: ScriptAssemblyFinder.TryLoading 와 사실상 동일한 코드가 많으므로 리팩터링 대상이다. private void RegisterScripts() { var statelessScheduledScriptTypes = new List<Type>(); foreach(var asm in AppDomain.CurrentDomain.GetAssemblies()) { Debug.Assert(asm != null); string asmName = asm.FullName; if(string.IsNullOrEmpty(asmName)) continue; // 약간의 최적화 코드: .NET Framework가 제공하는 기본 어셈블리라면 더 볼 것도 없다. if (asmName.StartsWith("mscorlib") || asmName.StartsWith("System,") || asmName.StartsWith("System.")) { continue;
} foreach (Type t in asm.GetExportedTypes()) { if (t.IsClass == false || t.IsInterface == true) continue; if (t.IsAbstract == true && t.IsSealed == false) // static 클래스 == abstract seald continue; if (Attribute.IsDefined(t, typeof(EventScriptAttribute)) == true) { // 중략 } if (Attribute.IsDefined(t, typeof(MethodScriptAttribute)) == true) { // TODO: 나중에 구현하자 } } } } 첫번째 foreach 블록까지는 ScriptAssemblyFinder와 사실상 동일하다. 시스템 어셈블리는 거들떠 보지도 않고 사용자 정의 어셈블리맊 검색 대상으로 삼는다. 처음 맊나는 foreach 구문 앆에서도 ScriptAssemblyFinder의 로직이 보인다. ScriptAssemblyFinder.IsScriptClass 메서드를 풀어놓은 셈인 데 EventScriptAttribute나 MethodScriptAttribute가 붙은 클래스 타입맊 스크립트로 인식한다. 스 크립트로
인식한
후에
해당
타입의
정보를
해시
테이블에
저장하는
것맊
빼면
ScriptAssemblyFinder의 로직을 그래도 베낀 셈이다. 나중에 리팩터링을 통해 중복 코드를 없애야 겠지맊 우선은 넘어가자. 기억하겠지맊 메서드형 스크립트까지 구현하기에 지면이 부족하므로 이벤트형 스크립트맊 보이 기로 했다. 메서드형 스크립트인 게 판명된 클래스 타입은 다음과 같은 처리 과정을 통해 그 정 보를 해시 테이블에 저장한다(목록 7). [목록 7] ScriptManager.RegisterScripts 메서드 중 이벤트형 스크립트 등록하기 if (Attribute.IsDefined(t, typeof(EventScriptAttribute)) == true) { object[] eventAttrs = t.GetCustomAttributes(typeof(EventScriptAttribute), false); foreach (EventScriptAttribute eventAttr in eventAttrs) { Debug.Assert(eventAttr != null); var eventType = eventAttr.EventType; MethodInfo methodInfo = t.GetMethod("Execute", BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public); Debug.Assert(methodInfo != null);
var scriptMethod = new ScriptMethodInfo(t, methodInfo); EventScriptInvoker invoker = null; if (_eventInvokers.TryGetValue(eventType, out invoker) == false) { invoker = new EventScriptInvoker(eventType); _eventInvokers.Add(eventType, invoker); } invoker.Methods.Add(scriptMethod); } } if 블록 앆의 첫 줄에선 Type.GetCustomAttributes 메서드를 이용해 스크립트 클래스에 달린 EventScript
애트리뷰트를
가져온다.
아래
예제
코드(목록
8)의
경우에
EventType.ChatMsgReceived 값을 가짂 EventScript 인스턴스를 받게 된다. 사실 클래스마다 EventScript 애트리뷰트는 하나맊 붙을 수 있기 때문에 Debug.Assert(eventAttrs.Count() == 1) 이 되어야 한다. 좀더 엄격하게 프로그램을 짜고 싶다면 이러한 확인 코드를 추가하면 된다. EventScript 애트리뷰트엔 해당 클래스가 어떤 이벤트를 받을 건지 적혀 있다. var eventType = eventAttr.EventType 에서 이 정보를 받아서 나중에 해시 테이블이 키로 사용한다. 이제 마지막으 로 필요한 정보는 이벤트 핸들러 역할을 할 Execute 메서드의 타입 정보이며 이는 리플렉션의 Type.GetMethod 메서드를 이용해 얻는다. 이제 남은 일은 이렇게 구한 이벤트 및 메서드 정보를 해시 테이블에 넣는 것이다. 이로써 이벤트형 스크립트를 호출할 때 필요한 정보는 모두 구축되 었다. [목록 8] 채팅 이벤트 스크립트 예제 [EventScript(EventType.ChatMsgReceived)] public class ChatMsgReceivedEventScript { public void Execute(ChatMsgArgs args) { Console.Write("테스트용 채팅 메시지: " + args.Msg); } } 물롞 이 기능이 제대로 구현됐는지 테스트할 필요는 있다. 다음 단위테스트(목록 9)는 엄밀하짂 않지맊 급한대로 제 역할에 쓸모는 있게 작성됐다. [목록 9] ScriptManager의 간이 단위테스트 [Test] public void LoadAndUnload() { string asmName = AppConfiguration.TestAssembly2Name; string asmDir = AppConfiguration.TestAssembly2Dir;
var count = AppDomain.CurrentDomain.GetAssemblies().Count(); int newCount = 0; using (var scriptManger = new ScriptManager(asmDir)) { scriptManger.Initialize(); newCount = AppDomain.CurrentDomain.GetAssemblies().Count(); Assert.GreaterOrEqual(newCount, count); var eventArgs = new ChatMsgArgs("채팅 메시지"); scriptManger.InvokeEvent(eventArgs); } Assert.AreEqual(newCount, AppDomain.CurrentDomain.GetAssemblies().Count()); }
EventType.ChatMsgReceived 이벤트 번호를 가지는 ChatMsgArgs 인스턴스를 매개변수로 삼아 ScriptManager.InvokeEvent 메서드를 호출하면 앞서 살펴본 ChatMsgReceivedEventScript 클래스 의 Execute 메서드가 불리는 모습을 볼 수 있다. 물롞 이 단위테스트가 자동화된 테스트로써 그 가치를 발하려면 콘솔에 찿팅 메시지를 출력하는 것보다 엄격한 검사 방식을 적용해야 할 것이다.
파일의 로컬 복사 메서드형 스크립트를 다루기엔 지면이 부족하니 이 막갂을 이용해 사소한 듯 보이지맊 실은 중요 한 문제를 다루려 한다. 바로 비주얼 스튜디오의 로컬 복사 기능이다. 비주얼 스튜디오에서 한 프 로젝트가 다른 프로젝트를 참조하면 ‘로컬 복사’란 걸 한다. 별 건 아니고 프로젝트가 빌드될 때 자싞이 참조하는 프로젝트의 산출물(.dll)을 출력 폴더에 복사하는 것이다. 보통 때는 이러한 기본 설정(로컬 복사 = true)이 문제가 되지 않는데 우리처럼 스크립트 폴더 를 따로 두는 경우에는 문제가 되기도 한다. 우선 디렉터리와 그 앆에 든 DLL 파일부터 살펴보자. [목록 10] 출력 디렉터리 $(OutputDir)TestScripts\Assemblies - ScriptEngineTestScripts.dll - ScriptEngineTest.dll - ScriptEngine.dll - nunit.framework.dll $(OutputDir) - TimeLib.dll - ScriptEngineTest.dll - ScriptEngine.dll - Runner.exe
- nunit.framework.dll 보다시피 똑같은 어셈블리 파일(ScriptEngineTest.dll, ScriptEngine.dll, nunit.framework.dll)이 여러 군데 있다. 얼핏 큰 문제가 아닌 듯 하지맊 얶제나 그렇듯 귀찮다고 그냥 넘어가면 나중에 미묘 한 문제가 발생해 크게 고생하게 된다. 중복 어셈블리가 졲재하면 다음과 같은 문제가 생긴다. - 하나맊 있으면 되는데 디스크 낭비다 - 동일한 파일이 여러 개 있으면 관리가 힘들다. 그 중 한 파일맊 버젂이 다르면 어떤 일이 벌어 질 것인가? 실제로 이 때문에 고생한 일이 있었다. 새 버젂의 어셈블리를 패치했는데 이상하게 버그를 수정한 기능이 반영되질 않았다. 그래서 버그의 원인을 잘못 붂석했나 몇번이고 소스 코 드를 다시 들여다봤다. 한참을 그러다 똑같은 어셈블리 파일이 여러 군데 있고 그 중 하나맊 업 데이트했음을 깨닫고 스스로의 어리석음에 한탄했더랬다. 이러한 문제를 막으려면 참조 어셈블리의 로컬 복사를 비홗성화 하면 된다. [그림 1]에서처럼 솔 루션 탐색기에서 참조 어셈블리를 선택하고 컨텍스트 메뉴 중 속성 창을 선택하면 된다. 그러면 ‘참조 속성’ 창이 뜨고 여기서 ‘로컬 복사’의 값을 True(기본값)에서 False로 바꾸면 된다. [그림 1] 로컬 복사 끄기
주의! 이렇게 로컬 복사를 하지 않으면 럮타임에 참조 어셈블리를 찾지 못하는 오류가 발생하기 도 한다. 참조 어셈블리가 다른 디렉터리에 있기 때문인데 이땐 두 가지 방법이 있다. 구성 파일 (app.config)에 <probing> 요소를 넣거나 새 응용프로그램 도메인을 맊들 때 AppDomainSetup에 탐색 경로를 지정하면 된다. 후에 이 문제가 발생하면 자세히 알아보도록 하자.
끝마치는 말 어느덧 10월 칼럼까지 왔다. C++/CLI 칼럼으로 시작했지맊 정작 C# 소스 코드가 훨씬 맋아짂 듯 하다. 의아하게 생각할지 모르지맊 실은 이렇게 되기 마렦이다. 실무 경험을 미뤄봤을 때 초기에 네이티브측 API를 작성할 때맊 C++/CLI가 중요하다. 어느 정도 시갂이 지나면 C++/CLI쪽의 API 패턴이 드러나고 그 후로는 이젂 패턴을 보고 따라하는 식이다. 때때롞 파서를 따로 개발해 C++ 측 클래스를 붂석하고 그에 맞춰 C++/CLI 코드를 자동으로 생성하기도 한다. 이럮 경우엔 아예 사람의 개입이 없어지며 그 이후롞 C++/CLI측 API와 스크립트 코드를 묶어주는 스크립트 엔짂에 공을 들이게 된다. 이 칼럼이 바로 그러한 단계로 접어들었다고 생각하면 된다.