어떤 목적으로 분산 응용 프로그램을 사용하든지 필수 요건에서 효율적인 데이터 전송이 빠지는 경우는 거의
없습니다. 이 기사에서는 COM과 Windows® 2000을 사용하여 네트워크를 통해 다량의 데이터를 전송하는 방법과 이 과정에서 데이터
마샬링이 어떤 역할을 하는지에 대해 알아 보겠습니다. 또한, 데이터 버퍼 크기 문제를 알아 보고, 전송된 버퍼 크기를 최적화하는 몇 가지 전략도
설명합니다. 특히, COM은 많은 Windows 기반 구성 요소를 하나로 연결하는 배관 역할을 하므로 COM에 대해 중점적으로 알아 보겠습니다.
이 외에도 Windows 2000에 제공되는 많은 데이터 전송 기능에 대해서도 설명합니다.
데이터 마샬링
Windows 2000의 데이터
전송 기능에 대해 설명하기 전에 먼저 데이터가 어떻게 한 컴퓨터에서 다른 컴퓨터로 이동되는지에 대해 설명하겠습니다. COM은 Microsoft®
RPC(Remote Procedure Calls)를 기반으로 합니다. 사실, DCOM은 기본적으로 "개체 RPC"이며 ORPC라고 하기도
하는데, 제 생각에는 DCOM보다는 ORPC가 더 정확한 용어인 것 같습니다. RPC를 기반으로 하기 때문에 COM은 PRC IDL을 인터페이스
설명 방식으로 사용합니다. IDL은 다음 세 가지 기능을 하는 MIDL(Microsoft IDL 컴파일러)과 컴파일됩니다. 첫째, 인터페이스
설명(예: 형식 라이브러리)을 작성합니다. 둘째, 언어 바인딩을 만들기 때문에 원하는 언어의 인터페이스를 사용할 수 있습니다. 실제로는, C와
C++ 언어만 사용할 수 있으며 나머지 언어의 경우에는 형식 라이브러리를 사용해야 합니다. 마지막으로, 이 인터페이스를 마샬링하는 프록시-스텁
DLL을 만들도록 컴파일할 수 있는 C 코드를 만듭니다.
이 프록시-스텁 DLL은
인터페이스 호출을 차단하며 이 DLL에는 컨텍스트 범위 전체에 메서드를 호출하는 코드가 들어 있습니다. 이 구조가 그림?1에 나와 있습니다.
 |
