COM, ATL

Process/Thread/Apartment

디버그정 2008. 8. 2. 07:43

 

윈도우는 멀티스레드 운영체제이다. 잘 짜여진 멀티스레드 프로그램은 기존의 프로그래밍 방법만으로는 쉽게 구현하기 힘든 편의성을 제공한다. 또한 여러 개의 프로세서를 사용한 멀티프로세스 시스템의 장점을 최대한 살릴려면 멀티스레드 프로그램을 이용해야만 하는 경우도 많다. 이러한 장점에 반해 멀티스레드 프로그램은 분명히 매우 복잡하다. 단순히 몇몇 API만으로 스레드를 생성하고 사용할 수 있지만 단순히 그래서는 멀티스레드 프로그램의 장점을 전혀 살릴 수 없다. 오히려 시스템 전체적인 성능을 낮추는 요인이 될 수 도 있다.

분명 이 글은 멀티스레딩에 관한 책은 아니지만 이러한 점에 대해 조금은 언급을 하도록 하자. 윈도우는 멀티스레드 운영체제이다. 하나의 스레드는 하나의 실행 단위이다. 하지만 멀티프로세스 시스템과 이를 지원하는 윈도우2000등의 운영체제를 쓰지 않는 한 실제로 동시에 수행될 수 있는 스레드의 개수는 1개이다. 멀티프로세스 프로그램의 경우도 동시에 수행되는 스레드의 개수는 프로세서의 개수에 제한된다. 따라서 프로그램이 여러 개의 스레드를 만들어서 쓴다고 해서 실제로 프로그램의 성능이 향상되는 것은 아니다. 멀티스레딩 프로그램을 이해하는 데 매우 중요하다. 비록 윈도우 2000등이 매우 우수한 스레드 스케쥴링(Thread Scheduling) 알고리즘을 사용하여 많은 스레드의 생성이 크게 문제되지는 않지만 스레드의 생성 역시 상당한 메모리를 잡아먹는 작업이여 메모리는 절대 꽁짜도 무한한 것도 아니다. 또한 오히려 한 스레드를 수행하다가 다른 스레드를 수행하게 되는 스레드 컨텍스트 스위칭(Thread Context Switching)에 소요되는 비용은 상상 이상이다. 따라서 잘 짜여진 멀티스레드 프로그램은 단순히 스레드를 많이 만드는 것이 아니라 매우 적절히 사용하여 가능한한 컨텍스트 스위칭을 줄이면서 프로그램의 반응성을 높인다던가 멀티프로세스의 이용을 목적으로 한다. 또한 윈도우와 같은 선점형 운영체제의 경우 스레드 컨텍스트 스위칭이 일어나는 시기를 프로그램 자체에서 결정할 수 없다는 점은 멀티스레드 프로그래밍의 복잡성을 가중시킨다. 어떤 자원을 사용하는 도중 스위칭이 될 수 도 있으며 그동안 다른 스레드가 그 자원을 사용하거나 변경할 수 도 있는 것이다. 이러한 점을 동기화등 다양한 방법을 통해 막지 않는다면 프로그램은 제대로 작동하지 못할 것이다.

이러한 복잡성으로 인해 몇몇 프로그래머의 경우 멀티스레딩의 장점에도 불구하고 사용하지 않게 되는 요인으로 작동할 수 있다. 이러한 점은 객체 단위의 재사용성을 제공하는 COM에게 있어 몇 가지 문제점이 된다. 어떤 사람은 멀티스레딩을 최대한 활용해서 컴포넌트를 짠다. 따라서 그는 모든 자원을 동기화 하며 그가 쓰는 함수는 멀티스레드의 안전한 함수이다. 반면 어떤 사람은 멀티스레딩을 전혀 생각하지 않고 프로그램을 짜며 그가 짠 컴포넌트는 전혀 멀티스레드에 안전하지 않다. 또 어떤 사람은 멀티스레딩을 사용하고 싶지만 기존의 자원, 예를 들면 MFC라던지 C런타임이라던지, 또는 그가 만들어낸 라이브러리가 멀티스레드에 안전하게 만들어져 있지 않기 때문에 멀티스레딩을 포기한다. 하지만 이미 만들어진 객체는 재사용되어야만 한다. 멀티스레드에 안전하게 만들어지지 않은 객체라도 멀티스레드를 사용하는 프로그램 상에서 안전하게 돌아가야만 한다. 이를 위해 COM은 아파트먼트라는 개념을 사용한다. 아파트먼트는 COM뿐만 아니라 COM+가 제공하는 서비스들을 이해하기 위해 매우 필수적인 요소이다. 반드시 이해하고 넘어가도록 하자.

 

아파트먼트란 무엇인가?

아파트먼트는 같은 스레딩 모델을 공유하는 객체들이 존재하는 곳이다. 모든 객체는 아파트먼트 안에 만들어져야 하며 모든 스레드는 객체를 사용하기위해서는 아파트먼트 안에 들어가야만 한다. 이 말을 꼭 기억하자. 아파트먼트는 스레드가 아니다. 스레드가 아파트먼트 안에 들어가는 것이며 스레드가 아파트먼트 안에 들어가 안에 있는 객체를 사용한다. 그렇다면 스레딩 모델이란 무엇인가? 앞의 논의를 보았다면 금방 알겠지만 멀티스레드에 안전한 객체들과 그렇지 못한 객체들을 나누기 위한 것이다. 하지만 몇몇 성능상의 이유로 스레딩 모델은 이것보다는 조금더 세분화 되었다. 다음은 스레딩 모델의 종류다.

