COM/OCX/ActiveX 개발에서 막히기 쉬운 등록과 bitness의 함정

· 업데이트: · · COM, ActiveX, OCX, Visual Studio, Windows 개발, 32bit, 64bit, Interop

수정 이력(5건, 최종 수정 2026년 08월 30일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

64bit 프로세스에서 32bit in-proc COM을 쓰기 위한 DllSurrogate 설명을 추가했습니다. regsvr32가 성공해도 64bit 프로세스는 32bit InprocServer32를 로드하지 못한다는 점, CLSID에 AppID를 붙이고 그 AppID 키에 빈 문자열 DllSurrogate를 쓰면 새 EXE 없이 dllhost.exe에 올릴 수 있다는 점, 시작되는 dllhost.exe의 bitness는 클라이언트가 아니라 DLL 쪽으로 정해진다는 점, LocalServer32가 있으면 EXE 시작이 우선된다는 점, 그리고 확인 절차를 정리했습니다. surrogate는 bitness 차이를 없애는 것이 아니라 동일 프로세스 제약만 없애며, 호출은 out-of-proc이 되어 marshaling과 IPC 비용이 따른다는 점도 명시했습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635259)
글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 모은 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)를 반영해 본문을 수정했습니다. 개별 변경 내용은 아래 이력을 참고하세요.
등록 위치 레지스트리 키 네 가지를 모두 보이고, 전수 탐색 스크립트와 결과 읽는 법을 추가했습니다. 더불어 STA/MTA와 marshaling 용어 정의, 메시지 루프가 없으면 멈추는 구조 그림, Regasm과 매니페스트 코드 예도 추가했습니다.
본문 속 관련 글 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 것을 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635258)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

小村 豪 (2026). 「COM/OCX/ActiveX 개발에서 막히기 쉬운 등록과 bitness의 함정」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635258 https://comcomponent.com/ko/blog/com-ocx-activex-pitfalls-visual-studio-bitness-admin-rights/

DOI(최신 버전)
10.5281/zenodo.21635258
DOI(이 버전)
10.5281/zenodo.22170292

COM 컴포넌트나 OCX / ActiveX 작업은 코드 자체보다 실행 환경·등록·호스트·권한의 경계에서 막히기 쉽습니다.

자주 보는 증상은 다음과 같습니다.

  • 빌드는 되는데 시작하면 0x80040154.
  • 자기 개발 PC에서는 되는데 다른 PC에서는 안 됩니다.
  • 런타임은 되는데 Visual Studio Designer만 죽습니다.
  • 관리자로 실행하면 되는데 일반 권한에서는 깨집니다.
  • regsvr32를 실행해도 고쳐지지 않습니다.

이런 경우는 개별 버그라기보다 COM의 전제가 어디선가 어긋난 상태입니다.

COM / ActiveX / OCX라는 말 자체를 먼저 정리하고 싶다면 COM / ActiveX / OCX란 무엇인가 - 차이와 관계를 모아서 해설을 먼저 읽는 편이 전체 그림을 잡기 쉽습니다. 이 글에서는 그다음 단계로, 실제로 어디서 막히기 쉬운지를 Visual Studio의 bitness와 관리자 권한까지 포함해 정리합니다.

1. 먼저 결론

실무에서 통하는 말로 먼저 정리하면 이렇습니다.

  1. COM / OCX / ActiveX 문제는 코드 로직보다 bitness(32bit / 64bit)·등록 위치·호스트·권한이 맞지 않아서 생기는 경우가 많습니다.
  2. Visual Studio 2022는 64bit 프로세스이므로, 예전에는 통하던 32bit 전제의 디자인 타임 연동이 그대로는 깨집니다.12
  3. regsvr32무엇이든 등록하는 만능 명령이 아닙니다. 네이티브 in-proc COM 서버(DLL / OCX)용입니다. .NET Framework를 COM에 공개할 때는 Regasm.exe, .NET 5+ / .NET 6+ / .NET 8+에서는 생성된 .comhost.dll을 등록하는 흐름입니다.3456
  4. 「관리자로 되면 OK」는 위험합니다. per-user 등록 때문에 우연히 보이는 경우, 또는 원래 설치 프로그램이 해야 할 등록을 개발 PC에서만 손으로 끝낸 경우가 드물지 않습니다.378

즉 COM / OCX / ActiveX를 다룰 때는 먼저 이 네 축으로 보는 편이 안전합니다.

  • 어느 프로세스가 호스트하는가
  • 그 프로세스는 32bit인가 64bit인가
  • 어디에 등록되어 있는가(HKCU / HKLM, 32bit view / 64bit view)
  • 그 조작이나 실행에 관리자 권한이 필요한가

이 글의 지식 맵

COM·OCX·ActiveX 문제는 코드 결함보다 bitness·등록 수단·등록 스코프·권한 불일치로 일어나기 쉽습니다. Visual Studio 2022는 devenv.exe가 64bit 프로세스이므로 x86 고정 ActiveX를 디자인 타임에 직접 로드하지 못하며, regsvr32는 네이티브 in-proc COM 서버용이고 .NET Framework 등록에는 Regasm.exe를 구분해서 써야 합니다. HKEY_CLASSES_ROOT는 HKLM과 HKCU의 merged view이며, CLSID 서브키는 Wow6432Node를 통해 32bit/64bit로 나뉘므로, 다른 view나 per-user에만 등록되어 있어도 Class not registered가 됩니다. STA에서는 메시지 루프와 CoInitializeEx가 호출을 전달하는 메커니즘 그 자체이며, 이를 멈추면 데드락이 발생합니다.

COM/ActiveX 등록 문제의 지식 맵COM·OCX·ActiveX 등록 문제가 Visual Studio 2022의 64bit화·WOW64 레지스트리 리디렉터·regsvr32와 Regasm 같은 등록 수단의 구분 사용·HKCU와 HKLM의 등록 스코프·STA 메시지 루프와 어떻게 관련되는지를 보여주는 그림이용한다이용한다양립하지 않는다전제로 한다구현을 담당한다이용한다에 저장된다이용한다권장되는 대응사용은 비권장권장되는 대응이용한다전제로 한다원인이 될 수 있다원인이 될 수 있다내용을 계승한다내용을 계승한다전제로 한다전제로 한다전제로 한다이용한다구현을 담당한다전제로 한다전제로 한다방지한다전제로 한다이용한다완화한다전제로 한다전제로 한다이용한다에 저장된다전제로 한다이용한다COM/OCX/ActiveX 등록 문제Visual Studio 2022WOW64 레지스트리 리디렉터ActiveXOCX비트수 일치 요건COM(컴포넌트 오브젝트 모델)HKEY_CURRENT_USER\Software\ClassesCLSID(Class ID)WOW6432Noderegsvr32In-proc COM(DLL 서버)Regasm.exe.NET Framework.NET(5 이후)의 EnableComHosting/.comhost.dll.NET(Core 이후)WOW64 파일 시스템 리디렉터관리자 권한0x80040154(Class not registered)per-user 등록HKEY_CLASSES_ROOT(HKCR)HKEY_LOCAL_MACHINE\Software\Classesper-machine 등록ActiveX의 디자인 타임/런타임 라이선스AxImp(ActiveX Control Importer)타입 라이브러리(TLB)AxHostCOM 아파트먼트 모델(STA/MTA)메시지 루프CoInitializeExSTA 스레드 차단에 의한 교착마샬링Reg-Free COM(등록 불필요 COM)어셈블리 매니페스트COM LocalServer(별도 프로세스 COM 서버)ProgID(Programmatic Identifier)

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 34건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 증상에서 거꾸로 보면 대체로 이렇게 보입니다