| 그림 1 COM 마샬링 구조
|
개념 상으로 클라이언트 코드는 구성 요소를 직접 액세스합니다. 하지만 실제로는 클라이언트 컨텍스트와 구성 요소 컨텍스트에서
COM이 별도의 두 개체, 인터페이스 프록시와 스텁을 자동으로 로드합니다. 이 두 컨텍스트가 서로 다른 프로세스나 컴퓨터에 있다면 COM은
RPC를 통해 프록시와 스텁 초기화 버퍼를 전송하는 채널 개체를 제공합니다. 이 채널 개체는 RPC를 통해 구현되지만 개념 상으로는 가져오기 및
내보내기 컨텍스트에서 모두 액세스할 수 있습니다. 프록시는 메서드 매개 변수를 이 채널에서 얻은 버퍼 안에 패키징하며 스텁은 이 버퍼를 받아
이를 이용해 구성 요소를 호출하는 스택 프레임을 구성합니다.
COM은 해당 구성 요소의
컨텍스트에서 원래의 구성 요소 포인터가 처음 클라이언트 컨텍스트로 마샬링될 때 인터페이스 프록시와 스텁을 로드합니다. 표준 마샬링의 경우
COM은 이 인터페이스 포인터를 매개 변수로 하여 CoMarshallnterface에 전달합니다. 이렇게 하면 컨텍스트 고유의 인터페이스
포인터가 생기며, 이 포인터는 마샬링 중인 인터페이스와 구성 요소의 정확한 위치를 표시하는 수 바이트의 컨텍스트 중립 데이터로 변환됩니다. 이
데이터는 클라이언트 컨텍스트에서 마샬링 해제되어 컨텍스트 중립 데이터가 컨텍스트 고유의 인터페이스 프록시 개체로 변환되고, 이 개체가 프록시
관리자에 통합됩니다. 프록시 관리자는 이 프록시 개체의 ID를 제공합니다.
이 구조의 중요한 점은 프록시
개체가 원래의 개체와 똑같아 보인다는 점입니다. 이 인터페이스 스텁은 인터페이스를 잘 알고 있으며 인-컨텍스트 클라이언트처럼 작동됩니다.
그러나, 구성 요소와 해당 클라이언트는 마샬링이나 마샬링 구현 방법에 대해 알지 못합니다. 프록시와 스텁 개체가 구성 요소로 전달되는 호출을
차단하기 때문입니다. COM 마샬링은 메서드 호출만을 차단한 채 컨텍스트 전체로 전송하며, 그림과 같이 기타 차단 코드에 네트워크 전체 호출을
최적화할 코드가 포함될 수 있습니다. 다음은 이러한 종류의 차단 코드를 작성하는 방법과 Microsoft가 이러한 용도로 제공한 몇 가지 코드에
대한 설명입니다.
ILD를 사용하여 데이터 전송 지정
인터페이스 프록시와 스텁 개체를
만드는 가장 간단한 방법은 IDL의 인터페이스를 설명하고 MIDL을 사용해 코드를 만드는 방법일 것입니다.
IDL은 호출 시 전송되는 데이터
양과 전송 방향(클라이언트에서 구성 요소로 또는 그 반대 방향)을 나타내는 데 사용됩니다. 방향은 [in] 및 [out] 속성으로 표시되며
용량(배열의 최대 크기)은 [size_is()] 또는 [max_is()]로 표시됩니다. 그리고 데이터 항목의 실제 수는
[length_is()]로 표시됩니다. [size_is()] 속성은 얼마나 많은 데이터가 스텁으로 전송될지를 표시하며, 프록시는 이 정보를
사용하여 채널에서 얼마나 큰 버퍼를 요청해야 하는지, 그리고 이 버퍼에 얼마나 많은 데이터를 복사해야 하는지를 결정합니다. 때로 전송될 배열이
완전히 채워지지 않는 경우도 있으므로 [length_is()](또는 이와 같은 [last_is()])를 사용하여 클라이언트에서 구성 요소로
전송되는 불필요한 바이트를 줄여 최적화할 수도 있습니다.
다음은 이 속성 사용 방법을
보여주는 예입니다.
HRESULT PassLongs([in] ULONG ulNum,
[in, size_is(ulNum)] LONG* pArrIn);
HRESULT GetLongs([in] ULONG ulNum,
[out, size_is(ulNum)] LONG* pArrOut);
HRESULT GetLongsAlloc([out] ULONG* pNum,
[out, size_is(, *pNum)] LONG** ppArr);
| 데이터를 클라이언트에서 구성 요소로 전달할 때 클라이언트는 항상 저장소를 할당하며 이
저장소의 할당 취소를 담당합니다. 앞의 예에서 ulNum 매개 변수는 클라이언트 코드에서 자동 변수일 가능성이 가장 높으며 pArrln은 최소한
ulNum LONG 배열의 첫 번째 요소를 가리키는 포인터로, 스택이나 힙에 할당될 수 있습니다. [size_is()] 속성을 사용하기 때문에
마샬러는 ulNum 항목만 전송합니다.
구성 요소에서 클라이언트로
데이터를 전달할 때 클라이언트는 마샬러가 이 데이터를 복사할 저장소로 포인터를 전달합니다. 따라서 GetLong을 다음과 같이 호출할 수
있습니다.
ULONG ulNum = 10;
LONG l[10];
hr = pArr->GetLongs(ulNum, l);
| 이 구성 요소 코드는 다음과 같습니다.
STDMETHODIMP CArrays::GetLongs(ULONG ulNum, LONG *pArr)
{
for (ULONG x = 0; x < ulNum; x++) pArr[x] = x;
return S_OK;
}
| 그림과 같이 이 구성 요소 코드는 데이터 저장소를 pArr 포인터를 통해 액세스할 수
있다고 가정합니다. [size_is()] 속성이 필수 크기를 알려 주므로 구성 요소측 마샬러는 충분한 저장소를 할당합니다.
앞서
언급한 것처럼, 저장소 할당 취소는 클라이언트의 책임입니다. 이 경우에는 스택에서 자동 변수를 사용했으므로 추가 코드가 필요하지 않습니다.
이러한 방법은 이용 가능한 항목 수를 클라이언트가 알고 있다는 가정을 전제로 한 것입니다.
클라이언트가 구성 요소에서
데이터를 요청하기 전에 항목의 숫자를 알 수 없는 경우에는 어떨까요? GetLongsAlloc이 들어 있는 앞의 예를 봅시다. 여기서, 이 구성
요소는 pNum 매개 변수를 통해 반환된 배열의 크기를 반환합니다. 하지만 이 크기를 결정하는 것은 구성 요소 메서드이므로 마샬러는 이 메서드를
호출하기 전에는 저장소를 할당할 수 있는 충분한 정보를 갖지 못합니다. 따라서 구성 요소가 이 메모리를 할당해야 합니다. 구성 요소는 마샬링
계층이 알고 있는 메모리 할당자인 CoTaskMemAlloc를 사용하여 이 작업을 합니다.
STDMETHODIMP CArrays::GetLongsAlloc(ULONG *pNum, LONG **ppArr)
{
*pNum = 10;
*ppArr = reinterpret_cast<LONG*>(CoTaskMemAlloc
(*pNum * sizeof(LONG)));
for (ULONG x = 0; x < *pNum; x++) (*ppArr)[x] = x;
return S_OK;
}
| 구성 요소가 메모리를 다시
할당하지 않으므로 구성 요소와 클라이언트가 서로 다른 컴퓨터에 있다면 메모리 누출로 보일 수도 있습니다. 하지만 사실은 그렇지 않습니다. 구성
요소측의 마샬링 코드가 데이터를 RPC로 전송하고 나면 CoTaskMemFree를 호출하여 이 구성 요소측 버퍼의 연결을 해제합니다. 한편,
클라이언트측에서는 마샬러가 *pNum 항목이 전송되었음을 확인하고 CoTaskMemAlloc를 다시 호출하여 이 배열을 클라이언트측에서 복사하며
항목을 그 안으로 복사합니다. 그런 다음 클라이언트는 이 항목을 액세스할 수 있지만 CoTaskMemFree를 호출하여 이 배열의 할당을
취소해야 합니다.
ULONG ulNum;
LONG* pl;
hr = pArr->GetLongsAlloc(&ulNum, &pl);
for (ULONG ul = 0; ul < ulNum; ul++) printf("%ld\n", pl[ul]);
CoTaskMemFree(pl);
| 항목 수와 배열 포인터가
GetLongsAlloc에서 클라이언트로 반환됩니다. 이것이 바로 pl 주소가 이 메서드로 전달되는 이유이며 IDL에 다음과 같은 이상한 표시가
생기는 이유이기도 합니다.
[out, size_is(, *pNum)] LONG** ppArr
| [size_is()]에서 콤마는 *pNum이 ppArr이 가리키는 배열의 크기라는 것을
나타냅니다.
앞서 언급한 배열 속성을 사용하는
경우에는 반드시 MILD가 만든 C 파일을 컴파일하고 연결하여 프록시-스텁 DLL을 만들어야 합니다. ATL AppWizard는
projectps.mk라는 파일을 만들어 이 작업을 실행합니다. 서버가 구성 요소의 인터페이스를 자동화 마샬러로 마샬링된 형식
라이브러리로 등록하지 않도록 해야 합니다. 자동화는 이 배열 속성을 인식하지 못하기 때문입니다.
형식 라이브러리 마샬링을 이용한 데이터 전송
클라이언트가 형식
라이브러리 마샬링을 이용할 경우에는 어떻게 하겠습니까? 이 경우 이용할 수 있는 방법은 두 가지입니다. BSTR이나 SAFEARRAY를 사용하여
데이터를 전송하는 방법입니다. BSTR은 길이 접두사가 붙은 OLECHAR 버퍼(각각 16비트)이지만 SysAllocStringByteLen을
호출하여 8비트 바이트 배열을 만들도록 COM에 요청할 수 있습니다.
// 첫 번째 매개 변수의 NULL을 전달하여
// 초기화되지 않은 버퍼를 얻습니다.
BSTR bstr = SysAllocStringByteLen(NULL, 10);
LPBYTE pv = reinterpret_cast<LPBYTE>(bstr);
for (UINT i = 0; i < 10; i++) pv[i] = i * i;
| MIDL은 BSTR에 길이
접두사가 추가되었다는 전제 하에 BSTR을 위한 마샬링 코드를 만듭니다. 이 작업을 확인하려면 인터페이스 메서드에 BSTR을 추가하고 MIDL이
만든 project_p.c 마샬링 파일을 확인하면 됩니다. BSTR은 OLE32.dll에 제시되어 있는 BSTR_UserSize,
BSTR_UserMarshal, BSTR_ UserUnmarshal, BSTR_UserFree 기능을 사용하여 사용자
마샬링됩니다.
이 마샬링 루틴은 BSTR
접두사를 사용하여 전송할 바이트 수를 결정합니다. 데이터를 문자열로 해석하지 않으므로 삽입된 널이 있는 이진 데이터일 수 있습니다. 데이터가
BSTR 내에 있으면 Visual Basic®에 응용 프로그램을 작성할 때 이 데이터를 사용하는 것이 자연스러울 수 있습니다. 이 방법이
가능하긴 하지만 Visual Basic은 BSTR을 이용하여 여러 작업을 하며 데이터를 액세스하려면 이 작업의 일부를 실행 취소해야
합니다.
예를 들어, 다음과 같은 메서드가
있다고 합시다.
HRESULT GetDataInBSTR([out, retval] BSTR* pBstr);
| Visual Basic을 사용하여 BSTR에서 이진 데이터를 액세스할 수 있습니다.
Dim obj As New DataTransferObject
Dim s As String
Dim a() As Byte
' BSTR을 얻습니다.
s = obj.GetDataInBSTR()
' BSTR을 바이트 배열로 변환합니다.
a = s
' 이제 이 데이터를 이용해 작업을 수행합니다.
For x = LBound(a) To UBound(a)
Debug.Print a(x)
Next
| C++에서 ATL을 사용하여
동일한 작업을 하려면 비슷한 양의 코드가 필요하지만 COM 코드의 경우에는 그렇지 않습니다.
CComPtr<IMyData> pObj;
pObj.CoCreateInstance(__uuidof(DataTransfer));
CComBSTR bstr;
// BSTR을 얻습니다.
pObj-> GetDataInBSTR(&bstr);
// BSTR에서 바이트 수를 확인합니다.
UINT ui = SysStringByteLen(bstr.m_str);
LPBYTE pv = reinterpret_cast<LPBYTE>(bstr.m_str);
// 이 데이터를 이용해 작업을 수행합니다.
for (UINT idx = 0; idx < ui; idx++)
printf("array[%d]=%d\n", idx, pv[idx]);
| BSTR에 이진 데이터를 넣는
것과 관련된 또 다른 문제는 대부분의 래퍼 클래스가 데이터가 유니코드 문자열이라고 가정한다는 점입니다. CComBSTR::Length는
BSTR에 있는 유니코드 문자 수를 반환하므로 SysStringByteLen을 호출하여 BSTR에서 바이트 수를 확인했습니다.
데이터를 전송하는 또 다른 방법은 Visual Basic SAFEARRAY를 사용하는 방법입니다. SAFEARRAYS는 자체 설명이 있으므로, 차원의 수와 각
차원의 크기뿐 아니라 이 배열의 항목 형식에 대한 설명도 볼 수 있습니다. 이러한 정보를 통합하여 마샬러는 전송할 데이터의 양을 정확하게 알 수
있습니다. 이 기술을 사용할 경우의 장점은 SAFEARRAY에 VARIANT가 있으면 클라이언트를 스크립팅하여 이 데이터를 읽을 수 있다는
것입니다. 하지만 데이터를 1바이트 확보하려면 개별 VARIANT 항목에 대한 16비트 오버헤드를 맞춰야 합니다.
스트림 개체를 이용하여 데이터 전송
마지막 데이터 전송 방법은 스트림
개체를 사용하는 것입니다. IStream 포인터는 형식 라이브러리 마샬링으로 마샬링할 수 있으며 C++ 클라이언트로 액세스할 수 있습니다.
하지만 Visual Basic 코드를 통해 직접 액세스할 수는 없습니다. Visual Basic의 지속 가능한 개체는
IPersistStream과 IPersistStreamlnit를 지원하지만 IStream에 대한 직접 액세스는 제공하지 않습니다. IStream
인터페이스는 구조화되지 않은 버퍼에 대한 액세스를 효율적으로 제공합니다. 스트림에 데이터를 작성하는 코드와 이 데이터를 판독하는 코드는 반드시
그림?2처럼 이 스트림에 들어 있는 데이터 형식을 이해해야 합니다.
스트림을 이용하여 데이터를 전송할
경우 Win32® 기반 운영 체제를 실행하는 모든 컴퓨터가 스트림 마샬링 코드를 갖게 됩니다. 그러나, 그림?2와 같이 IStream 인터페이스를 통해 이 스트림의 데이터를 직접 액세스할 수는 없습니다. 이 스트림에
많은 데이터 항목이 들어 있는 경우에는 이 데이터를 액세스하기 위한 많은 호출이 스트림으로 들어 오게 됩니다.
데이터 전송 성능 향상
지금까지 다양한 데이터 전송
방법을 설명했으므로 이제 성능 문제에 대해 좀더 자세히 살펴보도록 합니다. 분산 응용 프로그램을 사용하면 네트워크 상에 있는 많은 컴퓨터의
데이터와 구성 요소의 기능을 이용할 수 있으므로 프로그래머의 입장에서는 매우 유용합니다. Windows DNA는 이러한 분산 구성 요소를
액세스할 수 있는 플랫폼과 도구를 제공합니다. 하지만 성능면에서 볼 때 분산은 정말 곤란한 문제입니다. 컴퓨터 범위 전체에 호출을 하려면
인-컨텍스트 호출에 필요한 명령의 4배가 되는 명령을 내려야 합니다.
네트워크 호출을 완전히 배제할
수는 없으며 어떤 경우에는 알지 못하는 사이에 네트워크 호출을 할 수도 있습니다. MTS(Microsoft Transaction
Services)의 분산 트랜잭션에서 이러한 경우가 발생할 수 있습니다. MTS를 이용하면 한 컴퓨터에서 트랜잭션을 만든 다음 다른 컴퓨터의
리소스 관리자를 동일한 트랜잭션에 넣을 수 있습니다. 이는 MTS 구성 요소의 컨텍스트 개체에 구성 요소의 트랜잭션 조건에 대한 정보(MTS
카탈로그에 있음)와 이 구성 요소가 사용하는 기존 트랜잭션에 대한 정보가 들어 있기 때문입니다. 이러한 MTS 구성 요소가 inproc 리소스
분배자를 통해 리소스 관리자를 사용할 경우 MTS는 컨텍스트 개체를 확인하며, 트랜잭션이 있는 경우 MTS는 이 리소스 관리자를 트랜잭션에
넣도록 리소스 분배자에게 명령합니다. 트랜잭션 MTS 구성 요소가 필수 트랜잭션 속성을 가지고 있는 다른 MTS 구성 요소를 액세스하는 경우 이
트랜잭션을 새 구성 요소로 내보냅니다.
MTS는 일반 DCOM을 통해
이루어지므로 구성 요소 활성화 요청 및 메서드 호출, 이 트랜잭션을 유지하기 위한 Microsoft Distributed Transcation
Coordinator 메시지를 작성하기 위해 네트워크 전체에 전달되는 별도의 데이터 패킷이 있습니다. 결론적으로 트랜잭션을 로컬로 유지하고 분산
트랜잭션을 완전히 배제하면 MTS 응용 프로그램 성능을 크게 향상시킬 수 있습니다.
COM+의 알려지지 않은 개선
사항 중 하나는 원격 COM+ 구성 요소 액세스에 사용되는 DCOM 패킷을 중간에 차단하여 분산 트랜잭션 사용을 간소화했다는 것입니다. 이로
인해 COM+는 분산 트랜잭션이 필요한 응용 프로그램에 적합한 플랫폼이 되었습니다. 하지만, COM+는 DCOM 패킷을 사용해 트랜잭션 ID를
전송하지만 MTS는 그렇지 않으므로 이 둘은 상호 운용성이 없습니다. 따라서 MTS 기반 구성 요소와 COM+ 구성 요소를 동일한 트랜잭션에
사용할 수 없습니다.
이렇게 최적화되었지만 리소스
관리자가 다른 컴퓨터에서 작성되고 통합된 트랜잭션에 참여하는 경우에는 항상 추가 네트워크 호출을 통해 2단계 실행이 이루어집니다. 따라서
가능하면 언제나 트랜잭션을 로컬로 유지하는 것이 좋습니다.
반드시 원격 컴퓨터에 있는 구성
요소를 액세스해야 하는 경우라면 먼저, 이 트랜잭션을 반드시 로컬 컴퓨터에서 만들어서 해당 원격 구성 요소로 전달해야 하는지를 판단해야 합니다.
그렇지 않다면 로컬 COM+ 구성 요소에서 트랜잭션 지원을 제거합니다.
리소스 관리자는 보통 SQL
Server™와 같은 데이터 소스입니다. 일반적으로 구성 요소는 가능한 한 사용할 데이터와 가까이 있어야 합니다. 따라서 중간 계층은 사용하는
데이터 소스와 같은 컴퓨터에 있는 경우가 많습니다. 이 방법이 불가능하다면 저장된 프로시저를 사용하여 데이터 소스에 있는 데이터를 조작하는
방법을 고려해 봅니다. 이렇게 하면 저장된 프로시저에 트랜잭션을 만들고 이 트랜잭션을 사용할 컴퓨터에 대해 로컬로 유지할 수 있습니다.
버퍼 크기 및 다른 컴퓨터 호출
네트워크 호출 건수를 최소한으로
유지하는 것만큼 버퍼 크기를 최대한으로 유지하는 것도 중요합니다. 이는 상식적으로 생각하면 됩니다. DCOM 패킷의 RPC와 DCOM 헤더
정보는 약 250바이트 정도이며 개별 네트워크 호출에서 전달되는 버퍼 크기를 늘리면 DCOM 패킷 대부분이 프로토콜 오버헤드보다는 데이터로
구성되도록 할 수 있습니다. 물론 버퍼가 크다면, 여러 개의 네트워크 호출로 전송해야 하는 데이터를 하나의 호출에 모두 넣을 수
있습니다.
 |