1. Main Threaded
2. Apartment Threaded
3. Free Threaded
4. Both
5. Neutral

그렇다면 이러한 아파트먼트 모델이 어떻게 스레드에 안전하지 못한 객체들을 멀티스레드 프로그램과 동시에 쓰일 수 있게 만드는가? 모든 COM을 사용하는 스레드는 CoInitialize(Ex)를 통해 아파트먼트 안에 들어가야 한다. CoInitialize(Ex)를 통해 스레드가 들어갈 수 있는 아파트먼트는 두 가지 종류가 있는데 Single Threaded Apartment(이하 STA)와 Multi Threaded Apartment(이하 MTA)이다. STA의 경우 스레드가 STA에 들어가는 것을 요구할 때 마다 새로 생성된다. STA에는 하나의 스레드만이 들어갈 수 있으며 들어갈 수 있는 스레드는 아파트먼트를 만든 그 스레드 뿐이다. 따라서 하나의 프로세스 내에는 한 개 또는 여러 개의 STA가 존재하게 된다. MTA에는 여러 개의 스레드가 동시에 들어갈 수 있다. 프로세스 상에 MTA는 오직 하나만이 존재 하며 따라서 MTA는 Reference Counting 되어서 가장 먼저 MTA에 들어가길 요구한 스레드가 들어갈 때 생성되고 마지막 스레드가 아파트먼트를 떠날 때 사라진다. 중요한 것은 객체의 생성이다. 객체는 자신의 가진 스레딩 모델과 스레드가 속한 아파트먼트가 호환되는 가를 확인하고 만약 호환된다면 스레드가 속한 아파트먼트안에 생성이 되며 스레드는 그 객체에 대한 직접적인 포인터(raw pointer)를 얻게 된다. 반대로 호환이 되지 않는다면 객체는 자신이 호환이 되는 다른 아파트먼트에 생성이 되고 스레드는 그 객체에 대한 직접적인 포인터 대신 프록시(proxy) 객체에 대한 포인터를 얻게 된다. 모든 객체에 대한 호출은 반드시 스레드가 객체가 속한 아파트먼트 안에 들어 갔을 때만 가능하다. 이것은 프록시 객체도 마찬가지이다. 프록시 객체에 대한 내용은 잠시 후에 논의 하기로 하고 이 과정을 스레딩 모델 별로 좀더 자세히 알아보도록 하자.

Main Threaded

객체는 프로세스 상에서 가장 먼저 만들어진 STA와만 호환 된다는 것을 의미한다. 호환이 되지 않는 아파트먼트에 속한 스레드에서 객체의 생성을 요구하면 객체는 프로세스에서 가장 먼저 만들어진 아파트먼트 안에 만들어지며 객체의 생성을 요구한 스레드는 객체에 대한 프록시(Proxy) 객체에 대한 포인터를 받는다. 이 모델은 하위호환성을 위해서만 존재하며 만약 동기화를 지원하지 않는 객체를 작성할 때에는 이 모델의 사용을 피하고 Apartment Threaded 모델을 사용해야만 한다.

Apartment Threaded

객체는 STA와만 호환 된다는 것을 의미한다. 만약 MTA에 들어간 스레드에서 이 모델을 지닌 객체를 생성하려 한다면 객체는 새로운 스레드 하나를 만들고 이 스레드는 새로운 STA를 하나 만들게 된다. 그리고 객체를 이 STA 안에다 만들며 스레드는 프록시 객체에 대한 포인터를 얻게 된다. 반대로 만약 스레드가 STA에 들어가 있다면 객체는 스레드가 속한 STA안에 만들어진다. 이 모델은 Thread Affinity를 지니는 기능(CRITICAL_SECTION, TLS등)을 사용하거나 UI에 관련된 기능을 지니는 객체의 경우 추천되는 모델이다. 또한 이 모델을 사용하는 객체는 이름이 의미하는 데로 동기화 기능을 지원할 필요가 없다. 비주얼 베이직의 경우 Main Threaded와 이 모델만을 지원하는 데 Main Threaded는 많은 성능상의 문제가 있으므로 가능하면 모델로 구현해야 한다.

 

Free Threaded

객체는 MTA와만 호환이 된다는 것을 의마한다. 만약 STA에 들어간 스레드가 이런 스레딩 모델을 지닌 객체를 만들려고 하면 객체는 MTA상에 만들어진다. 만약 프로세스 상에 MTA가 이미 존재하면 그 MTA안 에 만들어지며 그렇지 않다면 새로운 스레드가 생성되고 그 스레드는 MTA를 생성한다. 그리고 그 MTA안에 객체가 만들어지고 역시 프록시객체에 대한 포인터를 얻게 된다. 이 모델로 만들어진 객체는 여러 개의 스레드가 동시에 접근 할 수 있으므로 자원에 대해 동기화 해야만 한다.

 

Both

객체는 스레드가 어떤 아파트먼트에 속해 있건 호환된다는 것을 의미한다. 스레드가 어떤 아파트먼트에 들어가 있건 객체는 스레드가 들어간 아파트먼트에 생성이 되며 무조건 직접적인 포인터(Raw Pointer)를 지니게 된다. 이 모델을 지원하는 객체 역시 여러 개의 스레드가 동시에 접근할 수 있으므로 반드시 동기화 기능을 제공하여야만 한다. 언뜻 보기에 Free Threaded 모델을 지원하는 객체와 Both 모델을 지원하는 객체는 똑 같은 요구조건을 가지며 Both 모델의 경우 항상 직접적인 포인터(Raw Pointer)를 지닌다는 사실 때문에 Free Threaded 모델의 필요성의 의문을 제공할지도 모르겠다. 하지만 반드시 그렇지는 않다. 이에 대한 자세한 논의는 잠시 후에 하도록 한다.

 