증상 먼저 의심할 것 흔한 실제 원인
0x80040154 Class not registered 미등록 실제로는 「다른 bitness 쪽에만 등록되어 있다」, 「그 사용자에게만 등록되어 있다」인 경우도 많습니다
DllRegisterServer failed: 0x80070005 권한 부족 표준 사용자로 등록하려는 중, post-build에서 elevation 없이 등록하는 중
VS2022에서 Designer만 깨진다 Designer 제약 32bit COM / ActiveX를 64bit Visual Studio가 직접 로드하지 못함
관리자로 실행할 때만 동작 권한 문제 본질은 권한 자체보다 등록 범위나 설치 설계가 어긋난 경우가 많습니다
64bit 앱에서 32bit OCX를 호출하지 못함 COM 구조의 제약 in-proc 서버는 호스트와 같은 bitness가 아니면 로드되지 않습니다
UI 스레드에서는 되는데 백그라운드 스레드에서 멈춤 스레드 모델 STA / MTA, CoInitializeEx, 메시지 루프 전제 위반

0x80040154는 공식 명칭이 REGDB_E_CLASSNOTREG이고, 말 그대로 Class not registered입니다.9 또한 regsvr320x80070005는 공식 문서에서도 관리자 권한이 없어 레지스트리나 System32에 쓰지 못하는 사례로 설명됩니다.10

여기서 중요한 점은 오류 이름을 글자 그대로 너무 믿지 않는 것입니다. 예를 들어 Class not registered는 「완전히 미등록」이 아닐 수 있고, 다른 레지스트리 view에 등록되어 있는 경우, 그 사용자에게만 보이는 위치에 등록되어 있는 경우에도 발생합니다.117

3. Visual Studio bitness에서 막힙니다

3.1 Visual Studio 2022는 64bit가 됐습니다

지금 COM / ActiveX 개발에서 가장 막히기 쉬운 지점입니다.

Visual Studio 2022는 devenv.exe64bit only입니다.1 그래서 WinForms 디자인 타임에서는 Visual Studio가 32bit 컴포넌트를 직접 로드하지 못합니다. Microsoft도 Visual Studio 2022는 64bit 프로세스이므로 32bit .NET / COM / ActiveX 컴포넌트를 로드하지 못한다고 명시합니다.2

예전에는 이렇게 되어 있었습니다.

  • 프로젝트는 x86
  • 참조하는 ActiveX도 x86
  • Visual Studio 자체도 32bit

이 전제에서 그럭저럭 성립했는데, VS2022에서는

  • 런타임 앱은 x86으로 동작할 수 있다
  • 그러나 Designer는 64bit Visual Studio 쪽에서 동작한다

는 어긋남이 생깁니다. 그 결과 런타임은 살아 있는데 Designer만 죽는, 원인을 찾기 어려운 상태가 됩니다. 런타임 호스트는 앱 프로세스이고 디자인 타임 호스트는 Visual Studio 프로세스라서, 호스트가 다르면 bitness도 다르기 때문입니다.2

3.2 AnyCPU로 바꾼다고 해결된다고 할 수 없습니다

흔한 오해이기도 합니다.

AnyCPU의존 대상까지 중립으로 만드는 마법이 아닙니다. Microsoft 설명에서도, AnyCPU로 보이는 컴포넌트라도 그 뒤에서 32bit 고정 COM / ActiveX를 참조하면 Visual Studio 2022 디자인 타임에 문제가 된다고 합니다.2

그래서 AnyCPU로 바꿔도 오류가 사라지지 않으면

  • 그 어셈블리 뒤에 32bit 네이티브 의존이 없는지
  • ActiveX / OCX가 x86 고정이 아닌지
  • Designer에서만 로드되는 코드가 없는지

를 의심하는 편이 빠릅니다.

3.3 System32SysWOW64의 함정

Windows x64에서는 이름이 주는 인상과 실제가 어긋나서 혼동하기 쉽습니다.

Microsoft Learn에서는 x64 Windows의 %windir%\System3264bit 애플리케이션용으로 설명합니다. 32bit 쪽은 WOW64 파일 시스템 redirector가 다른 위치로 안내합니다.12 레지스트리도 마찬가지입니다. WOW64 레지스트리 redirector가 32bit / 64bit에 다른 논리 view를 보여 줍니다.11

그래서 로컬에서 원인을 가를 때는 어느 bitness의 regsvr32를 쓰는지를 명시하는 편이 안전합니다.

# 64bit DLL / OCX 를 등록하고 싶은 경우
C:\Windows\System32\regsvr32.exe vendor.ocx

# x64 Windows 상에서 32bit DLL / OCX 를 등록하고 싶은 경우
C:\Windows\SysWOW64\regsvr32.exe vendor.ocx

잘못된 쪽 regsvr32로 등록하면 골치입니다. 등록 자체는 성공하는데 목적 프로세스에서는 보이지 않습니다. 결과적으로

  • regsvr32는 성공했다
  • 그런데 앱에서는 0x80040154
  • 레지스트리를 보면 있는 것처럼 보인다
  • 그러나 보고 있는 것은 다른 view

가 됩니다.1113

3.4 regsvr32로 무엇이든 등록할 수 있는 것은 아닙니다

여기도 오해가 많습니다.

네이티브 in-proc COM 서버는 보통 DllRegisterServer / DllUnregisterServer를 export해 자기 등록을 지원합니다.3 regsvr32는 그런 DLL / OCX에 쓰는 도구입니다.4

반면 .NET Framework 어셈블리를 COM에서 쓰려면 기본은 Regasm.exe입니다. Microsoft Learn에서도 COM에서 사용할 어셈블리 등록에는 Regasm.exe를 쓴다고 설명합니다.514

.NET 5+ / .NET 6+ / .NET 8+의 COM 공개는 사정이 조금 달라집니다. <EnableComHosting>true</EnableComHosting>을 설정하고 빌드하면 *.comhost.dll이 생성되고, 그것을 regsvr32로 등록합니다.6

대략 나누면 이렇습니다.

공개하려는 것 대표적인 등록 수단
네이티브 C++ DLL / OCX regsvr32
.NET Framework 어셈블리를 COM 공개 Regasm.exe
.NET 5+ / 6+ / 8+ COM 공개 생성된 .comhost.dllregsvr32

Regasm.exe는 Developer Command Prompt / Developer PowerShell에서 실행합니다. 자주 쓰는 방법은 다음 세 가지입니다.14

:: 1) 어셈블리 안의 공개 클래스를 등록한다
regasm myTest.dll

:: 2) 타입 라이브러리도 생성해 등록한다 (VBA / VB6 등 TLB가 필요한 쪽)
regasm myTest.dll /tlb:myTest.tlb

:: 3) 레지스트리를 직접 바꾸지 않고 내용을 .reg 파일로 내보내 확인한다
regasm myTest.dll /regfile:myTest.reg

해제는 /unregister입니다. /tlb로 등록한 타입 라이브러리는 /tlb/unregister함께 지정해 해제합니다.

regasm myTest.dll /tlb:myTest.tlb /unregister

