.NET 8의 DLL을 형 있게 VBA에서 쓰는 방법 - COM 공개 + dscom으로 TLB를 생성

· · C#, .NET 8, VBA, COM, Office, dscom

VBA에서 .NET 8의 처리를 호출하고 싶은 장면은 아직 평범하게 있습니다. 특히 Excel이나 Access의 기존 자산은 그대로 남기면서, 무거운 처리, 문자열 처리, HTTP, 암호화, 업무 로직 같은 부분만 C#으로 빼내고 싶을 때입니다.

다만 CreateObject로 지연 바인딩으로 치우치면 VBA 쪽에서는 Object투성이가 됩니다. IntelliSense는 약해지고, 메서드 이름의 오타는 실행 시까지 발견되지 않으며, 점점 문자열 의존의 진흙탕에 빠집니다.

그래서 이번에는 .NET 8의 DLL을 COM 공개하고, dscom으로 타입 라이브러리(TLB)를 생성하고, VBA에서 조기 바인딩으로 형 있게 이용하는 곳에 좁혀서 정리합니다.

.NET Framework + RegAsm의 옛날이야기, IDL을 손으로 쓰고 MIDL로 굳히는 이야기, Reg-Free COM의 이야기는 이번에는 옆에 둡니다. 여기서는 .NET 8 / COM host / dscom / VBA early binding의 외길만 다룹니다.

1. 먼저 결론

먼저 결론만 나열하면 흐름은 이렇습니다.

  • .NET 8의 클래스 라이브러리를 EnableComHosting=true로 빌드
  • COM에 보일 명시적인 인터페이스클래스를 만든다
  • 클래스는 ClassInterfaceType.None으로 하고, AutoDual로 도망치지 않는다
  • VBA에서 쓸 인터페이스는 InterfaceIsDual로 한다
  • 빌드 후 생긴 *.dll에서 dscom tlbexport*.tlb를 만든다
  • regsvr32*.comhost.dll을 등록한다
  • dscom tlbregister*.tlb를 등록한다
  • VBA에서 참조 설정을 추가하고, Dim x As 라이브러리명.IYourInterface처럼 형 있게 쓴다

요컨대 COM의 입구는 .NET SDK가 만드는 *.comhost.dll, 형 정보는 dscom이 만드는 *.tlb, VBA는 그 TLB를 보고 조기 바인딩한다는 구성입니다.

2. 이 구성의 전체상

우선 무엇이 무엇의 역할인지를 한 장으로 봅니다.

flowchart LR
    VBA["VBA / Excel / Access"] -->|참조 설정한 TLB로 형 정보 취득| TLB["VbaTypedComSample.tlb"]
    VBA -->|COM 호출| COMHOST["VbaTypedComSample.comhost.dll"]
    COMHOST --> DOTNET["VbaTypedComSample.dll (.NET 8)"]
    DOTNET --> RUNTIME[".NET 8 Runtime"]

각각의 역할은 다음과 같습니다.

파일 역할
VbaTypedComSample.dll .NET 8의 구현 본체
VbaTypedComSample.comhost.dll COM에서 호출되는 입구
VbaTypedComSample.tlb VBA가 보는 형 정보
VbaTypedComSample.deps.json 의존 관계의 해결 정보
VbaTypedComSample.runtimeconfig.json .NET 런타임 기동 정보

여기서 중요한 것은 VBA가 형을 알기 위해 필요한 것은 TLB이고, COM의 기동 입구로서 필요한 것은 comhost라는 점입니다.

.dll 단체를 건네고 끝이 아닙니다. COM의 세계는 여기가 솔직하지 않습니다.

3. 처음에 정할 것 - 32bit / 64bit를 맞춘다

여기를 빗나가면 꽤 높은 확률로 ActiveX 컴포넌트는 오브젝트를 작성할 수 없습니다. 방면으로 굴러갑니다.

Office / VBA와 COM 서버의 bitness는 맞춰 주세요.

이용 측 .NET 측의 기준 TLB 생성 등록 커맨드
64bit Office x64 / win-x64 dscom C:\Windows\System32\regsvr32.exe
32bit Office(64bit Windows 상) x86 / win-x86 dscom32.exe C:\Windows\SysWOW64\regsvr32.exe

.NET 5+ 이후의 COM host에서는 AnyCPU 그대로 두면 *.comhost.dll이 64bit 쪽으로 치우치기 쉽고, 32bit Office와 맞물리지 않는 경우가 있습니다. 그래서 Office에 맞춰 x86 / x64를 명시하는 편이 안전합니다.

이 글의 코드는 64bit Office용을 예로 합니다. 32bit Office라면 뒤에 나오는 x64x86, win-x64win-x86으로 바꿔 읽어 주세요.

4. .NET 8 쪽을 만든다