Neutral

윈도우 2000이 되면서 새로이 추가된 스레딩 모델이다. 객체는 Thread Neutral Apartment(이하 TNA)와만 호환이 되며 객체는 무조건 TNA에 만들어진다. 스레드는 항상 프록시 객체에 대한 포인터를 얻게 된다. 스레드가 CoInitialize(Ex)를 통해 STA 또는 MTA에만 들어가는 옵션이 있다는 점이 이 아파트먼트의 존재에 대해 궁금점을 불러일으킨다. 이 아파트먼트에 대한 논의는 차후에 하도록 하자. 이 아파트먼트 역시 여러 개의 스레드가 동시에 접근할 수 있다는 점에서 동기화를 지원해야만 한다.

 

그렇다면 프록시 객체라는 것은 무엇인가? 모든 객체에 대한 메서드 호출은 객체가 생성되어 있는 아파트먼트 안에 속한 스레드만이 할 수 있다. 만약 다른 아파트먼트 안에 들어가 있는 스레드가 메서드 호출을 할려면 객체가 속해 있는 아파트먼트에 들어간 스레드를 통해서여야만 한다. 프록시 객체는 메서드 호출을 하는 스레드 입장에서 투명하도록 만든다. 메서드 호출을 하는 스레드는 마치 자기가 들어간 아파트먼트 안의 객체에 대한 호출인양 메서드 호출을 한다. 하지만 사실 불리는 것은 프록시 객체의 메서드이며 프록시 객체는 나머지 복잡한 일들을 알아서 처리한다.

우선 STA에 있는 객체에 대한 메서드 호출에 대해 알아보자. STA안에 존재하는 객체들의 메서드들은 전혀 스레드에 안전하지 않기 때문에 객체에 대한 호출은 반드시 동기화 되어야만 한다. 또한 특정 STA안에 속한 객체에 대해 호출하기 위해서는 반드시 그 STA안에 들어가야만 하는 데 STA에는 하나의 스레드만이 들어갈 수 있으므로 다른 스레드에서 직접적으로 이 객체에 접근하는 것은 불가능하다. 이를 위해 COM은 윈도우 메시지를 이용한다. 즉 같은 STA에 속하지 않은 스레드(스레드A라 하자)가 특정 STA에 속한 객체(객체1)의 메서드를 호출한다고 하자. 이 스레드A는 이 객체1에 대한 프록시 객체를 지녔을 것이다. 객체1에 대해 특정 메서드를 호출하기 위해서는 이 프록시 객체에 있는 그 특정 메서드를 호출한다. 프록시 객체는 단지 스레드A에게 투명성을 제공하기 위한 객체이므로 실제 메서드의 수행은 실제 객체인 객체1에 가서 해야한다. 이를 위해 프록시 객체는 인자들을 직렬화(Serialize)하고 이를 채널(Channel)을 통해 객체1이 속해 있는 아파트먼트(STA)를 만든 스레드(스레드B라 하자)에게 이 정보를 보낸다. 여기서 채널은 정보를 보낼 수 있는 통로의 추상적인 표현으로 같은 프로세스내의 STA의 경우 윈도우 메시지가 그 통로로 이용된다. 어쨌든 스레드A는 이 순간부터 대기상태에 들어가게 되며 스레드B는 채널을 통해 정보를 얻은 후 이를 다시 의미있는 정보단위로 쪼갠 후 실제 메서드 호출을 한다. 그리고 받은 결과 값을 직렬화해서 다시 스레드A에게 보낸다. 이 때 대기 중이던 스레드 A가 깨어나게 된다.

여기서 중요한 점은 스레드B가 반드시 GetMessage 또는 PeekMessage를 통해 매세지를 받아들여야만 한다는 점이다. 그렇지 않다면 스레드B가 들어간 객체는 다른 스레드에 의한 호출을 전혀 처리하지 못하게 된다. 스레드 B가 반드시 메시지를 받아들여야 한다는 점에서 흔히 스레드 B는 UI스레드인 경우가 많지만 필수적인 것은 아니다. 예를 들면 스레딩 모델이 호환되지 않아 COM에 의해 생성된 스레드(흔히 ORPC Cache Thread라고 부른다.)의 경우 하는 일은 오직 메시지를 받아들이는 일뿐이며 또한 그 스레드에 속한 윈도우는 오직 다른 스레드로 부터의 호출을 받아들이기 위한 보이지 않는 윈도우 뿐이다.

MTA의 경우도 마찬가지다. 같은 MTA에 속하지 않은 스레드로부터 그 MTA에 속한 객체에 대한 메서드 호출을 한다면? 이것 역시 위와 비슷하게 프록시 객체를 통해서 이루어 진다. 하지만 한가지 다른 점은 MTA는 여러 개의 스레드가 동시에 접근 될 수 있음으로 프록시 객체는 메서드를 호출하기 위해 스레드를 생성하거나 이미 이런 목적을 위해 COM이 만들어놓은 스레드를 이용한다. 그래서 인자값을 직렬화 해서 이 스레드에 넘겨준 후 이 스레드를 MTA에 들어가게 한 후 메서드 호출을 하고 결과 값을 다시 직렬화해서 원래의 스레드에 넘겨 준다.