여기서 빠지기 쉬운 제약이 두 가지입니다.14

  • GAC에 넣지 않는 어셈블리는 /codebase가 필요합니다. /codebase는 등록 시점의 파일 경로를 레지스트리에 기록하므로, 나중에 어셈블리를 옮기면 시작에 실패합니다. 또한 Microsoft는 /codebase를 붙이는 어셈블리에 strong name(강한 이름)을 붙일 것을 강하게 권장합니다.
  • /regfile/unregister/tlb와 함께 쓸 수 없습니다. 또한 /regfile이 내보내는 것은 매니지드 클래스 항목뿐이며, TypeLibIDInterfaceID는 출력되지 않습니다. 「.reg만 배포하면 등록이 끝난다」고 생각하면 부족합니다.

이 차이를 섞으면 흔히

  • 매니지드 DLL에 regsvr32를 건다
  • 당연히 DllRegisterServer를 찾지 못한다
  • 그래서 「DLL이 깨졌다」고 오해한다

는 패턴이 됩니다.

타입 라이브러리가 필요한 세계(VBA / VB6 / 일부 early binding 대상)에서는 TLB 생성과 등록이 또 다른 문제입니다. .NET Framework에서는 Regasm.exe /tlb로 타입 라이브러리를 생성·등록할 수 있고, Microsoft도 「타입 등록」과 「타입 라이브러리 등록」은 별개 활동이라고 설명합니다.15

3.5 64bit 프로세스에서 32bit in-proc를 꺼내고 싶을 때 ── DllSurrogate

3.3과 3.4는 「등록 위치와 수단이 맞는지」였습니다. 여기서는 등록은 맞는데 bitness가 맞지 않을 때의 출구를 하나 보탭니다.

먼저 결론입니다.

  • regsvr32가 성공해도 64bit 프로세스는 32bit InprocServer32를 로드하지 못합니다. 등록이 잘못된 것이 아니라 in-proc 전제 그 자체입니다.
  • 새 EXE를 작성하지 않고 그 DLL을 64bit 쪽에서 쓰려면 CLSID에 AppID를 붙이고, 그 AppID 키에 빈 문자열 DllSurrogate를 쓰는 것이 최소 조치입니다.16
  • 그때 뜨는 것은 클라이언트 쪽이 아니라 InprocServer32가 가리키는 DLL 쪽 bitness의 dllhost.exe입니다.

왜 in-proc 그대로는 안 되는가

2장의 증상 표에 적은 「64bit 앱에서 32bit OCX를 호출하지 못한다」는 0x80040154와 겉모습은 같아도 내용이 다릅니다.

InprocServer32는 말 그대로 「호출 측 프로세스 안에서 동작하는 서버」지정입니다. 64bit 프로세스가 거기에 적힌 32bit DLL을 로드하는 일은 레지스트리를 어떻게 고쳐도 일어나지 않습니다. 3.3에서 레지스트리 view가 나뉘는 것도, 거슬러 올라가면 이 제약 때문입니다.

그래서 여기서부터는 「등록을 고친다」가 아니라 「다른 프로세스로 꺼낸다」 이야기가 됩니다.

surrogate가 하는 일

DllSurrogate는 in-proc DLL 서버를 surrogate 프로세스에 올려 Local Server로 꺼내기 위한 지정입니다.17 값을 빈 문자열로 두면 Windows에 포함된 기본 surrogate(dllhost.exe)가 쓰입니다.18

32bit DLL이면 32bit surrogate가 시작되고, DLL은 그 안에서 지금까지와 같이 in-proc로 로드됩니다. 64bit 클라이언트는 그 프로세스 밖에서 proxy를 통해 객체를 다룹니다.

오해하기 쉬우니 분명히 적습니다. surrogate는 bitness 차이를 없애지 않습니다. 없어지는 것은 「같은 프로세스에 올려야 한다」는 제약뿐입니다. 호출은 out-of-proc이 되고, marshaling과 프로세스 간 통신 비용이 따릅니다. 전제로 주고받는 타입을 marshaling할 수 있어야 합니다(IDispatch, 등록된 proxy / stub, 표준 marshaler가 다루는 타입 중 하나).

하나 더. surrogate로 다루기 쉬운 것은 메서드 호출과 이벤트로 끝나는 자동화 객체입니다. 폼에 붙여 그리게 하는 비주얼 OCX는 창을 호스트 프로세스 안에 둔다는 전제라서, 이 조치만으로는 끝나지 않습니다.

activation이 in-proc과 surrogate 중 어디로 떨어지는가in-proc을 포함하는 CLSCTX로 요청했을 때, 클라이언트와 같은 view의 CLSID에 InprocServer32가 있으면 먼저 in-proc으로 로드되고, 없으면 EXE 서버 등록이 있을 때 surrogate보다 먼저 그쪽이 시작되며, EXE 등록이 없고 빈 DllSurrogate가 있으면 DLL 쪽 bitness의 dllhost가 시작되고, 어느 것도 없으면 activation하지 못함을 나타내는 그림.있다없다있다없다있다없다클라이언트가 activation을 요청in-proc을 포함하는 CLSCTX로 요청같은 view의 CLSID에 InprocServer32가 있는가지금까지처럼 in-proc으로 로드EXE 서버 등록이 있는가surrogate보다 먼저 EXE가 시작빈 DllSurrogate가 있는가DLL 쪽 bitness의 dllhost가 시작activation하지 못하고 0x80040154

그림 1: 같은 bitness의 InprocServer32가 보이는지로 먼저 갈리고, 보이지 않을 때만 EXE 서버, 이어서 빈 DllSurrogate 순으로 떨어집니다.

등록의 최소 집합

Microsoft Learn은 DLL 서버가 surrogate에 올라가는 조건을 다음과 같이 듭니다.16

  1. CLSID 키에 AppID 값이 있고, 대응하는 AppID 키가 존재한다
  2. activation 호출에 CLSCTX_LOCAL_SERVER가 켜져 있고, CLSID 키에 LocalServer32 / LocalServer / LocalService없다
  3. CLSID 키에 InprocServer32가 있다
  4. InprocServer32가 가리키는 DLL이 실제로 있다
  5. AppID 키 아래에 DllSurrogate 값이 있다

그 DLL을 하나의 surrogate에서 단독으로 돌리려면 AppID를 CLSID와 같은 GUID로 두는 것이 Microsoft가 권하는 방식입니다.16

등록은 세 줄이면 됩니다. 아래는 32bit COM DLL을 64bit 호스트에서 쓰는 경우이고, GUID는 placeholder입니다. 먼저 SysWOW64regsvr32InprocServer32가 들어가 있는 것이 전제입니다(3.3 참조).

:: 관리자 권한 명령 프롬프트에서 실행한다
set CLSID={11111111-2222-3333-4444-555555555555}
set APPID={11111111-2222-3333-4444-555555555555}

:: 1) CLSID에 AppID를 붙인다. CLSID 아래는 32/64로 나뉘므로 view를 명시한다
::    64bit COM DLL을 32bit 호스트에서 쓰는 반대 방향이면 여기를 /reg:64로 바꿔 읽는다
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /v AppID /t REG_SZ /d "%APPID%" /f /reg:32

:: 2) 대응하는 AppID 키를 만든다. Classes\AppID는 32/64에서 공유되므로 view 지정은 필요 없다
reg add "HKLM\SOFTWARE\Classes\AppID\%APPID%" /f

:: 3) DllSurrogate를 빈 REG_SZ로 둔다. /d를 생략하면 빈 문자열로 만들어진다
reg add "HKLM\SOFTWARE\Classes\AppID\%APPID%" /v DllSurrogate /t REG_SZ /f

왜 빈 문자열로 두는가

DllSurrogateREG_SZ이고, 값 자체가 surrogate 경로입니다. 빈 문자열(또는 NULL)이면 시스템 기본 surrogate가 쓰이고, 경로를 적으면 그 경로의 사용자 지정 surrogate를 시작하려 합니다.1718