| 그림 3 전송 시간과 크기
|
그림?3은 데이터 버퍼의 전송 시간이 버퍼 크기에 따라 어떻게 달라지는지
보여주는 테스트 결과를 도표로 작성한 것입니다. 이 테스트에서는 일반적으로 사용되는 다양한 데이터 전송 방법을 사용하였으며 이에 대한 자세한
설명은 그림?4에 있습니다. 이 측정치는 트랜잭션이 많지 않은 네트워크 상에서 Windows 2000을 실행하는 두
컴퓨터 간의 데이터 전송을 측정한 값입니다. 그리고 데이터를 릴리스할 때 클라이언트가 사용한 버퍼를 정리하는 데 든 시간도 포함되어 있습니다.
네트워크와 컴퓨터에 따라 값이 달라지므로 절대값은 그리 중요하지 않으며, 중요한 것은 경향입니다. 버퍼 크기가 8KB를 초과하면 두 라인이 거의
같아지는 것을 볼 수 있습니다. 다시 말해, 8KB를 초과하면 버퍼 크기에 관계 없이 데이터 전송 효율이 같습니다. 하지만 8KB 미만에서는
버퍼 크기가 작아지면 데이터 전송 효율이 크게 떨어집니다.
다른 놀라운 사항은 스트림 개체를
통해 데이터를 전송하는 경우(일반적으로 다른 방법보다 오래 걸림)를 제외하면 전송률이 사실 상 같다는 것입니다. 이는 Windows
2000에서는 반드시 동일하거나 아니면 최소한 유사한 마샬링 코드를 사용해 BSTR, SAFEARRAY, 일치하는 배열을 전달해야 한다는 것을
보여 줍니다. 이는 Visual Basic을 사용하는 프로그래머에게는 반가운 소식입니다. 자동 마샬링을 사용하는 경우에는 일치하는 배열을
사용하는 데이터를 전달할 수 없으므로 마샬링 게임 사용을 포기할 필요가 없다는 것을 뜻하기 때문입니다. 이제, 이러한 프로그래머들도 컴퓨터 간에
다량의 버퍼를 효율적으로 전달할 수 있습니다.
마샬링 개체
분산 응용 프로그램에서 프로세스
간에 어떻게 데이터를 전송해야 할까요? 앞서 언급했듯이, 가장 중요한 문제는 소량의 데이터를 전달하는 호출을 많이 만드는 방법보다는 소수의
네트워크 호출에서 다량의 데이터 버퍼를 전달하도록 인터페이스를 계획하는 것입니다.
Visual Basic에서 사용한
속성의 액세스 방법은 분산 응용 프로그램에서는 분명 최악의 방법일 것입니다.
Dim day As New Day
day.Day = 8
day.Month = 9
day.Year = 2000
Debug.Print day.DayName
| Day 개체가 다른 컨텍스트에 있다면 이 개체를 호출할 때마다 마샬링이 이루어질
것입니다. 이 예에서는 이 개체에 대해 네 번의 호출이 이루어집니다. 이 속성을 다음과 같은 단순한 메서드로 바꾸면 손쉽게 호출을 한 번으로
줄일 수 있습니다.
Dim day As New Day
Debug.Print day.GetDayName(8, 9, 2000)
| 이러한 개체 액세스 방법은
MTS와 COM+ 개발자에게는 익숙한 방법입니다. 구성 요소의 상태가 메서드 매개 변수에서 전달되므로 이 종류의 구성 요소를 가끔 상태
없음이라고 합니다. MTS와 COM+ 트랜잭션 구성 요소를 이렇게 액세스하면 트랜잭션 분리 상태를 유지할 수 있으며, 이 구성 요소는
GetDayName을 실행하기 위해서만 활성화됩니다.
가능하면 참조가 아니라 값에 따라
데이터를 전송하는 것이 좋으며, 개체는 코드 판독을 쉽게 만들어 주기 때문에 개체를 사용하면 더 좋습니다. 분산 데이터 전송에 있어서 COM
구성 요소의 단점은 항상 참조에 따라 전송된다는 점입니다. 따라서 원격 컴퓨터에서 구성 요소를 만들면 이 구성 요소는 항상 컴퓨터에 상주하게
되고 이 구성 요소에 대한 액세스는 모두 마샬링된 인터페이스 포인터를 통해 이루어지게 됩니다. 그리고 이 구성 요소에 대한 메서드를 호출하면
항상 네트워크 호출이 이루어집니다.
개체 모델을 디자인할 때에는 구성
요소를 이용한 데이터 전달은 피하는 것이 좋습니다. 예를 들면 다음 Visual Basic 코드는 좋지 않습니다.
Dim person As New Person
' 인-컨텍스트라면 속성 액세스가 가능합니다.
person.ForeName = "Richard"
person.SurName = "Grimes"
Dim customers As CustomerList
Set customers = CreateObject("CustomerSvr.CustomerList", _
"MyRemoteServer")
customers.Add person
| 이 경우 person이라는 이름의 개체가 인-컨텍스트에서 만들어졌다고 보면 속성
액세스를 사용하여 호출할 수 있습니다. 이 개체는 원격 개체인 customers로 전달됩니다. 이 코드는 읽을 수 있으며 논리적입니다.
customres 목록에 새 person을 추가하면 Person 클래스에 새 인스턴스를 만들 수 있으며 CustomerList 클래스의
인스턴스에 추가할 수 있습니다. 하지만 이 코드는 분산 응용 프로그램에서는 좋지 않습니다. person 개체를 customers 개체로 직접
전달할 수 없으며 참조를 통해 전달해야 하기 때문입니다. 즉, person 개체에서 데이터를 얻으려면 customers 개체가 반드시 네트워크
호출을 해야 한다는 것을 뜻합니다. 이 간단한 예에서, CustomerList 클래스에 추가 개체를 사용하는 대신 customer의 이름을
이용해 전달할 수 있는 메서드가 있다면 훨씬 더 좋을 것입니다.
물론, 실제로 사용되는 코드는
이렇게 단순하지 않습니다. 따라서 개체에 많은 데이터가 들어 있는 경우에는 개체 전달이 특히 어렵습니다. 매개 변수가 10개인 메서드를 호출하고
E_INVALIDARG를 받은 다음 정확히 어느 매개 변수가 올바르지 않은지를 확인하기 위해 긴 시간을 낭비한 적이 있으십니까? 데이터를
속성으로 사용하여 인-컨텍스트 개체로 전달하면 이러한 문제를 해결할 수 있습니다. 그렇게 하면 개별 속성이 변경될 경우 개체가 확인을 하므로
속성이 올바르지 않으면 개체가 의미 있는 오류 코드를 반환합니다. 비효율적인 크로스-컨텍스트를 액세스하지 않고 개체를 통한 데이터 전달을
이용하려면 개체가 값에 따라 마샬링되지 않도록 해야 합니다.
값에 따른 마샬링
값에 따른 마샬링은 앞서
MSJ에서 설명했지만 이에 대해 나중에 보다 자세히 설명하려고 하므로 지금은 간단하게 다루겠습니다. 자세한 내용은 House
of COM (MSJ,1999년 3월)을 참조하십시오. 구성 요소가 마샬링 메커니즘에 말을 넣도록 하려면
IMarshal을 실행합니다. COM은 구성 요소를 만들 때 항상 이 인터페이스를 쿼리합니다. 구성 요소가 IMarshal을 실행하지 않으면
표준 마샬링으로 만족한다는 것을 뜻합니다. 하지만 구성 요소가 IMarshal을 실행하면 COM은 메서드를 호출하여 클라이언트 컨텍스트에서
사용되는 프록시 개체의 CLSID를 얻고 프록시로 전달되어 개체로 연결되도록 하는 정보가 들어 있는 데이터를 받습니다.
값에
따른 마샬링에서 구성 요소는 항상 인-컨텍스트로 액세스해야 한다고 표시되어 있습니다. COM이 클라이언트 컨텍스트에 구성 요소를 복제하도록 하면
됩니다. 이렇게 하려면 구성 요소는 이 상태를 연속으로 만들고 일련의 상태에서 자체 복사본을 초기화해야 합니다. COM이 구성 요소 인터페이스를
마샬링하면 프록시 개체의 CLSID를 요청합니다. 그런 다음 구성 요소는 자체 CLSID를 반환하여 COM이 클라이언트 컨텍스트에 초기화되지
않은 구성 요소 버전을 만들도록 만듭니다. COM이 IMarshal::MarshalInterface 호출을 이용해 마샬링 정보를 제공하도록 구성
요소에 요청하면 구성 요소는 마샬링된 패킷에 이 상태를 연속으로 진행합니다. 그러면 COM이 이 패킷을 프록시 개체(클라이언트 컨텍스트에 있는
초기화되지 않은 구성 요소 인스턴스)에 전달하고, 이 개체는 구성 요소 상태 정보를 추출하여 클론을 초기화합니다. 값에 따른 마샬링 메커니즘은
기본적으로 개체를 고정시킨 다음 클라이언트 컨텍스트로 복사하여 이 곳에서 구성 요소를 다시 원래 상태로 되돌립니다. 프록시는 인-컨텍스트
버전이며 모든 COM 호출은 이에 따라 서비스되므로 컨텍스트 이외의 개체와 연결되지 않아도 됩니다.
값에 따른 마샬링은 생각보다 자주
사용됩니다. ActiveX 데이터 개체에 연결이 끊긴 레코드 집합이 값에 따른 마샬링의 흔한 예입니다. 표준 오류
개체(CreateErrorInfo를 통해 만들어지며 GetErrorInfo를 통해 액세스됨) 또한 값에 따라 마샬링되므로 클라이언트 코드가 오류
개체를 액세스하여 오류에 대한 정보를 얻으려고 할 경우 이 호출에는 마샬링은 이루어지지 않습니다. 하지만, OLE DB에 사용되는 확장 오류
개체는 값에 따라 마샬링되지 않습니다. 대신, 클라이언트가 오류을 유발한 개체의 컨텍스트에서 실행되는 조회 개체라고 하는 추가 개체를 사용해
IErrorInfo::GetDescription을 호출하면 오류 설명이 만들어집니다. 이 경우 마샬링된 호출이 필요합니다.
값에
따른 마샬링 구성 요소는 한 가지 제한이 있습니다. 컨텍스트 외부 구성 요소와의 연결이 끊어지면 해당 프록시는 구성 요소에 값을 쓸 수 없으며
클라이언트가 받는 프록시는 읽기 전용이 됩니다.
처리기 마샬링
처리기 마샬링은 COM 규정에
표준 마샬링과 사용자 지정 마샬링 사이의 중간 계층으로 설명되어 있습니다. 즉, 개발자는 표준 마샬링 메커니즘을 연결하여 추가 코드를 제공하지만
기본적으로는 구조를 원래대로 유지합니다.
처리기 마샬링은 이제 새로운
방법이 아닙니다. 처리기 마샬링이 처음 사용된 것은 OLE 2로, 복합 문서에서 삽입된 개체로 사용되었습니다. OLE 2의 문제점 중 하나는
OLE 서버를 둘 이상 로드하면 사용되는 메모리 양 때문에 시스템 전체가 중단될 수 있다는 점이었습니다. Inproc 처리기는 inproc
코드가 실행할 수 있는 개체의 인터페이스 메서드(예: 렌더링) 일부를 실행할 수 있으므로 이 문제가 완화되었습니다. 처리기가 실행할 수 없는
작업을 클라이언트가 요청하면 처리기는 이 서버를 로드하여 이 작업을 실행하도록 할 수 있습니다.
Windows 2000 이전의
버전에서 처리기 마샬링의 한 형식을 구현할 수 있습니다. 이 구성 요소는 IMarshal을 실행하여 표준 마샬링 개체 대신 처리기라고 하는
사용자 정의 프록시 개체를 사용해야 한다고 표시합니다. COM이 IMarshal::MarshalInterface를 호출하여 구성 요소에 마샬
패킷을 요청하면 CoGetStandardMarshal이 호출되고 COM은 표준 마샬 패킷을 얻습니다. 즉, 개체 인터페이스는 표준 마샬링을
사용하여 마샬링되므로 개발자는 프로세스 간 통신 코드 작성에 대해 걱정할 필요가 없습니다. 구성 요소가 IMarshal을 실행하는 가장 큰
이유는 이렇게 함으로써 GetUnmarshalClass를 사용하여 처리기 개체의 CLSID를 반환할 수 있기 때문입니다. 하지만 구성 요소와
처리기 또한 IMarshal이 사용되고 있으며 추가 초기화 데이터를 마샬 패킷에 추가할 수 있다는 점을 이용할 수 있습니다.
구성
요소의 인터페이스가 표준 마샬링을 사용하기 때문에 처리기는 컨텍스트 외부의 개체를 액세스할 수 있으며 이 개체의 인터페이스 메서드 일부를 로컬로
처리할 수도 있습니다. 따라서 계수기는 Next 메서드를 실행하여 캐시에서 값을 반환하며 다수의 항목을 요청하는 실제 개체로의 호출을 이용하여
이 캐시를 보충할 수 있습니다. 하지만 사용자 정의 프록시의 CLSID를 표시하는 것이 IMarshal을 구현하는 유일한 목적이라면
IMarshal의 모든 메서드를 구현할 필요가 없을 수도 있습니다.
COM은 개체가 IMarshal을
구현할 필요가 없는 대안을 제공합니다. IMarshal을 실행하는 대신 다음 코드에 있는 IStdMarshalInfo라는 인터페이스를 실행하는
방법으로, 여기에서는 GetClassForHandler라는 단일 메서드가 GetUnmarshalClass 대신 사용됩니다.
[ local, object,
uuid(00000018-0000-0000-C000-000000000046) ]
interface IStdMarshalInfo : IUnknown
{
HRESULT GetClassForHandler([in] DWORD dwDestContext,
[in, unique] void *pvDestContext, [out] CLSID *pClsid);
}
| COM은 이 처리기를 실행하는 서버로의 패스와 InProcHandler32 키를 찾기
위해 CLSID 레지스트리 키에서 CLSID를 검색합니다.
Windows 2000의 처리기
마샬링을 사용하면 클라이언트측의 마샬링 프로세스로 후크할 수 있습니다. 이 경우 마샬링된 호출 필요 여부를 처리기가 판단하도록 하면 해당 구성
요소에 대한 호출 건수를 제한할 수 있습니다. 처리기가 구성 요소의 인터페이스를 실행하면 클라이언트가 호출할 수 있습니다. 이 처리기가 실행하지
않는 인터페이스를 클라이언트가 쿼리하면 호출 오류가 발생합니다.
 |
