WWolves Den

DirectX 11 · 약 4분

DirectX의 COM 인터페이스와 참조 수명 관리

IUnknown, AddRef, Release, HRESULT를 중심으로 그래픽 자원을 언제 만들고 언제 놓아야 하는지 이해합니다.

Wolfgadeok · 업데이트

인터페이스는 사용 가능한 동작에 대한 약속

Direct2D 코드에는 ID2D1Factory, ID2D1RenderTarget처럼 I로 시작하는 형식이 자주 등장합니다. 구현의 내부를 직접 다루는 대신 정해진 인터페이스로 기능을 호출하는 구조입니다. COM은 바이너리 수준의 인터페이스 규약이며, 모든 객체가 Windows 어딘가의 하나뿐인 공용 영역에 존재한다는 뜻은 아닙니다. 같은 프로세스 내부 DLL이 구현하는 객체도 있습니다.

IUnknown은 QueryInterface, AddRef, Release를 제공합니다. QueryInterface는 지원하는 다른 인터페이스를 얻는 수단이지 함수 목록을 자동으로 읽어 어떤 객체인지 추측하는 기능이 아닙니다. ID2D1Factory가 IUnknown에서 파생되었다고 해서 변수의 실제 형식이 IUnknown 하나로 바뀌는 것도 아닙니다. 인터페이스의 상속과 객체의 실제 구현을 구분해서 읽습니다.

참조 수는 프로그램 개수가 아니다

참조 수는 해당 객체에 대한 살아 있는 참조의 수명 관리와 관련됩니다. 한 프로그램 안에서도 여러 부분이 소유한 참조가 있을 수 있으므로 사용 중인 애플리케이션 개수라고 설명하면 틀립니다. AddRef는 참조를 추가하는 메서드이지 팩토리 함수의 다른 이름이 아닙니다. 소유한 참조의 사용이 끝나면 Release를 대응시켜야 하며 COM 객체에 직접 delete를 호출하지 않습니다.

원시 포인터의 복사만으로 소유권 규칙이 명확해지는 것은 아닙니다. 잠깐 빌려 쓰는 포인터인지 독립적으로 소유할 참조인지 API 계약을 확인합니다. C++에서는 ComPtr 같은 RAII 래퍼로 범위를 벗어날 때 정리를 맡길 수 있습니다. 다만 스마트 포인터를 사용해도 서로 참조하는 구조나 객체를 너무 오래 보관하는 문제까지 자동으로 해결되는 것은 아닙니다.

예제 · cpp

#include <d2d1.h>
#include <wrl/client.h>

Microsoft::WRL::ComPtr<ID2D1Factory> factory;
HRESULT hr = D2D1CreateFactory(
    D2D1_FACTORY_TYPE_SINGLE_THREADED,
    factory.GetAddressOf());
if (FAILED(hr)) {
    // 실패를 기록하고 초기화를 중단합니다.
    return hr;
}
// factory가 소유한 참조는 ComPtr의 수명에 따라 정리됩니다.

반환값과 초기화 실패 경로까지 설계하기

HRESULT는 단순한 bool이 아닙니다. FAILED와 SUCCEEDED 같은 규약으로 검사하고, 실패한 생성 함수가 돌려준 포인터를 정상 객체처럼 사용하지 않습니다. 여러 자원을 순서대로 만드는 중간에 실패했다면 앞서 만든 자원도 정리되어야 합니다. 렌더 타깃 생성에 성공했다고 그 다음 브러시 생성까지 성공했다고 가정하면 안 됩니다.

WIC처럼 별도의 COM 초기화가 필요한 API를 함께 쓰는 경우에는 호출 스레드의 초기화·해제 규칙도 문서에서 확인합니다. 모든 DirectX 함수가 같은 사전 조건을 요구한다고 묶지 않습니다. 실습에서는 정상 종료뿐 아니라 자원 생성이 중간에 실패한 상황도 상상해 보세요. 어떤 참조를 누가 소유하고 어느 경로에서 해제하는지 한 줄씩 설명할 수 있다면 수명 관리 구조가 훨씬 명확해집니다.

참고 자료

예제는 학습용이며, 모든 환경에서 실행 검증된 완성 프로젝트는 아닙니다. 적용 전 사용 중인 버전과 프로젝트 환경에서 동작을 확인하세요.