여기서 Reentrancy라는 문제가 발생한다. 만약 STA에서 MTA에게 메서드를 호출한다면 STA는 새로운 스레드를 생성 또는 Cache Thread를 얻어서 그 메서드를 호출한다. 문제는 STA에는 한 개의 스레드만이 존재하며 만약 메서드 호출 도중 원래의 객체에 대한 Callback 함수 즉 STA가 부른 MTA에 있는 객체에서 다시 원래의 객체의 메서드를 호출하는 경우가 존재한다면 이것은 Deadlock을 발생시키게 된다. 왜냐하면 STA에 있는 객체는 현재 MTA의 Cache Thread가 결과값을 리턴하기를 기다리고 있으며 따라서 메시지 펌핑을 멈추게 되는 것이다. 이런 문제를 해결하기 위해서 COM은 이 새로 생성된 스레드가 메서드 호출을 마칠 때 까지 기다리지 않는다. 다시 메시지 펌핑을 계속하며 메서드 호출을 마쳤을 때에는 윈도우 메시지 형태로 이를 통지 받는다. 그렇다면 무엇이 문제인가? 바로 이 Callback 함수가 불릴 때에 객체의 데이터를 조작할 수 도 있다는 점이다. 이것은 메서드 호출 도중 데이터가 변하지 않을 것이라고 기대하는 객체에게 있어 문제가 될 수 가 있다. Callback 함수를 행할 경우 반드시 이러한 점에 주의 하여야 한다.

지금까지 아파트먼트 간의 메서드 호출에 대해 알아보았다. 이것은 호출을 하는 스레드와 호출을 당하는 아파트먼트가 서로 같은 프로세스상에 있다는 가정 하에서다. 하지만 실제로 이 두 가지는 다른 프로세스 상에 존재할 수 있으며 심지어 다른 컴퓨터 상에 존재하는 것 역시 가능하다. 이럴 경우 윈도우 메시지를 보낸다던가 또는 특정 스레드를 직접 아파트먼트 안에 들어가게 하는 것은 불가능할 수 도 있다. 따라서 이런 경우에도 위치 투명성을 제공하기 위해 스텁(Stub) 객체가 존재한다. 스텁(Stub) 객체의 역할은 사실 STA의 보이지 않는 윈도우의 윈도우 프로시져와 거의 비슷하다. 다만 프로세스의 경계 또는 컴퓨터의 경계를 넘어서도 사용될 수 있도록 윈도우 메시지가 아닌 ORPC라는 기술을 이용해 통신한다. 프록시 객체는 ORPC Call을 통해 스텁(Stub) 객체를 호출한다. 그리고 스텁 객체는 위에서 말한 것과 동일한 방법으로 윈도우 메시지 또는 Cache Thread를 이용해서 원하는 객체에 접근해 메서드를 호출하고 그 결과 값을 다시 프록시 객체에게 보낸다. 그럼으로서 프록시 객체를 호출한 스레드는 객체가 자신의 프로세스던 다른 프로세스던 심지어 네트워크상의 어떤 컴퓨터에 있던 흡사 자신이 직접 그 객체의 메서드를 부른 듯한 결과를 지니게 된다. 다만 조금더 오래 걸린다는 점만 제외한다면 말이다.

 

아파트먼트 종류와 스레딩 모델에 따른 반응성 차이

 

TNA에 대해 설명하기 전에 우선 STA와 MTA의 반응성과 대해 알아보자. STA의 경우 한 아파트먼트 안에 있는 모든 개체는 하나의 스레드를 통해서만 실행될 수 있다. 이러한 점은 STA 안에 여러 개의 객체가 존재하게 될 때 큰 문제를 지닌다. 이 STA에 속하지 않은 스레드로부터의 메서드 호출은 이 STA를 만든 스레드를 이용해 처리된다고 위에서 배웠다. 그렇다면 만약 STA에 속한 하나의 객체의 메서드를 호출하고 있다면 다른 객체에 대한 메서드 호출은 이 객체의 사용이 끝날 때 까지 기다려야 한다는 것을 쉽게 깨달을 수 있을 것이다. 이것은 STA가 보이지 않는 윈도우를 이용해 호출을 동기화 하기 때문이며 메시지 큐에 쌓인 메시지는 차례차례 처리된다. 이러한 점은 메서드가 블록킹 호출을 할 때 더욱 악화된다. 예를 들어 WaitForSingleMessage등의 메서드 호출을 한다면 사실 스레드는 아무 일도 안하는 상태임에도 불구하고 다른 객체에 대한 메서드 호출은 상당기간 동안 유보하게 된다. 이러한 점은 STA가 흔히 UI스레드가 만들게 되는 아파트먼트 하는 점에서 프로그램 자체의 반응성을 떨어뜨릴 수 도 있다. 물론 이를 해결하기 위해 CoWaitForMultipleHandles 와 같은 API가 있다. 하지만 이러한 API를 사용할 때에는 반드시 Reenterancy에 대한 내용을 숙지하여야만 한다.