| 그림 5 처리기 마샬링 구조
|
그림?5는 클라이언트측 구조입니다. 그림과 같이 처리기는 IUnknown을
실행하는 클라이언트측 ID 개체에 따라 집계됩니다. 이 처리기는 인터페이스를 그대로 실행할 수도 있고 클라이언트 호출을 실제 개체로 위임하도록
할 수도 있습니다. 후자의 경우 처리기는 프록시 메니저로의 포인터를 받아야 하며 이 포인터를 사용하여 개체의 인터페이스를 액세스해야 합니다. 이
경우 처리기는 다음 호출을 합니다.
HRESULT CoGetStdMarshalEx(IUnknown* pUnkOuter,DWORD dwSMEXFlags,
IUnknown** ppUnkInner);
| 첫 번째 매개 변수는 처리기의
IUnknown을 제어하는 ID개체이고, 두 번째 매개 변수는 프록시 관리자와 서버측 표준 마샬러 중 무엇이 필요한지 지정하는 데 사용되는
클래스입니다. 이 때 처리기는 SMEXF_HANDLER 값을 전달합니다. 이 호출이 성공적으로 이루어지면 마지막 매개 변수에 프록시 관리자를
가리키는 포인터가 반환됩니다. 그러면 처리기는 필요한 인터페이스에 대한 포인터를 쿼리할 수 있으며 표준 인터페이스 프록시에 대한 포인터가
반환됩니다. 이것은 표준 마샬링에 대한 후크이므로 인터페이스는 사용자 정의 또는 이중 인터페이스일 수 있습니다.
그림?6은 폴더에 있는 파일 이름인 문자열 배열에 대한 액세스를 제공하는 인터페이스 처리기입니다. 이 코드는
FileEnum 예에 제시되어 있으며 이 예는 이 기사 상단 링크에서 다운로드할 수 있습니다.
 |
