^(코딩캣)^ = @"코딩"하는 고양이;

COM의 소개(파트 2) - COM 서버의 이면 (4)

API/COM
2020. 10. 8. 15:07

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

IUnknown부터 시작하여 인터페이스 구현하기

모든 인터페이스는 IUnknown으로부터 파생됨을 떠올려 봅시다. 왜냐하면 IUnknown은 COM 객체의 두 가지 기본 기능인 레퍼런스 카운트와 인터페이스 쿼리를 다루고 있기 때문입니다. 여러분이 coclass를 작성할 때, 여러분은 또은 여러분의 필요에 맞게 IUnknown을 구현해야 합니다. IUnknown만을 구현하는 coclass 예제를 살펴봅시다. 이는 여러분이 작성할 수 있는 coclass 중 가장 간단한 클래스입니다. 우리는 CUnknownImpl이라는 이름을 가진 C++ 클래스에서 IUnknown을 구현할 것입니다. 클래스의 선언은 다음과 같습니다.

class CUnknownImpl : public IUnknown {
public:
    CUnknownImpl(); // 생성자
    virtual ~CUnknownImpl(); // 소멸자

    /* 다음 3개는 IUnknown 인터페이스로부터 유래된 메소드 */
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void ** ppv);

protected:
    UINT m_uRefCount; // COM 객체의 레퍼런스 카운트
};

 

생성자와 소멸자

생성자와 소멸자는 COM 서버의 레퍼런스 카운트를 증감합니다.

extern UINT g_uDllRefCount;

CUnknownImpl::CUnknownImpl() {
    m_uRefCount = 0;
    g_uDllRefCount++;
}

CUnknownImpl::~CUnknownImpl() {
    g_uDllRefCount--;
}

 

생성자는 새로운 COM 객체가 생성될 때 호출됩니다. 그러므로 COM 서버가 메모리에 남아있게 하도록 COM 서버의 레퍼런스 카운트를 1만큼 증가시킵니다. 또한 생성자는 COM 객체의 레퍼런스 카운트를 0으로 초기화합니다. COM 객체가 소멸될 때, 소멸자는 서버의 레퍼런스 카운트를 1만큼 감소시킵니다.

 

AddRef와 Release

이들 두 메소드는 COM 객체의 수명을 제어하는 메소드입니다. AddRef는 간단하게 구현 가능합니다.

ULONG CUnknownImpl::AddRef() {
    return ++m_uRefCount;
}

 

AddRef는 COM 객체의 레퍼런스 카운트를 1만큼 증가시키고, 증가된 값을 반환합니다. 다만 ReleaseAddRef만큼 간단하지는 않습니다.

ULONG CUnknownImpl::Release() {
    ULONG uRet = --m_uRefCount;

    if (m_uRefCount == 0) delete this;
    return uRet;
}

 

COM 객체의 레퍼런스 카운트를 감소시키는 것뿐만 아니라, Release는 더 이상 명시적인 참조가 없는 경우 객체 스스로를 파괴합니다. Release는 또한 갱신된 레퍼런스 카운트를 반환합니다. 다만 이와 같은 Release 구현은 COM 객체가 힙(heap) 영역에 생성되었음을 가정하고 있습니다. 여러분이 COM 객체를 스택이나 전역 범위에 생성하였을 경우 COM 객체가 스스로를 소멸시키려 할 때 모든 것들이 엉망이 되고 맙니다.

이제 여러분의 클라이언트 앱에서 왜 AddRefRelease 메소드가 적절하게 호출되어야 하는지에 대한 이유가 명확해야 합니다. 여러분이 이들 메소드를 정확하게 호출해주지 않는다면 여러분이 사용하고 있는 COM 객체가 너무 빨리 소멸될 수도 있고, 아예 소멸되지 않을 수도 있습니다. COM 객체가 너무 일찍 소멸되어 버리면 이후 COM 서버 전체가 메모리에서 날아가버릴 수 있습니다. 이는 여러분의 어플리케이션이 서버에 접속을 시도할 때 충돌을 야기합니다.

