강의 다시보기
코드 샘플
•
샘플 1
•
샘플 2
•
샘플 3
◦
코드 샘플
◦
샘플 실행을 위해 필요한 데이터 파일
◦
실행 결과
시작하기
Windows 프로그래밍에서 COM (Component Object Model) 기술은 닷넷 프레임워크가 출시되기 전에 나온 고급 프로그래밍 기술입니다. COM은 각각의 구성 요소들을 독립적으로 재사용할 수 있게 해주고, 서로 다른 프로그래밍 언어로 만든 구성 요소를 활용할 수 있게 해주는 기술입니다.
Windows 운영 체제가 32비트 버전으로 처음 빌드되어 출시된 Windows 95 (버전 4.0)과 Windows NT 4.0에서 소개된 이후로 COM 기술은 OLE, ActiveX, NT 서비스, IIS용 확장 모듈 등 매우 다양한 종류의 소프트웨어 컴포넌트에서 채택되어 널리 사용되었습니다. 뿐만 아니라, Windows 운영 체제의 기능을 고도화하기 위한 부분에서도 2023년 현재까지 지속적으로 쓰이고 있습니다.
닷넷 프레임워크와 닷넷 코어로 크로스 플랫폼 프로그래밍을 배우는 것과 별개로, 여러분이 매일같이 사용하는 Windows를 좀 더 심도있게 제어하고 활용할 수 있는 기술이 바로 COM이라고 볼 수 있는거죠! 그리고 COM을 활용하면 순수한 닷넷 코드만으로는 알 수 없거나 활용하기 어려운 고급 기능도 마음껏 쓸 수 있습니다.
COM의 기본 동작 방식
컴포넌트 설치 과정
설치, 사용, 할당 해제, 제거까지의 플로우를 훑어보면 다음과 같습니다.
1.
COM 컴포넌트를 사용하기 위해서는 기본적으로 Windows 레지스트리에 COM 컴포넌트에 관한 상세 정보를 등록해야 합니다. 그래서 regsvr32.exe 라는 도구를 이용하여 COM 컴포넌트 코드가 담긴 EXE, DLL, OCX 파일의 DllRegisterServer 함수를 호출하여 설치를 진행합니다.
2.
DllRegisterServer 함수는 COM 컴포넌트를 만든 개발자가 재량껏 설치 코드를 작성해야 하는 부분입니다.
•
HKLM\SOFTWARE\CLASSES\CLSID 레지스트리 아래에 COM 컴포넌트들마다 부여된 고유 아이디, 컴포넌트 활성화 방식, 컴포넌트를 불러올 수 있는 모듈의 파일 경로를 레지스트리에 기록하는 코드를 개발자가 작성합니다.
다른 애플리케이션에서 사용하는 과정
1.
앞에서 등록한 한 개, 또는 여러 개의 클래스 아이디 값들을 근거로 CoCreateInstance Win32 API를 호출합니다.
2.
Windows는 주어진 클래스 아이디를 레지스트리 상에서 찾고, 컴포넌트 활성화 방식에 따라 적절한 위치에 컴포넌트 인스턴스를 메모리에 할당한 다음, 그 인스턴스의 주소 (포인터)를 함수를 호출한 쪽으로 되돌려줍니다.
•
2단계에서 등록한 레지스트리 키 내부에 기재된 EXE, DLL, OCX 파일에서는 DllGetClassObject 라는 함수를 개발자가 구현해서 제공해야 합니다.
•
Windows에서는 이렇게 만들어진 COM 컴포넌트의 참조 카운트를 관리하기 시작합니다.
3.
사용하는 쪽에서 목적을 달성하여 Release 함수를 호출하면 참조 카운트가 감소합니다. 이렇게해서 참조 카운트가 0이 되는 순간 COM 컴포넌트 측의 소멸자가 실행됩니다.
COM은 닷넷과 달리 가비지 컬렉터라는 개념이 없으며, 개체의 참조 카운트를 기반으로 참조 카운트가 0이 되는 순간 자동으로 개체를 메모리에서 할당 해제하는 원리로 메모리 관리를 수행합니다.
•
이 때 참조 카운트가 한 번이라도 0이 된 적이 있다면 다시 값이 증가해서는 안됩니다.
•
참조 카운트는 인터페이스 포인터를 가지고 있는 곳이라면 어디서든 더하거나 뺄 수 있으므로, InterlockedIncrement 함수를 이용해서 Atomic Operation으로 관리해야 합니다.
컴포넌트 제거 과정
COM 컴포넌트가 사용 중이 아닌 상태 (EXE, DLL, OCX 파일에 관한 잠금 처리가 걸려있지 않은 상태)가 되면 시스템 관리자는 regsvr32.exe /u 명령줄 옵션을 이용하여 레지스트리에서 타입 정보를 제거할 수 있습니다.
•
이 때 DllUnregisterServer 함수는 COM 컴포넌트를 만든 개발자가 재량껏 설치 코드를 작성해야 하는 부분입니다.
정리
복잡하게 느껴지시나요? 사실 본질은 단순합니다.
•
레지스트리를 기록 장부로 활용하고, Windows는 CoCreateInstance라는 Win32 API를 이용해서 다른 애플리케이션이 COM 컴포넌트를 활용할 수 있도록 도와주는 것입니다.
◦
Windows는 이 과정에서 만들어진 COM 컴포넌트의 라이프사이클도 같이 관리해줍니다.
◦
좀 더 공부하면 아시게 될 내용이지만, 보통은 In Process 활성화 메커니즘을 많이 사용합니다. 이 메커니즘은 COM 컴포넌트가 메모리에 할당되는 위치가 호출하는 쪽의 프로세스 메모리 내부여야 함을 의미하는 것입니다.
◦
드물지만 Out-of-Process 활성화 메커니즘을 사용하는 경우도 있습니다. 이 방식으로 동작하는 경우 새로운 프로세스가 만들어진 다음, 이 프로세스 메모리 안에 COM 컴포넌트를 할당하고 그 주소를 되돌려주는 방식입니다. 물론 프로세스가 종료되거나 참조 카운트가 0이 되면 프로세스 째로 종료되는 것이죠!
◦
COM+는 여기서 더 나아가서 소켓 통신을 이용하여 원격에서 실행되는 서비스를 COM 스타일의 프로그래밍 기법으로 기능을 제공하거나 소비하겠다는 아이디어를 구현한 것입니다. 그러나 2023년 10월 현재 COM+를 직접 사용하는 곳은 매우 드물고, gRPC, REST API 같은 최신 기술들이 그 자리를 대체했습니다.
•
개발자는 본인이 배포하는 COM 컴포넌트에 DllRegisterServer, DllUnregisterServer, DllGetClassObject 따위의 함수가 제 역할을 할 수 있도록 코드를 잘 작성해야 합니다. *
•
다른 소프트웨어 개발자들은 Windows가 돌려준 IUnknown 인터페이스 포인터를 이용하여 알고 있는 특정 기능이 “지원되는가” “지원되지 않는가”를 따지기 위해 QueryInterface 함수를 호출하여 원하는 기능을 사용할 수 있습니다.
다른 한편으로, 다음의 부작용도 있을 수 있습니다.
•
COM 컴포넌트를 추가/제거하는 과정에서 일시적이지만 관리자 권한이 부여된 상태에서 작업이 이루어지므로, 이 과정에서 레지스트리 등록/삭제에 오류가 발생하여 시스템이 손상될 여지가 있습니다.
•
레지스트리를 정확하게 등록/삭제하지 않아 불필요하거나 부정확한 정보가 남을 수 있습니다.
닷넷에서 COM 사용하기
IUnknown 인터페이스
COM 컴포넌트는 기본적으로 IUnknown이라는 인터페이스를 구현해야 합니다. 이 인터페이스에는 다음의 세 가지 함수가 정의되어있습니다.
•
AddRef: 개체의 참조 카운트를 1 증가시킵니다.
•
Release: 개체의 참조 카운트를 1 감소시킵니다.
•
QueryInterface: 개체가 특정 인터페이스를 지원하는지 여부를 확인하고, 지원하는 것이 확실하다면 참조 카운트를 1 증가시킨 후, 해당 인터페이스 타입의 포인터를 반환합니다.
그리고 IUnknown 만으로는 아무것도 할 수 있는 일이 없으므로, QueryInterface 함수와 함께 인터페이스 헤더 파일의 선언이 매우 중요합니다.
COM 컴포넌트를 활용하고 싶은 우리 닷넷 개발자들의 입장에서 볼 때, System.Runtime.InteropServices 네임스페이스의 각종 클래스, 메서드, 어트리뷰트를 활용해서 QueryInterface 함수에 전달할 타입 정보를 만드는 것이 이번 토픽에서 언급하려는 핵심 내용이라고 볼 수 있겠습니다.
IDispatch 인터페이스
기본적으로 IUnknown 인터페이스만 구현하는 COM 컴포넌트는 참조 카운트 관리와 함께, 알고 있는 인터페이스 중에 지원되는 인터페이스 타입이 있는지 조회하는 것 정도가 최선입니다. 그런데 닷넷 이전에 나왔던 VB, VBA, ASP, Python, JavaScript 같은 인터프리터 언어들은 개발자가 실행해 달라고 요청한 코드만 가지고 어떻게 COM 컴포넌트를 알아보고 메서드를 호출할 수 있는 것일까요?
모든 COM 컴포넌트가 해당되는 것은 아니고, COM 컴포넌트가 IDispatch 인터페이스를 구현했을 경우에 해당되는 이야기입니다.
•
GetIDsOfNames: 특정 멤버의 이름을 해당 인터페이스에서 정의한 고유 아이디값으로 바꿉니다. 이 값을 사용하여 Invoke 함수를 호출합니다.
•
GetTypeInfoCount: 이 COM 컴포넌트가 구현하는 타입의 갯수를 반환합니다.
•
GetTypeInfo: 각각의 컴포넌트 정보를 포함하는 ITypeInfo 개체의 주소를 각각 반환합니다.
•
Invoke: GetTypeInfo를 통해 조사한 타입 정보 중 하나를 택하여 원하는 멤버를 호출하고 결괏값을 되돌려받습니다. 그리고 이 멤버를 호출하고 결괏값을 돌려받기까지의 과정은 동기식 호출입니다.
여기서 제공하는 코드들을 이용하여 닷넷이나 다른 프로그래밍 언어들이 요즈음 기본으로 탑재하는 “리플렉션”과 비슷한 기능을 이 인터페이스를 통해서 구현할 수 있는 셈이죠. C#의 경우에는 dynamic 키워드를 이용하여 IDispatch 인터페이스의 기능을 활용할 수 있고, 닷넷은 DLR (동적 언어 런타임)을 이용하여 IDispatch 인터페이스를 활용합니다.
이벤트 처리
닷넷의 경우 C#의 event 키워드와 += 및 -= 연산자, 그리고 대리자와 이벤트 핸들러를 이용하여 이벤트를 구독/해지하는 모델이 매우 자연스럽게 구현됩니다. 반면 COM에서는 다소 번거로운 방식을 사용하는데, 연결 지점 (Connection Point)라는 것을 얻어와서, 여기에 이벤트를 받을 개체를 등록 (Advise)하고, 사용을 마치면 해지 (Unadvise)하는 것이 기본 사용법입니다.
그리고 이런 유형의 이벤트 정보들을 정의하는 타입은 주로 타입 이름 앞에 D라는 접두사를 붙입니다.
유용한 애플리케이션 코드 만들어보기
샘플 1 — Windows Shell 컨트롤하기
void Main()
{
// 전체 API는 https://learn.microsoft.com/en-us/windows/win32/shell/shell에서 확인 가능
dynamic shell = Activator.CreateInstance(
Type.GetTypeFromCLSID(Guid.Parse(CLSID_Shell)));
// ShellObject에서 특수 폴더 상수를 가리킬 때 Environment.SpecialFolder 열거형을 바로 사용할 수 있음.
// 별도로 ssf 계열 상수를 C++이나 IDL 헤더에서 번역해서 가져올 필요 없음.
var isSvcRunning = (bool)shell.IsServiceRunning("Audiosrv");
$"Windows Audio Service is {(isSvcRunning ? "running" : "not running")}.".Dump();
var folderObj = shell.NameSpace(Environment.SpecialFolder.CommonAdminTools);
var title = (string)folderObj.Title;
IEnumerable items = (IEnumerable)folderObj.Items();
foreach (dynamic eachItem in items)
{
var path = (string)eachItem.Type;
path.Dump();
}
//shell.Explore(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
//shell.Help();
//shell.FileRun();
}
const string CLSID_Shell = "13709620-C279-11CE-A49E-444553540000";
C#
복사
샘플 2 — Internet Explorer 예토전생
void Main()
{
// 전체 API는 https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa752084(v=vs.85) 에서 확인 가능
dynamic iexplore = Activator.CreateInstance(
Type.GetTypeFromCLSID(Guid.Parse(CLSID_InternetExplorer)));
iexplore.Visible = true;
var icpc = (IConnectionPointContainer)iexplore;
var guidRef = typeof(DWebBrowserEvents2).GUID;
icpc.FindConnectionPoint(
ref guidRef,
out IConnectionPoint point);
var handler = new WebBrowserEventHandler();
handler.DocumentCompleted += (sender, args) =>
{
// IHTMLDocument는 자바스크립트에서 사용하는 것과 동일한 기능 집합을 제공함.
// 정확히 Internet Explorer가 제공하는 명세는 https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa752052(v=vs.85) 에서 확인 가능.
IEnumerable headerTags = iexplore.Document.getElementsByTagName("h1");
foreach (dynamic eachTag in headerTags)
{
var text = (string)eachTag.innerText;
$"{text}".Dump();
}
};
point.Advise(handler, out int cookie);
iexplore.Navigate("https://www.example.com/");
Console.ReadLine();
point.Unadvise(cookie);
}
const string CLSID_InternetExplorer = "0002DF01-0000-0000-C000-000000000046";
// 필요한 인터페이스 멤버만 정의해서 활용 가능
[ComImport, SuppressUnmanagedCodeSecurity, InterfaceType(ComInterfaceType.InterfaceIsIDispatch), Guid("34A715A0-6587-11D0-924A-0020AFC7AC4D")]
public interface DWebBrowserEvents2
{
[DispId(0x103)]
void DocumentComplete([MarshalAs(UnmanagedType.IDispatch)] object pDisp, [In] ref object URL);
}
public class WebBrowserEventHandler : DWebBrowserEvents2
{
public event EventHandler<DocumentCompleteEventArgs> DocumentCompleted;
public void DocumentComplete([MarshalAs(UnmanagedType.IDispatch)] object pDisp, [In] ref object URL)
{
this.DocumentCompleted?.Invoke(
pDisp, new DocumentCompleteEventArgs((string)URL));
}
}
public sealed class DocumentCompleteEventArgs : EventArgs
{
public DocumentCompleteEventArgs(string url) { Url = url; }
public string Url { get; private set; }
}
C#
복사
샘플 3 — 한글립숨을 담은 Microsoft Word 문서를 PDF로 변환하기
한글립숨 생성기 — 아래 샘플을 실행하기 위해 필요한 코드 조각입니다.
void Main()
{
var currentDirecory = Path.GetDirectoryName(Util.CurrentQueryPath);
var loremIpsum = new LoremIpsumGenerator(
Path.Combine(currentDirecory, "guunmong.txt"),
Path.Combine(currentDirecory, "guunmong-dot.txt"));
var content = loremIpsum.GetLoremIpsum(
paragraphCount: 1,
sentenceCount: 10,
wordCount: 100,
chickenize: false);
dynamic word = Activator.CreateInstance(Type.GetTypeFromProgID("Word.Application"));
word.Visible = true;
dynamic newDoc = word.Documents.Add();
newDoc.Content.Text = content;
newDoc.SaveAs2(Path.Combine(currentDirecory, "Lipsum.docx"));
newDoc.ExportAsFixedFormat(Path.Combine(currentDirecory, "Lipsum.pdf"), wdExportFormatPDF);
newDoc.Close();
word.Quit();
var psi = new ProcessStartInfo(Path.Combine(currentDirecory, "Lipsum.pdf")) { UseShellExecute = true, };
using var proc = Process.Start(psi);
}
// https://learn.microsoft.com/en-us/office/vba/api/word.wdexportformat
public const int wdExportFormatPDF = 17;
C#
복사
마무리
Windows 프로그래밍에서 여전히 많은 부분을 차지하는 것은 바로 COM을 이용한 프로그래밍입니다. COM 기술이 처음 소개되었을 때보다 지금은 많은 것이 바뀌기도 했지만, 사람들의 업무 흐름에 직접적인 영향을 줄 수 있는 효과적인 애플리케이션 개발, 자동화 기능을 고려한다면 여전히 필수 요소라고 볼 수 있습니다.
오늘 소개해드린 것은 “이미 만들어져있고 잘 알려진 기능” 위주로 말씀드린 것입니다. 여기서 더 나아가서, 내가 개발하고있는 소프트웨어에 새로운 기능을 접목하는 것을 고려하고 있다면 더 많은 주제를 살펴볼 수 있습니다.
이 주제에 좀 더 흥미가 있으시다면, 고급 응용 예제도 하나 소개해드리오니 같이 살펴보시는 것을 추천드립니다.