즉 친절한 마음으로 dllhost.exe 경로를 넣으면 역효과입니다. 비워 두는 것 자체가 「기본값을 쓰라」는 지정입니다.

참고로 HKLM\SOFTWARE\Classes\AppID는 Windows 7 / Windows Server 2008 R2 이후 32bit / 64bit에서 공유되는 키라서 view 구분이 없습니다.19 반면 CLSID 아래는 view가 나뉘므로, 32bit 서버라면 AppID 값은 32bit view의 CLSID 쪽에 붙여야 합니다. 효과가 없으면 이 두 곳을 반드시 세트로 확인합니다.

여기는 3.3이나 6.3의 「32bit 쪽에만 등록되어 있으면 64bit 프로세스에서는 0x80040154」와 어긋나 보이지만, 그쪽은 in-proc 이야기입니다. out-of-proc 활성화에서는 클라이언트와 서버 모두 bitness 희망을 내지 않았을 때, COM은 클라이언트에 맞는 bitness 서버를 찾고, 없으면 다른 쪽 bitness 서버를 시작합니다.20 그래서 이 절차는 CLSID 쪽을 32bit view에 둔 채로 성립합니다. InprocServer32를 64bit view로 복사할 필요는 없습니다.

뜨는 dllhost.exe의 bitness

여기서 가장 많이 오해합니다. surrogate의 bitness는 클라이언트 쪽이 아니라 InprocServer32가 가리키는 DLL 쪽에서 정해집니다. DLL은 surrogate 안에서 in-proc으로 읽히므로 당연합니다.

안에서 읽히는 COM DLL 시작되는 dllhost.exe
32bit %SystemRoot%\SysWOW64\dllhost.exe
64bit %SystemRoot%\System32\dllhost.exe

작업 관리자나 프로세스 탐색기에서 명령줄을 보면 dllhost.exe /Processid:{...} 형태로 떠 있습니다. 여기 GUID는 CLSID가 아니라 AppID입니다. CLSID인 줄 알고 찾으면 안 나오니 주의합니다.

LocalServer32가 있으면 EXE가 이깁니다

Microsoft Learn은 LocalServer / LocalServer32 / LocalService가 있으면 DLL을 surrogate에 올리는 것보다 EXE 서버나 서비스 시작이 항상 우선된다고 명시합니다.16

즉 surrogate는 「EXE 서버가 없을 때의 출구」입니다. 이미 EXE 서버가 등록된 CLSID에 DllSurrogate를 더해도 그쪽은 쓰이지 않습니다. 여기서 우선하는 대상은 어디까지나 surrogate에 대해서이지, in-proc에 대해서가 아닙니다.

기존 호출 측에 미치는 영향도 알아 두면 안심입니다. DllSurrogate를 더해도 in-proc을 포함하는 CLSCTX로 만드는 클라이언트는 bitness가 맞는 한 지금까지처럼 in-proc입니다. CLSCTX는 여러 값을 OR로 넘기면 열거 순서(in-proc → local → remote)로 시도되고, in-proc 단계에서는 InprocServer32 키가 있으면 그것이 쓰이기 때문입니다.20 CLSCTX_ALL이나 CLSCTX_SERVER처럼 in-proc과 local을 함께 넘기는 호출이 여기에 해당합니다. 반대로 CLSCTX_INPROC_SERVER만으로 부르는 곳은 DllSurrogate를 더해도 surrogate로 가지 않습니다.

그리고 64bit 클라이언트에서 보면, 32bit view에 넣은 InprocServer32는 64bit view에 존재하지 않습니다. in-proc 단계는 「해당 키 없음」으로 통과하므로 그대로 local 쪽(surrogate) 활성화로 진행합니다. bitness가 다른 DLL을 로드하려다 실패하는 것이 아닙니다.2019

확인 방법

들어갔는지는 view를 명시해 다시 읽는 것이 확실합니다.

reg query "HKLM\SOFTWARE\Classes\CLSID\{11111111-2222-3333-4444-555555555555}\InprocServer32" /ve /reg:32
reg query "HKLM\SOFTWARE\Classes\CLSID\{11111111-2222-3333-4444-555555555555}" /v AppID /reg:32
reg query "HKLM\SOFTWARE\Classes\AppID\{11111111-2222-3333-4444-555555555555}" /v DllSurrogate

1행을 빼지 않습니다. reg add는 지정한 키가 없으면 만들기 때문에, GUID를 잘못 치거나 다른 view에 등록하면 AppID 값만 올라간 빈 CLSID 키가 생겨 2행은 통과합니다. InprocServer32가 같은 view에 있는 것까지 봐야 확인한 셈입니다.

DllSurrogate값이 빈 채로 나오면 맞습니다. 그다음 64bit 클라이언트에서 객체를 만들고, SysWOW64dllhost.exe가 뜨는지 봅니다.

  • 0x80040154 그대로 → AppID 값이 32bit view CLSID 쪽에 붙어 있는지, InprocServer32가 가리키는 DLL이 실제로 있는지를 다시 봅니다
  • dllhost.exe는 뜨는데 호출에서 죽는다 → marshaling 전제(IDispatch / proxy-stub / 표준 marshaler)를 확인합니다
  • 다른 EXE가 뜬다 → 그 CLSID에 LocalServer32 등이 남아 있습니다

여기까지가 「등록」 이야기

surrogate로 충분한지, 아니면 32bit 쪽에 자체 helper EXE를 써야 하는지는 등록이 아니라 구성 선택입니다. 그쪽은 ActiveX / OCX를 지금 어떻게 다룰까 - 남기기·감싸기·교체 판단표의 5.2 「32bit OCX를 64bit 쪽으로 가져가고 싶다」에서 정리합니다.

4. 관리자 권한에서 막힙니다

4.1 post-build로 등록해서 관리자일 때만 성공합니다

Visual Studio C++ 빌드에서 build event나 custom build step으로 regsvr32.exe를 호출하는 것 자체는 흔합니다. Microsoft Learn에도 post-build event 예로 regsvr32.exe 등록이 나옵니다.21

다만 할 수 있는 일안전한 일은 다릅니다.

표준 사용자로 regsvr32를 실행하면 레지스트리나 System32에 쓰지 못해 0x80070005가 나기도 합니다. Microsoft KB에서도 원인은 관리자 권한이 없는 것으로 정리합니다.10

여기서 자주 생기는 사고는 다음과 같습니다.

  • Visual Studio를 일반 권한으로 시작하면 빌드는 통과한다
  • 그러나 post-build 등록만 실패한다
  • 실패 로그를 놓친다
  • 직전의 오래된 등록이 남아 있어 로컬에서는 우연히 동작한다
  • 클린 환경에서는 당연히 동작하지 않는다

이런 프로젝트에서는 빌드와 등록을 나누는 것이 기본입니다.

  • 빌드는 바이너리만 만든다
  • 등록은 명시적인 install step / script / installer로 한다
  • CI에서는 「등록이 필요한 step」을 build와 다른 잡으로 둔다

이 분리만으로도 사고가 꽤 줄어듭니다.

4.2 per-user 등록과 per-machine 등록을 섞으면 깨집니다

COM은 HKCR을 본다,는 기억만으로는 여기서 막힙니다.