반면에 MTA의 경우 이러한 반응성 저하는 없다고 볼 수 있다. 모든 스레드는 원하는 객체에 대한 메서드를 행하기 위해 Cache Thread를 통해 바로 메서드 호출을 할 수 있다. 같은 아파트먼트 안에 속해 있는 다른 객체뿐 아니라 심지어 자기자신이 사용 중이라 해도 이것은 마찬가지다. 또한 MTA에 속하지 않은 스레드에서의 호출 역시 기존의 스레드가 아닌 ORPC CACHE 스레드가 새로 생겨 처리 되므로 매서드 호출이 블록킹 되는 경우는 생기지 않는다. 이러한 점은 프로그래머에게 동기화 지원을 직접적으로 해줄 것을 요구하여 복잡함을 가중시킨다. 하지만 오히려 이러한 점은 숙련된 프로그래머로 하여금 아파트먼트 단위 또는 객체 단위가 아닌 자원단위의 또는 최소한의 잠금을 가지게 되는 단위의 동기화를 할 수 있게 함으로서 훨씬 더 낳은 반응성을 가지게 한다. 이러한 점이 STA 안에 단 하나의 객체 밖에 없다하더라도 MTA 안의 객체가 더욱 우수한 반응성을 지니게 하는 원인이 된다.

 

아파트먼트 종류와 스레딩 모델에 따른 성능 차이

 

한가지 중요한 점은 여러 개의 스레드가 돌아간다고 해서 해서 특정 작업이 빨리 수행 되는 것은 아니라는 점이다. STA에서 블록킹 메서드 호출을 하는 경우가 아닌 한 사실 STA와 MTA의 성능은 MTA에 여러 개의 스레드가 동시접근 할 수 있다는 점만으로 MTA가 우수하다고 할 수는 없다. 만약 다음의 요인을 제외 한다면 사실 STA와 MTA의 성능 차이는 없다고 볼 수 있다.

하지만 실질적으로 COM의 함수 호출이 느려지게 만드는 요인은 따로 있다. 바로 Cross-Apartment Call의 비용이다. STA에서 다른 STA로 또는 STA에서 MTA로 또는 MTA에서 STA로의 함수 호출은 모두 Cross-Apartment Call이라고 볼 수 있다. 물론 다른 프로세스나 네트웍상의 다른 컴퓨터에 대한 호출 역시 Cross-Apartment Call이지만 이 경우 사실 다른 요인이 더욱 성능을 감소시키게 되므로 일단 논의에서 제외하자. 그렇다면 무엇이 문제인가?

이 글의 맨 처음에 Thread Context Switching은 매우 비싼 명령이라고 설명했다. 문제는 Cross-Apartment Call이 이 Thread Context Switching을 일으키게 된다는 점이다. 예를 들어 다른 아파트먼트에 들어간 스레드A로부터 어떤 STA에 있는 객체로 메서드 호출을 생각해 보자. 이 스레드A는 STA에 들어간 스레드B에게 메시지를 보낸 후 대기 상태가 된다. 이후 그러면 스레드B가 그때부터 활성화된다.(일반적으로 GetMessage에서 깨어난다.) 여기서 한번의 Thread Context Switching이 일어난다. 그리고 스레드B는 객체에 대해 실제로 메서드 호출한 후 이 결과 값을 다시 스레드A에게 넘겨준다. 여기서 또 한 번의 Thread Context Switching이 일어난다. 한 메서드 호출당 두번의 Thread Context 호출은 절대 쉽게 넘어갈 수 있는 비용이 아니다. 만약 이 메서드가 매우 자주 불린다면 이것은 엄청난 성능 저하로 이어 질 것이다. MTA의 경우 MTA에 호환되도록 만들어진 객체를 호출할 경우 같은 MTA에 속하는 객체이기만 하면 같은 아파트먼트에 속하므로 Thread Context Switching이 필요 없다는 점에서 STA보다 훨씬 낳은 상황을 만든다. 하지만 MTA에 들어가있는 스레드가 STA에 있는 객체에 접근 할려면 Cross-Apartment Call이 이루어져야만 하며 여전히 Context Switching이 필요하다.

 따라서 특정 객체의 스레딩 모델을 정하거나 스레드가 어떤 아파트먼트안에 들어갈 것인가를 결정할 때에는 매우 주의를 기울여야 한다. 만약 특정 스레드가 Single Threaded 모델을 지니고 있는 객체를 생성해서 주로 사용한다면 그 스레드는 반드시 STA 안으로 들어가야 한다. 단순히 MTA가 더 낳은 성능을 제공하겠지 하고 기대하는 것은 엄청난 실수다. 성능을 향상하는 것은 최대한 Thread Context Switching을 줄이는 것이며 이것은 곧 Cross-Apartment Call의 횟수를 줄이는 것과 직결된다. 물론 MTA와 호환되는 객체를 사용한다면 MTA로 들어가는 것이 성능향상에 도움이 된다.