여기서는 VBA에서 Add, Divide, Hello를 호출할 수 있는 최소 샘플로 합니다.

4.1 .csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <EnableComHosting>true</EnableComHosting>
    <PlatformTarget>x64</PlatformTarget>
    <NETCoreSdkRuntimeIdentifier>win-x64</NETCoreSdkRuntimeIdentifier>
  </PropertyGroup>
</Project>

포인트는 EnableComHosting입니다. 이것을 붙이면 빌드 시에 VbaTypedComSample.comhost.dll이 생성됩니다.

4.2 어셈블리 전체는 기본으로 COM 비공개로 해 둔다

COM에 보일 형만 ComVisible(true)로 하고 싶으므로, 어셈블리 전체는 false로 해 두는 것이 편합니다.

using System.Runtime.InteropServices;

[assembly: ComVisible(false)]

4.3 공개할 인터페이스와 클래스를 쓴다

using System.Runtime.InteropServices;

namespace VbaTypedComSample;

[ComVisible(true)]
[Guid("2A1BBEDE-DE6E-4C34-AD60-2E9E0E33E999")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICalculator
{
    [DispId(1)]
    int Add(int x, int y);

    [DispId(2)]
    double Divide(double x, double y);

    [DispId(3)]
    string Hello(string name);
}

[ComVisible(true)]
[Guid("FAD1C752-0BB6-4DDD-889F-FE446350847A")]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(ICalculator))]
public class Calculator : ICalculator
{
    public Calculator()
    {
    }

    public int Add(int x, int y) => checked(x + y);

    public double Divide(double x, double y)
    {
        if (y == 0)
        {
            throw new ArgumentOutOfRangeException(nameof(y), "0으로는 나눌 수 없습니다.");
        }

        return x / y;
    }

    public string Hello(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return "Hello";
        }

        return $"Hello, {name}";
    }
}

이 코드에서 잡아 두고 싶은 점은 다음입니다.

  • Guid인터페이스클래스에 따로 부여
  • ClassInterfaceType.None으로 해서 자동 생성 클래스 인터페이스에 의존하지 않는다
  • VBA에서 다루기 쉽도록 InterfaceIsDual로 한다
  • DispId를 붙여 두면 공개 후에 메서드 순서를 건드렸을 때의 사고를 줄이기 쉽다
  • COM에서 New되므로 public한 인수 없는 생성자를 준비한다

5. 빌드한다

Release 빌드합니다.

dotnet build -c Release

빌드 후, 출력 폴더에는 적어도 다음과 같은 파일이 늘어섭니다.

bin/
  Release/
    net8.0-windows/
      VbaTypedComSample.dll
      VbaTypedComSample.comhost.dll
      VbaTypedComSample.deps.json
      VbaTypedComSample.runtimeconfig.json

배포나 등록에 쓰는 것은 이 폴더입니다. 나중에 배치 장소를 바꾸면 등록도 다시 해야 합니다.

6. dscom으로 TLB를 생성한다

6.1 64bit인 경우

우선 dscom을 넣습니다.

dotnet tool install --global dscom

다음으로 빌드한 어셈블리에서 TLB를 생성합니다.

dscom tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb

6.2 32bit Office용의 경우

여기는 조금 덫입니다. 32bit Office용으로 TLB를 만든다면 dscom32.exe를 쓰는 것이 안전합니다.

.\tools\dscom32.exe tlbexport .\bin\Release\net8.0-windows\VbaTypedComSample.dll --out .\bin\Release\net8.0-windows\VbaTypedComSample.tlb

7. COM host와 TLB를 등록한다

여기는 관리자 권한의 커맨드 프롬프트 / PowerShell에서 실행해 주세요.

7.1 64bit Office / 64bit COM의 경우

$out = Resolve-Path .\bin\Release\net8.0-windows

C:\Windows\System32\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
dscom tlbregister "$out\VbaTypedComSample.tlb"

7.2 32bit Office(64bit Windows 상)의 경우

$out = Resolve-Path .\bin\Release\net8.0-windows

C:\Windows\SysWOW64\regsvr32.exe "$out\VbaTypedComSample.comhost.dll"
.\tools\dscom32.exe tlbregister "$out\VbaTypedComSample.tlb"

여기서 하는 것은 2가지입니다.

  • regsvr32*.comhost.dll을 COM 서버로서 등록
  • tlbregister*.tlb를 타입 라이브러리로서 등록

8. VBA에서 참조 설정해서 형 있게 쓴다

  1. Excel 또는 Access를 연다
  2. VBA 에디터를 연다
  3. 도구 -> 참조 설정
  4. 목록에 라이브러리가 나와 있으면 체크를 넣는다
  5. 목록에 안 보이면 참조...에서 VbaTypedComSample.tlb를 고른다
Option Explicit