실제로는 Microsoft Learn대로, COM은 HKEY_CURRENT_USER\Software\Classes를 먼저 보고 그다음 컴퓨터 전체 정보를 다룹니다.3 또한 HKEY_CLASSES_ROOTHKLM\Software\ClassesHKCU\Software\Classes의 merged view입니다.722

  • 개발자 A 사용자로 수동 등록했다
  • A 계정에서는 된다
  • 개발자 B에서는 안 된다
  • 서비스 계정에서도 안 된다
  • 관리자로 실행하면 동작이 바뀐다

는 일이 흔합니다.

게다가 Microsoft는 관리자 권한이 필요한 애플리케이션은 의존 COM 객체를 설치 시 per-machine COM 구성 저장소에 등록해야 한다고 합니다.87

그래서 개발 현장에서는 이 구분을 분명히 해야 합니다.

  • 자기 사용자만 쓰는 개발용 등록인가
  • 그 머신의 모든 사용자가 쓰는 운영 등록인가
  • 서비스나 권한이 오른 앱이 참조하는 등록인가

여기가 모호하면 「왜 관리자일 때만 되지」, 「Explorer 확장에서는 되는데 서비스에서는 안 된다」 같은 이야기가 됩니다.

4.3 Visual Studio를 항상 「관리자로 실행」하는 것은 해결이 아닙니다

필요한 장면이 있는 것은 사실입니다. 다만 Visual Studio를 항상 관리자로 켜 두면 원래 installer나 등록 스크립트가 풀어야 할 문제를 IDE 쪽 elevation으로 숨기는 경우가 있습니다.

게다가 Visual Studio 자체도 권한이 오르면 per-user extension 취급이 바뀝니다. Microsoft Learn에도 Visual Studio를 elevated로 돌리면 per-user extensions가 비활성화되는 설정이 있다고 설명합니다.23

그래서 운영은

  • 평소 개발은 일반 권한
  • 등록이 필요한 step만 명시적으로 권한을 올린 Developer Command Prompt / PowerShell / installer에서 수행
  • 「관리자일 때만 재현된다」면 그 전제 자체를 사양으로 정리

하는 편이 나중에 덜 고생합니다.

5. ActiveX / OCX 고유로 막히는 일

5.1 디자인 타임 라이선스와 런타임 라이선스가 다릅니다

OCX / ActiveX에서 은근히 골치인 것이 라이선스입니다.

특히 오래된 ActiveX 컨트롤은 design-time licenserun-time license가 나뉘어 있는 경우가 있습니다. MFC ActiveX 문서에도 라이선스 파일이나 라이선스 키로 design-time / run-time을 나누는 구조가 설명됩니다.2425

이 세계에서는

  • 런타임에서는 쓸 수 있다
  • 그런데 폼에 붙이려 하면 「라이선스가 없다」
  • 개발 PC A에서는 붙는다
  • 개발 PC B에서는 안 붙는다

는 일이 생깁니다.

「이 구성 요소의 라이선스 정보를 찾을 수 없습니다」「이 기능을 디자인 환경에서 사용할 적절한 라이선스가 없습니다」 같은 오류는 옛 ActiveX에서 드물지 않습니다.26

5.2 WinForms에 올리는 시점에 이미 wrapper가 들어 있습니다

WinForms에서 ActiveX를 쓸 때 Windows Forms는 ActiveX를 그대로 호스트하지 않습니다. Microsoft Learn의 Aximp.exe 문서대로, ActiveX Control Importer는 COM 타입 라이브러리에서 WinForms용 wrapper를 생성하고 그것을 AxHost 기반 컨트롤로 다룹니다.2728

즉 문제는 한 층이 아니라

  1. 원래 OCX / ActiveX 본체
  2. 타입 라이브러리
  3. 생성된 interop / wrapper
  4. WinForms Designer / runtime

의 여러 층으로 갈립니다.

그래서

  • 벤더 OCX를 바꾸니 이벤트 시그니처가 바뀌었다
  • 참조를 다시 붙이니 wrapper가 재생성되어 차이가 대량으로 났다
  • 개발 PC마다 interop 생성 결과가 미묘하게 다르다

는 일이 생깁니다.

Choose Toolbox Items에 보인다고 안심이 아닙니다. Designer가 올릴 수 있는지, 런타임에 이벤트가 오는지, 배포처에서 wrapper까지 성립하는지는 따로 보는 편이 안전합니다.

5.3 STA / MTA와 메시지 루프를 얕보면 멈춥니다

먼저 용어를 세 가지만 정의합니다.

용어 의미
apartment(아파트먼트) COM 객체와 스레드를 같은 동시 실행 규칙으로 묶은 논리 그룹입니다. 한 객체는 한 apartment에만 속합니다
STA (single-threaded apartment) 스레드 하나만 속하는 apartment입니다. 그 객체로의 호출은 반드시 그 한 스레드에서 실행됩니다
marshaling apartment를 건너 인터페이스 포인터를 넘길 때 COM이 호출을 중계하는 구조입니다. proxy와 stub을 거칩니다

COM은 사용하는 스레드마다 CoInitializeEx로 초기화해야 합니다. Microsoft Learn에도 COM을 쓰는 각 스레드는 개별적으로 CoInitializeEx를 호출해야 한다고 명시합니다.29

또한 STA(single-threaded apartment)에서는 메시지 루프가 필요합니다.2930

메시지 루프가 없으면 왜 멈추는가

결론만 외우면 응용이 안 되므로 구조를 한 단계만 열어 둡니다.

COM은 STA마다 OleMainThreadWndClass라는 창 클래스의 숨은 창을 하나 만듭니다. 다른 apartment에서 그 객체를 호출하면 직접 함수 호출이 되지 않고, 이 숨은 창 앞으로 가는 창 메시지로 도착합니다. STA 스레드가 메시지를 꺼내 dispatch하면 그 창 프로시저가 대응하는 인터페이스 메서드를 호출합니다.30

즉 메시지 루프는 「있으면 좋은 습관」이 아니라 호출을 배달하는 구조 그 자체입니다.

OCX / ActiveXSTA 스레드숨은 창의 메시지 큐다른 스레드의 호출 측OCX / ActiveXSTA 스레드숨은 창의 메시지 큐다른 스레드의 호출 측메시지 루프가 멈춰 있으면이 꺼냄이 일어나지 않아 호출 측은 계속 기다린다메서드 호출이 메시지로 쌓인다메시지 루프가 꺼낸다창 프로시저가 대응하는 메서드를 호출한다반환값이 돌아온다

그림 2: 다른 스레드에서의 호출은 숨은 창 메시지로 도착하고, 메시지 루프가 꺼낸 뒤에야 객체로 넘어갑니다.

그래서 STA 스레드를 막으면 멈춥니다. UI 스레드에서 Task.Wait()Task.Result를 쓰거나 WaitOne으로 기다리는 식의 코드는 메시지 꺼냄을 멈추므로, COM 콜백과 apartment 사이 호출이 배달되지 않아 데드락이 됩니다.30

같은 이유로 STA 객체의 인터페이스 포인터를 다른 스레드에 그대로 복사하면 안 됩니다. 복사하면 원래 STA 스레드로 배달되어야 할 호출이 다른 스레드에서 직접 실행되어, 객체 쪽이 가정하지 않은 동시 실행이 일어납니다. apartment를 건너갈 때는 CoMarshalInterThreadInterfaceInStreamCoGetInterfaceAndReleaseStream으로 marshaling합니다.30

특히 UI 계열 OCX / ActiveX는 STA 전제가 많아서

  • UI 스레드에서는 된다
  • Task.Run이나 ThreadPool에 던지면 멈춘다
  • 이벤트가 돌아오지 않는다
  • 가끔만 재현된다

는, 다루기 싫은 장애가 됩니다.

