프로그램을 다시 컴파일하지 않고 기능을 끼워 넣거나, 컨트롤을 담은 DLL을 상황에 따라 골라 불러와야 할 때가 있습니다. 플러그인 구조를 만들 때 쓰는 방식이죠. .NET의 리플렉션(System.Reflection)으로 DLL을 실행 중에 읽어서, 그 안의 타입을 찾아 인스턴스를 만들고 메서드를 호출할 수 있습니다.

Imports System.IO

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Dim SRA As System.Reflection.Assembly
        Dim FATabStrip As Object = Nothing
        Dim FATabStripItem As Object = Nothing
        Dim Class1 As Object = Nothing
        Dim mt As System.Reflection.MethodInfo = Nothing
        SRA = System.Reflection.Assembly.LoadFile(Path.Combine(Application.StartupPath, "TabStrip.dll"))
        Dim Types() As Type = SRA.GetExportedTypes()
        For Each T As Type In Types
            Select Case T.Name
                Case "FATabStrip"
                    FATabStrip = Activator.CreateInstance(T)
                Case "FATabStripItem"
                    FATabStripItem = Activator.CreateInstance(T)
                Case "Class1"   '// 동적 함수
                    Class1 = Activator.CreateInstance(T)
                    mt = T.GetMethod("Sum")
            End Select
        Next
        FATabStripItem.ResumeLayout(False)
        FATabStripItem.Title = "test"
        FATabStrip.Items.Add(FATabStripItem)
        FATabStrip.Dock = DockStyle.Fill
        Me.Controls.Add(FATabStrip)
        Dim vReturn As Object = mt.Invoke(Class1, New Object() {299, 300})
        MsgBox(vReturn.ToString())
    End Sub
End Class

흐름

Assembly.LoadFile 로 실행 파일 폴더의 TabStrip.dll 을 읽습니다. GetExportedTypes 로 그 DLL이 공개한 타입(Public 클래스들)을 전부 가져와, 이름으로 분기하면서 필요한 것만 Activator.CreateInstance(T) 로 인스턴스화합니다. Object 로 받는 이유는 컴파일 시점에 그 타입이 참조에 없기 때문입니다. 이렇게 늦게 바인딩(late binding)하면 FATabStripItem.Title = "test" 처럼 점으로 멤버를 쓸 수 있는데, Option Strict On 에서는 오류가 나니 이 코드는 Option Strict Off 를 전제로 합니다.

광고

탭 컨트롤(FATabStrip)과 탭 항목(FATabStripItem)을 만들어 항목을 추가하고, 컨트롤을 폼에 꽉 채워(Dock = Fill) 올립니다. 그리고 Class1 의 Sum 메서드를 MethodInfo.Invoke 로 호출하면서 인자 299, 300을 넘겨 결과를 메시지로 띄웁니다. 여기서 Invoke 의 첫 인자가 Class1(인스턴스)인 점이 중요합니다. 인스턴스 메서드라서 "어느 객체의 Sum이냐"를 넘겨야 합니다.

호출용 DLL 만들기

쓰려면 먼저 함수를 담은 DLL이 있어야 합니다. 클래스 라이브러리 프로젝트를 새로 만들고, 솔루션 탐색기의 My Project에서 루트 네임스페이스를 정합니다. 타입을 찾을 때 루트네임스페이스.클래스이름 형태로 접근하기 때문에 이 이름을 알아 둬야 합니다.

Public Class Class1
    Public Shared Function Test(a As String, b As Integer)
        MsgBox(a + b.ToString)
        Return "테스트"
    End Function
End Class

이 DLL을 실행 파일 쪽에서 부르는 코드는 이렇습니다.

Imports System.Reflection

Dim dllPath As String = "DLL.DLL"
Dim a As Assembly = Assembly.LoadFile(dllPath)
Dim t As Type = a.GetType("루트네임스페이스.Class1")
Dim m As MethodInfo = t.GetMethod("Test")
Dim 리턴값 As String = DirectCast(m.Invoke(Nothing, New Object() {"문자열", 1234}), String)

GetType 에 네임스페이스까지 붙인 전체 이름을 넘겨 타입을 바로 찾고, GetMethod 로 메서드를 얻어 Invoke 합니다. 인자는 New Object() {...} 배열로 순서대로 넘기면 됩니다.

Shared냐 인스턴스냐에 따라 Invoke가 다르다

여기서 헷갈리기 쉬운 부분이 있습니다. 위 Test 는 Public Shared Function 이라 공유 메서드입니다. 공유 메서드는 특정 객체에 속하지 않으니 Invoke 의 첫 인자로 Nothing 을 넘깁니다. 반면 맨 앞 예제의 Sum 은 Shared 가 아닌 인스턴스 메서드라서, Activator.CreateInstance 로 만든 객체(Class1)를 첫 인자로 넘겼습니다. 공유 메서드로 만들 거면 인스턴스를 만들 필요 없이 Nothing 을, 인스턴스 메서드면 만든 객체를 넘긴다 — 이 차이를 섞으면 Invoke 에서 예외가 납니다.

반환 타입도 DirectCast(..., String) 처럼 실제 타입으로 변환해 받으면 이후에 타입 안전하게 쓸 수 있습니다. 반환형이 없거나(Sub) 모를 때는 Object 로 받고요.

알아 둘 점

Assembly.LoadFile 은 상대 경로를 받지 않으니, 위의 "DLL.DLL" 처럼 파일명만 주면 안 되고 절대 경로를 줘야 합니다. 첫 예제처럼 Path.Combine(Application.StartupPath, "...") 로 실행 폴더 기준 전체 경로를 만드는 게 안전합니다. 그리고 LoadFile 로 연 DLL은 프로그램이 돌아가는 동안 파일이 잠겨서, 그 DLL을 교체하거나 지우려면 프로그램을 닫아야 합니다. 플러그인을 갈아 끼우는 구조라면 별도 AppDomain(구버전)이나 AssemblyLoadContext(최신 .NET)로 로드해 언로드할 수 있게 하는 걸 고려해야 합니다.

마지막으로 리플렉션 호출은 이름을 문자열로 다루니 오타를 컴파일 때 못 잡고 실행 중에 터집니다. GetType 이나 GetMethod 가 Nothing 을 돌려줄 수 있으므로, 실제 코드에서는 결과가 Nothing 인지 확인하고 넘어가는 게 좋습니다.