만약 이정도로만 끝난다면 얼마나 좋을까? 하지만 아직도 몇 가지 고려사항이 남았다. 바로 사용할 특정 객체가 다른 객체들을 사용할 경우다. 이점은 바로 Both 스레딩 모델과 Free Threaded 모델이 따로 존재하는 이유이기도 하다. 지금까지의 논의만을 생각한다면 당연히 동기화 기능을 제공하는 모든 객체는 Both 스레딩 모델일 경우 더욱 우수한 성능을 보일 것으로 기대된다. 하지만 그렇게 간단하지는 않다. 예를 들어 객체(객체 A라고 하자)를 사용하는 스레드(스레드A라고 하자)가 STA에 들어가야만 하고 만약 객체A에 대한 메서드 호출을 할 때 객체A가 다른 객체(객체 B라고 하자)로의 메서드 호출을 한다면? 특히 객체B가 MTA에 존재 한다면? 이것은 매우 심각한 고려 사항이 된다. 객체A가 스레드A와 같은 아파트먼트 안에 존재해야 할 것인가? 아니면 객체B와 같은 아파트먼트 안에 존재해야 할 것인가? 이것은 얼마나 자주 객체A가 객체B의 메서드를 호출하는가에 달려 있다. 만약 객체A가 스레드A와 같은 스레드에 존재한다면 스레드A에서 객체A로의 메서드 호출은 직접적인 메서드 호출이다. 다만 문제는 이 메서드 내에서 객체B를 호출할 경우다. 이 경우 이 메서드를 실행중인 스레드A는 객체B와 다른 아파트먼트 안에 있으므로 Cross-Apartment Call을 하게 된다. 만약 이 메서드 내에서 객체B에 대한 호출이 여러 번 있다면 그 횟수만큼 Cross-Apartment Call을 하게 된다. 하지만 만약 객체A가 객체B와 같은 아파트먼트 안에 존재한다고 하자. 스레드A는 객체 A에 대한 매서드 호출을 할 때 Cross-Apartment Call을 해야만 한다. 하지만 그 이후 객체 A에서 이루어지는 객체 B에 대한 매서드 호출은 직접적인 메서드 호출이다. 이런 점을 종합해보면 만약 객체 A에서 객체 B로의 메서드 호출이 잦다면  객체A는 Both 스레딩 모델을 사용해 스레드A와 같은 아파트먼트 안에 들어가는 것보다는 Free 스레딩 모델을 사용해 무조건 MTA에 들어가는 것이 더 효율적이다.

 

Standard Marshaling과 Custom Marshaling

위에서 채널을 통해 데이터를 직렬화해서 보낸다고 했다. 이렇게 데이터를 직렬화 하는 것을 COM의 용어로는 마샬링이라고 한다. 사실 지금까지의 논의는 객체의 인터페이스를 Standard 마샬링 하는 경우를 가정한 것이다. 그렇다면 마샬링이란 무엇인가? 마샬링이라는 것은 특정 아파트먼트 안에서의 데이터나 인터페이스를 다른 아파트먼트에 전달될 수 있는 스트림 형태로 바꾸는 과정을 의미한다. 우선 데이터의 마샬링에 관한 내용은 이곳의 내용과 관련이 적으므로 다음으로 미루도록 하고 인터페이스의 마샬링에 대한 내용을 살펴보자. 지금까지 다른 아파트먼트에 속한 객체를 호출 할 때 프록시 객체가 만들어진다고 했다. 그렇다면 과연 프록시 객체는 누가 어떻게 만드는 것인가? 이것 즉 프록시를 구현하는 일이 바로 인터페이스 마샬링이 하는 일이다. (좀 더 기술적으로 말하면 두 가지는 동일하지는 않다. 프록시를 구현한다기 보다는 프록시를 만들 수 있는 스트림을 만들어내는 것이 마샬링이며 이것을 특정 아파트먼트에 넘기고 언마샬링하고 COM Library 또는 타입라이브러리 기반의 프록시/스텁 코다가 이 스트림을 해석해 원래의 인터페이스에 대한 호출을 한다. 마샬링에 관한 자세한 논의는 다음에 하도록 하자.) 하지만 앞에서 보았다면 프록시가 하는 일이 그렇게 간단하지만은 않다는 것을 보았을 것이다. 이것을 매번 프로그래머가 해줘야 한다면? 더욱이 지금까지 알아본 프로세스 내에서의 Cross-Apartment Call은 네트워크 상의 다른 컴퓨터의 객체를 부르는 것보다 훨씬 더 간단하다는 것을 상기한다면 네트워크 까지 생각해야하는 마샬링 코드를 매번 프로그래머가 작성해야 한다면 아마 전세계에 COM을 하는 프로그래머는 손가락을 꼽을 정도가 될 것이다. 하지만 다행히도 그렇지는 않다. 비록 C++의 데이터형이 조금 모호함을 가지고 있어 (이 부분에 대한 얘기도 다음에 하자) IDL이라는 언어로 interface를 만들어줘야 하긴 하지만 어쨌든 인터페이스에 대한 정의만 제대로 내려주면 인터페이스의 마샬링 자동으로 이루어지게 된다. 해줘야 할 일은 오직 CoMarshalInterface와 CoUnmarshalInterface를 호출하는 일이다.

하지만 특정한 객체의 경우 이러한 방법을 통한 마샬링이 비효율적인 경우가 있을 수 있다. 비록 IDL이 다양한 기능으로 여러가지 경우에 매우 효율적인 프록시/스텁 코드를 만들어 주기는 하지만 아무래도 객체에 대해 더 잘 알고 있는 프로그래머가 조금 더 효율적인 코드를 만들 수 있는 것은 당연한 것 아닌가? 그러다면 커스텀 마샬링은 어떻게 구현 하면 되는가? 바로 커스텀 마샬링을 하고 싶은 개체한테 IMarshal 이라는 인터페이스를 구현하면 된다.

