이 글은 월간 마이크로소프트웨어(일명 마소) 2009년 12월호에 기고한 글입니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.
C++/CLI 칼럼을 쓰기로 결정한지 어느덧 1년이 지났다. 집 앞 초등학교 운동장에 나가 운동하기 귀찮아지는 걸 보니 틀림없다. 처음엔 12회를 과연 채울 만한 이야깃거리가 있을까 의구심이 들기도 했지만, 이제 와서 보니 되려 시간이 부족했던 듯 하다. 다뤄야 했던 주제, 다뤘으면 좋았을 이야기가 아직 많이 남았다. 그러나 다소 부족하더라도 짧게 끊었던 게 긴장감 유지에 도움이 되지 않았나 싶기도 하다. 이제 마지막 칼럼만 남았고 못했던 이야기를 이 한편에 엮어보려 한다.
연재 기간이 6개월이 넘는 장기 칼럼을 몇 해째 써왔다. 몇 번 해보니 감이랄까, “이렇게 하면 좋겠다”라는 통밥 비슷한 게 생겨서 처음엔 자신감이 넘쳤다. 이번엔 아주 전문적인 내용을 다룬다는 흥분이 더해 의욕이 넘쳤다. 그러나 뒤돌아보면 부족한 점이 많았다. 특히 방향을 똑바로 잡지 못하고 주제의 무게 중심을 여기저기 옮겼던 부분이 가장 아쉽다. C++/CLI 칼럼으로 시작했으나 C#을 더 많이 다뤘으니 말이다. 차라리 처음부터 닷넷 프레임워크 기반의 스크립트 엔진 개발하기 정도로 주제를 잡았으면 어땠을까 싶다.
그럼에도 불구하고 실무에서 실제로 다뤘던 기술은 거의 다 기억해냈다고 생각한다. 단지 체계적으로 전달 못한 아쉬움은 남는다. 그래서 이 자리에선 다루지 못했던 이야기를 풀고 향후 이런 시스템을 도입할 의향이 있는 사람이나 조직에게 제안을 남기려 한다.
시작하기에 앞서
이 글의 모든 예제 소스코드는 웹에서 제공한다. 구글 코드에서 호스팅 받는데 서브버전(Subversion) 클라이언트가 있으면 자유롭게 다운로드 받고 변경 내역을 검토할 수 있다. 질문도 여기서 받는다. 물론 마이크로소프트웨어 홈페이지에서도 된다
어디에 써야 제격인가?
자, C++/CLI 와 스크립트 엔진을 개발하고 다루는 법은 알아봤다. 그러나 기술의 세부 사항에 집중하다 보면 왜 이런 기술이 필요한지, 언제 써야 하는지 흐릿해지기 쉽다. 이론 과학자라면 어디 흥미로운 기술이 있다더라 정도에 만족할지 모르겠지만 공학자인 소프트웨어 엔지니어인 우리는 항상 기술의 용도에 집중하지 않으면 안 된다. 그런 의미에서 도대체 이 복잡해 보이는 기술을 어디에 쓰란 건지부터 다시 알아본다.
C++/CLI
C++/CLI란 프로그래밍 언어가 어떤 의미를 지니는지는 첫 칼럼에서 알아봤다. 분명한 것은 C++/CLI를 C#의 대체물로 쓰기는 어렵다는 것이다. 공통 언어 런타임(CLR)를 지원하는 프로그래밍 언어 중 당연 문법적인 복잡함이 두드러지기 때문이다.
C++/CLI는 기존에 네이티브 C++로 개발한 라이브러리 또는 응용프로그램이 있는 상황에서 가치를 발한다. 이런 라이브러리가 관리되는 환경(닷넷 프레임워크나 모노 등)과 연동되도록(이런 걸 두고 상호운용성이라 한다) 다리 역할을 하는 것이 C++/CLI이다. C#이나 Visual Basic .NET 개발자가 여러분의 제품(네이티브 C++ 기반일)을 구매하길 원한다면 C++/CLI를 이용하거나 COM 인터페이스를 이용해 상호운용성을 제공해야 한다.
스크립트 엔진
외부 고객에게 닷넷 프레임워크용 API를 제공하는 정도라면 스크립트 엔진이 필요 없다. C++/CLI로 API를 제공하는 정도면 충분하기 때문이다. 스크립트 엔진을 도입하는 경우는 대체로 내부 개발 속도를 높이려는 의도로 출발한다. 스크립트 엔진을 도입하기 전까진 기존 소스코드를 일부 뜯어고치고 API를 추가하고 스크립트 엔진까지 개발해서 붙여야 하므로 비용이 꽤 든다. 그러나 도입만 마치고 나면 개발 속도는 상당히 향상된다. 구체적인 통계를 내진 못했지만 도입 후 개발팀 중에서 가장 편하게 일했던 사람이 글쓴이 자신이었던 건 확실하다.
스크립트 엔진을 도입하면 왜 개발 속도가 빨라지는지 간단하게 요약해봤다. 이와 관련된 논의는 6월 칼럼에서 집중적으로 다뤘으니 참고하길 바란다.
-
닷넷 프레임워크가 제공하는 라이브러리와 기능이 방대하다. 새로 개발할 일이 준다.
-
대규모 C++ 프로젝트에서 겪기 마련인 늘어지는 빌드 시간 문제를 겪지 않는다. 어지간히 큰 프로젝트를 다시 빌드하지 않는 한 대개 1분, 30초 안에 끝난다. “컴파일 중이예요!”라며 개발자가 빈둥대는 꼴을 못 보겠다는 관리자가 혹시 있다면 이런 시스템을 도입해볼 것을 강력히 권한다. 개발자와 관리자 모두 인생이 편해질 것이다.
누가 맡아야 하는가?
이상적인 경우라면 C++, C++/CLI, C#을 모두 잘 다루는 전문가가 맡으면 좋다. 그러면 고민 끝!
하지만 그렇게 일이 잘 풀리기는 쉽지 않을 것이다. 차선책이라면 C++과 C#을 잘 다루는 개발자를 선택하는 것이다. 여기서 C++을 잘 다루는 것이 더 중요하며, C#은 상대적으로 덜 중요하다. 그리고 C# 개발 경험이 없더라도 유사한 프로그래밍 환경, 이를테면 Java를 다뤄봤어도 좋다. 어차피 프로그래밍 언어를 잘 다루는 것보단 CLR의 아키텍처와 구현 방식을 이해하는 게 더 중요하기 때문이다.
한가지 염두에 둘 것은 C++/CLI가 상당히 어려운 언어라는 점이다. 보통 C++ 다루기가 그렇게 힘들다고 이야기를 하는데 C++/CLI를 경험해보면 C++이 굉장히 축복받은 언어라는 생각(템플릿 메타 프로그래밍만 빼고 이야기한다면)이 들 것이다. 머리 아프게 어려운 코드를 보여주기 싫어서 이 칼럼에선 가급적 복잡한 예제를 피했지만 인터넷에서 긁어온 [목록 1]의 소스코드만 보더라도 이런 생각에 동의하리라 생각한다. 문법적인 측면이 어려운 건 쉽게 극복하더라도 서로 완전히 다른 설계 철학, 자원 관리 체계 등을 하나로 엮는 일인만큼 한동안 두통에 시달리기 쉽다. 그러므로 대학을 갓 졸업한 신입 사원에게 일을 떠넘기는 만행은 저질러선 안 되겠다.
- [목록 1] 실제론 간단한 코드지만 보기만 해도 어지럽다
-
// 출처: http://tomasp.net/blog/clinq-project.aspx // Declare parameter (variable) and method body (expression) Expr<int> var = Var<int>("x"); Expr<int> expr = Expr<String^>("Hello world!").IndexOf("w") + var; // First argument for the clq::fun function is lambda expression // parameter, the last argument is the lambda expression body Lambda<Func<int, int>^>^ lambda = clq::fun(var, expr); // Print string representation of lambda.. Console::WriteLine(lambda->ToLinq()); // Compile & execute lambda Func<int, int>^ compiled = lambda->Compile(); Console::WriteLine(compiled(100));
무엇을 참고해야 하는가?
이제 여러분이 C++/CLI 기반의 스크립트 엔진 개발을 맡게 됐다고 하자. 아니, 여러분이 이런 결정을 내린 관리자라고 해도 담당 개발자에게 무엇을 지원해줘야 하는지 알아두면 좋을 것이다.
앞선 말했듯이 담당 소프트웨어 엔지니어는 C++, C++/CLI, C#에 능통해야 한다. 만약 담당자가 이 중 경험이 부족한 언어가 있다면 다음과 같은 책을 추천한다.
-
C++ : Effective C++
-
C# : Effective C#
-
C++/CLI : Foundations of C++/CLI: The VIsual C++ Language for .NET 3.5, C++/CLI in Action
어디까지나 개인적인 경험을 토대로 추천하는 것이라 더 나은 책도 많으리라 본다. Effective 시리즈는 한국어판이 출판되었으나 C++/CLI 관련 서적은 전부 원서로 사야 한다. C++/CLI 쪽은 미국에서도 책이 몇 권 나오지 않았는데 그래도 위의 두 서적이 실무에 가장 쓸모가 있었다. 우선 Foundations of C++/CLI로 기초를 다지고 C++/CLI in Action을 읽으면 좋다. C++/CLI in Action은 몇 년이 지난 책이긴 하지만 이만한 고급 주제를 다룬 책은 어디에도 없기 때문에 필수라 하겠다. 그 고급 주제 중 일부를 이 칼럼에서 다루긴 했으나 역시 원서만큼 충실하진 못했기 때문이다.
세 가지 프로그래밍 언어를 모두 익혔으면 이제 CLR의 아키텍처와 실무 적용 사례를 수집해 연구할 차례이다. 스크립트 엔진을 다룬 책 자체가 미국에서도 흔치 않기 때문에 추천할만한 책이 거의 없다. 그래도 굳이 하나 꼽자면 제프리 리처가 쓴 CLR via C#이란 책이 도움이 된다.
이 책과 더불어 인터넷을 검색해보면 아키텍처에 대해 감 잡을만한 글이 몇 개 나온다. 이때 중요 키워드는 다음과 같다.
-
AppDomain, 응용프로그램 도메인
-
CLR 호스팅
-
Microsoft SQL Server 2005, ASP .NET, Internet Explorer
위에서 언급된 응용프로그램(ASP .NET 등)은 모두 CLR이 제공하는 응용프로그램 도메인, 메모리 격리 모델을 적용했다. 이는 스크립트 동적 적재와도 연관이 깊으니 반드시 숙지해야 한다. 이 외에도 가비지 콜렉션(GC) 등도 스크립트 엔진 설계시 영향을 크게 미치는 부분이니 살펴보는 편이 좋다.
참고로 MSSQL 2005의 아키텍처를 다룬 SQL Server 2005 Under the Hood: How We Host the CLR 이란 문서를 꼭 읽어보길 바란다. 구글에서 검색하면 바로 나오며 가장 도움이 됐던 문서 중 하나이다.
기존 라이브러리를 확장하려면?
처음부터 CLR로 기능을 확장할 생각으로 네이티브 C++ 라이브러리를 설계하진 않을 것이다. 그런 경우가 아주 없지야 않겠지만 보통은 꽤 역사가 오래된 레거시 코드가 있기 마련이다. 이런 상황에서 새로운 기술을 도입한다는 건 쉽지 않은 결정이다. 그러나 내가 일했던 조직도 처음부터 C++/CLI를 도입하면 어떤 문제가 있을지 미리 알고 C++ 코드를 작성하진 않았다. 99%의 조직은 그런 고려는 뒤로 미루고 핵심 기능을 구현하는데 집중할 것이다. 그러니 사정은 어디나 비슷하리라.
준비가 덜 되었고 위험에 노출된 불안감을 느낀다면, 다행이다. 그게 정상이기 때문이다. 아무렇지도 않게 모든 일이 잘 굴러가리라 믿는 낙천주의에 빠져서 준비를 소홀히 하는 것보단 낫지 않나?
먼저 이런 전환 작업이 결코 쉽지 않다는 걸 팀 구성원 전체가 이해할 필요가 있다. 때로는 기존 코드가 깨지는 일도 발생할 것이다. 빌드가 되도 실행시간에 충돌이 난다던가, 정확한 원인을 꼬집어 내기 힘든 그런 문제도 겪게 된다. 상당한 양의 소스코드를 수정하고 빌드 프로세스까지 바꿔야 하는 일도 생긴다. 이 일을 맡은 한두 명의 힘으론 벅찬 상황도 벌어진다. 그럴 때 팀 구성원들이 이해하고 도와주어야 한다. 담당 개발자를 위한다기 보다, 이 업무가 잘 끝나면 내 생활도 편해지리라 믿고 도와주면 더 좋다.
어지간히 심각한 이전(migration) 문제는 이 칼럼에서 전부 다뤘다고 생각한다. 주요 문제로는 다음과 같은 게 있다.
-
미리 컴파일된 헤더 구분하기: 네이티브 C++용, 관리되는 C++용
-
마샬링하기
-
Thread Local Storage 문제: C++/CLI에서 __declspec(thread)가 작동하지 않는 문제
-
정적 라이브러리를 동적 라이브러리로 바꾸기
이상의 주제는 1월 칼럼 ~ 5월 칼럼에서 다루었다. 이 문제들은 스크립트 엔진을 개발할 때 겪는 문제라기보다 그 전 단계, 그러니까 기존 라이브러리에 C++/CLI API 래퍼를 추가할 때 발생하는 문제이다.
스크립트 엔진을 개발할 때 겪는 문제는 대개 가비지 콜렉션이나 응용프로그램 도메인과 연관이 있다. 각 컴포넌트(비주얼 스튜디오 프로젝트) 간의 의존성 관계도 중요한 문제로써 다뤘다.
이와 같은 문제를 개발 시작 전에 숙지해두면 소위 말하는 “삽질”을 많이 줄일 수 있을 것이다.
개선할 점은 무엇인가?
이제 과거를 돌아보는 일은 이쯤하고 미래를 생각해보자.
버그
우선 여태까지 개발 소스 코드가 완벽하진 않으리란 점을 짚고 넘어가야겠다. 솔직히 고백하건대 다음 원고를 쓰느라 소스 코드를 다시 들여다 볼 때마다 버그를 찾아낸다. 변명을 하자면 실무에서 쓰던 라이브러리도 몇 개월 뒤에 버그를 찾아내곤 했기 때문에 상대적으로 시간을 훨씬 적게 투자한 예제 라이브러리야 오죽하겠나. 그러니 예제 소스코드를 황금률처럼 떠받들지 말고 항상 경계하길 바란다.
앞으로 두고 봐야겠지만 이 예제 소스코드를 조금 더 다듬고, 여건만 갖춰지면 꾸준히 개발할 생각이다. 이 칼럼은 연재를 마칠 테니 진행상황은 앞서 소개한 구글 코드나 블로그를 통해 전달할 것이다.
필수 기능
시간이나 그 밖의 여건이 되지 않아 다루지 못한 중요 기능이 몇 가지 있다. 그 중에서도 가장 중요한 것만 이야기하자면 다음과 같다.
세션 데이터 저장
이 칼럼에선 게임용 서버 시스템을 염두에 뒀다. 스크립트 엔진을 가장 적극적으로 도입하는 분야이기 때문이다. 덕분에 고객 사나 일반 사용자의 컴퓨터 사양을 고려하지 않고 성능이나 기능 확장성에만 신경을 쓸 수 있었다.
이러한 시스템은 보통 멀티스레딩 또는 다중 CPU를 활용한다. 이때 스레드 간 자원 공유로 인한 성능 저하 문제를 겪지 않기 위해 보통 각 스레드마다 스크립트 엔진 인스턴스를 따로 띄운다. 이는 루아나 파이썬을 쓸 때도 마찬가지다.
이렇게 스레드별로 스크립트 엔진 인스턴스를 띄우면, 각 엔진 인스턴스는 세션 데이터를 저장하지 못하는 문제가 생긴다. 스레드 1에서 별명(Nickname)을 바꿔달라는 요청을 처리했으면 그 결과를 어딘가 저장해야 다른 스레드에서도 새 별명을 이용할 수 있기 때문에 이는 중요한 문제이다.
스크립트 엔진을 도입하는 초기에는 이 문제가 그리 두드러지지 않는다. User.ChangeNickName 같은 기능이 네이티브 C++ 계층에 이미 구현된 상황이기 때문이다. 스크립트에선 이러한 API를 통해 기능에 접근하면 그만이다. 하지만 스크립트 개발을 한참 하면 기존 API가 제공 안 하는 기능이 들어가기 마련이다. 예를 들어 퀴즈 게임을 순수하게 스크립트만으로 개발한다고 해보자. 플레이어가 문제의 답을 고르면 그 값을 어딘가 저장했다가 나중에 답과 비교해 결과를 전송해주어야 한다. 이때 네이티브 C++ 쪽에 일일이 관련 메서드를 구현해도 되겠지만 이러면 스크립트 시스템을 도입하는 장점이 희석된다.
여기서 하나 확실한 사실은 세션 데이터를 저장하고 동기화 문제를 제어할 곳은 스크립트 엔진이 아니라는 것이다. 대부분의 응용프로그램에서 스레딩 모델은 네이티브 코드에서 결정한다. 순수하게 C# 등으로 짠 응용프로그램이라면 그렇지 않겠지만 네이티브 코드 위에 스크립트 엔진을 올리는 것이니 스레드 생성과 관리, 자원의 공유 및 동기화는 네이티브 측에서 책임지는 게 보통이다. 따라서 스크립트에서 쓸 세션은 네이티브 계층에서 제공해야 한다. 이때 스크립트 엔진이 네이티브 계층에 종속되지 않고 이식성을 확보하려면 네이티브 측이 구현해야 할 인터페이스를 엔진 수준에서 강제하는 방법을 고려해봐야 한다.
네이티브 계층에서 범용 세션 계층을 제공한다고 가정해보자. 아마도 이 인터페이스는 System.Stream과 유사한 형태가 될 텐데 그래야 유연하기 때문이다. 그런데 관리되는 자원 또는 클래스를 memcpy하듯 바이너리 덤프로 넘기진 못하기 때문에 직렬화 방식을 선택해야 한다. 문자열 기반으로 간다면 XML, Json을 고려하고 조금이라도 빨리 처리하고자 한다면 BinarySerializer를 선택하면 된다. 테스트 결과, 오히려 XML이나 Json이 메모리 공간은 덜 차지하는 경우가 많았다. 이런 문자열 기반은 사용자의 커스터마이징이 가능하기 때문이다. MyName이라는 공개 프로퍼티는 MyName이란 이름의 XML 요소로 변환되는 게 기본이지만, 사용자가 지정하면 “m”처럼 한 글자짜리 요소로 바뀔 수 있기 때문이다. 그러나 CPU 처리 속도로 봤을 땐 바이너리 직렬화 방법이 가장 효과적이었다.
동적 적재
동적 적재에 대해선 이미 여러 차례 언급했기 때문에 짧게 요약하고 마치려 한다. 스크립트를 동적으로 적재하고 해제하는 작업을 반복하려면 AppDomain을 활용해야 한다. 아키텍처에 상당한 제한이 가해지고 개발 난이도가 높아지므로 이 기능이 정말 필요한지 파악한 다음 도입하길 바란다.
고려할만한 기능
접근 제어
게임 개발을 예로 들면 이런 상황이 많다. 게임 마스터나 관계자만 사용 가능한 스크립트를 만들어 놓고 게임 규칙을 어기는 사용자를 제재할 때 쓴다던가, 개발 네트워크에서 개발자들이 마음대로 테스트해볼 수 있게 “Show me the money” 같은 명령어를 활성화시켜 놓는다던가 하는 일 말이다. 물론 일반 플레이어가 우연히 Show me the money 같은 명령을 찾아서 게임 포인트를 몇 백만씩 늘린다던가 하는 일이 있을 수 있기 때문에 이러한 기능엔 제한을 둬야 한다.
이러한 접근 제어 기능은 스크립트 엔진 차원에서 지원해주는 편이 좋은데, 구현 방식은 각기 요구사항에 따라 달라질 것이다. 하지만 닷넷 프레임워크에 이미 접근 제어 기능을 구현해놓은 게 있기 때문에 이 설계를 참고 삼으면 좀더 유연한 접근 제어 기능이 만들어지리라 생각한다.
메모리 제어하기
CLR은 메모리 관리 방식이 독특하다. 안 쓰는 객체를 그때그때 해제하는 게 아니라 한꺼번에 모았다가 가비지 콜렉션을 하기 때문이다. 이런 방식은 파이썬, 루아 같은 스크립트와 차별되는 점이다. 일전에 논의한 바가 있지만 이는 웹 서비스에 가장 적합한 아키텍처이다. 조금씩 미리미리 처리하는 것보다 한꺼번에 모아서 정리하는 편이 처리량 면에선 유리하기 때문이다. 하지만 일부 온라인 게임에선 이 방식이 독이 되기도 한다. 한꺼번에 처리하는 그 짧은 시간 동안 소위 말하는 랙이 발생하기 때문이다. 몬스터를 죽였다고 생각한 찰나, 랙이 발생하고 내 캐릭터가 죽어있는 경험은 누구나 해봤으리라 생각한다. 그래서 CLR 기반의 스크립트 엔진은 게임의 요구사항에 따라선 쓰지 못할 수도 있다.
일부에선 파이썬이나 루아처럼 메모리를 관리하게 바꾸지 못하냐고 묻기도 하는데 현재로선 전혀 불가능하다. 다만 메모리 할당 방식은 제어가 가능하다. CLR 호스팅에 쓰는 COM 인터페이스를 살펴보면 CLR의 몇 가지 기본 기능이 노출되어 있는데 메모리 할당 인터페이스도 그 중 하나이다.
이러한 커스터마이징을 자세히 알고 싶다면 CLR Hosting, COM 등의 키워드로 검색해보면 된다. I/O 완료 모델, 스레딩 모델 등도 일부 수정이 가능하다.
기획자를 위한 개발환경
여태까지는 서버 개발자가 직접 스크립트를 짠다고 가정했다. 그래서 프로그래밍에 약한 기획자는 고려 대상이 되지 못했다. 그러나 기획자가 직접 스크립트를 수정 가능한 수준이라면 더할 나위 없을 것이다. 설사 개발자가 직접 스크립트를 짜더라도 그 정도로 간단한 스크립트라면 개발 속도가 비약적으로 향상될 테니 말이다.
우선 스크립트 언어를 직접 개발하는 방법이 있다. CLR의 중간 언어 명세에 따라 프로그래밍 언어와 컴파일러를 직접 개발하는 방법을 다룬 책은 해외 서점에서 구매 가능하다. 하지만 개발 노력이 많이 들기 때문에 IronPython, IronRuby 같은 프로젝트의 산출물을 활용하는 것도 한 방법이다.
스크립트를 짜다 보면 공통적으로 드러나는 패턴이 보인다. 가장 단순한 걸론 이름 공간(namespace)를 끌어내는 using System; 같은 문장이다. 이러한 기본적인 패턴을 자동으로 생성해주는 도구를 개발하는 것도 좋은 생각이다.
끝마치는 말
이로써 12번에 걸친 칼럼이 끝났다. 한편으론 아쉽고, 한편으론 홀가분하다. 이후로도 질문이나 제안은 수용할 생각이다. 앞서 제시한 구글 코드나 블로그, 또는 이메일로 연락하면 된다. 2010년엔 독자 모두 뜻하는 바를 이루길~
C++/CLI 관련자료를 찾다가 이곳을 들르게되었는데 님이 올려주신 칼럼들이 이제 막 관련공부를 시작하는
저에게 큰도움이 되었습니다. 특히나 프로젝트 설정등 의 이슈등은 자칫 저의 미래의 삽질(?)이 될뻔한것을
예방하는데 큰도움이 된거 같습니다.
앞으로도 무궁한 발전이 있으시길…
도움이 되었다니 다행이네요. C++/CLI 를 만진 지 오래되어서 이젠 가물가물하네요.