동일한 내용의 코드를 세가지 버전으로 작성해내는 프로그래머의 모범적 자세... 라고 표현하면 나름 위로가 되기도 하겠지만, 최종 코드를 만들고 나니 이미 숱하게 보았던, 하지만 언제나 쉽사리 지나쳤던 바로 그 루틴이었다는데서 느끼는 허탈감.

배경
삽질 1의 그것과 동일하다. out of process에서 동작하는 COM Server와 통신하는 COM Client. 인스턴스로 올라간 COM Server에 client가 달라붙기 위해서 Running Object Table에 Server를 등록하고, client는 이 테이블에서 해당 Server를 찾아 연결하기까지의 내용. 알고보면 IPC(Inter Process Communication)를 이루는 가장 쉬운 방법(동기화 문제는 물론이요, 개체 기반 프로그래밍 패러다임까지 그대로 보존한 채 이를 이루기에)이 되겠다.

첫 번째 버전
위 시나리오를 그대로 따른 코드다. ROT와 이에 따른 Moniker에 대한 개념만 갖고 있다면 대강 reference 짜깁기해서 얻어낼 수 있는 코드이겠다.

서버측 코드
  1 ...
2 CComPtr<IMoniker> spMoniker;
3 HRESULT hr = ::CreateClassMoniker(CLSID_Communicator, &spMoniker);
4
5 CComPtr<IRunningObjectTable> spRot;
6 hr = GetRunningObjectTable(0, &spRot);
7 if(FAILED(hr)) { return hr; }
8
9 hr = spRot->Register(ROTFLAGS_REGISTRATIONKEEPSALIVE,
10 dynamic_cast<ICommunicator*>(this),
11 spMoniker,
12 &rotRegisterVal_);
13 ...
CLSID_Communicator는 서버측 클래스 개체의 클래스 GUID이고 ICommunicator는 서버가 노출할 인터페이스이다. 클래스 모니커를 하나 만들어 서버 인스턴스(this: 서버 자신)를 ROT에 등록하는 모습이 되겠다.

클라이언트측 코드
  1 CComPtr<ICommunicator>    spCommunicator_;
2 ...
3 CComPtr<IRunningObjectTable> spTable;
4 CComPtr<IEnumMoniker> spEnumMoniker;
5 CComPtr<IMoniker> spMoniker;
6
7 HRESULT hr = E_FAIL;
8 if(GetRunningObjectTable(0, &spTable) != S_OK) { return hr; }
9
10 spTable->EnumRunning(&spEnumMoniker);
11 spEnumMoniker->Reset();
12
13 CComPtr<IUnknown> spUnk;
14
15 while(spEnumMoniker->Next(1, &spMoniker, NULL) == S_OK)
16 {
17 hr = spTable->GetObject(spMoniker, &spUnk);
18 spMoniker.Release();
19
20 if(FAILED(hr))
21 {
22 if(spUnk) { spUnk.Release(); }
23 continue;
24 }
25
26 hr = spUnk.QueryInterface(&spCommunicator_);
27 spUnk.Release();
28
29 if(FAILED(hr))
30 {
31 if(spCommunicator_) { spCommunicator_.Release(); }
32 continue;
33 }
34 else { break; }
35 }
상당히 길다. 하지만 알고보면 단순해서 위 기본 시나리오에서 달라질 게 없다. ROT에 등록된 개체를 하나씩 조사하여 ICommunicator를 구현한 개체를 찾는 것 뿐이다.

두 번째 버전/클라이언트 코드
헌데, '이야, 내 생각대로 구현한게 잘 돌아가네?' 하며 잘 쓰고 있다가 불연듯 코드가 너저분하다는 생각이 드는거다. 특히나 client쪽에서. 편리한거 좋아하는 MS에서 이런 흔한 시나리오를 위한 utility 함수를 안만들었을리 없지, 생각하며 웹을 뒤져보니 BindMoniker()란 함수가 눈에 띈다. 특정 모니커와 바인딩을 한다... 위 클라이언트 코드가 하는 일과 맞아떨어지는 듯한. 아니나 다를까, 다음과 같이 확 줄어든 코드가 가능해진다.
  1 ...
2 CComPtr<IMoniker> spMoniker;
3 HRESULT hr = ::CreateClassMoniker(CLSID_Communicator, &spMoniker);
4 if(FAILED(hr)) { return hr; }
5
6 CComPtr<IUnknown> spUnk;
7 hr = ::BindMoniker(spMoniker,
8 0,
9 IID_ICommunicator,
10 (LPVOID*)&spCommunicator_);
11 spMoniker.Release();
12 ...
확연히 줄어든 코드. 게다가 서버측에서 만들었던 클래스 모니커 코드를 그대로 사용하기 때문에 의미론 상, 코드 가독성을 놓고 보아도 훨 뛰어나다. 아이, 좋아라~

위치 투명성 : location transparency
하지만 COM하면 위치 투명성, 즉 클라이언트에서는 서버가 in process에 있건, out-of process에 있건, remote에 있건 상관안하고 쓸수 있다는 게 자랑 아니던가. 코드가 짧아졌다지만 여전히 이 위치 투명성하고는 거리가 멀다.

그런 이유로 한번 더 웹을 뒤지다 눈에 걸린 함수, CoGetClassObject(). CoCreateInstance()는 서버 인스턴스를 생성하기에 아니겠다 싶어 딴놈을 찾다 눈에 걸린 함수다. 슬쩍보니 CoCreateInstance()랑 매개변수 목록도 똑같네. 어디, 이놈으로 바꿔보자.

최종 버전/클라이언트 코드
  1 ...
2 hr = ::CoGetClassObject(CLSID_Communicator,
3 CLSCTX_LOCAL_SERVER,
4 0,
5 IID_ICommunicator,
6 (LPVOID*)&spCommunicator_);
7 ...
이제는 달랑 한줄이다. 오, 행복해라. 근데 이 함수 어디서 많이 봤던거 같다. 맞다. out of process COM하면 항상 나오던 그 함수였다. 여기까지오니 이제 서버측 코드도 눈에 거슬린다. 또다시 웹을 뒤지니 항시 지나치던 CoRegisterClassObject()가 입질을 하는구만. 윽, 등록(Register)이란 말이 생략한 위치가 바로 ROT였구나. 이제 좀 감이 온다.

최종 버전/서버측 코드
  1 ...
2 return CoRegisterClassObject(CLSID_Communicator,
3 dynamic_cast<ICommunicator*>(this),
4 CLSCTX_LOCAL_SERVER,
5 REGCLS_MULTIPLEUSE,
6 &rotRegisterVal_);
7 ...
당연히 서버 종료시에는 등록 해제를 해야하고, 이를 위해 CoRevokeClassObject()를 사용한다.

허탈하기도 하고, 뿌듯하기도 하고. 내부 동작을 확인했다는데 의미를 둘 수도 있겠지만, 그간 상당히 보아온 코드를 싸그리 잊고 먼길로 돌아서왔다는 것은 움...이건 어떻게 해석해야 할까. 음냐.
Posted by 어쨌건간에