Tech/닷넷 일반2013. 10. 2. 18:10

응용프로그램을 배포하다보면 특정 조건 때문에 파일 하나로 만들어서 배포해야 할 경우가 있습니다. 사실 응용프로그램중에 EXE 파일 하나만으로 구성되는 경우가 드물지 않을까 싶은데, 지역화된 리소스만 집어넣어도 기본적으로 위성 어셈블리가 생성되기 때문이죠.


이번에 각종 DLL을 EXE에 임베드시키고 AssemblyResolve 이벤트를 사용해서 로드하는 방식을 사용해보았기에 정리해보려고 합니다.


실행시키면 아래와 같이 덩그라니 창이 뜨는 아주 단순한 WPF 응용프로그램입니다. (.NET 4 Client Profile 기반)


프로젝트 구조는 왼쪽 처럼 되어 있습니다. RESX 를 사용해서 문자열 리소스(여기서는 윈도우 제목 하나뿐이기 하지만...)를 지역화하고 있습니다. 리소스 어셈블리는 별도 프로젝트로 분리되어 있죠. 중립 언어는 en-US 로 AssemblyInfo.cs 에 설정이 되어 있고요.


기본적으로 이 프로젝트를 빌드해서 배포하려면 아래와 같은 파일들을 배포해야 합니다.

  • WpfSingSangSung.exe
  • WpfSingSangSung.Resources.dll
  • ko-KR/WpfSingSangSung.Resources.resources.dll
파일 하나로 만들어서 배포하려면 아래쪽 두 개의 리소스 DLL을 EXE 에 몰아넣어야 겠네요.


우선 빌드된 DLL 을 embed 시킬 것이므로, WpfSingSangSung 프로젝트 빌드 시에 자동으로 WpfSingSangSung.Resources 프로젝트의 빌드 결과물을 복사할 필요가 없습니다. 아래 그림과 같이 프로젝트 참조에서 Copy Local 을 False 로 바꿔줍니다.

빌드가 성공하려면 어셈블리 참조 자체는 유지해주어야 합니다.


다음 단계는 빌드된 리소스 DLL을 둘 곳을 정하는 건데요, 개발 중에 Debug 로 빌드했다가 Release 로 빌드했다가 할텐데, 그 때 빌드된 리소스 DLL 이 업데이트 되어야 하니까, 빌드된 리소스 DLL 들을 정해진 장소로 복사해주도록 하겠습니다.

프로젝트 폴더 구조는 위와 같습니다. WpfSingSangSung.Resources 프로젝트의 빌드 결과물을 bin/Result 폴더에 복사하도록 하죠. Post-build event 에 아래와 같이 설정해 줍니다.

xcopy "$(TargetPath)" "$(ProjectDir)bin\Result\" /Y
xcopy "$(TargetDir)ko-KR\$(ProjectName).resources.dll" "$(ProjectDir)bin\Result\ko-KR\" /Y

만약 다른 언어가 하나 더 추가된다면 ko-KR 과 동일한 패턴으로 복사하는 코드를 한 줄 넣어야겠죠.


이제 복사된 위치에 있는 리소스 DLL 들을 EXE 에 리소스로 임베드 시킵니다. 리소스로 임베드된 DLL 들만을 위해서 별도의 네임스페이스를 두는 것이 좋으니까, WpfSingSangSung 프로젝트에 폴더를 하나 추가하고, 리소스 DLL 들을 그냥 프로젝트에 추가하는 것이 아니라 Link 로 추가합니다.

바로 위 이미지는 DLL 을 추가하고, 리소스로 임베드시키는 작업이 다 된 상태입니다. 각 DLL 파일들의 Build Action 을 "Embedded Resource" 로 설정해준 것이 보이시죠? 그리고 또 하나 눈에 띄는 점은 ko-KR 폴더를 만들어서 한국어 리소스 DLL을 넣었다는 점이겠네요.


여기까지 해서 솔루션을 빌드하면, WpfSingSangSung.exe 에는 DLL들이 모두 리소스로 들어가 있는 상태입니다. 리소스 DLL을 찾아서 사용할 수 있게 해주는 기능이 없으니 실행은 아직 안되지만, ILSpy 로 열어보면 두 개의 리소스 DLL 이 리소스로서 포함되어 있는 것을 확인할 수가 있습니다.

이렇게 ILSpy 로 DLL의 리소스 이름을 확인해두면 뒤에 AssemblyResolve 이벤트 핸들러에서 어셈블리 로드하는 코드를 작성할 때 도움이 됩니다.


가장 중요한 내용으로 들어가기 전에, 기본적인 WPF 응용프로그램의 구조를 조금 바꿔서 코드 작성을 좀 쉽게 만들려고 합니다. 템플릿으로 WPF 응용프로그램을 생성하면 App.xaml 을 이용하는 형태로 만들어 주는데요, 이걸 지워버리고 Program.cs 를 만들어서 진입점으로 만들어주는거죠. 아래와 같이 Program.cs 를 만들어서 넣어줍니다.

    public class Program : Application
    {
        [STAThread]
        public static void Main(string[] args)
        {
            Program app = new Program();
            MainWindow mainWin = new MainWindow();
            app.Run(mainWin);
        }
    }


이제 AssemblyResolve 이벤트 핸들러만 만들어주면 끝납니다! 위에서 만들 Program.cs 에 코드 몇 줄만 추가하면 됩니다.

        public static void Main(string[] args)
        {
            // 이벤트 핸들러 연결
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

            Program app = new Program();
            MainWindow mainWin = new MainWindow();
            app.Run(mainWin);
        }

        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            AssemblyName missingAssemblyName = new AssemblyName(args.Name);
            CultureInfo ci = missingAssemblyName.CultureInfo;

            // ILSpy 에서 확인한 형태로 리소스 이름을 만들어줍니다.
            string prefix "WpfSingSangSung.Embedded.";
            string culturePart = "ci.Name.Replace("-", "_") + ".";
            string resourceName = prefix + culturePart + missingAssemblyName.Name + ".dll";
            // 중립 리소스 요청인 경우에는 Culture 이름이 비어있습니다.
            if (ci.Name == string.Empty)
                resourceName = prefix + missingAssemblyName.Name + ".dll";

            // 리소스에서 읽은 바이너리를 Assembly로 전환하여 리턴합니다.
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
            {
                byte[] bytes = new BinaryReader(stream).ReadBytes((int)stream.Length);
                return Assembly.Load(bytes);
            }
        }


이렇게 만들어서 빌드한 EXE 를 각각 영어 환경과 한국어 환경에 EXE만 복사해서 실행해보면 환경에 맞게 윈도우 제목이 영어와 한국어로 표시되는 것을 확인할 수 있습니다. 성공이네요!


Posted by wafe