Public Sub UseCalculator()
    Dim calc As VbaTypedComSample.ICalculator
    Set calc = New VbaTypedComSample.Calculator

    Debug.Print calc.Add(10, 20)
    Debug.Print calc.Divide(10, 4)
    Debug.Print calc.Hello("VBA")
End Sub

이것으로 VBA 쪽에서는 다음 혜택이 있습니다.

  • IntelliSense가 듣는다
  • 메서드 이름의 typo가 실행 전에 발견되기 쉽다
  • Object Browser로 공개 API를 확인할 수 있다
  • Object 베타쓰기보다 읽기 쉽다

8.1 예외는 VBA 쪽에서는 COM 에러가 된다

예를 들어 Divide(10, 0)처럼 .NET 쪽에서 예외가 던져지면 VBA 쪽에서는 COM 에러로 보입니다.

Option Explicit

Public Sub UseCalculatorWithErrorHandling()
    On Error GoTo EH

    Dim calc As VbaTypedComSample.ICalculator
    Set calc = New VbaTypedComSample.Calculator

    Debug.Print calc.Divide(10, 0)
    Exit Sub

EH:
    Debug.Print Err.Number
    Debug.Print Err.Description
End Sub

9. 배포할 때의 사고방식

배포 시에 중요한 것은 DLL 단체를 배포하는 것이 아니라 출력 일식을 두는 것입니다.

VbaTypedComSample.dll
VbaTypedComSample.comhost.dll
VbaTypedComSample.deps.json
VbaTypedComSample.runtimeconfig.json
VbaTypedComSample.tlb
(필요하다면 의존 DLL 일식)

또한 클라이언트 PC에는 대응하는 .NET 8 런타임이 필요합니다. COM host는 self-contained 배포가 아니라 기본적으로 framework-dependent한 운영이 됩니다.

10. 빠지기 쉬운 곳

10.1 AnyCPU 그대로 방치하지 않는다

VBA / Office의 bitness와 COM host의 bitness가 어긋나면 꽤 불쾌한 실패 방식을 합니다.

  • 64bit Office라면 x64 / win-x64
  • 32bit Office라면 x86 / win-x86

10.2 ClassInterfaceType.AutoDual을 쓰지 않는다

얼핏 편합니다. 하지만 공개 후에 멤버 순서나 구성을 건드리면 망가뜨리기 쉽습니다.

VBA에서 형 있게 안정적으로 쓰고 싶다면 명시 인터페이스를 정의하고, 클래스는 ClassInterfaceType.None으로 해 두는 것이 정석입니다.

10.3 GUID를 경솔하게 재생성하지 않는다

COM에서는 GUID가 계약 그 자체입니다.

  • IID
  • CLSID

를 공개 후에 경솔하게 바꾸면 기존의 VBA 참조나 등록이 망가집니다.

10.4 공개된 인터페이스를 망가뜨리지 않는다

COM은 「나중에 메서드를 1개 더했을 뿐」이어도 평화롭게 끝나지 않는 경우가 있습니다.

  • ICalculator는 남긴다
  • 변경이 크다면 ICalculator2를 새로 만든다
  • 클래스는 양쪽을 구현해도 된다

10.5 형은 수수하게 치우친다

VBA에 보이는 경계에서는 너무 폼을 잡지 않는 편이 안전합니다.

상성이 좋은 것은 우선 이쯤입니다.

  • int
  • double
  • bool
  • string
  • DateTime
  • decimal
  • enum

10.6 Office를 연 채로 갱신하지 않는다

Excel이나 Access가 DLL을 붙잡은 채가 되어, 빌드나 재등록에서 번거로움이 일어날 수 있습니다.

  • Office를 닫는다
  • 필요하다면 등록 해제한다
  • 빌드를 다시 한다
  • 다시 한 번 등록한다

11. 정리

.NET 8의 DLL을 형 있게 VBA에서 쓴다는 이야기를 COM 공개 + dscom으로 TLB 생성에 좁히면, 할 일은 의외로 정리할 수 있습니다.

  • .NET 8 쪽에서는 EnableComHosting=true
  • COM에 보일 명시 인터페이스를 만든다
  • 클래스는 ClassInterfaceType.None
  • VBA용 인터페이스는 InterfaceIsDual
  • dscom tlbexport로 TLB를 만든다
  • regsvr32*.comhost.dll을 등록
  • dscom tlbregister*.tlb를 등록
  • VBA에서 참조 설정을 넣고 조기 바인딩한다

요컨대 .NET 8 시대의 VBA 연계는 COM host와 TLB를 나누어 생각하는 것이 요령입니다.

  • 기동 입구는 *.comhost.dll
  • 형 정보는 *.tlb
  • 구현 본체는 *.dll

12. 참고 자료

관련 기사

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

관련 토픽

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

ActiveX 이관

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

토픽 보기

이 주제와 연결되는 서비스

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

블로그 목록으로 돌아가기