STA에서는 한 가지 더, 인터페이스 포인터를 그대로 다른 스레드에 복사하면 안 되고 필요하면 marshaling한다는 전제가 있습니다.2930

이런 장애는 0x80040154처럼 친절하지 않고 멈춘다·안 돌아온다·가끔 죽는다로만 보이므로, 레지스트리 계열만큼 시간을 먹습니다.

6. 현장에서 통하는 원인 분리 순서

실무에서는 처음부터 깊게 들어가기보다 이 순서로 가르는 편이 빠릅니다.

6.1 먼저 「어느 프로세스가 몇 bit인가」를 고정합니다

맨 먼저 볼 것은 여기입니다.

  • 호스트는 무엇인가(Visual Studio Designer / 자체 앱 / Office / Access / Explorer / 브라우저 호환 환경)
  • 그 호스트는 32bit인가 64bit인가
  • 대상 DLL / OCX는 32bit인가 64bit인가
  • 그것은 in-proc인가 out-of-proc인가

이 네 점이 정해지지 않은 채로 레지스트리를 보기 시작하면 어느 view를 봐야 할지 안 정해져서 조사가 겉돕니다. 먼저 여기를 고정합니다.

6.2 이어서 「등록 종류」를 확인합니다

다음에 볼 것은 무엇을 무엇으로 등록하는 것이 맞는가입니다.

  • 네이티브 DLL / OCX → regsvr32
  • .NET Framework COM 공개 → Regasm.exe
  • .NET 5+ / 6+ / 8+ COM 공개 → .comhost.dll
  • 애초에 self-registration이 없는 DLL → regsvr32가 아닙니다

이 분류만으로도 잘못된 시도를 꽤 막을 수 있습니다.

6.3 그다음에 「어디에 등록됐는가」를 봅니다

볼 곳은 HKCR만으로는 부족합니다.

  • HKCU\Software\Classes
  • HKLM\Software\Classes
  • 필요하면 32bit / 64bit registry view
  • 대상 ProgID / CLSID / TypeLib
  • InprocServer32 / LocalServer32
  • ThreadingModel
  • 참조 DLL의 실제 경로

HKCR에는 있다만으로는 부족하고, 누가, 어느 bitness로, 어느 view를 보는지까지 맞춰야 의미가 있습니다.711

봐야 할 키 경로

HKEY_CLASSES_ROOT\CLSID\{...}를 열면 지금 실행 중인 도구의 bitness와 사용자에게 보이는 것만 나옵니다. 원인 분리에서는 병합 전 실체를 네 곳으로 나눠 봅니다. CLSID 아래는 WOW64 리다이렉트 대상이고, 32bit 쪽 물리적 위치는 Classes 아래 Wow6432Node입니다.19

보고 싶은 것 레지스트리 경로
머신 전체 / 64bit view HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{CLSID}
머신 전체 / 32bit view HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Wow6432Node\CLSID\{CLSID}
그 사용자만 / 64bit view HKEY_CURRENT_USER\SOFTWARE\Classes\CLSID\{CLSID}
그 사용자만 / 32bit view HKEY_CURRENT_USER\SOFTWARE\Classes\Wow6432Node\CLSID\{CLSID}
ProgID에서 CLSID를 조회 HKEY_CLASSES_ROOT\{ProgID}\CLSID의 기본값

Windows 10 / 11의 regedit.exe는 64bit 프로세스이므로 위 경로를 그대로 입력하면 양쪽 view를 직접 열 수 있습니다. 주소 표시줄에 경로를 붙여 넣으면 그 위치로 이동합니다.

참고로 Wow6432Node를 포함한 경로는 물리적 위치이며, Microsoft는 애플리케이션 코드에서 직접 건드리지 말라고 안내합니다. 수동 조사에서 보는 것은 문제 없지만, 스크립트나 앱에서는 뒤에 나오는 view 지정을 쓰는 편이 안전합니다.11

명령으로 확인하기

reg.exe라면 /reg:32/reg:64로 view를 명시할 수 있습니다.

reg query "HKLM\SOFTWARE\Classes\CLSID\{00000000-0000-0000-0000-000000000000}\InprocServer32" /reg:32
reg query "HKLM\SOFTWARE\Classes\CLSID\{00000000-0000-0000-0000-000000000000}\InprocServer32" /reg:64

PowerShell에서 네 곳을 한 번에 보려면 RegistryKey.OpenBaseKey에 view를 넘깁니다. PowerShell 자체의 bitness에 결과가 끌려가지 않으므로 원인 분리에서는 이쪽이 확실합니다.

# 조사할 ProgID를 한곳에서 지정한다
$progId = 'Vendor.Control.1'

