DLL을 JS 처럼 import 단계에서 네트워크로 동적 로딩하기
고남현 @gnh1201@hackers.pub
Windows 앱에서 보이는 "DLL Hell" 문제
Windows (.NET) 기반으로 앱을 작성하다보면 빌드의 결과물로 주 실행파일인 *.exe 외에 수많은 *.dll 파일들이 기다리고 있다.
가령,
Program.exe # 주 실행파일
a.dll # 컴파일된 라이브러리 A
b.dll # 컴파일된 라이브러리 B
c.dll # 컴파일된 라이브러리 B
...
이런식으로 *.exe 파일을 보조하는 다양한 컴파일된 라이브러리(*.dll) 파일들이 생성되고, 배포시에도 이를 포함해야 한다. 이를 지칭하는 용어로 "DLL Hell"이라는 표현도 있다.
이러한 파일들을 하나로 합쳐주는 기존의 방법들(Costura.Fody, ILmerge, ILRepack 등)이 있지만 여기서 소개하는 방법은 이것과는 다르다.
어셈블리를 하나로 합치는건 권장사항이 아니다.
*.exe, *.dll 등 컴파일된 산출물을 Windows 환경에선 "어셈블리"라고 칭한다. 배포의 불편함때문에 이 어셈블리를 하나의 파일로 합치는 매우 다양한 방법이 존재한다.
앞서 언급한 병합 도구를 사용하는 방법도 있지만, 모든 추가 어셈블리를 압축하여 프로젝트 내에 넣어두는 방법 등 다양한 병합 방법이 있을 수 있다.
하지만 이러한 병합 방법은 생태계에서 충분히 합의된 방법을 사용한다기보단 개발자의 임기응변에 많이 의존하고 있기에, 결국 개발자 본인만 아는 병합 방법으로 남아 향후 협업에 걸림돌이 될 여지가 많다.
실제로 Microsoft는 어셈블리를 병합하는 예제를 공식적으로 제공하지 않으며, 이전에 공식 지원하던 ILmerge 도구의 경우 공식 지원까지 중단하였다.
정말 부득이한 상황이 아니라면 어셈블리는 파일 단위로 분리된 상태를 유지하며 배포할 것을 권장한다는 의미이다.
데이터 소스만 바꾸면 어떨까?
나는 어셈블리를 하나의 파일로 합치는 작업은 피하기로 결정하였다. 대신 그에 준하는 방법을 고민해보기로 했는데, 그 고민의 결과로 나온 것이 네트워크에서 동적으로 어셈블리를 받아내도록 하는 것이다.
C# 의 경우 라이브러리를 임포트하기 위해 다음과 같은 구문(using 문법)을 사용한다.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using WelsonJS.Esent;
.NET의 경우 라이브러리 임포트 과정(using 키워드를 해결(resolve)하는 과정) 중간에 개입을 해서 프로그래밍이 가능하다.
나는 추가 어셈블리(*.dll) 발생의 최소화가 요구되는 앱의 using 키워드 해결 과정에 개입하여서 어셈블리를 해결하는 부분에 CDN(여기서는 Azure Blob Storage라는 Object Storage를 사용하였다.)를 연결하기로 했다.
*.dll 파일을 받아오는 경로가 이제부터는 로컬이 아닌 네트워크인 것이다.
적용하였을 때 만약에 발생할 수 있는 악용 사례를 최소화하기 위해, 유효한 코드 서명을 가진 어셈블리만 로드될 수 있도록 검증 과정을 추가하였다.
실제 구현은 아래 링크를 참고하면 된다.
이미 어셈블리가 같은 디렉토리에 존재하는 경우 네트워크에서 받아오지 않는다.
.NET IL로 컴파일된 어셈블리가 아닌, 네이티브(C/C++ 계열) 어셈블리인 경우도 처리되나?
네이티브(C/C++ 계열)로 컴파일된 어셈블리에도 대응하기 위한 구현이 AssemblyLoader.cs (gnh1201/welsonjs)에 포함되어 있다.
단, 라이브러리가 .NET IL (C# 등)으로 컴파일 되어있는 경우 자동으로 처리되지만 네이티브 라이브러리를 사용하는 경우 명시적으로 로드해야 한다.
위 내용이 반영된 예시
아래 코드를 Main 또는 이에 준하는 Entry point에서 실행함으로서 추가 어셈블리 로드가 시작된다.
// load external assemblies
AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl");
AssemblyLoader.Logger = _logger;
AssemblyLoader.Register();
AssemblyLoader.LoadNativeModules("MyNativeLib", new Version(1, 0, 0, 0), new[] { "MyNativeLib.dll" });
using 키워드로 요청되는 .NET IL 계열 어셈블리는 자동으로 찾아 로드한다. 네이티브 계열 어셈블리는 명시적으로 어셈블리 정보를 기입하여 어떤걸 로드해야하는지 정의한다.
어셈블리 해석 중 접속하게 되는 원격 네트워크 주소의 예시는 다음과 같다. Base URL이 https://example.cdn.tld/packages 일 때,
- https://example.cdn.tld/packages/managed/MyManagedLib/1.0.0.0/MyManagedLib.dll (.NET IL 계열 어셈블리)
- https://example.cdn.tld/packages/native/MyNativeLib/1.0.0.0/MyNativeLib.dll (네이티브 계열 어셈블리)
반드시 보안통신(https)이 지원되는 서버여야 하며, 모든 추가 어셈블리(*.dll)은 유효한 코드서명을 가지고 있어야 한다.
네트워크를 통한 어셈블리 동적 로딩을 쓰면 배포에 추가 어셈블리 포함 안해도 되나?
네트워크를 이용한 어셈블리 동적 로딩을 적용한다 하여도, 여전히 배포 시 추가 어셈블리(*.dll)을 함께 배포하는 것이 권장된다.
이 방법은 사용자가 Windows 앱의 배포 과정에 대해 이해가 부족하여, 추가 어셈블리(*.dll 등)을 누락하고 주 실행파일(*.exe)만 다른 컴퓨터로 옮겼을 때 발생하는 라이브러리 누락 오류를 해결하는데 도움을 주는 보조적인 방법으로 쓰여야 한다.
물론 이 방법을 쓰면 배포할 때 추가 어셈블리를 전혀 포함하지 않아도 자동으로 네트워크에서 받기 때문에 지장이 없다.
다만, 어디까지나 보조적인 방법이지 배포 시 무조건 추가 어셈블리를 포함하지 않아도 된다는 의미가 아니라는 점을 유념해야 한다.