여러분이 멀티스레드 프로그래밍을 해본 경험이 있다면, 여러분은 왜 변수를 증감할 때 InterlockedIncrement, InterlockedDecrement 대신에 단순 증감연산자 ++--를 쓰는지 의아해 하실 것입니다. 싱글 스레드 COM 서버에서 증감연산자 ++--를 사용하는 것은 전적으로 안전합니다. 왜냐하면 클라이언트 어플리케이션이 멀티 스레드이고 메소드 호출이 제각각의 스레드에서 이루어진다고 해도, COM 라이브러리는 이러한 메소드 호출을 직렬화하기 때문입니다. 이것은 어떤 하나의 메소드 호출이 시작되면, 메소드를 호출한 다른 스레드들은 앞서 호출된 메소드 실행이 끝날 때까지 잠겨 있음을 뜻합니다. COM 라이브러리는 우리가 만들고 있는 COM 서버에 한 번에 하나 이상의 스레드가 진입하지 않을 것임을 보장합니다.

 

QueryInterface

QueryInterface, 줄여서 QI는 COM 클라이언트가 하나의 COM 객체에서 다양한 인터페이스들을 요청할 때 사용됩니다.

우리의 예제 coclass는 단지 하나의 인터페이스만을 구현하고 있기 때문에, 우리가 다룰 QI는 매우 단순할 것입니다. QI는 두 가지 파라미터를 받습니다. 하나는 요청하는 인터페이스의 IID이고 다른 하나는 쿼리 작업이 성공하였을 때 인터페이스 포인터를 보관하게 될 포인터 크기의 버퍼입니다.

HRESULT CUnknownImpl::QueryInterface(REFIID riid, void ** ppv) {
    HRESULT hrRet = S_OK;

    // 표준 QI 초기화로서 ppv를 NULL 참조하게 설정합니다.
    *ppv = NULL;

    if (IsQeualIID(riid, IID_IUnknown)) {
    // COM 클라이언트가 요청하는 인터페이스가 이 coclass에서 지원 가능한 형식이면
        *ppv = (IUnknown *)this;
    } else {
        // COM 클라이언트가 요청하는 인터페이스 형식이 이 coclass와 호환되지 않으면
        hrRet = E_NOINTERFACE;
    }

     // 인터페이스 포인터를 반환할 수 있다면 AddRef를 호출합니다.
    if (hrRet == S_OK) {
        ((IUnknown *)*ppv)->AddRef();
    }

    return hrRet;
}

 

예제 소스의 QI에는 다음의 세 가지 작업이 포함되어 있습니다.

1. 반환을 위해 전달된 포인터 파라미터를 NULL로 초기화합니다: *ppv = NULL;

2. COM 클라이언트가 요청한 riid 값이 우리의 coclass가 구현하고 있는 인터페이스의 IID 중에 있는지를 확인합니다: if (IsEqualIID(riid, IID_IUnknown))

3. 요청한 형식의 인터페이스를 우리의 coclass가 구현하고 있다면, COM 객체의 레퍼런스 카운트를 증가시킵니다: ((IUnknown *)*ppv)->AddRef();

 

AddRef를 호출하는 것은 매우 중요합니다. 다음의 문장

*ppv = (IUnknown *)this;

은 COM 객체에 대한 새로운 참조를 형성합니다. 그러므로 우리는 AddRef를 호출함으로써 이 객체에게 새로운 참조 관계가 추가되었음을 알려야만 합니다.

AddRef를 호출하는데 IUnknown * 형으로 캐스팅하는 것이 이상하게 보일 수는 있습니다. 그러나 특별한 coclass의 QI에서 *ppvIUnknown *이 아닐 수 있습니다. 그러므로 캐스트하여 호출하는 습관을 들이는 것이 좋습니다.

이제 우리는 DLL 서버의 내부적인 세부 사항을 일부 다루어 보았습니다, 다음 절에서는 처음으로 돌아가서 COM 클라이언트가 CoCreateInstance를 실행하였을 때 우리가 만들고 있는 COM 서버가 어떻게 사용되는지에 대해 살펴보겠습니다.

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (3)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (5)

 

카테고리 “API/COM”
more...