| 그림 7 FileEnum에서 사용되는
개체 |
그림?7에는 이 예에서 사용되는 개체가 제시되어 있습니다. 이 클라이언트 컨텍스트는 다음
코드로 구현됩니다.
Interface IFiles2 : IDispatch
{
HRESULT GetNextFile ([ out, retval] BSTR: pData);
};
| 이 서버 컨텍스트는 다음 코드에 해당합니다.
interface IFiles : IUnknown
{
HRESULT GetNextFiles ([ in] ULONG count,
[out, size_is(count), length_is(*pFetched)]
BSTR* pData, {out} ULONG* pFetched);
};
| 처리기와 구성 요소는 두 가지 다른 인터페이스를 실행합니다. 처리기는 IFiles2를
실행하며 여기에는 GetNextFile이라고 하는 하나의 메서드가 있습니다. 이렇게 되면 구성 요소가 지정된 폴더용으로 보관하고 있는 파일 이름
목록에 그 다음 파일 이름이 반환됩니다. 구성 요소는 네트워크용으로 최적화된 IFiles 인터페이스를 실행하여 GetNextFiles 메서드를
통해 많은 파일 이름을 얻게 됩니다. IFiles는 [size_is()]와 [length_is()]를 사용하므로 프록시-스텁 DLL을 이용해
마샬링됩니다. IFiels2는 인-컨텍스트로 액세스되므로 마샬링되지 않습니다.
IFiles::GetNextFile은 캐시를 로컬로 유지하여 실행되며 캐시가 비면 Files 개체를 호출하여 BUF_SIZE
항목 수를 얻습니다. 이 체계의 불편한 기능 중 하나는 클라이언트 컨텍스트 내에서 처리기가 만들어지지만 초기화는 되지 않는다는 것입니다. 따라서
클라이언트가 컨텍스트 내에 있는 처리기를 활성화하면 첫 번째 클라이언트 액세스가 이루어질 때 컨텍스트 외부 호출을 해야 합니다.
보다
효율적인 방법은 일부 초기화 값을 처리기에 전달하는 것입니다. Windows 2000의 처리기 마샬링을 이용하면 이렇게 할 수 있지만 개체와
마샬러 모두 IMarshal을 실행해야 합니다. 이 개체는 반드시 처리기가 실행해야 하는 유일한 메서드인
IMarshal::UnmarshalInterface를 제외하고는 나머지 모든 메서드 구현을 제공해야 합니다. 이 개체는 COM이
IMarshal::GetMarshalSizeMax를 호출할 때 지정된 데이터 크기를 이용하여 값에 따른 마샬링과 유사한 방법으로
IMarshal::MarshalInterface를 사용하여 마샬 패킷을 액세스하고 자체 데이터를 삽입할 수 있습니다. 하지만 이 개체는 어떻게
마샬 패킷을 액세스할까요? 이 경우에도 CoGetStdMarshalEx를 호출해야 합니다.
CComPtr<IMarshal> m_pMarshal;
CComPtr<IUnknown> m_pUnk;
HRESULT FinalConstruct()
{
HRESULT hr;
hr = CoGetStdMarshalEx(GetUnknown(), SMEXF_SERVER, &m_pUnk);
if (FAILED(hr)) return hr;
hr = m_pUnk->QueryInterface(&m_pMarshal);
if (SUCCEEDED(hr)) Release();
return hr;
}
| 이 코드는 개체의 IUnknown 인터페이스를 CoGetStdMarshalEx가 알 수
없는 제어로 전달하며 SMEXF_ SERVER를 dwSMEXFlags 매개 변수로 전달합니다. 표준 마샬러 개체는 이 포인터를
AddRef합니다. 과도한 참조가 이루어지므로 호출 코드가 Release를 호출하여 이 점을 처리하도록 합니다. 그런 다음 이 코드는
IMarshal을 쿼리합니다. IMarshal과 IUnknown 포인터를 캐시해야 합니다. FinalConstruct 끝에서 IUnknown
포인터를 릴리스하면 IMarshal 인터페이스가 올바르지 않게 됩니다.
그런 다음 그림?8과 같이 IMarshal 포인터를 사용하여 개체에서 IMarshal을 실행할 수 있습니다. 여기서는,
처리기로 마샬링하고자 하는 데이터가 DATA_SIZE 바이트인 ExtraData라는 버퍼에 있다고 가정했습니다. 표준 마샬러가
GetUnmarshalClass를 구현합니다. 즉, 표준 마샬러측에서 마샬링에 사용된다고 생각되는 인터페이스 마샬러가 크로스-컨텍스트 호출에
사용된다는 것입니다. 인터페이스는 형식 라이브러리 마샬링을 비롯하여 어떤 방식으로든 마샬링할 수 있으므로 사용자 클라이언트가 스크립팅
클라이언트가 될 수 있습니다.
클라이언트측에서 보면 그림?9와 같이 사용자 코드는 IMarshal::UnmarshalInterface만 실행해야 합니다. 프록시
포인터를 다른 컨텍스트로 마샬링하려는 시도가 이루어지는 경우가 아니라면 다른 메서드는 호출되지 않습니다. 이 상황을 해결하려면 메서드를 표준
마샬러로 위임하면 됩니다. 처리기가 이미 지정되어 있으므로 새 컨텍스트에서는 표준 마샬러가 로딩합니다.
통합된 표준
마샬러(CoGetStdMarshalEx에서 반환)는 Windows 2000에서만 사용할 수 있으므로 이 처리기는 다른 운영 체제에서는 실행되지
않습니다. 하지만 개체가 IStdMarshalInfo를 실행하는 경우에는 Windows 2000을 실행하지 않더라도 처리기에 대한 정보가 다시
클라이언트 컴퓨터로 전달됩니다. 하지만 이 경우에는 오류 코드가 만들어집니다. 처리기 마샬링을 해제할 수 없으므로 클라이언트와 서버 모두가
Windows 2000을 실행해야 합니다.
파이프를 사용하여 데이터 전달
수 메가바이트, 아니, 수
기가바이트에 이르는 데이터를 전송해야 하는 경우를 한 번 상상해 보십시오. 데이터 패킷은 8KB보다 훨씬 클 것이므로 네트워크에 비효율적인
호출을 하게 될 걱정은 할 필요가 없지만 염두에 두어야 할 다른 문제들이 있습니다. 호출과 결과 처리에 대해 생각해 봅시다. 먼저, 클라이언트는
구성 요소를 호출하고 데이터 반환을 요청합니다. 구성 요소는 어딘가에서 해당 데이터를 가져와서 RPC가 전송하는 버퍼로 데이터를 복사해야
합니다. RPC는 이 데이터를 네트워크 전체에 전달하며 클라이언트 컨텍스트 내에 있는 버퍼로 복사합니다. 일반 버퍼 내에 복사하면 클라이언트가
이 데이터를 액세스할 수 있습니다. 이 시간 동안 클라이언트 스레드는 차단됩니다.
이 때, 클라이언트 스레드는
데이터를 처리할 수 있지만 데이터 크기가 크므로 시간이 오래 걸립니다. 이 처리 시간 동안, 클라이언트의 경우 구성 요소는 사실 상 유휴 상태가
됩니다. 즉, 데이터를 만들어서 전달하기까지는 시간이 오래 걸리며 클라이언트는 그 때까지 기다려야 합니다.
이 대기 시간을
줄이기 위해 COM 파이프가 개발되었습니다. 이 파이프는 전송할 데이터 버퍼를 청크로 나누어 파이프를 통해 하나씩 전송한다는 개념을 바탕으로
합니다. 클라이언트가 전체 버퍼를 받을 때까지 긴 시간을 기다리는 대신 크기가 작은 청크가 도착할 때까지 짧은 시간을 기다리기만 하면 됩니다.
따라서 클라이언트에 버퍼가 도착하면 프로세스를 시작할 수 있습니다. 이제 클라이언트가 요청하지 않더라도 COM이 또 다른 데이터 청크 전송을
구성 요소에 요청합니다. 다른 데이터가 처리되고 있는 도중에 또 다른 데이터 청크를 요청하는 프로세스를 미리 읽기라고 합니다.
즉시
균형을 이루게 되면 청크 프로세스에 걸리는 시간은 또 다른 청크를 만들어서 전송하는 데 걸리는 시간과 같아집니다. 이는, 클라이언트가 기다리지
않고 즉시 다음 데이터 청크를 액세스한다는 것을 뜻합니다. 물론, 이러한 균형을 얻기가 쉽지는 않지만 시간을 많이 절약할 수
있습니다.
파이프는 새로운 기술은 아니며,
Microsoft RPC도 얼마간 이 기술을 지원했습니다. 차이점이라면 RPC의 경우에는 파이프를 통해 전송될 데이터를 정의해야 한다는 것이며,
RPC는 개체 기반이 아니므로 컨텍스트 핸들을 처리해야 합니다. Windows 2000 Platform SDK는 IPipeByte,
IPipeLong, IPipeDouble의 세 가지 파이프 인터페이스를 지원합니다(그림?10 참조). Windows 2000을 실행하는 모든 컴퓨터에는 각각의 마샬러가 있습니다. 이 인터페이스는
전송하는 데이터 형식만 다릅니다.
개별 파이프 인터페이스에는
Push와 Pull이라는 두 가지 메서드가 있습니다. 이는 COM 파이프가 양방향이라는 것을 뜻합니다. 한 인터페이스가 다른 쪽에서 파이프를
받으면 데이터를 수신(풀) 및 전송(푸시)할 수 있습니다. 실제로, 동시에 두 가지를 모두 실행할 수 있습니다. async_iid 속성을 사용해
인터페이스를 선언했기 때문에 동기 또는 비동기식(비차단)으로 인터페이스를 호출할 수 있습니다. 이에 대해서는 잠시 후 다시 설명하도록
합니다.
파이프를 사용할 경우에는 먼저
응용 프로그램의 어떤 부분에서 파이프 코드, 클라이언트 또는 구성 요소를 실행할지 결정하는 것입니다. 다음 두 메서드를 생각해 봅시다.
HRESULT ProvidePipe([in] IPipeByte* pPipe);
HRESULT GetPipe([out] IPipeByte** ppByte);
| 첫 번째 메서드는 IPipeByte 인터페이스를 구현한 클라이언트가 호출하도록 되어
있습니다. 이 메서드는 인스턴스를 만들어 구성 요소로 전달하며, 그러면 구성 요소는 데이터를 받거나 전송하는 호출을 시작합니다. 두 번째
메서드의 경우, 구성 요소는 서버 컨텍스트에 있는 파이프 구현을 액세스하며 이 경우에는 구성 요소가 아니라 클라이언트가 받거나 전송하는 작업을
시작합니다.
데이터 수신 및 전송이 매우
간단합니다. Windows 2000에 제공되는 파이프 마샬러가 미리 읽기 기능을 실행하므로 코드 작성 시 미리 읽기를 고려할 필요가 없습니다.
하지만 고려해야 할 한 가지 문제가 있습니다. 즉, 미리 읽기를 실행할 수 있을 만큼의 충분한 데이터가 있다는 사실을 COM이 어떻게 알 수
있는가 하는 문제입니다. 수신 작업이란 수신 코드가 되풀이해서 IPipeXXX::Pull을 호출해야 하며, 그 동안 개별 버퍼 COM은 구성
요소를 호출하여 다음 버퍼를 받아야 한다는 것을 뜻합니다. COM은 미리 읽기를 계속하지 않도록 데이터가 모두 사용되는 시기를 알아야 합니다.
이를 위해서는 파이프 구현이 pcReturned 매개 변수에 0을 반환해야 합니다. 결과적으로 Pull을 사용하는 경우에 비해 한 번 더
네트워크를 호출해야 합니다.
그림?11은 파이프를 통해 텍스트 파일을 전송하는 경우에 대한 단순한 파이프 구현입니다. 이 구성 요소는
Pull 메서드를 구현하며 이 메서드는 0바이트가 반환되었다고 표시될 때까지 반복해서 호출됩니다. 파이프는 파일에 데이터가 더 이상 남아 있지
않으면 0을 반환합니다. 그림?12에 제시된 GetFileData 메서드도 이 파이프를 반환할 수 있습니다.
Push 메서드도 단순합니다. 데이터를 모두 전송할 때까지 반복해서 Push를 호출합니다. 하지만, 데이터가 모두 전송되었으며
미리 읽기를 실행할 수 없다는 것을 COM이 알 수 있도록 데이터 전송기는 그림?13과 같이 반드시 0바이트를 전송해야 합니다.
실제 데이터 전송은 파이프가
실행하므로 더 이상 파이프가 필요하지 않을 경우 파이프에 이를 알리도록 해야 합니다. Push를 호출하고 0바이트를 전달하거나, Pull을
구현하여 전송이 완료되면 0바이트를 반환하도록 하는 것을 잊어버리면 파이프가 계속해서 활성 상태로 남아 있고 COM은 계속해서 파이프를
참조합니다. 파이프 프록시가 있는 아파트를 종료하려고 하면, 호출이 컴퓨터 경계 전체에 이루어지는 경우 COM 호출 시간이 끝날 때까지
CoUninitialize 호출이 지속되는 것을 볼 수 있을 것입니다.
파이프를 호출할 때마다 전송되는
버퍼의 크기는 어떨까요? 두 가지 기준을 고려해야 합니다. 첫 번째 기준은 네트워크 효율성입니다. 어떤 패킷 크기가 가장 효율적인지를 보려면
일반적인 조건에서 네트워크에 대한 기본 타이밍 테스트를 실행해야 합니다. 그림?3에 제시된
결과에서 볼 수 있는 것처럼 제 네트워크에서는 데이터 패킷이 8KB 이상일 경우 효율적이었습니다. 다른 한 가지 기준은 데이터를 수신하는 코드가
데이터에서 실행하는 프로세싱입니다. 이상적인 경우는 각 버퍼를 처리하는 데 걸리는 시간이 버퍼를 만들어서 전송하는 데 걸리는 시간과 같은
경우입니다. 이 경우에는 한 버퍼가 처리되면 COM이 다음 버퍼를 수신하여 처리할 수 있습니다.
최적의 버퍼 크기를 결정할 수 있는 유일한 방법은 대상 네트워크에서
코드를 테스트하는 방법입니다. 그림?14에는 이러한 테스트를 실행하는 단순한 클래스와 코드가 제시되어 있습니다. 하지만 Heisenberg의
불확실성의 원리 즉, 한 시스템의 측정치는 그 시스템에 영향을 미친다는 점을 기억하십시오(이 경우, 이 시간에는 이 타이밍에 사용된 시간이
포함됨). 하지만 이 클래스에서 데이터 프로세싱에 걸리는 최대 시간이 어느 정도인지를 알 수 있습니다. 버퍼 크기를 달리하면서 데이터 전송을
테스트해야 합니다.
이
테스트의 다음 단계는 클라이언트에서 정적인 데이터를 사용하여(즉, 데이터를 전송하지 않고) 데이터 처리에 시간이 얼마나 걸리는지를 테스트하는
것입니다. 그런 다음 이 두 수치를 비교하여 데이터 전송 시간과 프로세싱 시간이 가장 일치하는 버퍼 크기를 선택하면 됩니다. 이 기사에 대한
다운로드 파일에는 파이프를 통해 파일 내용을 읽고 작성할 수 있도록 해 주는 프로젝트가 포함되어 있습니다.
비동기 파이프
파이프 인터페이스의 비동기 버전은
어떻습니까? 파이프 미리 읽기를 사용하면 데이터 전송과 프로세싱을 동기화할 수 있지만 데이터 버퍼가 전송되는 동안 클라이언트 스레드가 차단될 수
있습니다. 이러한 상황이 발생하지 않도록 하려면 차단되지 않은 파이프 인터페이스 버전을 사용하여 파이프를 호출할 수 있습니다. 파이프 구현자는
이러한 비차단 메커니즘을 사용하여 RPC 스레드 풀보다는 사용자 정의 스레드 풀을 사용하는 파이프를 구현해 파이프 코드를 실행할 수 있습니다.
이렇게 하면 파이프 구현자가 스레드를 보다 효율적으로 관리할 수 있습니다.
차단되지 않은 파이프 인터페이스
버전을 통해 데이터를 수신하면 호출자 스레드가 Begin_Pull 메서드를 호출하여 얼마나 많은 항목이 필요한지를 표시하여 전송을 초기화합니다.
그런 다음 다른 프로세싱을 실행할 수 있으며 실행이 끝나면 Finish_Pull 메서드를 호출하여 되돌아와 이 데이터(및 반환된 항목의 수)를
수신합니다. 그 동안 COM은 데이터를 수신하고 캐시하여 컬렉션에 맞도록 준비합니다. Finish_Pull이 호출되면 COM은 미리 읽기를
실행하여 다음 버퍼를 받습니다. 다음은 이 메서드의 비차단 버전입니다.
HRESULT Begin_Pull([in] ULONG cRequest);
HRESULT Finish_Pull([out, size_is(*pcReturned)] BYTE* buf,
[out] ULONG* pcReturned);
| 실제 메서드는 MIDL이
작성하므로 이 메서드는 의사-IDL입니다. 실제 전송이 이루어진 다음 Finish_Pull을 호출할 때 채우고자 하는 버퍼를 전달하기 때문에
초기 검사에서는 이상하게 보일 수 있습니다. 아마도, COM은 일부 개인 버퍼에 있는 데이터를 읽으며, Finish_Pull을 호출하면 이
버퍼의 데이터를 사용자 버퍼로 복사합니다.
차단되지 않은 Push 버전은 현
스레드를 차단하지 않으면서 데이터를 다른 프로세스로 전송하는 데 유용합니다. 이 방법은 데이터 크기가 클 경우 특히 유용합니다. Push에는
[out] 매개 변수가 없으므로 COM이 사용했던 리소스를 정리하도록 하고 전송이 성공적으로 이루어졌는지 확인하려면 Push_Finish를
호출해야 합니다.
파이프를 사용하려면 대부분의 최근
Platform SDK에서 헤더와 라이브러리를 받아야 하며 자신의 stdafx.h가 0x500 값을 갖도록 _WIN32_WINNT를 정의해야
합니다.
요약
COM을 통해 데이터를 전송하려면
통신을 통해 데이터를 전달하는 최적의 방법에 대해 신중히 고려해야 합니다. 가능한 한 네트워크 호출 수를 적게 유지해야 합니다. 그리고 호출을
할 때에는 가능한 한 전송된 데이터 버퍼 크기를 크게 유지해야 하며 항상 Visual Basic에서 사용되는 종류의 속성 액세스는 피해야
합니다.
데이터 전송을 촉진하기 위해
COM은 몇 가지 도구를 제공합니다. 첫째, 많은 매개 변수를 갖는 메서드 호출과 관련된 문제를 피하기 위해 한 개체 내에서 데이터를 전달할 수
있습니다. 단, 그 개체가 값에 따라 마샬링된 경우에 한합니다. 이렇게 하면 값에 따라 데이터를 전달하는 경우 네트워크 호출의 효율성에 한
개체가 제공하는 확인의 장점을 결합할 수 있습니다. 그리고, Windows 2000에서는 컨텍스트 이외의 구성 요소를 호출할지 여부에 대한
현명한 결정을 내릴 수 있는 Lightweight 클라이언트 처리기를 만들 수 있습니다. 이러한 처리기는 결과를 캐시하고 버퍼된 읽기 및 쓰기를
실행할 수 있습니다.
마지막으로, Windows
2000은 네트워크를 통해 다량의 데이터를 효율적으로 전송할 수 있도록 해 주는 파이프 인터페이스를 제공합니다. 이 방법은 데이터를 상당한
크기의 청크로 분할하여 COM이 파이프를 통해 청크 전송을 처리하도록 하는 방법입니다.
|