# 1) ProgID에서 CLSID를 조회한다 (HKCR은 HKLM과 HKCU의 병합 view)
$clsid = (Get-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\$progId\CLSID" -Name '(default)' -ErrorAction Stop).'(default)'
"ProgID $progId -> CLSID $clsid"

# 2) 하이브 2종 x view 2종 총 4곳을 모두 확인한다
foreach ($hive in [Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryHive]::CurrentUser) {
    foreach ($view in [Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32) {
        $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, $view)
        $key  = $base.OpenSubKey("SOFTWARE\Classes\CLSID\$clsid\InprocServer32")
        if ($null -eq $key) {
            '{0,-12} {1,-11} : 등록 없음' -f $hive, $view
        }
        else {
            '{0,-12} {1,-11} : {2} (ThreadingModel={3})' -f $hive, $view, $key.GetValue(''), $key.GetValue('ThreadingModel')
            $key.Dispose()
        }
        $base.Dispose()
    }
}

이 네 줄 결과가 그대로 원인 분리의 답이 됩니다.

  • 네 곳 모두 「등록 없음」 → 정말로 미등록입니다. 등록 수단이 6.2 분류와 맞는지 다시 봅니다.
  • Registry32에만 나온다 → 32bit 쪽에만 등록되어 있습니다. 64bit 프로세스에서는 0x80040154가 됩니다.
  • CurrentUser에만 나온다 → 그 사용자에게만 보입니다. 다른 사용자, 서비스 계정, 권한이 오른 프로세스에서는 보이지 않습니다.
  • 경로는 나오는데 동작하지 않는다 → InprocServer32 기본값이 가리키는 파일이 실제로 있는지, 그 파일 bitness가 호스트와 일치하는지 확인합니다.

6.4 마지막으로 「권한에 가려져 있지 않은가」를 봅니다

마지막으로, 문제가 정말 권한인지, 아니면 권한 때문에 다른 등록이 보일 뿐인지를 확인합니다.

  • 표준 사용자 / 관리자에서 동작이 바뀌는가
  • Visual Studio를 elevate하면 무엇이 바뀌는가
  • 서비스 계정이나 다른 사용자에서도 재현되는가
  • installer를 거친 클린 환경에서도 성립하는가

개발 PC 한 대만 보면 여기를 정말 오해합니다.

7. 먼저 정해 두면 사고가 줄어드는 운영

COM / ActiveX / OCX 개발과 유지보수에서는 구현 기법보다 운영을 어떻게 정하는지가 먹히는 경우가 있습니다.

7.1 먼저 bitness 방침을 정합니다

맨 먼저 이것을 정합니다.

  • x86 고정으로 갈 것인가
  • x64를 기준으로 할 것인가
  • 양쪽을 지원할 것인가
  • 그 부품은 in-proc이어야 하는가

특히 벤더 OCX가 x86 고정이면, 그곳을 무시한 채 앱만 x64로 바꿔도 나중에 막힙니다. 이 주제는 더 큰 구성 이야기로 32bit 앱에서 64bit DLL을 호출하는 COM 브리지 실례도 관련됩니다.

7.2 등록 전략을 정합니다

등록도 그때그때 하지 않는 편이 낫습니다.

  • 머신 전체에서 쓴다 → installer로 per-machine 등록
  • 그 사용자만 쓴다 → per-user를 의도적으로 쓴다
  • 자기 앱 안에서만 닫는다 → registration-free COM을 검토
  • 개발용으로만 필요하다 → 명시적인 dev setup script에 가둔다

registration-free COM은 레지스트리가 아니라 manifest로 활성화 정보를 둘 수 있어 등록 지옥을 줄이는 수단으로 유효합니다. Win32 쪽 registration-free COM도 .NET 쪽 RegFree COM도 공식 설명이 있습니다.31632

구조로는 COM이 CoCreateInstance 등을 처리할 때 먼저 활성화 컨텍스트를 찾고, 거기에 정보가 없으면 레지스트리를 보는 순서입니다. 매니페스트는 그 활성화 컨텍스트의 내용을 선언하는 파일입니다.31

필요한 매니페스트는 두 개이고, 둘 다 exe와 같은 폴더에 둡니다.

첫 번째는 컴포넌트 쪽 어셈블리 매니페스트(VendorCtl.manifest)입니다. 어느 파일이 어느 CLSID를 제공하는지를 적습니다.33

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity type="win32"
                    name="MyCompany.VendorCtl"
                    version="1.0.0.0"
                    processorArchitecture="x86" />
  <file name="vendor.ocx">
    <comClass description="Vendor Control"
              clsid="{00000000-0000-0000-0000-000000000000}"
              threadingModel="Apartment"
              progid="Vendor.Control.1" />
  </file>
</assembly>

두 번째는 앱 쪽 애플리케이션 매니페스트(MyApp.exe.manifest)입니다. 위 어셈블리에 의존한다는 것만 적습니다.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity type="win32"
                    name="MyCompany.MyApp"
                    version="1.0.0.0"
                    processorArchitecture="x86" />
  <dependency>
    <dependentAssembly>
      <assemblyIdentity type="win32"
                        name="MyCompany.VendorCtl"
                        version="1.0.0.0"
                        processorArchitecture="x86" />
    </dependentAssembly>
  </dependency>
</assembly>

쓸 때 틀리기 쉬운 점은 다음과 같습니다.33

  • 의존 측 assemblyIdentity는 컴포넌트 측 assemblyIdentity와 완전히 일치해야 합니다. name version processorArchitecture 중 하나라도 어긋나면 해석되지 않습니다.
  • 어셈블리 이름과 DLL 이름은 따로 둡니다. 매니페스트를 별도 파일로 둘 때는 어셈블리 이름과 매니페스트 이름을 DLL 이름과 다른 이름으로 해야 합니다.
  • 요소 이름과 특성 이름은 대소문자를 구분합니다. comClassComClass로 쓰면 읽히지 않습니다.
  • processorArchitecture는 bitness 그 자체입니다. x86 OCX에 대해 amd64 앱에서는 해석되지 않습니다. 7.1의 bitness 방침과 반드시 맞춥니다.
  • OCX를 붙여 넣을 때는 miscStatus 계열 특성이 필요할 수 있습니다. 레지스트리 MiscStatus 키에 해당하는 정보를 매니페스트 쪽에 miscStatus / miscStatusContent 등으로 옮겨 적어야 합니다.

7.3 생성물을 source control 밖에만 두지 않습니다

OCX / ActiveX 계열에서 흔한 것이 의존물 유실입니다.

  • OCX 본체
  • 의존 DLL
  • TLB
  • .lic
  • interop DLL
  • AxHost wrapper
  • 등록 스크립트
  • 샘플 호스트

이런 것이 사람 로컬에만 있으면 몇 달 뒤 반드시 사고가 납니다.

최소한

  • 어느 버전을 전제로 하는가
  • 무엇을 어느 순서로 넣는가
  • 어느 명령으로 등록하는가
  • x86 / x64 중 어느 쪽용인가

는 코드와 같은 장소에 남기는 편이 안전합니다.

8. 이런 상담과 궁합이 좋습니다

이 주제는 전면 손질에 들어가기 전의 원인 분리방침 정리만으로도 가치가 나오기 쉽습니다.

예를 들어 아래 같은 상담과 특히 궁합이 좋습니다.

  • 0x800401540x80070005 원인을 bitness / 등록 / 권한으로 나눠 정리하고 싶다
  • Visual Studio 2022로 올렸더니 Designer가 깨져서 어디까지 살릴 수 있는지 보고 싶다
  • 벤더 OCX는 남긴 채 주변을 .NET이나 C#으로 옮기고 싶다
  • x86 고정 자산을 어디까지 수명을 연장하고 어디서부터 bridge / wrap / replace할지 정하고 싶다
  • 수작업 regsvr32 의존을 그만두고 install / deploy를 다시 설계하고 싶다

바꾸기·감싸기·남기기의 판단은 ActiveX / OCX를 지금 어떻게 다룰까 - 남기기·감싸기·교체 판단표도 참고가 될 것입니다.

9. 정리

COM 컴포넌트나 OCX / ActiveX 개발에서 막힐 때 원인은 대체로 이 넷에 닿습니다.

  1. bitness가 맞지 않는다
  2. 등록 방법이 잘못됐다
  3. 등록 범위(HKCU / HKLM, 32bit / 64bit view)가 어긋났다
  4. 권한 때문에 우연히 보이는 상태를 정상이라고 생각한다

Visual Studio 2022의 64bit화로, 예전에 「그럭저럭 되던」 설계가 꽤 표면화되기 쉬워졌습니다.12 그래서 COM / OCX / ActiveX를 다룰 때는 먼저 코드를 쓰기 전에 환경 전제를 맞추는 것이 지름길입니다.

regsvr32를 몇 번 실행하는가보다

  • 어느 프로세스가 호스트하는가
  • 그 프로세스는 몇 bit인가
  • 어디에 등록해야 하는가
  • 그 등록은 정말 관리자 전제인가
  • Designer와 runtime을 나눠 보고 있는가

를 정리하는 편이 훨씬 빨리 풀립니다.


참고

  1. Microsoft Learn, Visual Studio 2022 version 17.0 Release Notesdevenv.exe is now 64-bit only 2 3

  2. Microsoft Learn, Troubleshoot 32-bit problems - Windows Forms — Visual Studio 2022는 64bit 프로세스이며 32bit .NET / COM / ActiveX를 직접 로드하지 못한다는 점, out-of-process designer 제약.  2 3 4 5

  3. Microsoft Learn, Classes and Servers — COM 등록, HKCU / HKCR, self-registration과 DllRegisterServer 2 3 4

  4. Microsoft Learn, regsvr32regsvr32 구문과 역할.  2

  5. Microsoft Learn, COM에 어셈블리 등록 — .NET Framework COM 등록은 Regasm.exe 2

  6. Microsoft Learn, COM에 .NET Core 구성 요소 공개EnableComHosting, 생성되는 .comhost.dll, regsvr32, EnableRegFreeCom 2 3

  7. Microsoft Learn, Merged View of HKEY_CLASSES_ROOT — HKCR은 HKLM과 HKCU의 merged view.  2 3 4 5

  8. Microsoft Learn, HKEY_CLASSES_ROOT Key — 관리자 권한이 필요한 앱은 per-machine COM 구성으로의 등록을 권장.  2

  9. Microsoft Learn, COM Error Codes (Generic) (Winerror.h)REGDB_E_CLASSNOTREG (0x80040154) 등. 

  10. Microsoft Learn, You receive 0x80070005 error when you try to register a DLL by using Regsvr32.exe — 권한 부족으로 DLL 등록이 실패하는 전형 예.  2

  11. Microsoft Learn, Registry Redirector — WOW64의 32bit / 64bit 레지스트리 view, HKLM\Software의 물리적 위치가 Wow6432Node인 점, 앱에서 물리 경로를 직접 건드리지 말 것.  2 3 4 5

  12. Microsoft Learn, File System Redirector — x64 Windows의 %windir%\System32와 WOW64 파일 시스템 리다이렉트. 

  13. Microsoft Learn, 64비트 버전 Windows에서 32비트 프로그램 호환성 고려 사항 개요 — WOW64에 의한 파일 / 레지스트리 리다이렉트. 

  14. Microsoft Learn, Regasm.exe (Assembly Registration Tool)Regasm.exe 역할과 /tlb 등 옵션.  2 3

  15. Microsoft Learn, Packaging a .NET Framework Assembly for COM — 타입 라이브러리와 Regasm.exe /tlb

  16. Microsoft Learn, Registering the DLL Server for Surrogate Activation — surrogate에 올라가는 조건, LocalServer / LocalServer32 / LocalService가 있으면 EXE 서버나 서비스 시작이 우선되는 점, AppID를 CLSID와 같은 GUID로 두는 구성.  2 3 4

  17. Microsoft Learn, DllSurrogateAppID 아래 DllSurrogateREG_SZ이며, 빈 문자열이면 시스템 기본 surrogate, 경로를 적으면 그 경로의 사용자 지정 surrogate가 쓰이는 점.  2

  18. Microsoft Learn, Using the system-supplied surrogate — 빈 문자열 또는 NULL을 지정하면 시스템 기본 surrogate가 시작되는 점, surrogate 안 스레드 모델과 프로세스 수명 취급.  2

  19. Microsoft Learn, Registry Keys Affected by WOW64HKLM\SOFTWARE\Classes\CLSIDHKCU\SOFTWARE\Classes\CLSID는 리다이렉트 대상인 점, HKLM\SOFTWARE\Classes\AppID는 Windows 7 / Windows Server 2008 R2 이후 공유(shared)인 점, HKCR이 둘의 병합 view인 점.  2 3

  20. Microsoft Learn, CLSCTX enumeration — 여러 CLSCTX를 OR로 넘기면 열거 순서로 시도되는 점, in-proc 단계에서는 InprocServer32 키가 있으면 그것이 쓰이는 점, 클라이언트도 서버도 bitness 희망을 내지 않으면 클라이언트에 맞는 bitness 서버가 선택되고 없으면 다른 쪽이 시작되는 점.  2 3

  21. Microsoft Learn, Understanding Custom Build Steps and Build Events — post-build event에서 regsvr32.exe를 쓰는 예. 

  22. Microsoft Learn, 고급 사용자를 위한 Windows 레지스트리HKCU\Software\ClassesHKLM\Software\Classes, HKCR 동작. 

  23. Microsoft Learn, Find, install, and manage extensions for Visual Studio — elevated 실행 시 per-user extension 취급. 

  24. Microsoft Learn, MFC ActiveX Controls: Licensing an ActiveX Control — design-time / run-time license, .LIC

  25. Microsoft Learn, Application Settings, MFC ActiveX Control Wizard — 런타임 라이선스 생성과 .lic 파일. 

  26. Microsoft Learn, License information for this component not found. You don’t have an appropriate license to use this functionality in the design environment. 

  27. Microsoft Learn, Aximp.exe (Windows Forms ActiveX Control Importer) — ActiveX를 WinForms용 wrapper로 변환. 

  28. Microsoft Learn, AxHost Class — ActiveX Control Importer가 생성하는 AxHost 기반 wrapper. 

  29. Microsoft Learn, COM 라이브러리 초기화CoInitializeEx, 스레드별 초기화, STA 메시지 루프.  2 3

  30. Microsoft Learn, Single-Threaded 아파트먼트 — STA 메시지 루프, marshaling, ThreadingModel 2 3 4 5

  31. Microsoft Learn, Registration-Free COM 객체 만들기 — activation context에 의한 등록 불필요 COM.  2

  32. Microsoft Learn, 등록이 필요 없는 COM 상호 운용 기능 — .NET Framework의 registration-free COM interop. 

  33. Microsoft Learn, Assembly Manifestsassembly / assemblyIdentity / dependency / file / comClass 각 특성, REF 측과 DEF 측 assemblyIdentity가 일치해야 하는 점, 이름 대소문자 취급, miscStatus 계열 특성.  2

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

ActiveX 이관

COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

0x80040154(Class not registered)는 어떻게 조사하면 되나요?
공식 명칭은 REGDB_E_CLASSNOTREG이고, 말 그대로 「클래스가 등록되어 있지 않다」는 오류입니다. 다만 완전히 미등록인 경우만은 아닙니다. 다른 bitness 쪽 레지스트리 view에만 등록되어 있거나, 해당 사용자에게만 보이는 HKCU 쪽에만 등록되어 있어도 발생합니다. 먼저 호스트 프로세스가 32bit인지 64bit인지 고정하고, 이어서 등록 수단이 맞는지, 그리고 HKCU / HKLM과 32bit / 64bit view 중 어디에 등록됐는지를 순서대로 확인하는 편이 빠릅니다.
regsvr32로 등록해도 동작하지 않는 이유는 무엇인가요?
regsvr32는 DllRegisterServer를 export하는 네이티브 in-proc COM 서버(DLL / OCX)용이며, 무엇이든 등록하는 만능 명령이 아닙니다. .NET Framework 어셈블리를 COM에 공개할 때는 Regasm.exe를 쓰고, .NET 5 이후에는 EnableComHosting으로 생성되는 .comhost.dll을 regsvr32로 등록하는 흐름입니다. 또한 x64 Windows에서는 64bit용은 System32, 32bit용은 SysWOW64의 regsvr32를 구분해야 하며, 잘못된 쪽에 등록하면 성공했는데도 목적 프로세스에서는 보이지 않는 상태가 됩니다.
Visual Studio 2022에서 Designer만 깨지는 이유는 무엇인가요?
Visual Studio 2022의 devenv.exe는 64bit 프로세스라서 32bit COM / ActiveX 컴포넌트를 직접 로드하지 못하기 때문입니다. 런타임 앱은 x86으로 돌아가더라도 Designer는 Visual Studio 쪽 64bit 프로세스에서 동작하므로, 런타임은 살아 있는데 Designer만 죽는 어긋남이 생깁니다. AnyCPU로 바꿔도 참조 대상이 32bit 고정 COM / ActiveX에 의존하면 문제는 남습니다.
관리자로 실행하면 되는데 일반 권한에서는 안 되는 이유는 무엇인가요?
본질은 권한 자체보다 등록 범위나 설치 설계가 어긋난 경우가 많습니다. COM은 HKCU\Software\Classes를 먼저 보고, HKEY_CLASSES_ROOT는 HKLM과 HKCU의 merged view입니다. 개발자 A 사용자로 수동 등록하면 A 계정에서는 되는데 다른 사용자나 서비스 계정에서는 안 되는 일이 흔합니다. 개발용 per-user 등록과 설치 프로그램이 수행하는 운영 per-machine 등록을 구분하고, 빌드와 등록을 분리하는 것이 기본입니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기