개체의 수명 관리
아직 언급하지 않은 COM 인터페이스에 대한 규칙이 있습니다. 모든 COM 인터페이스는 IUnknown이라는 인터페이스에서 직접적으로나 간접적으로 상속해야 합니다. 이 인터페이스는 모든 COM 개체가 지원해야 하는 몇 가지 기준 기능을 제공합니다.
IUnknown 인터페이스는 다음 세 가지 메서드를 정의합니다.
QueryInterface 메서드를 통해 프로그램은 런타임에 개체의 기능을 쿼리할 수 있습니다. 자세한 내용은 다음 항목 인터페이스에 대한 개체 요청에서 다룹니다. AddRef 및 Release 메서드는 개체의 수명을 제어하는 데 사용되며, 이 항목의 주제입니다.
참조 계산
프로그램이 어떤 작업을 수행하든 어느 시점에서 리소스를 할당하고 해제합니다. 리소스 할당은 간단합니다. 리소스를 해제할 시기를 파악하는 것은 어려운 일이며, 리소스의 수명이 현재 범위 이상으로 확장되는 경우는 더욱 그렇습니다. 이것은 COM만의 문제가 아닙니다. 힙 메모리를 할당하는 모든 프로그램은 동일한 문제를 해결해야 합니다. 예를 들어 C++는 자동 소멸자를 사용하고 C# 및 Java는 가비지 수집을 사용합니다. COM은 참조 계산이라는 접근 방식을 사용합니다.
모든 COM 개체는 내부 개수를 유지 관리합니다. 이를 참조 개수라고 합니다. 참조 개수는 현재 활성화된 개체에 대한 참조 수를 추적합니다. 참조 수가 0으로 떨어지면 개체가 자체 삭제됩니다. 개체가 자체 삭제된다는 마지막 부분은 반복할 가치가 있습니다. 프로그램은 개체를 명시적으로 삭제하지 않습니다.
참조 계산 규칙은 다음과 같습니다.
- 개체를 처음 만들 때 참조 개수가 1입니다. 이때 프로그램에는 개체에 대한 단일 포인터가 있습니다.
- 프로그램은 포인터를 복제(복사)하여 새 참조를 만들 수 있습니다. 포인터를 복사하는 경우 개체의 AddRef 메서드를 호출해야 합니다. 이 메서드는 참조 개수를 1씩 늘립니다.
- 개체에 대한 포인터 사용을 마치면 Release를 호출해야 합니다. Release 메서드는 참조 개수를 1씩 줄입니다. 또한 포인터를 무효화합니다. Release를 호출한 후에 포인터를 다시 사용하지 마세요. 동일한 개체에 대한 다른 포인터가 있는 경우 해당 포인터를 계속 사용할 수 있습니다.
- 모든 포인터에서 Release를 호출하면 개체 참조 개수가 0에 도달하고 개체가 자체 삭제됩니다.
다음은 간단하지만 일반적인 사례를 보여 주는 다이어그램입니다.
프로그램은 개체를 만들고 개체에 대한 포인터(p)를 저장합니다. 이때 참조 개수는 1입니다. 프로그램이 포인터 사용을 마치면 Release를 호출합니다. 참조 개수가 0으로 줄어들고 개체가 자체 삭제됩니다. 이제 p가 유효하지 않습니다. 추가 메서드 호출에 p를 사용하는 것은 오류입니다.
다음 다이어그램에서는 더욱 복잡한 예제를 보여 줍니다.
여기서 프로그램은 이전과 같이 개체를 만들고 포인터 p를 저장합니다. 그런 다음, 프로그램은 p를 새 변수 q에 복사합니다. 이때 프로그램은 AddRef를 호출하여 참조 개수를 늘려야 합니다. 이제 참조 개수는 2이고 개체에 대한 유효한 포인터가 두 개 있습니다. 이제 프로그램이 p 사용을 마쳤다고 가정합니다. 프로그램은 Release를 호출하고 참조 개수는 1이 되고 p는 더 이상 유효하지 않습니다. 그러나 q는 여전히 유효합니다. 나중에 프로그램은 q 사용을 마칩니다. 따라서 Release를 다시 호출합니다. 참조 개수는 0이 되고 개체가 자체 삭제됩니다.
프로그램이 p를 복사하는 이유가 궁금할 수 있습니다. 두 가지 주요 이유가 있습니다. 첫째, 목록과 같은 데이터 구조에 포인터를 저장하려고 할 수 있습니다. 둘째, 포인터를 원래 변수의 현재 범위 이상으로 유지하려고 할 수 있습니다. 따라서 더 넓은 범위의 새 변수에 복사하게 됩니다.
참조 계산의 한 가지 장점은 개체를 삭제하기 위해 다양한 코드 경로를 조정하지 않고도 코드의 여러 섹션에서 포인터를 공유할 수 있다는 점입니다. 대신 각 코드 경로는 해당 코드 경로에서 개체 사용을 마쳤을 때 Release를 호출하면 됩니다. 개체는 올바른 시간에 자체 삭제를 처리합니다.
예
다음은 열기 대화 상자 예제의 코드입니다.
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));
if (SUCCEEDED(hr))
{
hr = pFileOpen->Show(NULL);
if (SUCCEEDED(hr))
{
IShellItem *pItem;
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
PWSTR pszFilePath;
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
if (SUCCEEDED(hr))
{
MessageBox(NULL, pszFilePath, L"File Path", MB_OK);
CoTaskMemFree(pszFilePath);
}
pItem->Release();
}
}
pFileOpen->Release();
}
CoUninitialize();
}
참조 계산은 이 코드의 두 위치에서 수행됩니다. 첫째, 프로그램이 공통 항목 대화 상자 개체를 성공적으로 만들면 pFileOpen 포인터에서 Release를 호출해야 합니다.
hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));
if (SUCCEEDED(hr))
{
// ...
pFileOpen->Release();
}
둘째, GetResult 메서드가 IShellItem 인터페이스에 대한 포인터를 반환하면 프로그램은 pItem 포인터에서 Release를 호출해야 합니다.
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
// ...
pItem->Release();
}
두 경우 모두 포인터가 범위를 벗어나기 전에 Release 호출이 마지막으로 수행됩니다. 또한 HRESULT에서 성공인지 테스트한 후에만 Release가 호출됩니다. 예를 들어 CoCreateInstance 호출이 실패하면 pFileOpen 포인터가 유효하지 않습니다. 따라서 이 포인터에서 Release를 호출하는 것은 오류입니다.