COM이란 무엇인가 - Windows COM 설계가 지금도 아름다운 이유
· 업데이트: · 小村 豪 · COM, ActiveX, Windows 개발
수정 이력(1건, 최종 수정 2026년 09월 01일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- 일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635075)
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635074)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
小村 豪 (2026). 「COM이란 무엇인가 - Windows COM 설계가 지금도 아름다운 이유」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635074 https://comcomponent.com/ko/blog/2026/01/25/001-why-com-is-beautiful/
- DOI(최신 버전)
- 10.5281/zenodo.21635074
- DOI(이 버전)
- 10.5281/zenodo.22217395
COM이란?
COM(Component Object Model)이란 Windows에서 컴포넌트끼리 주고받기 위한 「바이너리 계약」입니다. 언어와 컴파일러의 차이를 넘어 인터페이스라는 엄격한 계약으로 통신하는 구조이며, 그 밑바탕에는 「구현이 아니라 계약에 대해 프로그래밍한다」는 설계 사상이 있습니다.
이 글의 지식 맵
COM은 Windows에서 컴포넌트끼리 주고받기 위한 바이너리 계약으로, 모든 인터페이스가 상속하는 IUnknown이 QueryInterface에 의한 기능 확인과 AddRef/Release에 의한 참조 카운트 관리를 제공합니다. CLSID와 IID라는 GUID로 컴포넌트와 인터페이스를 유일하게 식별함으로써 이름 충돌을 피하고, 기존 인터페이스를 바꾸지 않고 새 인터페이스를 추가해 QueryInterface로 확인하는 설계가 버전 공존을 실현합니다. 호출하는 쪽과 같은 프로세스에 로드하는 In-proc(DLL 서버)은 비트 수의 일치가 필수이며 상대의 크래시에 함께 죽지만, 별도 프로세스에서 동작하는 Out-of-proc(LocalServer)은 그 제약에서 자유로운 대신 상대가 사라졌을 때의 RPC_E_DISCONNECTED 같은 연결 끊김 오류에 대한 대비가 필요합니다. HRESULT라는 반환값에 의한 성공·실패 표현과 Proxy/Stub, IDL에 의한 계약의 사전 정의가 이 구조 전체를 언어와 프로세스의 경계로부터 독립시킵니다.
flowchart LR
accTitle: COM 설계가 아름다운 이유의 지식 맵
accDescr: COM이 IUnknown에 의한 참조 카운트와 QueryInterface, CLSID·IID를 통한 식별을 토대로 바이너리 호환성·인터페이스 분리·버전 공존·프로세스 경계를 넘는 재사용이라는 강점을 실현하고, In-proc과 Out-of-proc의 배치 형태나 HRESULT, .NET과의 상호운용으로 이어지는 관계를 보여주는 그림
com["COM(컴포넌트 오브젝트 모델)"]
iunknown["IUnknown"]
queryinterface["QueryInterface"]
com_reference_counting["COM 참조 카운트(AddRef/Release)"]
clsid["CLSID(Class ID)"]
iid["IID(인터페이스 식별자)"]
hresult["HRESULT"]
com_binary_compatibility["COM 바이너리 호환성"]
com_interface_versioning["COM 버전 공존"]
in_proc_com["In-proc COM(DLL 서버)"]
com_localserver["COM LocalServer(별도 프로세스 COM 서버)"]
bitness_match_requirement["비트수 일치 요건"]
in_proc_crash_propagation["In-proc 크래시 동반 다운"]
out_of_proc_com_disconnection_error["Out-of-proc COM의 연결 끊김 오류"]
proxy_stub["Proxy/Stub"]
idl["IDL(인터페이스 정의 언어)"]
dotnet_com_interop[".NET의 COM 상호 운용(COM Interop)"]
progid["ProgID(Programmatic Identifier)"]
activex["ActiveX"]
com -->|"이용한다"| iunknown
com -->|"이용한다"| queryinterface
com -->|"이용한다"| com_reference_counting
com -->|"이용한다"| clsid
com -->|"이용한다"| iid
com -->|"이용한다"| hresult
com -->|"구현을 담당한다"| com_binary_compatibility
com_binary_compatibility -->|"전제로 한다"| iunknown
com -->|"구현을 담당한다"| com_interface_versioning
com_interface_versioning -->|"전제로 한다"| queryinterface
com -.->|"이용한다"| in_proc_com
com -.->|"이용한다"| com_localserver
in_proc_com -->|"전제로 한다"| bitness_match_requirement
in_proc_com -->|"원인이 될 수 있다"| in_proc_crash_propagation
com_localserver -->|"방지한다"| in_proc_crash_propagation
com_localserver -.->|"원인이 될 수 있다"| out_of_proc_com_disconnection_error
com -.->|"이용한다"| proxy_stub
com -->|"이용한다"| idl
dotnet_com_interop -.->|"이용한다"| progid
dotnet_com_interop -->|"이용한다"| queryinterface
dotnet_com_interop -->|"이용한다"| hresult
dotnet_com_interop -->|"이용한다"| com_reference_counting
activex -->|"전제로 한다"| com
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 23건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
COM의 3가지 중요한 요소
이 장에서 다루는 것은 COM이라는 구조를 이루는 부품입니다. 뒤에 나오는 「COM의 4가지 강점」은 이 부품들을 조합한 결과로 얻어지는 성질이므로, 같은 내용을 두 번 설명하는 것이 아닙니다. 대응 관계는 강점 장의 마지막에 표로 보여 드립니다.
1. 인터페이스 중심 설계
COM에서는 「구현보다 계약이 먼저」입니다. 객체의 내부 구현을 몰라도, 공개된 인터페이스만 알면 이용할 수 있습니다.
2. GUID(CLSID / IID)를 통한 식별
모든 컴포넌트와 인터페이스에는 전 세계에서 유일한 ID(GUID)가 부여되므로, 이름 충돌이 애초에 일어나지 않습니다.
3. IUnknown
모든 COM 인터페이스가 상속하는 기본 인터페이스입니다. 다음 세 가지 기능을 제공합니다.
| 메서드 | 역할 |
|---|---|
QueryInterface |
다른 인터페이스를 가지고 있는지 묻는다 |
AddRef |
참조 카운트를 늘린다 |
Release |
참조 카운트를 줄인다(0이 되면 자신을 파괴) |
이 세 요소가 결합되면, 호출하는 쪽과 구현 사이에 남는 것은 「GUID로 식별되고 메서드 순서가 정해진 인터페이스」뿐입니다. 언어도 컴파일러도 이 계약에는 나타나지 않습니다.
flowchart LR
subgraph CALLER["호출하는 쪽 ── 언어는 상관없음"]
A1["C++ 앱"]
A2["C# 앱"]
A3["VBA / Python 등"]
end
CONTRACT["계약(바이너리로 고정된 부분)<br/>·IID(GUID)로 「어떤 계약인지」를 유일하게 결정<br/>·IUnknown의 3개 메서드로 시작하는 순서<br/>·메서드별 인자·반환값 타입과 호출 규약"]
subgraph IMPL["구현 ── 언어는 상관없음"]
B1["C++로 작성한 컴포넌트"]
B2["C#으로 작성한 컴포넌트"]
end
A1 --> CONTRACT
A2 --> CONTRACT
A3 --> CONTRACT
CONTRACT --> B1
CONTRACT --> B2
그림 1: COM의 바이너리 계약. 호출하는 쪽의 언어도 구현 언어도 계약에는 나타나지 않으므로, 어느 쪽을 교체해도 상대를 다시 빌드할 필요가 없다
코드로 보는 「계약에 대해 프로그래밍한다」
글만으로는 이해하기 어려우므로 최소한의 코드로 만들어 보겠습니다. 덧셈만 하는 컴포넌트 ICalcService를 구현을 전혀 모른 채 사용하는 예입니다.
먼저 C++(순수 COM)부터 보겠습니다. 여기에 적힌 ICalcService의 정의가 계약 그 자체이며, 구현이 C++로 작성되었는지 C#으로 작성되었는지는 호출하는 쪽 코드 어디에도 나타나지 않습니다.
#include <objbase.h>
// 계약의 정의. 보통은 IDL에서 이런 형태의 헤더가 생성된다
// IUnknown을 상속하므로 QueryInterface / AddRef / Release를 반드시 가진다
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001"))
ICalcService : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;
};
// 나중에 추가된 확장판 계약. GUID는 별개가 된다
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B002"))
ICalcServiceEx : public ICalcService
{
virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};
// 구현 컴포넌트의 CLSID. 보통은 IDL에서 생성되는 헤더에 정의된다
static const CLSID CLSID_CalcService =
{ 0x1C9B6F4D, 0x1E9A, 0x4E61, { 0x9A, 0x4F, 0x6A, 0x0F, 0x1D, 0x2D, 0x9A, 0x11 } };
// 호출하는 쪽
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) { return hr; }
ICalcService* calc = nullptr;
hr = CoCreateInstance(CLSID_CalcService, nullptr, CLSCTX_ALL,
__uuidof(ICalcService), reinterpret_cast<void**>(&calc));
// 여기서 반환된 포인터는 AddRef가 이미 된 상태다(참조 카운트는 1)
if (SUCCEEDED(hr))
{
int sum = 0;
hr = calc->Add(1, 2, &sum); // 구현을 몰라도 호출할 수 있다
// 「확장판 계약도 가지고 있는지」를 실행 시점에 묻는다 = 버전 공존의 구현
ICalcServiceEx* calcEx = nullptr;
if (SUCCEEDED(calc->QueryInterface(__uuidof(ICalcServiceEx),
reinterpret_cast<void**>(&calcEx))))
{
// 새 버전 컴포넌트였을 때만 여기로 온다
calcEx->Release(); // 받은 쪽이 반환한다
}
// 가지고 있지 않으면 E_NOINTERFACE가 반환될 뿐, 구버전이어도 계속 동작한다
calc->Release(); // 참조 카운트가 0이 되어 파괴된다
}
CoUninitialize();
눈여겨볼 점은 세 가지입니다.
AddRef를 직접 작성하는 경우는 거의 없습니다.CoCreateInstance도QueryInterface도 반환하는 포인터에 대해 이미AddRef를 마친 상태로 돌려줍니다. 즉 「포인터를 받은 쪽이Release한다」가 원칙이며,AddRef를 명시적으로 호출하는 것은 같은 포인터를 다른 한 곳에서 더 보유하기 시작할 때뿐입니다.QueryInterface의 실패는 이상 상태가 아닙니다. 「그 계약은 가지고 있지 않다」(E_NOINTERFACE)는 정상적인 답이며, 이 덕분에 「기존 컴포넌트를 깨뜨리지 않고 새 기능을 추가한다」가 성립합니다.- 반환값은 모두
HRESULT입니다. 예외가 아니라 반환값으로 성공·실패를 나타내는 것은 언어 경계를 넘나들기 위해 필요한 약속입니다. 예외를 던지는 방식은 언어마다 다르지만, 정수 반환값이라면 누구나 해석할 수 있습니다.
flowchart TB
accTitle: 참조 카운트와 Release의 원칙
accDescr: CoCreateInstance나 QueryInterface는 AddRef가 끝난 포인터를 반환하므로, 받은 쪽이 사용을 마치면 Release를 호출하고 참조 카운트가 0이 되면 객체가 파괴된다는 원칙을 보여주는 그림.
api["CoCreateInstance나 QueryInterface"] --> ret["AddRef가 끝난 포인터가 반환됨"]
ret --> use["받은 쪽이 사용"]
use --> rel["받은 쪽이 Release를 호출"]
rel --> zero["참조 카운트가 0이 되면 파괴"]
use -.->|"AddRef를 명시"| add["같은 포인터를 다른 한 곳에서 보유"]
그림 2: 포인터를 받은 쪽이 Release하는 것이 원칙이며, AddRef를 명시하는 것은 보유하는 곳을 늘릴 때뿐이다.
같은 계약을 C#에서 사용하면 이렇게 됩니다. GUID가 같으면 같은 계약으로 간주되므로, 상대가 C++ 구현이어도 상관없습니다.
using System;
using System.Runtime.InteropServices;
// C++ 쪽과 같은 GUID를 적는다. 이것이 「같은 계약」이라는 선언
[ComImport]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
int Add(int a, int b);
}
// 호출하는 쪽
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService")
?? throw new InvalidOperationException("COM 서버가 등록되어 있지 않습니다.");
object server = Activator.CreateInstance(t)!;
try
{
var calc = (ICalcService)server; // 이 캐스트가 QueryInterface에 해당한다
int sum = calc.Add(1, 2);
Console.WriteLine(sum); // 3
}
finally
{
Marshal.ReleaseComObject(server); // Release에 해당한다
}
C# 쪽에서 Add의 반환값이 int로 되어 있는 것은, .NET의 COM 상호운용이 「마지막 [out, retval] 인자를 반환값으로 만들고, 실패한 HRESULT는 예외로 변환한다」는 정해진 변환을 해 주기 때문입니다. AddRef/Release도 호출하는 쪽에서는 보이지 않게 되지만, 사라진 것이 아니라 .NET이 준비하는 얇은 래퍼(RCW)가 대신 호출하고 있습니다. 같은 계약을 언어마다 자연스러운 작성 방식으로 사용할 수 있다── 이것이 「바이너리 계약」의 실제 모습입니다.
flowchart TB
accTitle: C# 작성 방식과 COM의 대응
accDescr: C#에서는 인터페이스로의 캐스트가 QueryInterface에, Marshal.ReleaseComObject가 Release에 해당하며, 실패한 HRESULT는 예외로 변환되고, AddRef와 Release는 RCW가 대신 호출한다는 대응을 보여주는 그림.
cs["C# 코드"] --> cast["타입 캐스트"]
cs --> rco["ReleaseComObject"]
cs --> hrx["실패 HRESULT"]
cast --> qi["QueryInterface에 해당"]
rco --> rel["Release에 해당"]
hrx --> ex["예외로 변환됨"]
cs -.-> rcw["RCW가 참조 카운트 대행"]
그림 3: 같은 계약이라도 C# 쪽에서는 언어별로 자연스러운 작성 방식으로 바뀌며, 변환은 RCW와 상호운용 계층이 담당한다.
COM의 4가지 강점
1. 바이너리 호환성
한 번 빌드한 컴포넌트는 프로그래밍 언어나 런타임에 상관없이 재사용할 수 있습니다. C++로 만든 COM 컴포넌트를 C#이나 Python에서 호출하는 일이 일상적으로 가능한 것입니다.
2. 인터페이스 분리
구현을 완전히 숨기고 계약만 공개하므로, 내부 구현을 자유롭게 변경해도 호출하는 쪽에는 영향을 주지 않습니다.
3. 버전 공존
하위 호환성을 유지한 채 기능을 추가하기 위해 새 인터페이스를 추가해 나가는 설계가 기본입니다. 기존 인터페이스를 바꾸지 않고도 새 기능을 제공할 수 있습니다.
신구 호출하는 쪽과 신구 컴포넌트를 조합하면, 4가지 조합 중 3가지는 그대로 동작하고, 남은 1가지도 「가지고 있지 않다」는 답이 돌아올 뿐입니다.
flowchart LR
OLDC["오래된 호출하는 쪽<br/>ICalcService만 앎"]
NEWC["새로운 호출하는 쪽<br/>ICalcServiceEx를 물어봄"]
OLDS["구버전 컴포넌트<br/>ICalcService만 구현"]
NEWS["신버전 컴포넌트<br/>둘 다 구현"]
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK"| OLDS
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK ── 갱신해도 깨지지 않음"| NEWS
NEWC -->|"QueryInterface(IID_ICalcServiceEx)<br/>S_OK ── 새 기능 사용 가능"| NEWS
NEWC -.->|"QueryInterface(IID_ICalcServiceEx)<br/>E_NOINTERFACE ── 기존 기능으로 계속"| OLDS
그림 4: 기존 인터페이스를 바꾸지 않고 새 IID를 추가하면, 신구 어떤 조합이든 계속 동작한다. 점선은 「그 계약은 가지고 있지 않다」는 정상적인 답을 나타낸다
4. 프로세스 경계를 넘는 재사용
COM 컴포넌트를 배치하는 방식에는 두 가지가 있습니다. 호출하는 쪽과 같은 프로세스에 DLL로 로드하는 In-proc(DLL 서버)와, 별도 프로세스의 EXE로 실행하는 Out-of-proc(EXE 서버, LocalServer)입니다. 호출하는 코드의 겉모습은 어느 쪽이든 달라지지 않습니다.
flowchart TB
accTitle: 컴포넌트의 두 가지 배치 위치
accDescr: 겉모습이 같은 호출 코드에서, 호출하는 쪽과 같은 프로세스에 DLL로 로드하는 In-proc과, 별도 프로세스의 EXE로 실행하는 Out-of-proc 어느 쪽으로도 이어진다는 것을 보여주는 그림.
caller["호출하는 쪽 코드(겉모습은 동일)"] --> ip["In-proc(DLL 서버)"]
caller --> op["Out-of-proc(EXE 서버)"]
ip -.-> ipd["같은 프로세스에 DLL로 로드"]
op -.-> opd["별도 프로세스의 EXE로 실행"]
그림 5: 배치 위치는 In-proc과 Out-of-proc 두 종류지만, 호출하는 코드의 겉모습은 달라지지 않는다.
| In-proc(DLL 서버) | Out-of-proc(EXE 서버) | |
|---|---|---|
| 실행되는 위치 | 호출하는 쪽과 같은 프로세스 | 별도 프로세스 |
| 실제 호출 방식 | 함수 포인터를 통한 직접 호출 | 인자를 다시 채워(마셜링) 프로세스 간 통신으로 전달 |
| 속도 | 빠름 | 프로세스 간 통신만큼 느림 |
| 상대가 죽었을 때 | 호출하는 쪽도 함께 죽는다 | 호출하는 쪽은 살아남는다(호출이 실패로 반환됨) |
| 비트 수(32/64bit) | 일치하지 않으면 로드할 수 없음 | 달라도 무방 |
Out-of-proc COM(EXE 서버)을 사용하면 다른 프로세스의 기능을 안전하게 호출할 수 있습니다. 「32bit/64bit가 달라도 된다」는 마지막 행은 실무에서 효과를 발휘하는 장면이 있으며, 32bit 앱에서 64bit DLL을 사용하는 구체적인 구성 방법은 「32bit 앱에서 64bit DLL을 호출하는 COM 브리지 실례」에서 다루고 있습니다.
다만 별도 프로세스라는 것은 상대가 언제든 사라질 수 있다는 것도 의미합니다. 서버 프로세스가 크래시하거나 종료되면 호출하는 쪽에는 다음과 같은 실패가 반환됩니다. 이것들은 「망가졌다」가 아니라 「상대가 이제 없다」를 나타내는, Out-of-proc 특유의 오류입니다.
| 오류 코드 | 의미 |
|---|---|
RPC_E_DISCONNECTED |
호출하려던 객체가 클라이언트에서 분리되어 있음(상대 객체가 이미 없음) |
RPC_S_SERVER_UNAVAILABLE |
호출 대상 서버(프로세스)에 도달할 수 없음 |
In-proc이라면 「상대가 죽으면 자신도 죽는다」이므로 신경 쓰지 않아도 되었던 이야기가, Out-of-proc에서는 재시작이나 재연결 복구 처리로서 설계에 들어옵니다. 안전성과 맞바꿔 늘어나는 작업이라고 보는 것이 정확합니다.
flowchart TB
accTitle: 상대 프로세스가 사라졌을 때의 흐름
accDescr: Out-of-proc에서는 서버 프로세스가 크래시나 종료로 사라지면 호출이 실패로 반환되고, 호출하는 쪽은 살아남으므로 재시작이나 재연결 복구 처리를 설계에 넣어야 한다는 흐름을 보여주는 그림.
gone["서버 프로세스의 크래시·종료"] --> fail["호출이 실패로 반환됨"]
fail --> alive["호출하는 쪽은 살아남음"]
alive --> rec["재시작·재연결 복구 처리로"]
fail -.-> mean["망가진 것이 아니라 상대가 이제 없음"]
그림 6: Out-of-proc에서는 상대가 사라져도 함께 죽지 않는 대신, 복구 처리가 설계에 들어온다.
3가지 요소와 4가지 강점의 대응
앞서 언급했듯이, 「3가지 요소」는 부품이고 「4가지 강점」은 그 결과입니다. 어느 부품이 어느 강점을 뒷받침하는지 나열하면, 겹쳐 보이던 것들의 역할 분담이 분명해집니다.
| 강점 | 주로 작용하는 요소 |
|---|---|
| 1. 바이너리 호환성 | 인터페이스 중심 설계 + IUnknown(호출 순서가 바이너리 수준에서 결정됨) |
| 2. 인터페이스 분리 | 인터페이스 중심 설계(계약만 공개함) |
| 3. 버전 공존 | GUID + QueryInterface(다른 계약을 실행 시점에 물어볼 수 있음) |
| 4. 프로세스 경계를 넘는 재사용 | 인터페이스 중심 설계(구현의 위치까지 계약에서 분리할 수 있음) |
COM은 지금도 현역
「오래된 기술」이라고 여겨지기 쉽지만, COM은 Windows의 핵심에서 지금도 계속 쓰이는 구조입니다.
COM이 쓰이는 곳
- 탐색기 확장(우클릭 메뉴, 미리보기 표시)
- Office 자동화(Excel이나 Word의 외부 제어)
- .NET과의 상호운용(COM Interop)
- ActiveX를 포함한 기존 시스템
- DirectX, Windows Shell API 등 다수의 Windows API
「자신과는 상관없다」고 생각해도, Windows 개발을 하는 한 COM은 어딘가에서 등장합니다.
마무리
COM 설계의 중심은 「언어·프로세스·구현으로부터의 독립」입니다. 언어에 중립적인 인터페이스 설계, GUID를 통한 유일 식별과 버전 관리, IUnknown을 통한 참조 카운트, 그리고 프로세스 간 통신을 투명하게 다룰 수 있는 구조입니다.
flowchart TB
accTitle: COM 설계의 중심
accDescr: 언어에 중립적인 인터페이스 설계, GUID를 통한 유일 식별과 버전 관리, IUnknown의 참조 카운트, 투명한 프로세스 간 통신이라는 4가지 구조가, 언어·프로세스·구현으로부터의 독립이라는 설계의 중심을 뒷받침한다는 것을 보여주는 그림.
i1["언어에 중립적인 설계"] --> core["언어·프로세스·구현으로부터의 독립"]
i2["GUID를 통한 유일 식별"] --> core
i3["IUnknown의 참조 카운트"] --> core
i4["투명한 프로세스 간 통신"] --> core
그림 7: 4가지 구조가 모여 「언어·프로세스·구현으로부터의 독립」이라는 COM의 중심을 뒷받침한다.
「아름답다」는 주관적인 표현이지만, 그렇게 평가하는 근거는 구체적입니다. COM이 풀려고 했던 문제와 그 해법을 나열하면 다음과 같습니다.
| 당시(그리고 지금도) 존재하는 제약 | COM의 답 |
|---|---|
| C++에는 표준 바이너리 규약(ABI)이 없어 컴파일러만 달라도 재사용할 수 없음. 이름 맹글링도 객체 배치도 맞지 않음 | 「가상 함수 테이블의 순서」만을 규약으로 삼았다. 이 한 가지로 좁혔기 때문에 언어도 컴파일러도 상관없어졌다 |
| 라이브러리를 교체하면 그것을 사용하는 모든 것을 다시 빌드해야 함 | 계약(인터페이스)이 바뀌지 않는 한 재빌드가 필요 없다. 바이너리 그대로 교체할 수 있다 |
| 기능을 추가하고 싶지만 기존 호출하는 쪽을 깨뜨릴 수 없음 | 인터페이스를 바꾸지 않고 추가하고, QueryInterface로 「가지고 있는지」를 실행 시점에 묻는다 |
| 이름이 충돌함(같은 클래스 이름의 다른 것이 공존함) | GUID로 유일하게 식별한다. 이름 조정 자체를 필요 없게 만들었다 |
| 별도 프로세스·별도 머신의 기능을 호출하면 호출 방식이 완전히 달라짐 | Proxy/Stub을 두어 호출하는 쪽 코드를 같은 형태로 유지한다 |
즉 COM의 설계는 「어떻게 하면 깔끔할까」에서 시작한 것이 아니라, 재사용을 가로막던 구체적인 장애물을 하나씩 없앤 결과로 지금의 형태가 되었습니다. 제약과 해법의 대응을 이렇게까지 그대로 따라갈 수 있는 설계는 그리 많지 않습니다.
그리고 이 해법은 현대의 컴포넌트 지향 개발에 그대로 남아 있습니다.
| COM | 현대의 대응물 |
|---|---|
| IDL로 계약을 먼저 정하고, 거기서 양쪽 코드를 생성한다 | OpenAPI나 Protocol Buffers의 스키마에서 클라이언트/서버를 생성한다 |
| 기존 인터페이스를 바꾸지 않고 새 인터페이스를 추가한다 | Protocol Buffers에서 필드 번호를 재사용하지 않고 추가한다, API를 버전으로 나눈다 |
| 구현 언어를 상관하지 않는다(바이너리 계약) | 구현 언어를 상관하지 않는다(네트워크 너머의 메시지 계약) |
| Proxy/Stub이 프로세스 경계를 숨긴다 | RPC의 클라이언트 스텁이 네트워크 경계를 숨긴다 |
다른 것은 경계의 종류(같은 머신의 프로세스 경계인지, 네트워크인지)와 계약의 표현 방식(바이너리인지, 텍스트/스키마인지)뿐입니다. COM을 이해해 두면, 마이크로서비스 간 인터페이스 설계에서 「왜 계약을 먼저 정하는가」 「왜 기존 필드를 지워서는 안 되는가」가 유행이 아니라 필연으로 납득됩니다.
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리해야 할 것
Windows 앱의 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 개수, 장치 연계, COM/ActiveX, 배포·업데이트, 보수와 관련해 정리해야 할 포인트를 설명합니다.
COM 컴포넌트나 OCX / ActiveX 개발에서 빠지기 쉬운 것 - Visual Studio의 32bit / 64bit, 등록, 관리자 권한의 덫을 정리
COM 컴포넌트와 ActiveX, OCX 개발에서 자주 만나는 0x80040154나 0x80070005를 비트 수, 등록 방식, HKCU와 HKLM 스코프, 관리자 권한이라는 네 축으로 풀어 Visual Studio 2022의 64bit화 시대에...
COM / ActiveX / OCX란 무엇인가 - 차이와 관계를 정리해서 해설
Windows 레거시 프로젝트에서 자주 마주치는 COM과 ActiveX, OCX의 차이를 토대·부품·파일이라는 축으로 정리해, regsvr32와 32/64bit, IE 모드 같은 키워드를 만났을 때 헷갈리지 않고 조사와 이전 방침을 잡을 수 있도...
ActiveX / OCX를 지금 어떻게 다룰 것인가 - 남길지·감쌀지·교체할지 판단표
ActiveX / OCX 자산을 만났을 때 무작정 폐기하지 말고, 그 부품이 무엇을 떠맡고 있는지를 기준으로 남길지·감쌀지·교체할지를 32bit / 64bit, 등록 방식, 브라우저 의존, 벤더 보수까지 함께 따져 판단하는 표를 정리합니다.
클립보드와 드래그 앤 드롭의 구조 ── 업무 앱에서 OLE 데이터 전송을 올바르게 다루기
Excel 표를 붙여 넣으면 서식이 무너지고, 원본 앱을 닫으면 붙여 넣을 수 없게 되는 일── 둘 다 클립보드가 같은 내용을 여러 형식으로 한꺼번에 두기 때문입니다. 표준 형식, 지연 렌더링, OLE 드래그 앤 드롭, 클립보드 기록과 클라우드 ...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
ActiveX 이관
COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- COM이란 무엇인가요?
- COM(Component Object Model)이란 Windows에서 컴포넌트끼리 주고받기 위한 「바이너리 계약」입니다. 언어와 컴파일러의 차이를 넘어 인터페이스라는 엄격한 계약으로 통신하는 구조입니다. 그 밑바탕에는 「구현이 아니라 계약에 대해 프로그래밍한다」는 설계 사상이 있습니다.
- IUnknown이란 무엇인가요?
- 모든 COM 인터페이스가 상속하는 기본 인터페이스입니다. 다른 인터페이스를 가지고 있는지 묻는 QueryInterface, 참조 카운트를 늘리는 AddRef, 참조 카운트를 줄여서 0이 되면 자신을 파괴하는 Release라는 세 가지 기능을 제공합니다. COM의 객체 수명 관리는 이 참조 카운트로 이루어집니다.
- COM은 지금도 사용되고 있나요?
- 네, COM은 Windows의 핵심에서 지금도 계속 쓰이는 구조입니다. 탐색기 확장(우클릭 메뉴나 미리보기 표시), Excel과 Word의 Office 자동화, .NET과의 COM Interop, ActiveX를 포함한 기존 시스템, DirectX나 Windows Shell API 등 다수의 위치에서 등장합니다. Windows 개발을 하는 한 COM은 어딘가에서 등장합니다.
- COM의 강점은 무엇인가요?
- 크게 네 가지입니다. 한 번 빌드한 컴포넌트를 언어나 런타임에 상관없이 재사용할 수 있는 바이너리 호환성, 구현을 숨기고 계약만 공개하는 인터페이스 분리, 새 인터페이스를 추가해 하위 호환성을 유지하는 버전 공존, 그리고 Out-of-proc COM(EXE 서버)을 통한 다른 프로세스 기능의 안전한 호출입니다.