interface IMarshal : IUnknown
    {
    HRESULT GetUnmarshalClass(REFIID iid, void *pvInterface
        , DWORD dwDestContext, void *pvDestContext, DWORD mshlflags
        , CLSID *pclsid);
    HRESULT GetMarshalSizeMax(REFIID iid, void *pvInterface
        , DWORD dwDestContext, void *pvDestContext, DWORD mshlflags
        , DWORD *pcb);
    HRESULT MarshalInterface(IStream *pstm, REFIID iid, void *pvInterface
        , DWORD dwDestContext, void *pvDestContext, DWORD mshlflags);
    HRESULT UnmarshalInterface(IStream *pstm, REFIID iid, void **ppv);
    HRESULT DisconnectObject(DWORD dwReserved);
    HRESULT ReleaseMarshalData(IStream *pstm);
    };

사실 IMarshal 인터페이스를 구현하는 경우는 매우 드물다. 하지만 이 인터페이스를 이해하고 있는 것은 큰 도움이 된다. COM Library는 객체를 마샬링할 필요가 있을 때 객체에게 QueryInterface를 통해 IMarshal 인터페이스를 구현하고 있는 지 알아본다. 그리고 만약 구현하고 있지 않다면 스탠다드 마샬링을 사용한다. 하지만 만약 구현하고 있다면 여기의 매서드를 통해 인터페이스를 마샬링한다. 가장 중요한 매서드는 MarshalInterface와 UnmarshalInterface 이다. 인자는 거의 명확하다. 직렬화 시킬 저장 공간과 인터페이스의 IID와 포인터 그리고 위치에 대한 정보다. 여기서 위치에 대한 정보는 이것이 같은 프로세스 안에서 사용 될 것인지 아니면 다른 프로세스 또는 다른 컴퓨터에서 사용될 것인지를 결정하는 내용이다. UnmarshalInterface의 경우는 반대로 이 스트림에서 프록시 객체를 만들어 내는 메서드이다.

 

Free Threaded Marshaler

 

여기서 이전의 논의를 기억해내자. 같은 프로세스 내에서 STA에서 MTA에 있는 객체의 메서드를 부르는 것도 아파트먼트의 경계를 지나게 되므로 프록시를 통해 매서드 호출을 하게 된다고 했다. 하지만 사실 이 객체는 MTA와 호환되는 객체이므로 분명 여러 스레드가 접근하는 것에 대해 안전한 객체일 것이다. 즉 STA에 있는 스레드가 직접 접근해도 문제가 생기지 않는 다는 뜻이다. 하지만 스탠다드 마샬링을 쓸 경우 프록시 객체를 쓰게 되며 이는 스레드 컨텍스트 스위칭을 발생시키고 상당한 성능 저하를 나타내게 된다. 하지만 우리에겐 Custom Marshaling이 있다. MarshalInterface와 UnmarshalInterface를 통해 마샬링 할 때 위치정보가 주어진다는 사실을 상기하며 만약 이 위치정보가 같은 프로세스 내를 가르키고 있을 때에는 프록시 객체가 아닌 실제 객체에 대한 포인터를 돌려 준다면? 그렇다면 만약 다른 아파트먼트에 있는 스레드가 접근 하더라하더라도 스레드 컨텍스트 스위칭을 일으키지 않고 메서드를 호출할 수 있게 된다. 사실 이러한 마샬링 방법은 흔히 사용되는 방법이기도 하다. 그래서 이러한 구현을 이미 해놓고 통합(Aggregation)을 통해 재사용 할 수 있는 API가 제공된다. 이것이 바로 CoCreateFreeThreadedMarshaler 라는 API이다.

HRESULT CoCreateFreeThreadedMarshaler(
  LPUNKNOWN punkOuter,
  LPUNKNOWN * ppunkMarshaler
);

첫번째 인자로 FTM을 구현하고자 하는 객체를 넣으면 두 번째 인자로 만들어진 객체가 나온다. 이렇게 만들어진 객체는 같은 프로세스내에서는 Cross-Apartment Call로 인한 어떠한 성능저하도 겪지 않는다.

주의 : 커스텀 마샬링을 구현하는 것은 COM+에서 Configured Component로 사용될 수 없음을 의미한다. FTM의 경우도 마찬가지 이므로 주의 하자. 사실 COM+는 마샬링 과정을 (COM+용어로 말하면 Interception이다) 자신이 제공하는 동기화, 보안, Queued Component등의 서비스를 제공하는데 매우 유용하게 이용하고 있다. 만약 Custom Marshaling을 구현한다면 이를 이용할 수 없게 되는 것이다. 만약 이 점이 COM+의 유용성에 큰 단점이 된다고 생각하면 COM+와 함께 새로 소개된 TNA라는 새로운 아파트먼트에 대해서 알아보자.

 

Thread Neutral Apartment

 

윈도우 2000, COM+는 TNA라고 하는 새로운 아파트먼트 모델을 발표하였다. 이 아파트먼트 역시 MTA 처럼 모든 프로세스에서 하나씩만 존재하는 아파트먼트이다. 하지만 중요한 점은 이 아파트먼트 안에 들어가기 위해 CoInitializeEx를 호출 할 필요가 없다는 점이다. 이 아파트먼트는 CoInitializeEx를 통해 들어가는 것이 아니다. 이 아파트먼트에는 어떤 스레드던 자신이 원한다면 들어갈 수 있다. 즉 STA에 들어가있는 스레드던 MTA에 들어가 있는 스레드건 TNA에 직접 들어가 메서드를 실행시킬 수 있는 것이다.

