32bit 앱에서 64bit DLL을 호출하는 COM 브리지 사례
· 업데이트: · Go Komura · COM, Windows 개발, 32bit, 64bit
수정 이력(10건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.22170264)
- 3장 앞에 한 단락을 추가했습니다. 64bit 쪽에서 쓰고 싶은 것이 원래 in-proc COM 서버(InprocServer32로 등록하는 DLL)라면, CLSID에 AppID를 붙이고 그 AppID 키에 빈 문자열의 DllSurrogate를 쓰는 것만으로 Windows에 포함된 dllhost.exe에 올릴 수 있어 EXE 서버를 쓰지 않아도 되는 경우가 있다는 보충입니다. 기동되는 서로게이트의 bitness는 클라이언트가 아니라 DLL 쪽에서 정해진다는 점, LocalServer32가 있으면 서로게이트는 쓰이지 않는다는 점도 덧붙였습니다. 다만 이 글처럼 호출하려는 것이 그냥 네이티브 DLL이라면 올릴 COM 서버 자체가 없으므로, 뒤에서 설명하는 EXE 서버 방식이 여전히 필요합니다. 등록 절차와, 서로게이트로 충분한지 자체 EXE를 쓸지의 경계는 각각 다른 글의 링크에 맡겼습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635078)
- 리뷰 지적에 따라, 오늘 추가한 그림 가운데 폭이 너무 컸던 것을 세로 구성으로 고치고, 일부 그림과 캡션 표현을 본문 서술과 맞게 바로잡았습니다. 본문 문장은 바꾸지 않았습니다.
- 비트 수 제약, 브리지 구성, 레지스트리 등록의 함정, 채택 판단 등을 그림으로도 따라갈 수 있도록 Mermaid 그림을 10개 추가했습니다(본문 500~750자당 1그림 규약에 맞춘 것입니다). 본문 문장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 모은 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대한 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
- `LocalServer32`에 실행 파일 경로를 따옴표 없이 넣고 있던 것을 고쳤습니다. `/d "%SERVER%"`라고 쓰면 따옴표는 `reg.exe`의 인수 해석에서 사라지므로, `C:\Program Files\...`는 「`C:\Program`에 `Files\...`를 넘긴다」고도 해석될 수 있고, `C:\Program.exe`를 둘 수 있는 환경에서는 그쪽이 기동될 수 있습니다. 따옴표까지 포함해 저장하는 형태로 바꿨고, 확인용 `reg query`도 덧붙였습니다.
- 서두에 대상 독자와 전제 환경을 추가하고, 레지스트리 등록 절차를 새로 만들었습니다(필요한 3개 키 표, `reg add /reg:64`와 `/reg:32`로 등록·해제, EXE 쪽의 클래스 팩터리 등록, 관리자 권한 없이 HKCU에 등록하는 대안). ProgID가 무엇을 위한 것인지 설명, 용어 미니 사전, .NET Framework와 .NET 5 이후의 차이 대조표, 정리 장을 추가했습니다.
- 샘플 코드의 클라이언트가 ProgID로 서버를 얻는데, 서버 쪽에 ProgID를 지정하는 특성이 없어 그대로는 재현할 수 없었습니다. 특성을 보완했습니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635077)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
Go Komura (2026). 「32bit 앱에서 64bit DLL을 호출하는 COM 브리지 사례」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635077 https://comcomponent.com/ko/blog/com-case-study-32bit-to-64bit/
- DOI(최신 버전)
- 10.5281/zenodo.21635077
- DOI(이 버전)
- 10.5281/zenodo.22217400
32bit 앱에서 64bit DLL을 호출하고 싶다는 요건은 Windows에서는 꽤 전형적입니다. 특히 기존 자산을 남긴 채 64bit 쪽 기능만 쓰고 싶을 때, COM 브리지 구성이 현실적인 해답이 되기 쉽습니다.
대상 독자: 32bit 기존 Windows 앱을 유지보수하면서, 64bit 쪽 DLL이나 라이브러리를 쓰고 싶은 분. COM을 「들어는 봤지만 직접 짜 본 적은 없다」 정도의 전제로 읽을 수 있게 썼습니다.
전제 환경: 64bit판 Windows(x64)와, C#을 쓸 수 있는 개발 환경(Visual Studio 등). COM 서버를 머신 전체(HKEY_LOCAL_MACHINE 아래)에 등록하는 작업에는 관리자 권한이 필요합니다. COM의 기본 발상은 「COM이란 무엇인가 - Windows COM 설계가 지금도 아름다운 이유」에서 정리했습니다.
목차
이 글의 지식 맵
32bit 앱은 비트 구성이 일치하지 않는 in-proc COM으로는 64bit DLL을 직접 로드할 수 없기 때문에, 이 기사에서는 64bit 쪽 처리를 COM LocalServer(EXE 서버)로 별도 프로세스에 분리하고, IDL과 TypeLib로 공유한 COM 인터페이스를 통해 타입 있는 형태로 호출합니다. 마샬링과 Proxy/Stub이 프로세스 경계를 넘는 호출을 다리 놓아 주며, CLSID와 ProgID의 등록, 그리고 CLSID 하위 키가 32bit view와 64bit view에서 별도가 되는 WOW64 레지스트리 리다이렉터에 대한 양면 등록이 전제가 됩니다. 컴퓨터 전체에 대한 등록에는 관리자 권한이 필요하며, .NET(5 이상)은 표준 EnableComHosting이 in-proc 전용이므로 Out-of-proc 서버의 등록 처리는 직접 작성해야 합니다.
flowchart LR
accTitle: 32bit/64bit COM 브리지의 지식 맵
accDescr: 32bit 앱이 64bit DLL을 COM LocalServer의 EXE 서버로 호출하는 구성이 TypeLib·마샬링·CLSID/ProgID 등록·WOW64 레지스트리 리다이렉터와 어떻게 관계되는지 보여주는 그림
com_bridge_32_64["32비트/64비트 COM 브리지"]
com_localserver["COM LocalServer(별도 프로세스 COM 서버)"]
type_library["타입 라이브러리(TLB)"]
com_marshaling["마샬링"]
proxy_stub["Proxy/Stub"]
in_proc_com["In-proc COM(DLL 서버)"]
bitness_match_requirement["비트수 일치 요건"]
clsid["CLSID(Class ID)"]
progid["ProgID(Programmatic Identifier)"]
wow64_registry_redirector["WOW64 레지스트리 리디렉터"]
wow6432node["WOW6432Node"]
reg_exe["reg.exe(/reg:32 /reg:64)"]
class_factory_registration["클래스 팩토리 등록"]
admin_rights["관리자 권한"]
regasm["Regasm.exe"]
dotnet[".NET(Core 이후)"]
dotnet_framework[".NET Framework"]
dotnet_comhost[".NET(5 이후)의 EnableComHosting/.comhost.dll"]
com["COM(컴포넌트 오브젝트 모델)"]
com_bridge_32_64 -->|"이용한다"| com_localserver
com_bridge_32_64 -->|"전제로 한다"| type_library
com_localserver -->|"이용한다"| com_marshaling
com_marshaling -->|"이용한다"| proxy_stub
in_proc_com -->|"전제로 한다"| bitness_match_requirement
com_localserver -->|"완화한다"| bitness_match_requirement
com_localserver -->|"전제로 한다"| clsid
progid -->|"전제로 한다"| clsid
com_localserver -.->|"전제로 한다"| wow64_registry_redirector
clsid -.->|"에 저장된다"| wow6432node
wow64_registry_redirector -->|"에서 구성할 수 있다"| reg_exe
com_localserver -->|"전제로 한다"| class_factory_registration
com_bridge_32_64 -.->|"전제로 한다"| admin_rights
regasm -->|"사용은 비권장"| dotnet
regasm -->|"전제로 한다"| dotnet_framework
dotnet_comhost -->|"이용한다"| dotnet
dotnet_comhost -->|"사용은 비권장"| com_bridge_32_64
dotnet -.->|"전제로 한다"| clsid
com -.->|"이용한다"| type_library
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 19건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
1. 대상 상황
32bit 기존 앱은 그대로 두고, 64bit DLL의 처리를 쓰고 싶은 경우입니다. 그런데 32bit 프로세스는 64bit DLL을 로드하지 못합니다. 이는 OS 수준의 제약이지, 재주로 어떻게든 되는 이야기가 아닙니다.
흔히 이런 상황입니다.
- 기존 32bit 앱은 자산으로서 크고, 바로 이전할 수 없다
- 64bit DLL 쪽에 새 기능이 있거나, 의존 라이브러리가 64bit만 있다
- 32bit 쪽에서 「타입이 있는 형태로」 호출하고 싶다
이 조합에서는 같은 프로세스 안에서 호출하는 길이 처음부터 막혀 있습니다.
flowchart TB
accTitle: 대상 상황의 구도
accDescr: 32bit 기존 앱이 64bit DLL의 처리를 쓰고 싶은데, 32bit 프로세스는 64bit DLL을 로드하지 못한다는 OS 수준 제약 때문에, 같은 프로세스 안에서 호출하는 길이 막혀 있음을 나타내는 그림.
app["32bit 기존 앱"] --> want["64bit DLL의 처리를 쓰고 싶다"]
want --> deny["같은 프로세스에서는 로드할 수 없다"]
deny -.-> os["OS 수준 제약이므로 우회할 수 없다"]
그림 1: 32bit 프로세스는 64bit DLL을 로드하지 못하므로, 같은 프로세스 안에서 호출하는 길은 처음부터 없다.
2. 해결 방법
이 장부터 전문 용어가 이어지므로, 먼저 꼭 필요한 말만 정리해 둡니다.
| 용어 | 의미 |
|---|---|
| In-proc COM(DLL 서버) | COM 컴포넌트를 호출 측과 같은 프로세스에 로드해 쓰는 형태. 빠르지만 비트 수가 맞지 않으면 로드할 수 없다 |
| Out-of-proc COM(EXE 서버) | COM 컴포넌트를 별도 프로세스로 기동해 쓰는 형태. 비트 수가 달라도 연동할 수 있다 |
| LocalServer | 같은 PC 안의 별도 프로세스로 동작하는 COM 서버. 레지스트리의 LocalServer32 키에 EXE 경로를 등록한다 |
| IDL / TypeLib | 인터페이스의 모양(메서드 이름·인수 타입)을 적은 정의(IDL)와, 그것을 바이너리화한 것(TypeLib). 양쪽이 같은 「계약」을 보기 위해 쓴다 |
| 마샬링 | 프로세스 경계를 넘기기 위해 인수나 반환값을 보낼 수 있는 형태로 다시 담는 일. 반대는 언마샬링 |
| Proxy / Stub | 마샬링을 실제로 수행하는 대리 코드. 호출 쪽에 Proxy, 서버 쪽에 Stub이 만들어진다 |
| WOW6432Node | 64bit Windows에서 32bit 앱용 레지스트리 내용이 물리적으로 놓이는 장소. 같은 키 이름이라도 32bit 쪽과 64bit 쪽의 내용이 달라진다 |
해결의 기본은 Out-of-proc COM(EXE 서버)으로 분리하는 것입니다. 64bit DLL은 64bit COM 서버(EXE)에서 호출하고, 32bit 앱은 COM을 통해 사용합니다.
flowchart TB
accTitle: COM 브리지의 기본 구성
accDescr: 32bit 앱이 COM을 통해 64bit COM 서버 EXE를 호출하고, 그 서버가 내부에서 64bit DLL을 호출하는 별도 프로세스 분리 구성을 나타내는 그림.
a32["32bit 앱"] -->|"COM을 통해 호출한다"| srv["64bit COM 서버(EXE)"]
srv -->|"내부에서 호출한다"| dll["64bit DLL"]
그림 2: 64bit DLL은 64bit EXE 서버에 맡기고, 32bit 앱은 COM을 통해 그 서버를 사용한다.
흐름은 다음과 같습니다.
- 64bit COM LocalServer(EXE)를 준비하고, 내부에서 64bit DLL을 호출한다
- COM 인터페이스(IDL/TypeLib)를 공유하고 타입을 공개한다
- 32bit 앱은 COM을 「타입이 있는 형태로」 호출한다(Proxy/Marshal로 주고받는다)
다만 주의점도 있습니다.
- 32bit/64bit 등록은 별개(WOW6432Node 포함)
- 독자 구조체는 마샬링 설계가 필요
- IPC 오버헤드가 있으므로 고빈도 호출은 주의
즉 「64bit 처리를 별도 프로세스로 빼내고, COM으로 다리를 놓는 것」이 정석입니다.
flowchart TB
accTitle: 브리지를 만드는 3가지 절차
accDescr: 64bit COM LocalServer를 준비해 내부에서 64bit DLL을 호출하고, IDL과 TypeLib로 인터페이스 타입을 공개한 뒤, 32bit 앱이 타입이 있는 형태로 호출하는 3단계 흐름을 나타내는 그림.
s1["64bit COM LocalServer를 준비"] --> s2["IDL / TypeLib로 타입을 공개"]
s2 --> s3["32bit 앱이 타입이 있는 형태로 호출"]
s3 -.-> ps["Proxy / Marshal로 주고받음"]
그림 3: LocalServer 준비, 타입 공개, 타입이 있는 호출이라는 3단계로 브리지가 성립한다.
덧붙이면, 64bit 쪽에서 쓰고 싶은 것이 원래 in-proc COM 서버(InprocServer32로 등록하는 DLL)인 경우에는 EXE 서버를 쓰지 않아도 되는 경우가 있습니다. CLSID에 AppID를 붙이고, 그 AppID 키에 빈 문자열의 DllSurrogate를 쓰면, 그 DLL은 Windows에 포함된 서로게이트 프로세스(64bit DLL이라면 System32\dllhost.exe)에 올라가고, 32bit 클라이언트에서는 별도 프로세스에서 동작하는 out-of-proc COM 서버로 보입니다(LocalServer32에 EXE 경로를 등록하는 것이 아닙니다. 오히려 LocalServer32가 있으면 서로게이트는 쓰이지 않습니다). 반대 방향(64bit 앱에서 32bit COM DLL을 쓰는 경우)에서도 구조는 같고, 기동되는 서로게이트의 bitness는 클라이언트가 아니라 DLL 쪽에서 정해집니다. 다만 이 글처럼 호출하고 싶은 것이 그냥 네이티브 DLL이라면, 서로게이트에 올릴 COM 서버가 애초에 없으므로 이후의 EXE 서버 방식이 필요합니다. 등록 절차는 COM/OCX/ActiveX 개발에서 빠지기 쉬운 등록과 bitness의 덫의 3.5(그 글은 32bit DLL 전제이므로, AppID 값을 써 넣는 view만 바꿔 읽으세요), 서로게이트로 충분한지 자체 EXE를 쓸지의 경계는 ActiveX / OCX를 지금 어떻게 다룰 것인가 - 남길지・감쌀지・교체할지 판단표의 5.2에 정리해 두었습니다.
3. 처리 흐름(시퀀스 다이어그램)
다음은 32bit 앱이 64bit DLL의 처리를 호출할 때의 흐름입니다.
sequenceDiagram
participant App as 32bit 클라이언트 앱
box rgba(100,100,255,0.1) 등록된 COM 마샬링 기반이 처리
participant Proxy as COM Proxy<br/>(32bit 쪽)
participant RPC as RPC/IPC<br/>(프로세스 간 통신)
participant Stub as COM Stub<br/>(64bit 쪽)
end
participant Server as 64bit COM Server<br/>(EXE)
participant DLL as 64bit DLL
App->>Proxy: ICalcService.Add(1, 2)
rect rgba(100,100,255,0.1)
Note over Proxy: 파라미터를 마샬링
Proxy->>RPC: 직렬화된 데이터
RPC->>Stub: 프로세스 경계를 넘어 전송
Note over Stub: 파라미터를 언마샬링
end
Stub->>Server: Add(1, 2)
Server->>DLL: 네이티브 함수 호출
DLL-->>Server: 결과: 3
Server-->>Stub: 결과: 3
rect rgba(100,100,255,0.1)
Note over Stub: 반환값을 마샬링
Stub-->>RPC: 직렬화된 결과
RPC-->>Proxy: 프로세스 경계를 넘어 전송
Note over Proxy: 반환값을 언마샬링
end
Proxy-->>App: 결과: 3
그림 4: 32bit 앱의 호출은 Proxy·프로세스 간 통신·Stub을 거쳐 64bit 서버와 64bit DLL에 닿고, 결과는 같은 경로로 돌아온다.
포인트:
- 32bit 앱은
ICalcService인터페이스를 통해 타입 안전하게 호출할 수 있다 - COM 런타임은 등록된 Proxy/Stub DLL, TypeLib 마샬러, 표준 마샬러 등을 사용해 프로세스 경계를 넘는다
- 프로세스 간 통신 오버헤드가 있으므로, 세세한 호출보다 일괄 처리가 바람직하다
flowchart TB
accTitle: 호출 단위에 대한 생각
accDescr: 프로세스 간 통신에는 오버헤드가 있으므로, 세세한 호출을 고빈도로 하면 누적되고, 일괄 처리로 모으는 편이 바람직하다는 생각을 나타내는 그림.
ipc["프로세스 간 통신의 오버헤드"] --> fine["세세한 호출의 반복에서는 누적된다"]
ipc --> batch["일괄 처리로 모으면 영향이 작다"]
batch -.-> rec["바람직한 쪽은 이쪽"]
그림 5: 프로세스 경계를 넘을 때마다 비용이 들므로, 호출은 거친 단위로 모으는 편이 바람직하다.
4. 샘플 코드(개념)
4.1. 공유 인터페이스와 서버·클라이언트
다음은 개념의 이미지입니다. 실제로 돌리려면 이어지는 4.2의 등록이 필요합니다.
// 공유 인터페이스(IDL에 해당)
[ComVisible(true)]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
int Add(int a, int b);
}
// 64bit COM LocalServer(EXE 쪽)
[ComVisible(true)]
[Guid("1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("KomuraSoft.CalcService")]
public class CalcService : ICalcService
{
public int Add(int a, int b)
{
// 여기서 64bit DLL을 호출한다
return a + b;
}
}
// 32bit 앱 쪽(클라이언트)
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService");
var calc = (ICalcService)Activator.CreateInstance(t);
int result = calc.Add(1, 2);
이 형태로 만들면 32bit 쪽은 「타입이 있는 형태로」 다룰 수 있습니다. COM이 내부에서 프록시/스텁을 쓰고, IPC를 통해 호출해 줍니다.
[ProgId("KomuraSoft.CalcService")]를 붙인 것은, 클라이언트 쪽이 Type.GetTypeFromProgID("KomuraSoft.CalcService")로 찾을 수 있게 하기 위해서입니다. ProgID는 「사람이 읽을 수 있는 별칭」일 뿐이고, 실제로 서버를 찾는 것은 다음에 설명할 CLSID 등록입니다.
flowchart LR
accTitle: ProgID에서 서버에 닿기까지
accDescr: 클라이언트가 지정하는 ProgID는 사람이 읽을 수 있는 별칭일 뿐이고, 거기에서 CLSID가 조회되며, CLSID 등록으로 실제 서버가 찾아진다는 해석 순서를 나타내는 그림.
progid["ProgID(사람이 읽을 수 있는 별칭)"] --> clsid["CLSID 등록"]
clsid --> srv["서버를 찾는다"]
그림 6: ProgID는 입구의 별칭이고, 서버를 실제로 가리키는 것은 CLSID 등록 쪽이다.
4.2. 최소한의 등록 절차
COM은 「레지스트리에 등록된 CLSID를 COM 런타임이 찾아 기동하는」 구조이므로, 등록하지 않은 코드는 절대 동작하지 않습니다(Type.GetTypeFromProgID가 null을 반환하거나, CreateInstance에서 REGDB_E_CLASSNOTREG가 됩니다). EXE 서버(LocalServer)에 필요한 등록은, 따져 보면 다음 3개 키뿐입니다.
| 등록하는 것 | 키 | 값 |
|---|---|---|
| ProgID → CLSID 대응 | HKEY_CLASSES_ROOT\KomuraSoft.CalcService\CLSID |
{1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11} |
| CLSID → EXE 경로 | HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\LocalServer32 |
64bit COM 서버 EXE의 전체 경로 |
| CLSID → ProgID 역조회 | HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\ProgID |
KomuraSoft.CalcService |
여기에 이 글의 주제 그 자체의 함정이 있습니다. HKEY_LOCAL_MACHINE\SOFTWARE\Classes는 32bit 앱과 64bit 앱에서 공유되는 반면, 그 아래의 CLSID 하위 키(및 Interface 등)는 32bit 쪽과 64bit 쪽이 별개(32bit 쪽 실체가 WOW6432Node)라고 Microsoft 문서에 명시되어 있습니다. 즉 ProgID 키는 한 번 쓰면 양쪽에서 보이지만, CLSID 등록은 32bit 뷰와 64bit 뷰 양쪽에 쓰지 않으면 32bit 클라이언트에서 서버를 찾지 못합니다.
flowchart TB
accTitle: 레지스트리의 공유 부분과 나뉘는 부분
accDescr: Classes 바로 아래의 ProgID 키는 한 번 쓰면 32bit와 64bit 양쪽에서 보이는 반면, CLSID 아래는 32bit 뷰와 64bit 뷰가 별개라서 양쪽에 써야 하며, 32bit 쪽이 빠지면 32bit 클라이언트에서 서버를 찾지 못함을 나타내는 그림.
progk["ProgID 키(Classes 바로 아래)"] --> shared["양쪽에서 보인다"]
clsk["CLSID 아래의 등록"] --> v64["64bit 뷰에 쓴다"]
clsk --> v32["32bit 뷰에 쓴다"]
v32 -.-> wow["실체는 WOW6432Node"]
v32 -.-> warn["빠지면 32bit에서 보이지 않는다"]
그림 7: ProgID 키는 공유되지만, CLSID 아래는 32bit/64bit 뷰마다 별개이므로 양쪽에 등록한다.
관리자 권한 명령 프롬프트에서 reg 명령의 /reg:32 /reg:64를 쓰는 것이 확실합니다(Wow6432Node를 경로에 직접 쓰는 것은 Microsoft가 비권장으로 두고 있습니다).
:: 관리자 권한 명령 프롬프트에서 실행한다
set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService
set SERVER=C:\Program Files\KomuraSoft\CalcServer.exe
:: 1) ProgID → CLSID(HKLM\SOFTWARE\Classes 바로 아래는 32/64에서 공유)
reg add "HKLM\SOFTWARE\Classes\%PROGID%\CLSID" /ve /d "%CLSID%" /f
:: 2) CLSID → LocalServer32와 ProgID(CLSID 아래는 32/64에서 별개이므로 양쪽에 쓴다)
:: LocalServer32 값에는 실행 파일 경로를 따옴표까지 포함해 넣는다.
:: /d "%SERVER%"라고 쓰면 따옴표는 reg.exe의 인수 해석에서 사라지고, 값은
:: C:\Program Files\... 그대로 저장된다. COM은 이를 명령줄로
:: 해석하므로, 공백 앞에서 잘린 C:\Program.exe를 먼저 찾으러 간다
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID" /ve /d "%PROGID%" /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:32
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID" /ve /d "%PROGID%" /f /reg:32
:: 저장된 값을 확인한다. "C:\Program Files\..."처럼 따옴표가 붙어 나오면 올바르다
reg query "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /reg:64
LocalServer32의 따옴표는 단순한 형식 문제가 아닙니다. 따옴표가 없으면 C:\Program Files\...는 「C:\Program에 Files\...를 인수로 넘긴다」는 해석이 성립해 버립니다. 그 결과, C:\Program.exe를 만들 수 있는 권한이 있는 상대가 있는 환경에서는 그쪽이 먼저 기동될 수 있습니다. 공백을 포함한 경로에 넣을 때는 반드시 따옴표를 붙이세요.
flowchart TB
accTitle: LocalServer32 따옴표 유무에 따라 달라지는 해석
accDescr: LocalServer32 값에 따옴표 없이 경로를 넣으면 공백 앞에서 잘린다는 해석이 성립하고, C:\Program.exe를 둘 수 있는 환경에서는 그쪽이 먼저 기동될 수 있는 반면, 따옴표까지 포함해 저장하면 의도한 EXE가 기동됨을 나타내는 그림.
noq["따옴표 없이 저장"] --> cut["공백 앞에서 잘린다는 해석이 성립한다"]
cut --> evil["C:\Program.exe가 먼저 기동될 수 있다"]
q["따옴표까지 포함해 저장"] --> safe["의도한 EXE가 기동된다"]
그림 8: 공백을 포함한 경로를 따옴표 없이 등록하면, 다른 실행 파일이 기동될 여지가 생긴다.
해제는 같은 키를 지우기만 하면 됩니다.
set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService
reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:64
reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:32
reg delete "HKLM\SOFTWARE\Classes\%PROGID%" /f
덧붙여, EXE 쪽은 기동했을 때 「자신이 이 CLSID의 담당이다」라고 COM에 알려야 합니다. C/C++이면 CoRegisterClassObject, .NET Framework이면 RegistrationServices.RegisterTypeForComClients가 이에 해당합니다. 레지스트리 등록은 COM이 EXE를 기동하는 곳까지만 봐 주므로, 이 알림이 빠지면 EXE는 기동하는데 객체를 만들지 못한다는, 알아채기 어려운 실패가 됩니다.
flowchart TB
accTitle: 레지스트리 등록과 EXE 쪽의 알림
accDescr: 레지스트리 등록이 봐 주는 것은 COM이 EXE를 기동하는 곳까지이고, 기동한 EXE가 자신이 이 CLSID의 담당이라고 알려야 비로소 객체를 만들 수 있으며, 알림이 빠지면 EXE는 기동하는데 객체를 만들지 못하는 실패가 됨을 나타내는 그림.
regd["레지스트리 등록"] --> boot["COM이 EXE를 기동한다"]
boot --> ann["EXE가 CLSID의 담당이라고 알린다"]
ann --> ok["객체를 만들 수 있다"]
boot -.->|"알림이 빠지면"| ng["기동은 하는데 만들지 못하는 실패"]
그림 9: 레지스트리 등록은 EXE 기동까지이고, 그 다음은 EXE 자신의 알림이 없으면 객체를 만들지 못한다.
참고로, 개발 중에 시험만 한다면 HKLM 대신 HKCU\SOFTWARE\Classes에 같은 구조로 쓰면 관리자 권한 없이도 등록할 수 있습니다(HKEY_CLASSES_ROOT는 HKLM과 HKCU의 합성 뷰입니다). 다만 HKCU\SOFTWARE\Classes\CLSID도 마찬가지로 32bit/64bit에서 별도 취급이므로, 양쪽 뷰에 쓴다는 점은 달라지지 않습니다.
4.3. .NET Framework와 .NET(5 이후)은 만드는 법이 다르다
위 코드는 C#이지만, 어느 .NET을 쓰느냐에 따라 절차가 크게 달라집니다. 여기를 섞으면 막힙니다.
| .NET Framework | .NET(Core 3.0 / 5 이후) | |
|---|---|---|
| 등록 도구 | RegAsm.exe가 있다(다만 만들어지는 것은 in-proc용 InprocServer32 등록이므로, LocalServer32는 결국 직접 쓴다) |
RegAsm에 해당하는 도구는 없다 |
| 표준적인 COM 공개 | 어셈블리에 특성을 붙이고 RegAsm |
<EnableComHosting>true</EnableComHosting>으로 *.comhost.dll을 생성하고 regsvr32로 등록(in-proc만) |
| TypeLib(.tlb) 생성 | TlbExp / RegAsm /tlb로 생성할 수 있다 |
지원되지 않는다. IDL을 손으로 쓰고 MIDL로 컴파일한다(.NET 6 이후에는, 만들어진 .tlb를 comhost에 넣는 것은 가능하다) |
| CLSID 지정 | 생략 가능 | COM에서 생성시킬 클래스에는 CLSID를 반드시 명시 |
| AnyCPU 취급 | 32bit/64bit 양쪽 클라이언트에서 쓸 수 있다 | 따라오는 *.comhost.dll이 기본으로 64bit이므로, 64bit 클라이언트에서만 쓸 수 있다 |
이 글의 구성(EXE 서버)은 .NET(5 이후)에서는 표준 EnableComHosting의 범위 밖이므로, 등록 처리를 직접 쓰게 됩니다. Microsoft 공식 샘플로 OutOfProcCOM이 준비되어 있으므로, .NET 쪽에서 짤 때는 거기가 출발점이 됩니다.
flowchart TB
accTitle: .NET(5 이후)에서 EXE 서버를 짤 때의 길
accDescr: 이 글의 EXE 서버 구성은 .NET 5 이후에서는 표준 EnableComHosting의 범위 밖이므로, 등록 처리를 직접 쓰게 되고, 공식 샘플 OutOfProcCOM이 출발점이 된다는 길을 나타내는 그림.
exe[".NET(5 이후)에서의 EXE 서버"] --> range["표준 EnableComHosting의 범위 밖"]
range --> self["등록 처리를 직접 쓴다"]
self -.-> smp["공식 샘플 OutOfProcCOM이 출발점"]
그림 10: .NET(5 이후)의 EXE 서버는 표준 기능의 범위 밖이므로, 등록 처리는 직접 쓴다는 전제가 된다.
5. 전체 샘플 코드
위의 개념을 실제로 동작하는 형태로 구현한 샘플을 GitHub에서 공개하고 있습니다.
Call64bitDLLFrom32bitProc - GitHub
이 리포지토리에는 다음이 포함되어 있습니다.
- Call64bitDLLFrom32bitProc/ - 64bit COM LocalServer (EXE)
- X64DLL/ - 64bit DLL(실제 처리)
- X86App/ - 32bit 클라이언트 (WinForms)
- scripts/ - COM 서버 등록·해제 스크립트
README에 적힌 절차를 따라 빌드·등록하면, 실제로 32bit 프로세스에서 64bit DLL을 호출하는 동작을 확인할 수 있습니다.
6. 정리
COM 브리지는 만능이 아니며, 맞는 일과 맞지 않는 일이 뚜렷이 갈리는 구성입니다. 채택을 정하기 전에, 다음 표에 자신의 경우를 맞춰 보세요.
| 맞는 케이스 | 맞지 않는 케이스 |
|---|---|
| 32bit 앱 본체를 다시 만들 수 없다(수정 비용이 수지에 맞지 않는다) | 애초에 32bit 쪽을 64bit로 다시 빌드할 수 있다(그게 가장 짧은 길이다) |
| 호출이 거친 단위(한 번에 이미지 1장, 한 번에 파일 1개 등) | 요소 하나씩 수만 번처럼 세세한 호출을 고빈도로 돌린다(IPC 오버헤드가 지배적이 된다) |
| 주고받는 것이 수치·문자열·배열처럼 마샬링하기 쉬운 타입 | raw 포인터나 복잡한 독자 구조체를 대량으로 왕복시킨다 |
| 64bit 쪽 처리가 죽어도 앱 본체는 살려 두고 싶다(프로세스 분리가 이점이 된다) | 서버 쪽 크래시나 재기동을 다루는 복구 처리를 쓰고 싶지 않다 |
| 타입이 있는 호출(IntelliSense나 컴파일 시 검사)을 남기고 싶다 | 단발 배치 처리로 끝나고, 표준 입출력이나 파일 경유 주고받기로 충분하다 |
마지막 행을 반대로 말하면, 「64bit 쪽 처리를 단순한 콘솔 EXE로 만들고, 인수와 파일로 주고받는다」는 소박한 대안도 항상 검토할 가치가 있습니다. COM 브리지가 효력을 내는 것은, 타입이 있는 호출을 유지하고 싶을 때와, 상태를 가진 서버를 여러 번 호출하고 싶을 때입니다.
flowchart TB
accTitle: COM 브리지와 소박한 대안의 갈림점
accDescr: 타입이 있는 호출의 유지나 상태를 가진 서버를 여러 번 호출하는 것이 필요하면 COM 브리지가 효력을 내고, 단발 배치 처리로 끝나면 콘솔 EXE로 인수와 파일을 주고받는 소박한 대안도 검토할 가치가 있다는 갈림점을 나타내는 그림.
q{"타입이 있는 호출이나 상태 유지가 필요한가?"} -->|"예"| br["COM 브리지"]
q -->|"아니요"| alt["콘솔 EXE로 인수와 파일 주고받기"]
그림 11: 타입이 있는 호출과 상태 유지가 필요한지가, 브리지와 소박한 대안의 갈림점이 된다.
다음 액션으로는 이 순서를 권합니다.
- 먼저 5장의 샘플 리포지토리를 clone하고, README대로 빌드·등록해 동작하는 상태를 하나 로컬에 만든다.
- 자신의 64bit DLL 함수를 하나만 골라, 샘플의
ICalcService에 해당하는 인터페이스에 메서드를 하나 더해 통과시킨다. - 통과하면 호출 횟수와 한 번당 데이터양을 잰다. 여기서 「거친 단위로 모은다」는 설계 판단(여러 번의 호출을 한 번으로 합친다)을 먼저 끝내 두면, 되돌아가는 일이 줄어듭니다.
7. 참고 자료
- Component Object Model (COM)의 개요 https://learn.microsoft.com/en-us/windows/win32/com/component-object-model–com–portal
- COM LocalServer32의 등록 https://learn.microsoft.com/en-us/windows/win32/com/localserver32
- COM 인터페이스의 기본 https://learn.microsoft.com/en-us/windows/win32/com/the-component-object-model
- COM Interop(.NET에서의 이용) https://learn.microsoft.com/en-us/dotnet/standard/native-interop/cominterop
- WOW64의 레지스트리 리디렉터(
HKLM\SOFTWARE\Classes는 공유,CLSID아래는 32/64에서 별개) https://learn.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys - .NET(Core / 5 이후) 컴포넌트를 COM에 공개하기 https://learn.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
COM/OCX/ActiveX 개발에서 막히기 쉬운 등록과 bitness의 함정
COM, OCX, ActiveX 개발에서 자주 막히는 32bit/64bit, Visual Studio 2022, regsvr32/Regasm, 관리자 권한, HKCR, STA/MTA를 실무 관점에서 정리합니다.
Office 2024/Microsoft 365에서 ActiveX가 작동하지 않는 원인과 확인 절차
Office 2024/Microsoft 365에서 ActiveX가 작동하지 않을 때, 기본 비활성화, 32bit/64bit, COM 등록, 의존 DLL, IE 모드, Click-to-Run 로그를 구분하는 순서를 정리합니다.
클립보드와 드래그 앤 드롭의 구조 ── 업무 앱에서 OLE 데이터 전송을 올바르게 다루기
Excel 표를 붙여 넣으면 서식이 무너지고, 원본을 닫으면 붙여 넣을 수 없다── 원인은 같은 내용을 여러 형식으로 두는 클립보드입니다. 표준 형식·지연 렌더링·OLE 드래그 앤 드롭부터 기록과 클라우드 동기 정책까지 설명합니다.
Windows 셸 통합의 현재 ── 컨텍스트 메뉴, 파일 연결, Windows 11의 변화
Windows 11에서 컨텍스트 메뉴가 「더 많은 옵션 표시」 뒤로 숨는 이유를, 확장자→ProgID→verb라는 파일 연결의 기본, 기존형 셸 확장의 주의점, IExplorerCommand와 MSIX/sparse package의 새 방식까지 이...
Windows on Arm에서 업무 앱은 동작하는가 ── x64 에뮬레이션(Prism)과 네이티브 DLL·COM의 현실
「Windows on Arm에서 업무 앱은 동작하는가」에 개발자와 사내 IT를 위해 답합니다. x64 에뮬레이션(Prism)의 구조, 드라이버처럼 동작하지 않는 계층, .NET의 AnyCPU와 P/Invoke 문제, Arm 대응 체크리스트까지 정...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
ActiveX 이관
COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.
32비트 / 64비트 상호 운용
32비트 / 64비트 상호 운용, 네이티브 경계, 관련된 Windows 설계 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
기존 자산 활용 & 이관 지원
32bit 자산을 남긴 채 64bit 쪽으로 다리를 놓는 이야기이므로, 기존 자산 활용·이전 지원에 바로 이어지는 주제입니다.
기술 상담 & 설계 리뷰
COM 브리지나 프로세스 경계를 어떻게 나눌지를 먼저 정리하고 싶은 단계라면, 기술 상담·설계 리뷰로 비교해 볼 수 있습니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 32bit 앱에서 64bit DLL을 직접 호출할 수 있나요?
- 할 수 없습니다. 32bit 프로세스는 64bit DLL을 로드하지 못하며, 이는 OS 수준의 제약이지 재주로 피할 수 있는 이야기가 아닙니다. 같은 프로세스 안에서 호출하는 길은 처음부터 막혀 있으므로, 64bit 쪽 처리를 별도 프로세스로 분리하는 구성이 필요합니다.
- 32bit 앱에서 64bit DLL의 기능을 쓰려면 어떻게 하면 되나요?
- Out-of-proc COM(EXE 서버)으로 분리하는 것이 정석입니다. 64bit DLL은 64bit COM LocalServer(EXE)에서 호출하고, 32bit 앱은 COM 인터페이스를 통해 타입이 있는 형태로 사용합니다. COM 인터페이스(IDL/TypeLib)를 공유해 타입을 공개하면, COM 런타임이 프록시/스텁과 프로세스 간 통신으로 경계를 넘어 줍니다.
- COM 브리지 구성에서 주의할 점은 무엇인가요?
- 크게 세 가지입니다. 32bit/64bit 등록은 별개라는 점(WOW6432Node 포함), 독자 구조체는 마샬링 설계가 필요하다는 점, 그리고 프로세스 간 통신 오버헤드가 있어 고빈도의 세세한 호출에는 주의가 필요하다는 점입니다. 세세한 호출을 대량으로 반복하기보다 일괄 처리로 모으는 편이 바람직합니다.
- 실제로 동작하는 샘플 코드가 있나요?
- 있습니다. GitHub의 Call64bitDLLFrom32bitProc 리포지토리에 64bit COM LocalServer(EXE), 64bit DLL, 32bit 클라이언트(WinForms), COM 서버 등록·해제 스크립트를 포함한 샘플 전체가 공개되어 있습니다. README의 절차를 따라 빌드·등록하면, 32bit 프로세스에서 64bit DLL을 호출하는 동작을 확인할 수 있습니다.