애플리케이션 실행 중 DLL 파일을 동적으로 로드하거나 언로드할 수 있는 플러그인 아키텍처는 유연한 확장성을 제공합니다. 이 방식은 새로운 기능을 추가하거나 기존 기능을 수정할 때 전체 애플리케이션을 재시작하지 않아도 된다는 장점이 있습니다. 이 글에서는 C# WebAPI 프로젝트를 기준으로 플러그인 DLL을 동적으로 로딩하는 방법을 단계별로 설명합니다.
1. 인터페이스 정의
먼저 공유 계약(contract) 역할을 할 플러그인 인터페이스를 정의합니다. 모든 플러그인 DLL은 이 인터페이스를 구현해야 합니다.
public interface IPluginModule
{
string ModuleName { get; } // 플러그인 이름
string Execute(string operation, object parameters);
string Save(object input);
string Query(object filter);
}
2. DLL 로더 유틸리티 클래스 구현
다음으로 동적 로딩과 언로딩을 담당하는 유틸리티 클래스를 작성합니다. AssemblyLoadContext를 사용하여 각 어셈블리를 격리된 컨텍스트에서 로드하고 필요 시 해제할 수 있습니다.
using System.Reflection;
using System.Runtime.Loader;
public static class PluginLoader
{
private static List<IPluginModule> _loadedPlugins = new List<IPluginModule>();
private static readonly string PluginDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
/// <summary>
/// Plugins 폴더 내 모든 DLL을 스캔하여 플러그인 인스턴스 생성
/// </summary>
public static void LoadAllPlugins()
{
if (!Directory.Exists(PluginDirectory))
{
Directory.CreateDirectory(PluginDirectory);
Console.WriteLine("Plugins 폴더가 없어 새로 생성했습니다.");
return;
}
_loadedPlugins.Clear();
foreach (string dllFile in Directory.GetFiles(PluginDirectory, "*.dll"))
{
try
{
// 각 DLL마다 별도의 AssemblyLoadContext 생성 (언로드 가능)
var loadContext = new AssemblyLoadContext(Path.GetFileNameWithoutExtension(dllFile), isCollectible: true);
Assembly assembly = loadContext.LoadFromAssemblyPath(dllFile);
// IPluginModule 인터페이스를 구현한 콘크리트 클래스 찾기
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IPluginModule).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
foreach (var type in pluginTypes)
{
if (Activator.CreateInstance(type) is IPluginModule pluginInstance)
{
_loadedPlugins.Add(pluginInstance);
Console.WriteLine($"플러그인 로드 성공: {pluginInstance.ModuleName}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"DLL 로드 실패: {dllFile}, 오류: {ex.Message}");
}
}
}
/// <summary>
/// 특정 이름의 플러그인을 언로드하고 리스트에서 제거
/// </summary>
public static void UnloadPlugin(string moduleName)
{
var plugin = _loadedPlugins.FirstOrDefault(p => p.ModuleName == moduleName);
if (plugin != null)
{
_loadedPlugins.Remove(plugin);
// 참고: 실제 어셈블리 언로드는 GC가 수집할 때 이루어지며,
// 필요 시 AssemblyLoadContext.Unload()를 호출할 수 있음
Console.WriteLine($"플러그인 언로드: {moduleName}");
}
}
/// <summary>
/// 로드된 모든 플러그인 목록 반환
/// </summary>
public static List<IPluginModule> GetPlugins() => _loadedPlugins;
}
3. 플러그인 DLL 프로젝트 생성
별도의 클래스 라이브러리 프로젝트를 만들고, 위에서 정의한 IPluginModule 인터페이스를 포함하는 어셈블리를 참조합니다. 그런 다음 인터페이스를 구현하는 클래스를 작성하고 빌드합니다.
public class SamplePlugin : IPluginModule
{
public string ModuleName => "SamplePlugin";
public string Execute(string operation, object parameters)
{
// 실제 비즈니스 로직 구현
return $"SamplePlugin executed with operation '{operation}'";
}
public string Save(object input)
{
return "Data saved by SamplePlugin";
}
public string Query(object filter)
{
return "Data queried by SamplePlugin";
}
}
4. 플러그인 배치 및 초기 로드
빌드하여 생성된 SamplePlugin.dll (및 해당 종속성)을 메인 WebAPI 프로젝트의 Plugins 폴더에 복사합니다. 애플리케이션 시작 시 PluginLoader.LoadAllPlugins()를 호출합니다.
// Program.cs 또는 Startup.cs
var app = builder.Build();
PluginLoader.LoadAllPlugins(); // 모든 DLL 로드
// ... 나머지 설정
5. WebAPI에서 플러그인 활용
API 컨트롤러에서 PluginLoader를 통해 로드된 플러그인 인스턴스를 가져와 동적으로 메서드를 호출할 수 있습니다.
[ApiController]
[Route("api/plugins/{pluginName}")]
public class PluginController : ControllerBase
{
[HttpPost("execute")]
public IActionResult ExecutePlugin(string pluginName, [FromBody] object parameters)
{
var plugin = PluginLoader.GetPlugins().FirstOrDefault(p => p.ModuleName == pluginName);
if (plugin == null)
return NotFound($"Plugin '{pluginName}' not found.");
var result = plugin.Execute("dynamicCall", parameters);
return Ok(new { Result = result });
}
[HttpPost("save")]
public IActionResult SavePlugin(string pluginName, [FromBody] object data)
{
var plugin = PluginLoader.GetPlugins().FirstOrDefault(p => p.ModuleName == pluginName);
if (plugin == null)
return NotFound($"Plugin '{pluginName}' not found.");
var result = plugin.Save(data);
return Ok(new { Result = result });
}
[HttpGet("reload")]
public IActionResult ReloadPlugins()
{
PluginLoader.LoadAllPlugins();
return Ok("Plugins reloaded.");
}
}
이 구조를 사용하면 런타임 중에도 /api/plugins/reload를 호출하여 새로운 DLL을 다시 로드하거나, 특정 이름의 플러그인을 언로드하는 로직을 추가할 수 있습니다. 각 플러그인은 독립적인 AssemblyLoadContext를 가지므로 필요 시 완전히 해제될 수 있습니다(단, 모든 참조가 제거되어야 함).