그러다면 TNA에 있는 객체와 FTM을 구현하는 객체와는 어떻게 다른가? FTM의 경우도 어떠한 아파트먼트에서의 호출이던 객체에 직접 접근하지 않는가? 바로 그것은 TNA에 있는 객체의 경우 다른 아파트먼트에서 이에 객체에 대한 인터페이스를 얻을려면 이것은 역시 직접적인 포인터가 아닌 프록시 객체라는 점이다. 다만 이 프록시 객체가 다른 아파트먼트의 경우처럼 스레드 컨텍스트 스위칭을 일으키지는 않는다. 다만 COM+가 제공하는 서비스에 대한 검사만을 행하고 메서드 호출을 한 스레드가 직접 TNA에 들어가서 메서드를 실행 시킨다.

그렇다면 무엇이 FTM에 비해 낳은가? 아무리 검사만을 행한다고 하지만 분명 FTM처럼 직접적인 포인터를 통한 접근은 아니다. 이는 조금은 속도 저하를 가져올 수도 있는 부분이다. 하지만 그 차이는 스레드 컨텍스트 스위칭과 비교한다면 정말 아무것도 아닌 비용이다. 하지만 이에 비해 COM+의 서비스가 제공하는 각종 서비스를 받을 수 있다는 점을 생각한다면 TNA는 성능과 유연성을 겸비한 매우 가치있는 아파트먼트 모델이 된다.

한가지 더 얘기 하자면 만약 Essential COM등의 책을 보신 분이나 기존에 COM+에 대한 개발방향등을 접하신 분이라면 RTA라는 모델에 대해서 들어보신 분이 있을 것이다. 이 모델은 TNA와 매우 비슷하지만 TNA의 경우 여러 개의 스레드가 동시에 접근 가능하고 RTA의 경우 한 스레드만이 접근할 수 있는 모델이다. (RTA란 모델은 실제 하지 않는다.) 하지만 마이크로소프트는 TNA를 RTA처럼 제한 되게 만들지 않고 기본적으로 여러 스레드의 접근을 허용한 후 COM+의 동기화등을 통해 RTA처럼 사용될 수 있게 만들었다.

 

결론

 

아파트먼트와 스레드에 관한 내용은 사실 보안과 함께 COM의 가장 어려운 부분이다. 사실 이 부분은 COM뿐 아니라 모든 어플리케이션에게 있어 가장 힘든 부분이기도 하다. 게다가 소스코드 거의 한 줄 없는 이 글은 매우 어려울지도 모른다. 하지만 이것은 분명 스레딩 모델의 차이 때문에 기존의 객체를 사용하지 못하는 것 보다는 낫다. 특히 Apartment Threaded 모델의 경우는 스레딩에 관해 생각하지 않고 짜도 멀티스레드 프로그램에서도 쉽게 재사용할 수 있다. 하지만 조금 더 우수한 컴포넌트를 만들기 위해 다른 스레딩 모델에 대해서도 알아보도록 하자. 최근들어 많은 컴포넌트의 개발이 VB를 통해 이루어지면서 Apartment Threaded 밖에 지원하지 않는 VB의 특성상 다른 아파트먼트 모델에 대해 관심을 가지는 경우는 매우 드문 경우 였던거 같다. 아니 VC++를 이용해 COM Component를 만드는 것 자체를 낭비라고 생각하는 것 같다. 이러한 점은 기존의 MTS가 Apartment Threaded모델 만을 지원했던 점과 맞물려 심지어 COM+ Component는 VB만으로 만들 수 있다, 내지는 COM+가 STA만 지원한다는 설까지 만들어 낸 거 같다. 물론 개인적으로 VB를 매우 유용한 언어라고 생각하며 프로젝트시 VC++보다 많이 쓰일 가능성이 높다는 점을 인정한다. 또한 개발기간이 프로젝트에 매우 중요한 요소임도 안다. 하지만  VC++로 좀 더 우수한 성능을 낼 수 있는 모델로 개발할 수 있다는 가능성마저 잊지는 말자. 어쨌든 최소한 개발기간 다음으로 중요한 요인은 성능과 안정성이니까.

또 하나 언급하고 싶은 점은 COM+의 경우 아파트먼트 보다 조금더 세분화 된 Context라는 단위로 객체가 존재하는 공간을 나눈다. 그리고 Context 간의 메서드 호출을 interceptor를 이용해 처리해 각종 서비스를 제공한다. 위의 논의의 상당부분이 이를 통해 논의되는 것이 정확함에도 불구하고 객체를 아파트먼트 단위로만 논의 했다. 이는 스레드에 관한 내용에 집중하기 위해서이다. 또한 마샬링에 관한 많은 내용이 자세히 설명되지 못했는데 이에 관한 논의는 다음에 하도록 하자.

 

참고서적

Essential IDL(Addison Wesley)
Essential COM (Addison Wesley)
COM+ Programming
A Practical Guide Using Visual C++ and ATL (Prentice Hall)
Application Programming for Microsoft Windows 4th(Microsoft Press)
MSJ
House of COM Don Box


'COM, ATL' 카테고리의 다른 글

Silan Liu의 COM과 ATL.html  (0) 2008.08.13
REGSVR32 사용법  (0) 2008.08.12
COM 기본 개념 ~~ 정리해 보자  (0) 2008.08.08
How to Use IMessageFilter  (1) 2008.08.02
Single Threaded Apartment(STA)에서 고려해야 할 몇가지 것들  (0) 2008.08.02
Apartment Types  (0) 2008.07.30
COM Message Filters  (0) 2008.07.30