채팅 스크립트를 짜다가 스크립트 엔진에서 엄청난 버그를 발견했다. 꼼수를 써서 고치긴 했는데, 마지막 순간까지도 제대로 된 해결책인지 확신이 안 서서 마음을 졸였다.
CLR 환경(C#, C++/CLI 등)에는 Thread Local Storage가 몇 개나 있을까? 스레드 갯수만큼 있을까? 대부분의 경우엔 스레드 갯수만큼 있다. 하지만 항상 그렇지는 않다. CLR 환경에선 정확히 ‘스레드 갯수 x 응용프로그램 도메인 갯수’ 만큼 TLS가 있다. 이 점을 간과했다가 하마터면 피볼뻔 했는데, 내가 남긴 티켓에 상황이 설명되어 있다.
애플리케이션 초기화 때 스레드마다 스크립트 엔진 인스턴스를 하나씩 붙인다. 그런데 문제는 각 AppDomain마다 TLS 를 따로 갖는다는 것이다. 스크립트는 별도의 AppDomain을 쓰는데, 이 스크립트가 API를 호출할 때 기본 도메인이 아닌 두번째 도메인 컨텍스트 안에 있게 된다. 당연히 두번째 도메인 컨텍스트에는 스크립트 엔진 인스턴스가 없다. 왜? 응용 도메인마다 TLS가 다르고, 기본 AppDomain에만 스크립트 엔진을 붙였으니까.
기본 응용프로그램 도메인에서 스레드마다 스크립트 엔진 인스턴스를 만들어놨다. 그리고 스크립트는 플러그인 응용프로그램 도메인에서 실행된다. 문제는 스크립트 안에서, 즉 플러그인 응용프로그램 도메인에서 스크립트 엔진 인스턴스에 접근할 때 벌어진다. 응용프로그램 도메인마다 TLS가 따로 있고, 기본 응용프로그램 도메인에만 인스턴스를 만들어놨으니, 스크립트에서 스크립트 엔진에 접근하려 하면 널 참조 예외가 발생한다. 이 문제를 가장 손쉽게 해결하는 방법은 역시 ‘스레드 갯수 x 응용프로그램 도메인 갯수’만큼 스크립트 엔진 인스턴스를 만드는 것이지만, 메모리 사용량도 많아지고 원래 의도한 바도 아니기에 다른 해결책을 모색했다.
그래서 찾아낸 해결책이 다음과 같다.
ref class ScriptEngineManaged sealed { public: [System::ThreadStatic] static LocalScriptManager^ Instance = nullptr; }
class ScriptEngineWrapper { public: explicit ScriptEngineWrapper(LocalScriptManager^ scriptManager) : Instance(scriptManager) { } gcroot<LocalScriptManager^> Instance; }; DECLARE_SELECTANY_TLS(ScriptEngineWrapper*, ScriptEngine, NULL);
간단히 설명하자면, ThreadLocalStorage를 닷넷 기능을 써서 할당받는 대신, C++쪽에서 할당받아 쓰는 방식으로 문제를 해결했다. CRT(네이티브 C++) 환경에선 정확히 스레드 갯수만큼 TLS가 있으므로 위와 같은 문제는 발생하지 않는다. 만약 정황을 설명하지 않고 위의 두 코드만 주면 완전히 동일한 코드라 판단할테지만, 이렇게 미묘한 차이가 있다. 스크립트 엔진 만들면서 C++/CLI를 극한까지 다뤄보는 느낌이다.