C# 6.0 완벽 가이드/ 어셈블리

Contents

  • 어셈블리(Assembly)는 .NET의 기본 배치(deployment) 단위이다. 모든 형식은 어셈블리 안에 담긴다. 하나의 어셈블리는 컴파일된 형식과 해당 IL(Intermediate Language) 코드, 실행시점 자원, 그리고 보조 정보(버전 관리, 보안, 다른 어셈블리 참조 등을 위한)로 구성된다.
    • 어셈블리는 또한 형식 결정과 보안 권한 부여의 경계선을 정의하는 역할도 한다.
  • 보통의 경우 하나의 어셈블리는 하나의 Windows PE(Portable Executable) 파일인데, 응용 프로그램 실행 파일(executable)에 해당하는 어셈블리 파일의 확장자는 .exe이고 재사용 가능한 라이브러리의 확장자는 .dll이다.
    • 단 WinRT 라이브러리는 확장자가 .winmd이다. WinRT 라이브러리 파일은 .dll과 비슷하나, 메타자료만 있고 IL 코드가 없다는 점이 다르다.
  • 이번 장에 등장하는 형식들은 대부분 다음 이름공간들에 정의되어 있다.
    • System.Reflection
    • System.Resources
    • System.Globalization

어셈블리의 구성

  • 어셈블리를 구성하는 요소는 다음 네 가지이다.
    • 어셈블리 매니페스트
      • 어셈블리 매니페스트(Manifest)는 .NET 런타임에 관한 정보를 제공한다. 예컨대 어셈블리의 이름, 버전, 어셈블리가 요청한 권한들, 그리고 어셈블리가 참조하는 다른 어셈블리들의 목록이 어셈블리 매니페스트에 들어 있다.
    • 응용 프로그램 매니페스트
      • 응용 프로그램 매니페스트는 운영체제에 필요한 정보를 제공한다. 예컨대 어셈블리를 배치하는 방법이나 관리자로의 권한 상승이 필요한지의 여부 등이 응용 프로그램 매니페스트에 들어 있다.
    • 컴파일된 형식
      • 어셈블리가 정의하는 형식들을 컴파일해서 나온 IL 코드와 형식들의 메타자료(metadata)
    • 자원
      • 이미지나 현지화용 텍스트 등의 기타 자료
  • 이들 중 필수인 것은 어셈블리 매니페스트 뿐이다. 그러나 WinRT 참조 어셈블리가 아닌 한 어셈블리에는 거의 항상 컴파일된 형식들이 들어 있다.
  • 실행 파일 어셈블리와 라이브러리 어셈블리의 구조는 거의 같다. 주된 차이는 실행 파일 어셈블리에는 진입점(entry point)이 정의되어 있다는 점이다.

어셈블리 매니페스트

  • 어셈블리 매니페스트의 목적은 다음 두 가지이다.
    • 관리되는 호스팅 환경에 어셈블리에 관한 정보를 제공한다.
    • 어셈블리에 들어 있는 모듈, 형식, 자원들의 ‘주소록’ 역할을 한다.
  • 이런 목적을 위해, 어셈블리는 자기 서술적(self-describing)으로 만들어진다. 즉, 어셈블리의 소비자는 다른 파일들을 참고할 필요 없이 어셈블리 파일만 보고도 어셈블리의 모든 자료와 형식, 기능을 알 수 있다.
  • 프로그래머가 어셈블리 매니페스트를 어셈블리에 직접 추가하지는 않는다. 어셈블리 매니페스트는 컴파일 과정에서 자동으로 어셈블리에 내장된다.
  • 어셈블리 매니페스트에 담긴 자료 중 기능상 중요한 자료를 요약하자면 다음과 같다.
    • 어셈블리의 간단한 이름(이하 ‘단순명’)
    • 버전(AssemblyVersion)
    • 어셈블리의 공개키와 서명된 해시(강력한 이름이 있는 경우)
    • 어셈블리가 참조하는 다른 어셈블리의 목록(해당 버전 및 공개 키 포함)
    • 어셈블리를 구성하는 모듈들의 목록
    • 어셈블리에 정의되어 있는 형식들의 목록과 각 형식을 담은 모듈 정보
    • (선택적) 어셈블리가 요청 또는 거부한 보안 권한들의 집합(SecurityPermission)
    • (위성 어셈블리의 경우) 대상 문화권(AssemblyCulture)
  • 어셈블리 매니페스트에 다음과 같은 정보성 자료가 들어 있을 수도 있다.
    • 전체 제목과 설명(AssemblyTitle과 AssemblyDescription)
    • 회사명과 저작권 정보(AssemblyCompany와 AssemblyCopyright)
    • 화면 표시용 버전 정보(AssemblyInformationVersion)
    • 커스텀 자료를 위한 추가 특성
  • 이 자료의 일부는 컴파일 시 지정한 옵션들에서 비롯된다. 이를테면 참조하는 어셈블리 목록이나 서명용 공개 키 등이 그렇다. 그 나머지는 어셈블리 특성들(목록에서 괄호 안에 표히된)에서 비롯된다.
  • 어셈블리 매니페스트의 내용을 .NET 도구 중 하나인 ildasm.exe로 볼 수 있다.

어셈블리 특성의 지정

  • 어셈블리 매니페스트 내용의 상당 부분을 어셈블리 특성으로 제어할 수 있다. 예컨대 다음과 같다.
[assemly: AssemblyCopyright("\x00a9 Corp Ltd. All rights reserved.")]
[assemly: AssemblyVersion("2.3.2.1")]
  • 보통은 이런 선언들을 모두 프로젝트의 한 파일에 담아 둔다. Visual Studio에서 C# 프로젝트를 생성하면 Properties 폴더에 AssemblyInfo.cs라는 파일이 생기는데,  여기에 기본적인 어셈블리 특성들이 선언되어 있다. 이를 추가적인 커스텀화를 위한 출발점으로 삼으면 된다.

응용 프로그램 매니페스트

  • 응용 프로그램 매니페스트는 어셈블리에 관한 정보를 운영체제에 제공하는데 쓰이는 XML 파일이다.
    • 어셈블리에 응용 프로그램 매니페스트가 있으면 .NET은 자신이 관리하는 호스팅 환경에 그 어셈블리를 적재하기 전에 응용 프로그램 매니페스트를 읽어서 처리한다. 그 처리 결과에 따라 운영체제가 응용 프로그램의 프로세스슬 띄우는 방식이 달라질 수 있다.
  • .NET 응용 프로그램 매니페스트의 XML 뿌리 요소는 assembly이고 해당 XML 이름공간은 urn:schemas-microsoft-com:asm.v1이다.
<?xml version-"1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <!--... 매니페스트 내용...-->
</assembly>
  • 예컨대 다음은 관리자 권한 상승(administative elevation)을 운영체제에 요청하는 응용 프로그램 매니페스트이다.
<?xml version-"1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="requireAdministrator" />
      </requestPrivileges>
    </security>
  </trustInfo>
</assembly>
  • Package.appxmanifest 파일에 담긴 Windows 스토어 앱의 매니페스트는 이보다 훨씬 복잡하다. 이 파일에는 해당 프로그램의 능력들에 대한 선언이 포함되어 있는데, 운영체제는 이에 기초해서 프로그램에 허용할 권한들을 결정한다.
    • Visual Studio를 이용하면 이 파일을 손쉽게 편집할 수 있다. Visual Studio에서 매니페스트 파일을 더블클릭하면 편집용 UI가 나타난다.

.NET 응용 프로그램 매니페스트의 배치

  • .NET 응용 프로그램 매니페스트를 배치하는 방법은 두 가지이다.
    • 어셈블리가 있는 폴더에 특별한 이름의 파일을 둔다.
    • 어셈블리 자체에 내장한다.
  • 전자의 경우 어셈블리 이름에 .manifest를 붙인 파일을 만들어야 한다. 예컨대 어셈블리가 MyApp.exe이면 해당 응용 프로그램 매니페스트 파일은 MyApp.exe.manifest이어야 한다.
  • 응용 프로그램 매니페스트 파일을 어셈블리 자체에 내장하려면, 먼저 어셈블리를 구축한 후 .NET 도구 mt를 다음과 같은 형태로 실행하면 된다.
mt -manifest MyApp.exe.manifest -outputresource:MyApp.exe;#1
  • .NET 도구 ildasm.exe는 내장된 응용 프로그램 매니페스트를 인식하지 못한다. 그러나 Visual Studio는 인식한다. Visual Studio의 솔루션 탐색기에서 어셈블리를 더블클릭하면 내장된 응용 프로그램 매니페스트의 존재를 볼 수 있다.

모듈

  • 어셈블리의 구성요소들은 모듈(module)이라고 부르는 중간 수준 컨테이너들로 조직화된다. 하나의 모듈은 어셈블리의 구성요소들을 담은 하나의 파일에 대응된다. 이처럼 중간 수준의 컨테이너들을 두는 이유는 하나의 어셈블리를 여러 개의 파일로 구성할 수 있게 하기 위한 것이다.
    • 이러한 능력은 다양한 프로그래밍 언어로 작성된 코드를 컴파일한 결과를 담은 어셈블리를 구축할 때 유용하다.
  • 그림 18-1은 보통의 경우 즉 어셈블리가 하나의 모듈로 이루어진 경우를 보여준다. 그림 18-2는 여러 모듈로 이루어진 다중 파일 어셈블리의 경우이다.
    • 다중 파일 어셈블리(multifile assembly)에서 ‘주’ 모듈은 항상 어셈블리 매니페스트가 있는 모듈이다.
    • 다른 모듈들은 IL 코드나 자원을 담는다. 매니페스트에는 어셈블리를 구성하는 다른 모듈들의 상대적 위치가 들어 있다.

  • Vsual Studio에는 다중 파일 어셈블리 작성 기능이 없기 때문에 명령줄 도구들을 사용해야 한다. csc 컴파일러에 /t 옵션을 주어서 각 모듈을 만들고 그 모듈들을 어셈블리 링커 도구인 al.exe로 링크하면 된다.
  • 다중 파일 어셈블리가 필요한 경우는 드물지만 모듈이라는 추가적인 수준의 컨테이너들이 존재한다는 사실을 아는 것이 중요한 경우는 종종 있다. 심지어 단일 모듈 어셈블리를 다룰 때도 그렇다.
    • 대표적인 예는 반영 기능을 사용할 때 이다.

Assembly 클래스

  • System.Reflection의 Assembly 클래스는 실행시점에서 어셈블리 메타자료에 접근하기 위한 관문에 해당한다.
    • 어셈블리를 나타내는 Assembly 객체를 얻는 방법은 다양한데, 가장 간단한 것은 다음처럼 해당 프로그램 형식의 Assembly 속성을 이용하는 것이다.
Assembly a = typeof(Program).Assembly;
  • Windows 스토어 앱에서는 다음과 같이 해야 한다.
Assembly a = typeof(Program).GetTypeInfo().Assembly;
  • 데스크톱 응용 프로그램에서는 Assembly의 다음과 같은 정적 메서드들을 이용해서도 Assembly 객체를 얻을 수 있다.
    • GetExecutingAssembly
      • 현재 실행 중인 함수를 정의하는 형식의 어셈블리를 돌려준다.
    • GetCallingAssembly
      • 현재 실행 중인 함수를 호출한 함수를 정의하는 형식의 어셈블리를 돌려준다.
    • GetEntryAssembly
      • 응용 프로그램의 원래 진입점을 정의하는 어셈블리를 돌려준다.
  • 일단 Assembly 객체를 얻었으면 여러 속성과 메서드를 이용해서 어셈블리의 메타자료를 조회하거나 해당 형식에 반영할 수 있다. 아래 표는 그러한 여러 속성과 메서드를 정리한 것이다.
함수 용도
FullNae, GetName 완전 한정 이름 또는 AeemblyName 객체를 돌려준다.
CodeBase, Location 어셈블리 파일의 위치를 돌려준다.
Load, LoadFrom, LoadFile 현재 응용 프로그램 도메인에 어셈블리를 직접 적재한다.
GlobalAssemblyCache 어셈블리가 GAC에 있는지의 여부를 나타낸다.
GetSatelliteAssembly 주어진 문화권에 해당하는 위성 어셈블리의 위치를 돌려준다.
GetType, GetTypes 어셈블리에 정의되어 있는 하나의 형식 또는 모든 형식을 돌려준다.
EntryPoint 응용 프로그램 진입점에 대한 정보를 담은 MethodInfo 객체를 돌려준다.
GetModules, ManifestModule 어셈블리의 모든 모듈 또는 주 모듈을 돌려준다.
GetCustomAttributes 어셈블리의 특성들을 돌려준다.

 

강력한 이름과 어셈블리 서명

  • 강력한 이름(strong name)이 부여된 어셈블리(강력 이름 어셈블리)는 고유한, 그리고 변조될 수 없는 신원(identity)을 가진다. 강력한 이름을 위해서는 어셈블리 매니페스트에 다음 두 가지 메타자료가 있어야 한다.
    • 어셈블리 작성자를 식별하는 고유 번호
    • 이 어셈블리를 해당 고유 번호 소지자가 만들었음을 증명하는 서명된 해시(signed hash)
  • 신원 확인과 서명을 위해서는 공개 키/비밀 키 쌍이 필요하다. 공개 키(public key)는 고유한 식별 번호로 쓰이고 비밀 키(private key)는 서명(signing)에 스인다.
    • 강력한 이름 서명은 Microsoft의 Authenticode 서명과는 다른 것이다.
  • 공개 키는 어셈블리 참조들의 고유성을 보장하는데 중요한 역할을 한다. 공개 키는 강력 이름 어셈블리의 신원에 포함된다.
    • 서명은 보안에 중요하다. 서명은 불순한 의도를 가진 누군가가 독자의 어셈블리를 변조하지 못하게 한다. 독자의 비밀 키가 없는 누군가가 어셈블리를 수정하면 서명이 깨진다.(그러면 적재 시 오류가 발생한다)
    • 물론 누군가가 다른 키 쌍으로 어셈블리의 서명을 갱신할 수는 있지만 그러면 어셈블리의 신원 자체가 바뀐다.
    • 원래의 어셈블리를 참조하는 모든 응용 프로그램은 원래의 공개 키를 기억하고 있기 때문에, 재서명된 어셈블리를 만나면 신원이 달라졌음을 인식하고 참조를 거부한다.
  • 약한 이름(단순명)을 가진 기존 어셈블리에 강력한 이름을 부여하면 어셈블리의 신원이 바뀐다. 따라서 실무에 사용할 어셈블리를 만들 때에는 처음부터 강력한 이름을 부여하는 것이 장기적으로 이득이 된다.
  • 강력 이름 어셈블리의 또 다른 장점은 GAC(전역 어셈블리 캐시)에 등록할 수 있다는 것이다.

어셈블리에 강력한 이름을 부여하는 방법

  • 어셈블리에 강력한 이름을 부여하려면, 우선 sn.exe 유틸리티로 공개 키/비밀 키 쌍을 생성해야 한다.
sn.exe -k MyKeyPair.snk
  • 명령줄에서 위의 명령을 실행하면 새 키 쌍이 생성되어서 MyApp.snk라는 파일에 저장된다. 만일 이 파일을 잃어버리면 어셈블리에 같은 신원으로 다시 서명하는 능력을 영영 잃는 것이라는 점을 명심하기 바란다.
  • 다음 단계는 컴파일 시 /keyfile 옵션으로 그 키 쌍 파일 지정하는 것이다.
csc.exe /keyfile:MyKeyPair.snk Program.cs
  • Visual Studio의 프로젝트 속성 창에 이 두 단계에 해당하는 옵션들이 존재한다.
  • 강력 이름 어셈블리는 약한 이름 어셈블리를 참조하지 못한다. 이는 모든 실무용 어셈블리에 강력한 이름을 부여하는 것이 좋은 또 다른 이유이다.
  • 하나의 키 쌍으로 여러 어셈블리를 서명할 수도 있다. 단순명이 각자 다르다면 어셈블리들은 각각 다른 신원을 가지게 된다.
    • 하나의 기업이나 조직에서 몇 개의 키 쌍 파일을 사용할 것인지는 여러 가지 요인을 고려해서 결정해야 한다.
    • 어셈블리마다 개별적인 키 쌍을 사용하면 나중에 특정 응용 프로그램과 그것이 참조하는 어셈블리들의 소유권을 다른 단위로 넘겨줄 때 정보 유출이 최소화된다는 장점이 있다.
    • 그러나 조직의 모든 어셈블리를 식별하는 하나의 단일한 보안 정책을 만들기가 어려워진다. 또한 동적으로 적재된 어셈블리의 유효성을 검증하기도 어려워진다.
  • C# 2.0 이전에는 C# 컴파일러가 /keyfile 옵션을 지원하지 않았으며, 대신 AeemblyKeyFile 특성으로 키 파일을 지정해야 했다. 이는 보안상의 위험 요인이었다. 키 파일 경로가 어셈블리의 메타자료에 내장되어 있기 때문이다.
    • 예컨대 ildasm을 이용하면 CLR 1.1의 mscorlib에 서명하는데 쓰인 키 파일의 경로를 확인할 수 있다.
    • 물론 Microsoft의 .NET 프레임워크 빌드용 컴퓨터에 접근할 수 없는 한, 저 경로를 안다고 해서 써먹을 방법은 없다.
F:\qfe\Tools\devdiv\EcmaPublicKey.snk

서명 연기

  • 개발자가 수백 명인 조직에서는 어셈블리 서명에 사용하는 키 쌍들에 특정 직원만 접근할 수 있게 하는 것이 바람직하다. 그 이유는 두 가지이다.
    • 만일 누군가의 실수로 키 쌍이 유출되면 어셈블리의 변조 방지가 더 이상 보장되지 않는다.
    • 서명된 시험용 어셈블리가 유출된 경우 누군가가 그것을 진짜 어셈블리라고 속여서 배포할 수 있다.
  • 그러나 개발자들이 키 쌍에 접근하지 못하게 되면 개발 도중 개발자들이 실제 신원으로 어셈블리를 컴파일해서 시험할 수 없게 된다. 서명 연기(delay signing)는 바로 이러한 문제를 피하기 위한 체계이다.
  • 서명이 연기된 어셈블리는 실제 공개 키를 포함하지만 비밀 키로 서명되지는 않는다. 사실 서명 연기 어셈블리는 변조된 어셈블리와 다를 바 없으며, 보통의 경우 CLR은 그런 어셈블리를 거부한다.
    • 그러나 개발자는 현재 컴퓨터에 대해서만큼은 서명 연기 어셈블리의 검증 과정을 건너뛰라고 CLR에게 요청할 수 있다. 나중에 최종적으로 어셈블리를 배포할 때가 되면 비밀 키 소지자가 실제 키 쌍으로 어셈블리에 다시 서명하면 된다.
  • 서명을 연기하려면 우선 공개 키만 담은 파일을 만들어야 한다. -p 스위치를 지정해서 sn을 실행하면 공개 키/비밀 키 쌍에서 공개 키만 추출할 수 있다.
sn -k KeyPair.snk
sn -p KeyPair.snk PublicKeyOnly.pk
  • KeyPair.snk는 특정 직원들만 접근할 수 있게 하고, PublicKeyOnly.pk는 자유로이 배포해도 된다.
  • 기존의 서명된 어셈블리에서 공개 키를 추출할 수도 있다. 다음처럼 -e 스위치를 사용하면 된다.
sn -e YourLibrary.dll PublicKeyOnly.pk
  • 다음으로 csc 실행 시 PublicKeyOnly.pk와 함께 /delaysign+ 스위치를 지정한다. 이러면 서명이 연기된다.
csc /delaysign+ /keyfile: PublicKeyOnly.pk /target:library YourLibrary.cs
  • Visual Studio에서는 프로젝트 속성 창의 ‘보안’ 탭에 있는 ‘서명만 연기’ 체크 상자를 체크하면 같은 효과가 난다.
  • 다음으로 할 일은 .NET 런타임에게 서명이 연기된 어셈블리를 실행하는 개발용 컴퓨터에서 어셈블리의 신원 검증 과정을 건너뛰라고 지시하는 것이다.
    • Vr 스위치를 지정해서 sn 도구를 실해아면 되는데, 어셈블리마다 적용할 수도 있고 공개 키에 대해 적용할 수도 있다.
    • 다음은 개별 어셈블리에 적용하는 예이다.
sn -Vr YourLibrary.dll
  • Visual Studio에는 이 단계를 자동으로 실행하는 기능이 없다. 반드시 명령줄에서 어셈블리 검증을 직접 비활성화해야 한다. 그렇게 하지 않으면 어셈블리가 실행되지 않는다.
  • 개발을 마치고 배포(배치)할 단계가 되면 어셈블리에 실제로 서명해야 한다. 즉, 비밀 키까지 적용해서 진짜 서명을 어셈블리에 추가해야 한다. 다음처럼 R 스위치를 지정해서 sn을 실행하면 된다.
sn -R YourLibrary.dll KeyPair.snk
  • 또한 개발용 컴퓨터에서 다음과 같은 형태로 sn 도구를 실행해서 어셈블리 검증을 다시 활성화해야 한다.
sn -Vu YourLibrary.dll
  • 서명 연기 어셈블리를 참조하는 응용 프로그램들은 다시 컴파일할 필요가 없다. 어셈블리의 서명이 바뀌었을 뿐 신원이 바뀐 것은 아니기 때문이다.

어셈블리 이름

  • 어셈블리의 ‘신원’은 매니페스트에 있는 다음과 같은 네 가지 메타자료로 구성된다.
    • 단순명 (간단한 이름)
    • 버전 (없는 경우 ‘0.0.0.0’)
    • 문화권 (위성 어셈블리가 아닌 경우 ‘neutral’)
    • 공개 키 토큰 (강력한 이름이 없는 경우 ‘null’)
  • 단순명은 어떤 어셈블리 특성이 아니라 애초에 컴파일된 파일의 이름(확장자 제외)으로 결정된다.
    • 예컨대 System.Xml.dll 어셈블리의 단순명은 ‘System.Xml’이다. 그 파일 이름을 변경해도 어셈블리의 단순명은 바뀌지 않는다.
  • 버전은 AssemblyVersion 특성에서 비롯된다. 버전은 다음과 같이 네 부분으로 이루어진 하나의 문자열이다.
주 번호.부 번호.빌드 번호.리비전 번호
  • 다음은 버전 번호는 지정하는 예이다.
[assembly: AssemblyVersion("2.5.6.7")]
  • 문화권 설정은 AssemblyCulture 특성에서 비롯되며 위성 어셈블리(satellite assembly)에 적용된다.
  • 공개 키 토큰은 컴파일 시 /keyfile 옵션으로 지정된 키 쌍에서 비롯된다.

완전 한정 이름

  • 어셈블리의 완전 한정 이름(fully qualified name)은 어셈블리의 신원을 구성하는 네 요소로 이루어진, 다음과 같은 형태의 문자열이다.
단순명, Version=버전, Culture=문화권, PublicKeyToken=공개-키-토큰
  • 예컨대 System.Xml.dll의 완전 한정 이름은 다음과 같다.
"System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  • 어셈블리에 AssemblyVersion 특성이 없으면 버전이 ‘0.0.0.0’으로 나온다. 서명 되지 않은 어셈블리의 공개 키 토큰은 ‘null’로 나온다.
  • Assembly 객체의 FullName 속성은 해당 어셈블리의 완전 한정 이름을 돌려준다.
    • 컴파일러는 어셈블리 참조들을 매니페스트에 기록할 때 항상 완전 한정 이름을 사용한다.
  • 디스크에 어셈블리가 저장되어 있는 디렉터리 경로는 어셈블리의 완전 한정 이름에 포함되지 않는다. 다른 디렉터리에 있는 어셈블리를 찾아내는 것은 어셈블리의 신원과는 전혀 무관한 문제이다.

AssemblyName 클래스

  • AssemblyName은 어셈블리의 완전 한정 이름을 구성하는 네 요소 각각에 대한 형식 있는 속성들을 제공한다. AssemblyName의 용도는 다음 두 가지이다.
    • 완전 한정 이름을 파싱하거나 생성한다.
    • 어셈블리의 환원(위치 파악)을 돕는 추가적인 자료를 저장한다.
  • AssemblyName 객체를 얻는 방법은 여러가지이다.
    • 완전 한정 이름 구성요소들을 지정해서 AssemblyName 인스턴스를 직접 생성한다.
    • 기존 Assembly 객체에 대해 GetName을 호출한다.
    • 디스크에 있는 어셈블리 파일의 경로를 지정해서 AssemblyName.GetAssemblyName을 호출한다(데스크톱 응용 프로그램에서만)
  • 아니면 아무 인수 없이 AssemblyName 객체를 인스턴스화한 후 각 속성을 설정해서 완전 한정 이름을 구축할 수도 있다. 그런 식으로 생성한 AssemblyName 객체는 변경이 가능하다(mutable)
  • 다음은 AssemblyName의 핵심 속성과 메서드이다.
string FullName { get; set; }  // 완전 한정 이름
string Name { get; set; }  // 단순명
Version Version { get; set; }  // 어셈블리 버전
CultureInfo CultureInfo { get; set; }  // 위성 어셈블리에 쓰임
string CodeBase { get; set; }  // 위치

byte[] GetPublicKey();  // 160바이트
void SetPublicKey(byte[] key);
byte[] GetPublicKeyToken();  // 8바이트 버전
void SetPublicKeyToken(byte[] publicKeyToken);
  • Version 속성은 Major(주 번호), Minor(부 번호), Build(빌드 번호), Revision(리비전 번호)이라는 속성이 있는 객체이다.
  • GetPublicKey 메서드는 완전한 암, 복호화 공개 키를 돌려주고, GetPublicKeyToken은 그 공개 키의 마지막 여덟 바이트(신원을 확립하는데 쓰이는)를 돌려준다.
  • 다음은 AssemblyName으로 어셈블리의 단숨영을 얻는 예이다.
Console.WriteLine(typeof(string).Assembly.GetName().Name);  // mscorlib
  • 어셈블리 버전을 얻으려면 다음과 같이 하면 된다.
string v = myAssembly.GetName().Version.ToString();

어셈블리 정보와 파일 버전

  • 어셈블리의 이름에는 버전이 포함되어 있으므로 AssemblyVersion 특성을 바꾸면 어셈블리의 신원이 바뀐다. 이는 어셈블리를 참조하는 다른 어셈블리들과의 호환성에 영향을 미친다.
  • 한 어셈블리를 갱신했을 때 그것을 참조하는 다른 모든 응용 프로그램과 어셈블리를 갱신해야 한다는 것은 바람직하지 않다. 이를 피하는 한 가지 방법은 어셈블리를 갱신할 때 어셈블리의 신원에 포함되지 않는 다음 두 특성만 바꾸는 것이다. CLR은 이들을 무시한다.
    • AssemblyInformationalVersion
      • 최종 사용자에게 표시되는 ‘정보성(informational)’ 버전 번호에 해당한다. 이 정보는 이를테면 Windows 파일 속성 대화상자의 ‘제푸 ㅁ버전’ 항목에 나타난다.
      • 이 특성에 지정하는 문자열에는 특별한 제약이 없다. 이를테면 ‘5.1 베타 2’ 같은 문자열을 사용해도 된다.
      • 일반적으로 한 응용 프로그램의 모든 어셈블리에는 동일한 정보성 버전 번호를 부여한다.
    • AssemblyFileVersion
      • 어셈블리의 빌드 번호에 해당한다. 이는 이를테면 Windows 파일 속성 대화상자의 ‘파일 버전’ 항목에 나타난다.
      • AssemblyVersion 특성과 마찬가지로 네 정수를 마침표로 연결한 형태의 문자열을 지정해야 한다.

Authenticode 서명

  • Authenticode는 Microsoft의 코드 서명 시스템으로 주된 용도는 게시자(pubisher)의 신원을 증ㅁ여하는 것이다. Authenticode 서명과 강력한 이름 서명은 별개이다. 하나의 어셈블리를 둘 중 하나로만 서명할 수도 있고 둘 다로 서명할 수도 있다.
  • 강력한 이름 서명은 어셈블리 A, B, C를 같은 사람 또는 조직이 제공했음을 증명할 수 있다(비밀 키가 유출되지 않았다고 할 때).
    • 그러나 당사자가 누구인지는 알려주지 않는다. 당사자가 누구인지 확인하려면 Authenticode가 필요하다.
  • Authenticode는 인터넷에서 프로그램을 내려받을 때 유용하다. 내려받은 프로그램을 인증 기관(CA)이 확인한 당사자가 제공한 것이 맞으며, 전송 도중 어딘가에서 수정되지 않았음을 Authenticode가 보장해주기 떄문이다. 또한 Authenticode로 서명한 프로그램은 사용자가 처음 내려받아서 실행할 때 아래 그림과 같은 ‘알 수 없는 게시자’ 경고가 뜨지 않는다.
    • 앱을 Windows 스토어에 제출할 떄는 좀 더 일반화하자면 어셈블리를 Windows Logo 프로그램의 일부로서 제출할 때에도 Authenticode 서명이 필수이다.

  • Authenticode는 .NET 어셈블리뿐만 아니라 관리되지 않는 실행 파일과 ActiveX 컨트롤 또는 .msi 설치 파일 같은 이진 파일에도 적용된다.
    • 물론 주어진 프로그램이 악성 프로그램이 아님을 Authenticode가 보장하지는 않는다. 다만 악성 프로그램이 아닐 가능성이 큼을 알려주긴한다.
    • 실행 파일이나 라이브러리에 자신의 이름을 박아 넣은 사람 또는 회사가 악성 프로그램을 배포할 가능성은 크지 않다.
  • CLR은 Authenticode 서명을 어셈블리 신원의 일부로 간주하지 않는다. 그러나 요청이 있으면 Authenticode 서명을 읽어서 검증해준다.
  • Authenticode 서명을 위해서는 인증 기관(certificate authority, CA)에 독자의 신원 증명 문서 또는 기업의 신원 증명 문서(정관 문서 등)를 제출해야 한다. CA는 그 문서를 점검한 후 5년간 유효한 X.509 코드 서명 인증서를 발급한다.
    • 이 인증서가 있으면 signtool 유틸리티를 이용해서 Authenticode로 어셈블리에 서명할 수 있다.
    • makecert 유틸리티를 이용해서 CA를 거치지 않고 독자가 직접 인증서를 생성할 수도 있지만, 그런 자체 발급 인증서는 해당 인증서 파일을 명시적으로 설치한 컴퓨터에서만 인식된다.
  • 인증서(자체 발급이 아닌)가 그 어떤 컴퓨터에서도 작동하는 것은 공개키 기반구조(public key infrastructure) 덕분이다.
    • 본질적으로 독자의 인증서는 특정 CA가 소유한 또 다른 인증서로 서명된다. 그 CA를 신뢰할 수 있는 것은, 모든 CA가 운영체제에 적재되어 있기 때문이다.
    • 어떤 게시자의 인증서가 유출되어서 해당 CA가 그 인증서를 무효화하는 일이 일어나기도 하므로, Authenticode 서명을 제대로 검증하기 위해서는 주기적으로 CA들에 최근 인증서 무효화 목록을 요청해야 한다.
  • Authenticode는 암호화 서명을 사용하므로 누군가가 프로그램 파일을 조작하면 Authenticode의 서명이 유효하지 않게 된다.

Authenticode 서명 방법

인증서 얻기와 설치

  • 첫 단계는 CA로부터 코드 서명 인증서를 얻는 것인데, 이에 관해서는 ‘코드 서명 인증서 발급처’에서 이야기한다. 일단 인증서를 얻었다면 그것을 패스워드로 보호회든 파일 형태로 사용할 수도 있고 컴퓨터의 인증서 저장소에 등록할 수도 있다.
    • 후자는 매번 패스워드를 지정하지 않아도 된다는 장점이 있다. 자동화된 빌드 스크립트나 일괄 실행(batch) 파일에 평문 패스워드를 바아두지 않아도 된다는 점에서 이는 큰 장점이다.
  • 인증서를 컴퓨터의 인증서 저장소에 등록하는 과정은 이렇다.
    • Windows 제어판에서 ‘인터넷 옵션’ -> ‘내용’ 탭 -> ‘인증서’ 버튼 -> ‘가져오기’ 버튼을 클릭하면 ‘인증서 가져오기 마법사’가 뜬다. 마법사의 지시를 따라 인증서를 등록하면 된다.
    • 등록을 마친 후 ‘인증서’ 대화상자에서 해당 인증서를 선택해서 ‘보기’ 버튼을 클릭한 후 ‘자세히’ 탭을 보면 인증서의 ‘지문’이 있다. 이것은 이후 Authenticode 서명 시 인증서를 지정하는데 필요한 SHA-1 해시이다.
  • 응용 프로그램에 강력한 이름으로도 서명하고 싶다면(이를 강력히 추천한다) 반드시 강력한 이름 서명을 Authenticode 서명보다 먼저 수행해야 한다.
    • 이는 CLR은 Authenticode 서명을 알지만 Authenticode 시스템은 CLR(의 강력한 이름 서명)을 모르기 때문이다.
    • 따라서 만일 어셈블리에 Authenticode로 서명한 다음에 강력한 이름으로 서명하면 Authenticode 시스템은 CLr의 강력한 이름이 추가된 어셈블리가 인증 없이 변조되었다고 따라서 유효하지 않다고 간주한다.

signtool.exe를 이용한 서명

  • Visual Studio와 함께 설치되는 sintool 유틸리티를 이용해서 프로그램에 Authenticode 서명을 가할 수 있다.
    • signwizard 플래그를 주어서 이 유틸리티를 실행하면 UI가 표시된다. 아니면 다음처럼 그냥 명령줄 방식으로 서명을 진행해도 된다.
signtool sign /sha1 (지문) 파일이름
  • 여기서 (지문) 에는 앞서 말한 컴퓨터의 인증서 젖ㅇ소에 있는 인증서 지문(SHA-1 해시)을 복사해서 지정하면 된다. (인증서가 패스워드로 보호되는 파일인 경우에는 /f 옵션으로 파일 이름을 /p 옵션으로 패스워드를 지정해야 한다.)
  • 다음은 signtool로 Authenticode 서명을 가하는 예이다.
signtool sign /sha1 ff813c473... LINQPad.exe
  • 이때 /d, /du 옵션으로 제품 설명과 URL을 함께 지정할 수도 있다.
... /d LINQPad /du http://www.linqpad.net
  • 또한 대부분의 경우에는 시점 확인 서버도 함께 지정한다.

시점 확인

  • 만료된, 즉 유효 기간이 지난 인증서로는 프로그램에 서명할 수 없다. 그러나 인증서가 만료되기 전에 서명한 프로그램은 인증서가 만료된 후에도 유효하다. 단, 이를 위해서는 서명 시 /t 옵션으로 시점 확인 서버(time-stamping server)를 지정해야 한다. CA는 이를 위한 URI를 제공한다.
    • 다음은 Comodo(또는 Ksoftware)의 시점 확인용 URI를 지정하는 예이다.
... /t http://timesamp.comodoca.com/authenticode

프로그램 서명 여부 확인

  • 어떤 파일이 Authenticode로 서명된 것인지 확인하는 가장 쉬운 방법은 Windows 탐색기에서 파일의 속성 대화상자를 띄우는 것이다. signtool 유틸리티도 이를 위한 옵션을 제공한다.

Authenticode 검증

  • Authenticode 서명의 검증은 운영체제 차원에서 일어나기도 하고, CLR 수준에서 일어나기도 한다.
  • Windows는 ‘차단됨’으로 표시된 프로그램을 실행하기 전에 Authenticode 서명을 검증한다.
    • 현실적으로 인터넷에서 내려받은 후 처음으로 실행되는 프로그램은 ‘차단됨’ 프로그램에 속한다.
  • CLR은 프로그램이 다음과 같은 형태의 코드를 통해서 어셈블리 증거(evidence)를 요청하면 Authenticode 서명을 읽어서 검증한다.
Publisher p = someAssembly.Evidence.GetHostEvidence<Publisher>();
  • Publisher 클래스는 Certificate라는 속성을 제공하는데, 이 속서잉 널이 아닌 객체를 돌려준다면 해당 어셈블리는 Authenticode로 서명된 것이다. 그런 경우 그 객체를 통해서 인증서의 세부사항을 조회할 수 있다.
  • .NET Framework 4.0 이전에는 프로그램이 명시적으로 GetHostEvidence를 호출하지 않아도 CLR이 어셈블리 적재 과정에서 Authenticode 서명을 읽어서 검증했다. 그런데 이러한 방식은 성능에 악영향을 미칠 수 있다.
    • Authenticode 서명을 검증하는 과정에서 CA의 인증서 무효화 목록을 갱신하기 위해 CA 서버와 통신 왕복이 일어날 수 있는데, 인터넷 연결에 문제가 있는 경우 최대 30초의 지연 후 갱신(따라서 검증도) 실패하게 된다.
    • 이런 이유로 .NET Framework 3.5나 그 이전의 어셈블리들에는 Authenticode 서명을 피하는 것이 최선이다.(단, .msi 설치 파일에 서명하는 것은 괜찮다)
  • .NET Framework 버전과 무관하게 만일 프로그램에 깨진 또는 검증할 수 없는 Authenticode 서명이 있어도 CLR은 그냥 그 사실을 GetHostEvidence를 통해서 전달하기만 한다. 사용자에게 어떤 대화상자를 띄우거나 그 어셈블리의 실행을 금지하지는 않는다.
    • Authenticode 서명은 어셈블리의 신원이나 이름에는 아무런 영향도 주지 않는다.

전역 어셈블리 캐시(GAC)

  • .NET Framework를 컴퓨터에 설치하면, .NET 어셈블리들을 저장하는 중앙 저장소인 전역 어셈블리 캐시(Global Assembly Cache, GAC)가 컴퓨터에 만들어진다.
    • GAC는 .NET Framework 자체의 중앙집중적 복사본을 저장한다. 독자가 작성한 어셈블리들을 GAC에 저장하는 것도 가능하다.
  • 독자의 어셈블리를 GAC에 저장할 것인지 결정할 때 주된 요인은 버전 관리이다.
    • GAC에 담긴 어셈블리의 버전은 컴퓨터별로 컴퓨터 관리자가 중앙집중적으로 관리한다. GAC 바깥에 있는 어셈블리의 버전은 응용 프로그램별로 작용한다.
    • 즉, 각 응용 프로그램이 자신의 의존성과 갱신 관련 문제들을 처리한다.(보통은 자신이 참조하는 각 어셈블리의 복사본을 따로 두어서)
  • GAC는 컴퓨터 단위의 중앙집중적 버전 관리가 독자의 응용 프로그램에 실제로 이득이 되는 소수의 경우에 유용하다.
    • 예컨대 일단의 상호의존적 플러그인들이 어떤 공유 어셈블리들을 참조한다고 하자. 각 플러그인이 각자 개별적인 디렉터리에 들어 있다고 가정하면, 공유 어셈블리의 여러 복사본이 여러 디렉터리에 존재할 것이다. (어떤 복사본들은 버전이 서로 다를 수도 있다)
    • 더 나아가서, 효율성과 형식 호환성 떄문에 호스팅 응용 프로그램이 각각의 공유 어셈블리를 단 한 번만 적재하려 한다고 가정하자. 이런 상황에서는 호스팅 응용 프로그램이 특정 어셈블리의 정확한 위치를 파악하기가 쉽지 않다.
    • 이를 제대로 처리하려면 호스팅 응용 프로그램 작성자는 어셈블리 적재 문맥의 미묘한 세부사항을 숙지하고 계획을 신중하게 세워야 한다.
    • 이 경우 간단한 해결책은 그냥 공유 어셈블리들을 GAC에 두는 것이다. 그럼녀 CLR은 항상 어셈블리를 직접적이고 일관된 방식으로 찾아낸다.
  • 그러나 좀 더 전형적인 시나리오에서는 GAC를 피하는 것이 최선이다. GAC를 사용하면 다음과 같은 복잡한 문제들이 발생하기 때문이다.
    • XCOPY나 ClickOne 방식의 배치가 불가능하다. 응용 프로그램을 설치하려면 관리자 권한이 필요하다.
    • GAC 안의 어셈블리를 갱신하는데에도 관리자 권한이 필요하다.
    • GAC를 사용하면 개발과 검사가 복잡해진다. 융합(fusion)이라고 부르는 CLR의 어셈블리 환원 메커니즘은 항상 지역 복사본보다 GAC의 어셈블리를 우선시 하기 때문이다.
    • 버전 관리와 병행(side-by-side) 실행을 위해서는 세심한 계획이 필요하며, 실수를 저지르면 다른 응용 프로그램들의 작동이 실패할 수 있다.
  • 장점을 보자면, GAC를 사용하면 아주 큰 어셈블리의 시동 시간이 줄어들 수 있다. 이는 CLR이 GAC에 있는 어셈블리의 서명을 설치 시 한 번만 검증하기 떄문이다.(어셈블리를 적재할 때마다 검증하는 것이 아니라).
    • 확률적으로 만일 ngen.exe 도구로 어셈블리의 네이티브 이미지를 생성했다면, 그리고 생성 시 겹치지 않는 기준 주소들을 선택했담녀 이러한 성능 이득이 중요할 수 있다.
    • 이 문제에 관해서는 MSDN 블로그의 “To NGen or Not to NGen?”이라는 좋은 글이 도움이 될 것이다.
  • GAC에 있는 어셈블리들은 항상 완전히 신뢰된 상태이다. 권한이 제한된 모래상자에서 실행되는 어셈블리에서 호출하는 경우에도 그렇다.

어셈블리를 GAC에 설치하는 방법

  • 어셈블리를 GAC에 설치하려면 먼저 어셈블리에 강력한 이름을 부여해야 한다. 그런 다음에는 .NET의 명령줄 도구 gacutil로 어셈블리를 설치하면 된다.
gacutil /i MyAssembly.dll
  • 설치하려는 어셈블리와 공개키 및 버전이 같은 어셈블리가 이미 GAC에 있으면 그 어셈블리가 갱신된다. 기존 어셈블리를 먼저 제거할 필요가 없다.
  • 어셈블리의 설치를 제거하려면 다음과 같이 하면 된다. (파일 확장자를 지정하지 않음을 주의할 것)
gacutil /u MyAssembly
  • Visual Studio의 설치 프로젝트의 일부로 어셈블리의 GAC 설치를 지정할 수도 있다.
  • /l 옵션을 주어서 gacutil을 실행하면 GAC에 있는 모든 어셉를리가 나열된다.
  • 일단 어셈블리를 GAC에 집어 넣으면, 그 후부터는 응용 프로그램들이 어셈블리의 지역 복사본 없이도 어셈블리를 참조할 수 있다.
  • 사실 지역 복사본이 있어도 무시된다. CLR은 GAC에 있는 복사본을 우선시하기 때문이다.
    • 이 때문에 개발 도중 라이브러리를 다시 컴파일해서 바로 참조하거나 시험해 볼 방법이 없다. 어셈블리의 버전과 신원을 변경하지 않는 한 반드시 GAC에 있는 복사본을 갱신해야 한다.

GAC와 버전 관리

  • 어셈블리의 AssemblyVersion을 변경하면 신원 자체가 바뀐다.
    • 이해를 돕기 위한 예로 utils라는 어셈블리를 작성해서 버전을 ‘1.0.0.0’으로 설정한 후 GAC에 설치했다고 하자. 이후 몇 가지 기능을 추가해서 버전을 ‘1.0.0.1’로 변경한 후 다시 컴파일해서 GAC에 다시 설치하면 기존 어셈블리가 갱신되는 것이 아니라 새로운 어셈블리가 추가된다. 즉, GAC 이전 버전과 새 버전이 공존한다. 이는 다음 두 가지를 의미한다.
      • utils을 사용하는 다른 응용 프로그램을 컴파일 할 때 어떤 버전을 참조할 것인지 선택할 수 있다.
      • utils 버전 1.0.0.0을 참조하도록 컴파일된 기존 응용 프로그램들은 계속해서 버전 1.0.0.0을 참조한다.
  • 이를 어셈블리의 병행(side-by-side) 실행이라고 부른다. 병행 실행은 공유 어셈블리를 일방적으로 갱신했을 때 발생하는 소위 ‘DLL 지옥’을 방지한다.
    • DLL 지옥은 공유 라이브러리의 예전 버전에 맞게 설계된 응용 프로그램이 공유 라이브러리를 갱신한 후부터 예기치 않게 오작동하는 상황을 말한다.
  • 그러나 기존 어셈블리의 버그를 잡거나 사소한 개선을 적용하련느 경우에는 병행 실행이 오히려 걸림돌이 될 수 있다. 이 경우 선택은 다음 두 가지이다.
    • 갱신된 어셈블리를 이전과 같은 버전 번호로 컴파일해서 GAC에 재설치한다.
    • 갱신된 어셈블리를 새 버전 번호로 컴파일해서 GAC에 설치한다.
  • 첫째 방법의 난점은 갱신을 특정 응용 프로그램들만 선택적으로 적용할 방법은 없다는 것이다. 전부 적용하거나 아예 적용하지 않는 것뿐이다.
  • 둘째 방법의 난점은 보통의 경우 새 버전의 어셈블리를 사용하려면 응용 프로그램을 다시 컴파일해야 한다는 것이다. 다행히 이 문제에 대한 우회책이 존재한다.
    • 어셈블리 버전 재지정(redirection)을 허용하는 게시자 정책을 만들면 된다. 단 이렇게 하면 배치가 복잡해 진다.
  • 병행 실행은 공유 어셈블리의 몇 가지 문제점을 완화하는데 도움이 된다. 그러나 GAC를 아예 사용하지 않는다면 다시 말해 각 응용 프로그램이 utils의 복사본을 따로 두게 한다면, 공유 어셈블리의 모든 문제가 사라진다.

자원과 위성 어셈블리

  • 일반적으로 응용 프로그램의 구성에는 실행 가능한 코드 뿐만 아니라 텍스트나 이미지, XML 같은 비실행 내용도 포함된다. 그런 내용을 어셈블리 안의 자원(resource) 형태로 담아둘 수 있다. 다음은 자원이 두 가지 용도이다.(이 두 용도는 어느 정도 겹친다)
    • 이미지 등 소스 코드에 담을 수 없는 자료를 담는다.
    • 다언어 응용 프로그램의 번역에 필요할 수 있는 자료를 저장한다.
  • 본질적으로 어셈블리의 자원은 이름이 붙은 바이트 스트림이다. 어셈블리에 문자열이 키이고 바이트 배열이 값인 사전이 포함되어 있다고 생각하면 된다.
    • 실제로 예컨대 banner.jpg라는 이름의 자원과 data.xml이라는 이름의 자원이 있는 어셈블리를 ildasm 도구를 이용해서 역어셈블(disassemble)하면 다음과 같은 코드를 볼 수 있다.
.mresource public banner.jpg
{
  // Offset: 0x0000F58 Length: 0x00004F6
}

.mresource public data.xml
{
  // Offset: 0x00001458 Length: 0x000027E
}
  • 이 경우 banner.jpg와 data.xml은 어셈블리에 개별적인 자원 형태로 직접 내장되어 있다. 이는 자원의 가장 간단한 구성에 해당한다.
  • 이보다 좀 더 복잡한 구성으로, 중간 수준의 .resources 컨테이너에 자원들을 담아 둘 수도 있다. .resources 컨테이너는 원래 여러 언어로의 번역에 필요한 내용을 담는 용도로 고안된 것이다.
    • 그리고 각 언어에 맞게 지역화된 .resources들을 각각의 위성 어셈블리(satellite assembly)로 만들 수도 있다. 그러면 CLR은 사용자의 기본 언어에 기초해서 적절한 위성 어셈블리를 선택한다.
  • 아래 그림은 직접 내장된 자원 두 개와 welcome.resources라는 .resources 컨테이너 하나를 포함하는 주 어셈블리와 welcome.resources에 맞게 지역화된 두 위성 어셈블리를 나타낸 것이다.

자원을 어셈블리에 직접 내장

  • Windows 스토어 앱에서는 어셈블리에 자원을 직접 내장하는 방식을 사용할 수 없다. 대신 배포 및 설치 패키지에 개별적인 자원 파일들을 추가하고 응용 프로그램의 저장소 폴더에서 해당 파일을 읽어야 한다.
  • 명령줄에서 자원을 직접 내장하려면 캄파일 시 /resource 옵션으로 자원 파일들을 지정하면 된다.
csc /resource:banner.jpg /resource:data.xml MyApp.cs
  • 어셈블리 안의 자원이 해당 파일 이름과는 다른 이름이 되게 하려면 다음과 같이 하면 ㅁ된다.
csc /resource:<파일-이름>,<자원-이름>
  • 명령줄을 사용하지 않고 Visual Studio에서 직접 내장을 설정하려면 다음과 같이 한다.
    • 자원 파일을 프로젝트에 추가한다.
    • 그 파일의 속성에서 ‘빌드 작업’을 ‘포함 리소스’로 설정한다.
  • Visual Studio는 자원 이름 앞에 항상 프로젝트의 기본 이름공간과 해당 파일이 있는 하위 폴더 이름들을 붙인다. 예컨대 프로젝트의 기본 이름공간이 Westwind.Rports이고 자원 파일 이름이 banner.jpg이며 그 파일이 pictures에 들어 있다면 자원의 이름은 Westwind.Reports.picutres.banner.jpg가 된다.
    • 자원 이름은 대소문자를 구분한다. 따라서 Visual Studio에서 자원 파일을 담은 프로젝트 하위 폴더 이름들은 사실상 대소문자를 구분한다.
  • 프로그램에서 특정 자원을 가져오려면 그 자원을 담은 어셈블리에 대해 GetManifestResourceStream을 호출해야 한다. 그 메서드가 돌려주는 스트림을 다른 스트림과 마찬가지 방식으로 읽으면 된다.
Assembly a = Assembly.GetEntryAssembly();

using (Stream s = a.GetManifestRecourceStream("TestProject.data.xml"))
using (XmlReader r = XmlReader.Creates(s))
  ...

System.Drawing.Image image;
using (Stream s = a.GetManifestRecourceStream("TestProject.nabber.jpg"))
  image = System.Drawing.Image.FromStream(s);
  • 반환된 스트림은 탐색이 가능하므로, 다음과 같은 연산도 할 수 있다.
byte[] data;
using (Stream s = a.GetManifestRecourceStream("TestProject.nabber.jpg"))
  data = new BinaryReader(s).ReadBytes((int).s.Length);
  • Visual Studio를 이용해서 자원을 내장했다면 이름공간 기반 접두사도 반드시 지정해야 함을 기억하기 바란다. 오류를 피하는데 다음처럼 형식을 이용해서 접두사를 개별적인 인수로 지정하는 것이 도움이 된다. 이렇게 하면 주어진 형식의 이름공간이 접두사로 쓰인다.
using (Stream s = a.GetManifestRecourceStream(typeof(X), "XmlData.xml"))
  • 여기서 X는 자원의 이름공간에 속한 임의의 형식이다(보통은 같은 프로젝트 폴더에 있는 한 형식을 지정하면 된다)
  • WPF 응용 프로그램의 경우, Visual Studio에서 프로젝트 항목의 빌드 작업 속성을 ‘Resource’로 설정하는 것과 ‘포함 리소스’로 설정하는 것은 다르다.
    • 전자는 해당 항목을 <어셈블리_이름>.g.resources라는 하나의 .resources 파일로서 실제로 추가한다.
    • 프로그램 안에서는 WPF의 Application 클래스를 이용해서 그 파일의 내용에 접근할 수 있다(URI를 키로 사용해서)
    • 더욱 헷갈리는 것은 WPF에서 ‘resource’라는 용어가 여러가지 의미로 쓰인다는 점이다. static resource(정적 자원)과 dynamic resouce(동적 자원)은 둘 다 어셈블리의 자원과는 무관하다.
  • GetManifestResourceNames는 어셈블리에 있는 모든 자원의 이름을 돌려준다.

.resources 파일

  • .NET 프레임워크에서 .resources 파일은 지역화 가능성이 있는 내용을 담는 컨테이너로도 쓰인다. .resources 파일은 결국에는 어셈블리 안에 내장되는 하나의 자원이 된다. 이점은 다른 종류의 파일과 마찬가지이다. 다른 파일들과의 차이점을 들자면
    • 처음부터 내용을 .resources 파일에 넣어야 한다.
    • GetManifestResourceStream이 아니라 ResourceManager 또는 팩 URI를 통해서 내용에 접근한다.
  • .resources 파일은 이진 파일이므로 사람이 직접 편집할 만한 것이 아니다. 따라서 .NET Framework와 Visual Studio가 제공하는 도구를 사용해야 한다.
    • 문자열이나 단순 자료 형식을 담을 때의 표준적인 접근방식은 먼저 .resx 형식의 XML 파일을 작성한 후 그것을 Visual Studio나 resgen 도구를 이용해서 .resources 파일로 변환하는 것이다. .resx 파일은 Windows Forms 응용 프로그램이나 ASP.NET 응용 프로그램에 사용할 이미지들을 자원으로 만드는데에도 적합하다.
  • WPF 응용 프로그램에서 URI로 참조해야 하는 이미지나 그와 비슷한 자원 항목은 반드시 Visual Studio에서 빌드 작업 속성을 ‘Resource’로 설정해야 한다. 지역화가 필요하지 않은 자원에 대해서도 마찬가지이다.

.resx 파일

  • resx 파일은 .resources 파일을 만드는데 쓰이는 설계 시점 파일 형식이다. 구체적으로 .resx 파일은 다음같이 이름/값 쌍으로 구성된 XML 파일이다.
<root>
  <data name="Greeting">
    <value>hello</value>
  </data>
  <data name="DefaultFontSize" type="System.Int32, mscorlib">
    <value>10</value>
  </data>
</root>
  • Visual Studio에서 .resx 파일을 만드는 방법은 간단하다. ‘리소스 파일’ 형식의 새 프로젝트 항목을 추가하면 나머지는 자동으로 처리된다. 특히 Visual Studio는
    • 정확한 헤더를 만들어 주고
    • 문자열, 이미지, 파일, 기타 자료를 추가할 수 있는 디자이너를 제공하며
    • 컴파일시 .resx 파일을 자동으로 .resources 형식으로 반환해서 어셈블리에 내장하고
    • 이후 해당 자료에 접근하는데 도움이 되는 클래스도 작성해 준다.
  • 자원 편집용 디자이너는 이미지를 바이트 배열이 아니라 Image 형식의 객체로 추가한다. 그래서 WPF 응용 프로그램에는 적합하지 않다.

명령줄에서 .resx 파일을 .resource 파일로 변환

  • 명령줄을 사용하는 경우 일단은 유효한 헤더가 있는 .resx 파일이 있어야 한다. 가장 쉬운 방법은 프로그램으로 그런 .resx 파일을 생성하는 것이다. 다음처럼 System.Resources.ResXResourceWriter 클래스를 이용하면 간단히 해결된다. (이상하게도 이 클래스는 System.Windows.Forms.dll에 있다)
using (ResXResourceWriter w = new ResXResourceWriter("welcom.resx")) { }
  • 이때 using 블록 안에서 ResXResourceWriter의 AddResource 메서드로 자원들을 직접 추가할 수도 있고 아니면 생성된 .resx 파일을 텍스트 편집기로 직접 편집해도 된다.
  • 이미지를 다루는 가장 쉬운 방법은 이미지 파일을 이진 자료로 취급해서 자원에 추가하고, 조회시 바이트 배열을 다시 이미지로 복원하는 것이다.
    • 이는 구체적인 Image 형식의 객체로 저장/복원하는 것보다 더 유연한 방식이기도 하다. 이진 자료를 .resx 파일에 포함하는 방법은 두 가지이다.
    • 하나는 다음처럼 Base64로 부호화한 문자열을 사용하는 것이다.
<data name="flag.png" type="System.Byte[], mscorlib">
  <value>Qk32BAAAA....</value>
</data>
  • 또 하나는 이미지 파일에 대한 참조만 지정하는 것이다(나중에 resgen이 그 파일을 읽어서 .resources 파일에 추가한다)
<data name="flag.png" type="System.Resources.ResXFileRef, System.Windows.Forms">
  <value>flag.png;System.Byte[], mscorlib</value>
</data>
  • .resx 파일을 다 작성했으면 resgen 도구로 그 파일을 .resources 파일로 변환해야 한다. 다음은 welcome.resx를 welcome.resources로 변환하는 예이다.
resgen welcom.resx
  • 마지막으로 할 일은 컴파일 시 .resources 파일을 어셈블리에 내장하는 것이다. 다음과 같이 하면 된다.
csc /resource:welcom.resources MyApp.cs

.resources 파일 읽기

  • Visual Studio에서 .resx 파일을 만들면, Visual Studio는 각 자원 항목에 접근하는 속성들을 가진 같은 이름의 클래스를 자동으로 생성한다.
  • 어셈블리에 내장된 .resources 파일을 읽을 떄 사용하는 클래스는 ResourceManager이다.
    • (자원을 Visual Studio에서 컴파일했다면, 생성자의 첫 인수는 반드시 이름공간 접두사가 붙은 자원 이름이어야 한다)
ResourceManager r = new ResourceManager("welcome", Assembly.GetExecutingAssembly());
  • ResourceManager 인스턴스를 생성했다면, GetString이나 GetObject와 캐스팅을 이용해서 자원 항목을 얻을 수 있다.
string greeting = r.GetString("Greeting");
int fontSize = (int)r.GetObject("DefaultFontSize");
Image image = (Image)r.GetObject("flag.png");  // (Visual Studio)
byte[] imgData = (byte[])r.GetObject("flag.png");  // (명령줄)
  • .resources 파일의 내용을 나열하려면 다음과 같이 하면 된다.
ResourceManager r = new ResourceManager(...);
ResourceSet set = r.GetResourceSet(CultureInfo.CurrentUICulture, true, true);

foreach (System.Collections.DictionaryEntry entry in set)
  Console.WriteLine(entry.Key);

Visual Studio에서 팩 URI 자원 생성

  • WPF 응용 프로그램에서는 XAML 파일에서 URI로 자원을 참조한다. 예컨대 다음과 같다.
<Button>
  <Image Height="50" Source="flag.png"/>
</Button>
  • 다음은 다른 어셈블리에 있는 자원을 참조하는 예이다.
    • (Component는 리터럴 키워드이다)
<Button>
  <Image Height="50" Source="UtilsAssembly;Component/flag.png"/>
</Button>
  • 이런 식으로 참조, 적재되는 자원은 .resx 파일로는 만들 수 없다. 반드시 Visual Studio에서 자원 항목 파일들을 프로젝트에 추가하고 빌드 작업을 ‘Resource’로 (‘포함 리소스’가 아니라) 설정해야 한다.
    • 그러면 Visual Studio는 <어셈블리_이름>.g.resources라는 .resources 파일로 컴파일한다.
    • 이 .resources 파일에는 컴파일된 XAML 파일(.baml)들도 포함된다.
  • 프로그램 안에서 URI를 키로 해서 자원을 적재할 때는 Application.GetResourceStream 메서드를 사용한다.
Uri u = new Uri("flag.png", UriKind.Relative);
using (Stream s = Application.GetResourceStream(u).Stream)
  • 상대적 URI를 사용했음을 주목하기 바란다. 정확히 다음과 같은 형태의 절대적 URI를 사용할 수도 있다
    • (쉼표 세 개는 오타가 아니다)
Uri u = new Uri("pack://application:,,,/flag.png");
  • URI 기반 접근 대신, Assembly 객체와 ResourceManager 객체를 이용해서 내용을 조회하는 것도 가능하다.
Assembly a = Assembly.GetExecutingAssembly();
ResourceManager r = new ResourceManager(a.GetName().Name + ".g", a);
using (Stream s = r.GetStream("flag.png"))
  ...
  • ResourceManager를 이용하면 주어진 어셈블리 안에 있는 .g.resources 컨테이너의 내용을 열거할 수도 있다.

위성 어셈블리

  • .resources 안에 내장된 자료는 지역화(현지화)가 가능하다.
  • 모든 것을 개발용 컴퓨터의 것과는 다른 언어로 표시하도록 만들어진 Windows 버전에서 실행될 응용 프로그램을 만들 때는 자원의 지역화가 중요하다. 일관성을 위해서는 응용 프로그램 역시 해당 Windows와 같은 언어의 텍스트를 표시해야 한다.
  • 지역화를 위한 자원의 전형적인 설정은 다음과 같다.
    • 주 어셈블리에는 기본 언어 또는 대체(fallback) 언어를 위한 .resources를 담는다.
    • 다른 언어들로 번역된, 지역화된 .resources 파일들을 각각 개별적인 위성 어셈블리(satellite assembly)로 만든다.
  • 응용 프로그램 실행 시 .NET Framework는 운영체제의 현재 언어를 파악한다(CultureInfo.CurrentUICulture에서)
    • 프로그램에서 ResourceManager를 이용해서 어떤 자원을 요청하면 .NET Framework는 그에 맞게 지역화된 위성 어셈블리를 찾는다. 그런 위성 어셈블리가 있으면, 그리고 요청된 자원 키가 존재하면, 주 어셈블리의 버전 대신 해당 버전의 자료를 사용한다.
    • 이러한 작동 방식 덕분에 주 어셈블리를 변경하지 않고 그냥 새 위성 어셈블리를 추가하는 것만으로도 응용 프로그램의 언어 지원을 개선할 수 있다.
  • 위성 어셈블리에는 자원만 담을 수 있다. 실행 가능한 코드는 담을 수 없다.
  • 위성 어셈블리들은 어셈블리 폴더의 하위 폴더들에 다음과 같은 형태로 배치된다.
programBaseFolder\MyProgram.exe
programBaseFolder\MyLibrary.exe
programBaseFolder\XX\MyProgram.resources.dll
programBaseFolder\XX\MyLibrary.resources.dll
  • 여기서 XX는 영문자 두 자의 언어 코드(독일어는 “de”) 또는 언어 및 지역 코드(영국 영어는 “en-GB”)이다. 이러한 표준 명명 체계 덕분에 ClR은 정확한 위성 어셈블리를 자동으로 찾아서 적재한다.

위성 어셈블리의 구축

  • 앞에서 제시한 .resx 예제에 다음과 같은 부분이 있었다.
<root>
  ...
  <data name="Greeting">
    <value>hello</value>
  </data>
</root>
  • 그리고 이 문자열을 실행시점에서 다음과 같은 코드로 조회했다.
ResourceManager r = new ResourceManager("welcome", Assembly.GetExecutingAssembly());
Console.Write(r.GetString("Greeting"));
  • 이 코드를 독일어 버전 Windows에서 실행하면 “hello”가 아니라 “hallo”가 표시되게 하고 싶다고 하자.
    • 가장 먼저 할 일은 hello에 대입할 hallo를 담은 welcom.de.resx라는 또 다른 .resx 파일을 만드는 것이다.
<root>
  <data name="Greeting">
    <value>hallo</value>
  </data>
</root>
  • Visual Studio에서는 이 파일ㅇ르 프로젝트에 추가하기만 하면 모든 일이 끝난다. 이제 프로그램을 다시 빌드하면 자동으로 MyApp.resources.dll이라는 위성 어셈블리가 de라는 하위 디렉터리에 만들어진다.
  • 명령줄을 사용하는 경우에는 우선 해당 .resx 파일을 resgen을 이용해서 .resources 파일로 직접 변환하고
regen MyApp.de.resx
  • 그런 다음 al 도구를 이용해서 위성 어셈블리를 생성해야 한다.
al /cuture:de /out:MyApp.resources.dll /embed:MyApp.de.resources /t:lib
  • 주 어셈블리의 강력한 이름을 도입하려면 al 실행시 /template:MyApp.exe를 지정하면 된다.

위성 어셈블리 시험

  • 위성 어셈블리를 시험해 보기 위해 다른 언어를 사용하는 운영체제를 설치할 필요는 없다. 다음처럼 Thread 클래스를 이용해서 CurrentUICulture 속성을 변경하면 된다.
    • CultureInfo.CurrentUICulture는 이 속성의 읽기 전용 버전이다.
System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de");
  • 한 가지 유용한 시험 전략은 영어 단어를 표준 로마자가 아닌 유니코드 문자들로 지역화해 보는 것이다.

Visual Studio 디자이너 지원

  • Visual Studio의 디자이너들은 지금까지 말한 것 이외의 방식으로도 구성요소와 시각적 요소들의 지역활르 지원한다.
    • WPF 디자이너에는 독자적인 지역화 작업흐름(workflow)이 있다.
    • 그 외의 Component 기반 디자이너들은 설계 시점 전용 속성을 이용해서 어떤 구성요소나 Windows Forms 컨트롤에 Language 속성이 있는 것처럼 보이게 만든다.
    • 어떤 구성요소를 다른 언어에 맞게 커스텀화 하려면 그 Language 속성을 변경한 후 구성요소를 수정하면 된다.
    • 한 컨트롤의 속성 중 Localizable로 간주되는 모든 속성은 해당 언어의 .resx 파일에 기록된다.
    • Language 속성을 변경함녀 언제라도 다른 언어로 전환할 수 있다.

문화와 하위 문화

  • 문화권 설정은 문화 설정과 하위문화(subculture) 설정으로 나뉜다. 문화는 특정 언어를 나타내고, 하위문화는 그 언어의 지역적 변형을 나타낸다.
    • .NET Framewrok는 RFC1766 표준을 따르는데, 그 표준에서 문화와 하위문화는 영문자 두 자로 된 부호로 대표된다.
    • 예컨대 영어 문화와 독일어 문화의 영문 2자 부호는 en, de가 되고
    • 호주(Australia) 영어 하위문화와 오스트리아(Austria) 독일어 하위 문화는 en-AU, de-AT 가 된다.
  • .NET Framework 프로그램 안에서 문화권을 대표하는 클래스는 System.Globalization.CultureInfo이다. 응용 프로그램의 현재 문화권 설정은 다음과 같이 알아낼 수 있다.
Consoel.WriteLine(System.Threading.Thread.CurrentThread.CurrentCulture);
Consoel.WriteLine(System.Threading.Thread.CurrentThread.CurrentUICulture);
  • 이 코드를 호주 영어에 맞게 지역화된 컴퓨터에서 실행하면 두 속성의 차이가 드러난다.
EN-AU
EN-US
  • CurrentCultrue는 Windows 제어판에 있는 지역 설정을 반영하지만, CurrentUICulture는 운영체제의 언어를 반영한다.
  • 지역 설정(reginal settings)에는 시간대와 통화(화폐), 날짜 서식 등이 포함된다. CurrentCulture는 DateTime.Parse 같은 함수들의 기본 행동 방식을 결정한다. 지역 설정을, 그 어떤 특정 문화권과도 비슷하지 않을 정도로까지 커스텀화할 수도 있다.
  • CurrentUICulture는 컴퓨터가 사용자와 소통하는데 쓰이는 언어를 결정한다.
    • 예컨대 호주 사용자에게는 특화된 버전의 영어가 필요하지 않으므로, 호주용 Windows는 그냥 미국 영어를 사용한다.
    • 또 다른 예로 독일어를 모르는 미국 영어권 개발자가 오스트리아의 독일어 사용자를 위한 위성 어셈블리를 시험해 보는 동안 제어판에서 CurrentCultrue에 해당하는 설정을 오스트리아-독일어로 변경할 수 있지만, 독일어를 실제로 사용하는 것은 아니므로 CurrentUICulture는 그대로 미국 영어로 남겨두어야 할 것이다.
  • ResourceManager는 기본적으로 현재 스레드의 CurrentUICulture 속성에 기초해서 적절한 위성 어셈블리를 선택한다. 자원 적재 시 ResourceManager는 대체(fallback) 메커니즘을 사용한다.
    • 구체적으로 말하자면 만일 하위 문화 어셈블리가 정의되어 있으면 그것을 사용하고, 그렇지 않으면 일반적(generic) 문화권으로 후퇴한다. 일반적 문화권이 없으면 주 어셈블리의 기본 문화구너으로 후퇴한다.

어셈블리의 환원 및 적재

  • 전형적인 응용 프로그램은 주된 실행 파일 어셈블리와 응용 프로그램이 참조하는 일단의 라이브러리 어셈블리들로 구성된다. 이를테면 다음과 같다.
AdventureGame.exe
Terrain.dll
UIEngine.dll
  • 어셈블리 환원(assembly resolution)이란 응용 프로그램이 참조하는 라이브러리 어셈블리(이하 참조 어셈블리)를 찾는 과정을 말한다.
    • 컴파일 시점의 어셈블리 환원 과정은 아주 간단하다. 현재 디렉터리에 없는 참조 라이브러리의 위치를 컴파일 시 독자 또는 Visual Studio가 컴파일러에게 구체적으로 알려주므로 컴파일러는 그냥 주어진 위치의 라이브러리를 사용하면 된다.
  • 그러나 실행시점의 어셈블리 환원은 좀 더 복잡하다. 컴파일러는 참조 어셈블리의 강력한 이름을 매니페스트에 기록하지만, 그 어셈블리의 위치에 관해서는 아무 것도 기록하지 않는다.
    • 간단한 예로는 참조 어셈블리들을 모두 주 실행 파일과 같은 폴더에 넣으므로 문제가 되지 않는다. 그곳이 바로 CLR이 가장 먼저 살펴보는 장소(에 가까운 곳)이기 때문이다.
    • 그러나 다음과 같은 경우에는 문제가 복잡해진다.
      • 참조 어셈블리들을 다른 장소에 배치했을 때
      • 프로그램에서 어셈블리를 동적으로 적재할 때
  • Window 스토어 앱은 어셈블리의 환원 및 적재와 관련해서 커스텀화 할 수 있는 부분이 아주 적다. 특히 Window 스토어 앱은 임의의 파일 위치에서 어셈블리를 적재하는 기능을 제공하지 않으며 AssemblyResolve 이벤트도 없다.

어셈블리와 형식 환원 규칙

  • 모든 형식은 어셈블리에 속한다. 어셈블리는 형식의 주소와도 같다. 비유하자면 어떤 사람을 ‘조’라고 칭할 수도 있고, ‘조 블로그’ 라고 칭할 수도 있고, ‘워싱턴 주 베이커가 100번지 사는 조 블로그’라고 칭할 수도 있다.
  • 컴파일 과정에서는 전체 형식 이름(full type name)만으로도 주어진 형식을 고유하게 식별할 수 있다.
    • 어차피 한 프로그램에서 같은 전체 형식 이름을 정의하는 두 어셈블리를 참조할 수는 없기 때문이다.(특별한 요령을 이용하면 가능하긴 하지만 그것은 논외이다)
    • 그러나 실행시점에서는 전체 형식 이름이 같은 형식들이 메모리에 여러 개 들어 있을 수 있다.
    • 예컨대 Visual Studio 디자이너에서 설계 중인 구성요소들을 재빌드 할 때마다 그런 일이 생긴다. 그런 형식들은 해당 어셈블리를 봐야 구분할 수 있다.
    • 그런 점에서 어셈블리는 형식의 실행시점 신원(identity)을 구성하는 필수 요소이다. 또한 어셈블리는 형식의 코드와 메타자료에 접근하기 위한 관문이기도 하다.
  • CLR은 프로그램 실행 도중 어셈블리가 처음으로 필요해진 시점에서 어셈블리를 적재한다. 간단히 말하면, 그 시점은 프로그램에서 어셈블리에 속한 한 형식을 지칭하는 시점이다.
    • 예컨대 AdventureGame.exe가 TerrainModel.Map이라는 형식의 인스턴스를 생성한다고 하자. 참고할 만한 다른 구성 파일이 전혀 없다고 가정할 때, CLR은 다음 두 질문의 답을 얻어야 한다.
      • AdventureGame.exe를 컴파일할 당시 TerrainModel.Map을 담고 있던 어셈블리의 완전 한정 이름은 무엇인가?
      • 동일한 문잭(해소 문맥)에서 같은 완전 한정 이름의 어셈블리를 이미 메모리에 적재하지는 않았는가?
    • 만일 같은 어셈블리를 적재한 적이 있다면 CLR은 메모리에 있는 복사본은 사용하고, 그렇지 않으면 해당 어셈블리를 실제로 찾는다.
    • CLR은 우선 GAC를 점검하고, 그런 다음 탐색 경로(probing path)들을 살펴보고(보통은 응용 프로그램의 기준 디렉터리를 검색한다), 최후의 보루로 AppDomain.AssemblyResolve 이벤트를 발동한다.
    • 만일 어떤 방법으로도 어셈블리를 찾지 못하면 CLR은 예외를 던진다.

AssemblyResolve 이벤트

  • AssemblyResolve 이벤트는 CLR이 찾지 못한 어셈블리의 위치를 프로그램이 직접 알려주는 수단이다.
    • 예컨대 프로그램이 참조하는 라이브러리 어셈블리들이 여러 비표준 장소에 배치되어 있는 경우 프로그램에서 이 이벤트를 적절히 처리함으로써 그 어셈블리들이 제대로 적재되게 할 수 있다.
  • AssemblyResolve 이벤트 처리부에서 할 일은 요청된 어셈블리를 찾아서 Assembly 클래스의 세 정적 메서드 Load, LoadFrom, LoadFile 중 하나를 호출하는 것이다.
    • 이 메서드들은 새로 적재된 어셈블리에 대한 참조를 돌려주는데, 그것을 이벤트 처리부를 호출한 쪽에 반환하면 된다.
static void Main()
{
  AppDomain.CurrentDomain.AssemblyResolve += FindAssembly;
  ...
}

static Assembly FindAssembly(object sender, ResolveEventArgs args)
{
  string fullyQualifiedName = args.Name;
  Assembly a = Assembly.LoadFrom(...);
  return a;
}
  • AssemblyResolve 이벤트는 반환 형식이 있다는 점이 특이하다. 만ㅇ리 이 이벤트에 대한 처리부가 여러 개인경우 CLR은 가장 먼저 반환된 널이 아닌 Assembly 참조를 사용한다.

어셈블리 적재

  • Assembly의 Load 메서드는 AssemblyResolve 이벤트 처리부 안에서는 물론 밖에서도 유용하다. 이벤트 처리부 밖에서 이 메서드는 컴파일 시점에서 참조하지 않은 어셈블리를 적재, 실행하는 용도로 쓰인다.
    • 이를테면 플러그인을 실행할 때 그런 접근방식이 유용하다.
  • Load나 LoadFrom, LoadFile 메서드는 신중하게 호출해야 한다. 이 메서드들은 어셈블리를 현재 응용 프로그램 도메인에 영구적으로 적재한다. 메서드가 반환한 Assembly 객체로 아무것도 하지 않아도 어셈블리는 도메인에 계속 남는다.
    • 또한 어셈블리의 적재에는 부수효과가 있다. 어셈블리를 적재하면 어셈블리 파일이 잠기며, 이후 형식 해소에도 영향을 미친다.
    • 일단 적재된 어셈블리는 응용 프로그램 도메인 전체를 해제해야 해제된다.
  • 구체적인 위치를 지정하지 않고 완전 한정 이름만으로 어셈블리를 적재할 때에는 Assembly.Load를 사용한다. 이 메서드를 호출하면 CLR은 통상적인 자동 해소 시스템을 이용해서 해당 어셈블리를 찾는다.
    • 참조 어셈블리를 찾을 때 CLR이 사용하는 것도 바로 이 Load이다.
  • 파일 이름으로 어셈블리를 적재할 때는 LoadFrom이나 LoadFile을 사용한다.
  • URI로 어셈블리를 적재할 때는 LoadFrom을 사용한다.
  • 바이트 배열로 부터 어셈블리를 적재할 때는 Load를 사용한다.
  • 메모리에 현재 적재된 어셈블리들은 AppDomain의 GetAssemblies 메서드로 알아낼 수 있다.
foreach (Assembly a in AppDomain.CurrentDoamin.GetAssemblies())
{
  Console.WriteLine(a.Location);  // 파일 경로
  Console.WriteLine(a.CodeBase);  // URI
  Console.WriteLine(a.GetName().Name);  // 단순명
}

파일 이름으로 어셈블리 적재

  • LoadFrom과 LoadFile 둘 다 주어진 파일 이름에 해당하는 어셈블리를 적재한다. 둘의 차이는 두 가지이다.
    • 첫째로 신원이 같고 위치가 다른 어셈블리가 이미 메모리에 적재되어 있으면 LoadFrom 은 기존 복사본을 돌려준다. 반면 LoadFile은 새 복사본은 돌려준다.
Assembly a1 = Assembly.LoadFrom(@"c:\temp1\lib.dll");
Assembly a2 = Assembly.LoadFrom(@"c:\temp2\lib.dll");
Console.WriteLine(a1 == a2);  // true
Assembly a1 = Assembly.LoadFile(@"c:\temp1\lib.dll");
Assembly a2 = Assembly.LoadFile(@"c:\temp2\lib.dll");
Console.WriteLine(a1 == a2);  // false
  • 동일한 위치의 어셈블리를 두 번 적재하면 두 메서드는 모두 캐시에 있는 기존 복사본을 돌려준다. (반면, 동일한 바이트 배열로부터 어셈블리를 두 번 적재하면 서로 다른 두 Assembly 객체가 반환된다.)
  • 메모리에 있는 동일한 두 어셈블리의 형식들은 서로 호환되지 않는다. 이는 어셈블리를 중복해서 적재하는 것이 바람직하지 않은, 따라서 LoadFile 보다는 LoadFrom을 선호해야 하는 이유이다.
  • LoadFrom과LoadFile의 두 번째 차이점은, LoadFrom은 이후의 참조들에 대한 위치를 CLR에게 귀띔해 주는 반면 LoadFile은 그렇지 않다는 점이다.
    • 예컨대 \folder1에 있는 응용 프로그램이 \folder2에 있는 TestLib.dll이라는 어셈블리를 적재하며, TestLib.dll 자체는 \folder2\Another.dll을 참조한다고 하자.
\folder1\MyApplication.exe
\folder2\TestLib.dll
\folder2\Another.dll
  • 만일 LoadFrom으로 TestLib를 적재하면 이후 CLR은 Another.dll을 찾아서 적재한다. 그러나 LoadFile로 TestLib를 적재하면 CLR은 Another.dll을 찾지 못해서 예외를 던진다 (AssemblyResolve 이벤트를 처리하지 않는 한)

형식의 정적 참조와 LoadFrom/LoadFile

  • 코드에서 어떤 형식을 직접 지칭하는 것을 가리켜서 형식을 ‘정적으로 참조한다’라고 말한다.
    • 정적 참조의 경우 컴파일러는 그 형식에 대한 참조와 그 형식이 속한 어렘블리의 이름을 어셈블리(컴파일 중인)에 명시적으로 박아 넣는다(그러나 실행시점에서 그 어셈블리를 찾을 수 있는 위치에 관한 정보는 전혀 기록하지 않는다)
  • 예컨대 foo.dll이라는 참조 어셈블리에 Foo라는 형식이 있다고 하자. 그리고 응용 프로그램이 bar.exe에 다음과 같은 코드가 있다고 하자.
var foo = new Foo();
  • 이 경우 bar.exe 응용 프로그램은 foo 어셈블리의 Foo 형식을 정적으로 참조한다. 이와는 달리, foo를 다음과 같이 동적으로 적재할 수도 있다.
Type t = Assembly.LoadFrom(@"d:\temp\foo.dll").GetType("Foo");
var foo = Activator.CreateInstance(t);
  • 만일 한 프로그램에서 이 두 접근 방식을 섞어 사용하면 메모리에 같은 어셈블리의 복사본이 두 개 남게 된다. CLR은 두 어셈블리가 서로 다른 ‘해소 문맥’에 속한다고 간주하기 때문이다.
  • 앞서 말했듯이 정적 참조를 해소할 때 CLR은 먼저 GAC를 보고, 그런 다음 탐색 경로(보통은 응용 프로그램 기준 디렉터리)를 찾고, 거기에도 없으면 AssemblyResolve 이벤트를 발동한다.
    • 그런데 이런 탐색을 시작하기 전에 CLR이 제일 먼저 하는 일은 어셈블리가 이미 적재되어 있는지 점검하는 것이다. 단 CLR은 다음과 같은 어셈블리들에 대해서만 그러한 점검을 수행한다.
      • CLR이 직접찾을 수 있었을 경로(탐색 경로)에서 적재한 어셈블리
      • AssemblyResolve 이벤트에 대한 응답으로 적재된 어셈블리
  • 따라서 만일 탐색 경로 이외의 경로에 있는 어셈블리를 LoadFrom이나 LoadFile로 적재해 두었다면, 그 어셈블리는 이미 적재된 것으로 간주되지 않으며, 따라서 같은 어셈블리를 적재하면 메모리에 복사본 두 개가 생긴다(그리고 그 둘의 형식들은 호환되지 않는다.)
    • 이를 피하려면 LoadFrom이나 LoadFile을 호출할 때 우선 어셈블리가 응용 프로그램 기준 디렉터리에 존재하는지부터 점검해야 한다.
  • AssemblyResolve 이벤트 처리부에서 어셈블리를 적재할 때는(LoadFrom을 사용하든 아니면 LoadFile을 사용하든, 심지어는 바이트 배열을 적재하든) 이런 문제가 생기지 않는다. 그 이벤트는 탐색 경로 바깥의 어셈블리에 대해서만 발동하기 때문이다.
  • LoadFrom을 사용하든 아니면 LoadFile을 사용하든 CLR은 항상 제일 먼저 GAC에서 어셈블리를 찾는다. GAC 탐색을 피하고 싶으면 ReflectionOnlyLoadFrom 메서드를 사용하면 된다. (이 메서드는 어셈블리를 반영 전용 문맥(reflection-only context)으로 적재한다)
    • 바이트 배열에서 어셈블리를 적재할 때도 GAC 탐색이 일어나는 것은 마찬가지지만 대신 어셈블리 파일이 잠기는 문제는 없다.
byte[] image = File.ReadAllBytes(assemblyPath);
Assembly a = Assembly.Load(image);
  • 이 방법을 사용하는 경우 적재된 라이브러리 자체가 참조하는 어셈블리들이 제대로 해소되게 하려면 그리고 적재된 모든 어셈블리를 관리하려면 반드시 AppDomain의 AssemblyResolve 이벤트를 처리해야 한다.

Location 속성 대 CodeBase 속성

  • 보통의 경우 Assembly의 Location 속성은 파일 시스템에 있는 어셈블리의 물리적 위치를 돌려준다. 한편 CodeBase 속성은 그 위치의 URI 버전을 돌려주는데, 몇 가지 예외가 있다.
    • 예컨대 인터넷에서 어셈블리를 적재하는 경우 CodeBase는 인터넷 URI이고 Location은 내려받은 어셈블리 파일의 임시 경로이다.
    • 또 다른 예외는 그림자 복사(shadow-copying)가 적용된 어셈블리이다. 이 경우 Location은 빈 값이고 CodeBase는 그림자 복사 적용 전의 원본 위치이다.
    • ASP.NET과 유명한 NUnit 검사 프레임워크는 웹사이트나 단위 검사를 실행하는 도중에 어셈블리를 갱신하는 목적으로 그림자 복사를 사용한다
    • LINQPad도 사용자의 코드가 커스텀 어셈블리를 참조할 때 비슷한 기법을 적용한다.
  • 따라서 Location만으로 디스크 상의 어셈블리 위치를 찾는 것은 위험한 일이다. 두 속성을 모두 점검하는 것이 더 낫다. 다음 메서드는 어셈블리가 담긴 폴더를 돌려준다(그런 폴더를 결정할 수 없으면 널을 돌려준다)
public static string GetAssemblyFolder(Assembly a)
{
  try
  {
    if (!string.IsNullOrEmpty(a.Location))
      return Path.GetDirectoryName(a.Location);

    if (string.IsNullOrEmpty(a.CodeBase)) return null;

    var uri = new Uri(a.CodeBase);
    if (!uri.IsFile) return null;

    return Path.GetDirectotyName(uri.LocalPath);
  }
  catch (NotSupportException)
  {
    return null;  // Reflection.Emit으로 생성된 동적 어셈블리의 경우
  }
}
  • CodeBase가 URI를 돌려주므로 이 메서드는 Uri 클래스를 이용해서 지역 파일 경로를 얻는다는 점도 참고하리 바란다.

기준 폴더 바깥에 어셈블리 배치

  • 어셈블리들을 응용 프로그램의 기준 디렉터리 바깥에 배치하는 것이 바람직한 경우도 종조 ㅇ있다. 다음은 그러한 구성의 예이다.
..\MyProgram\Main.exe
..\MyProgram\Libs\V1.23\GameLogic.dll
..\MyProgram\Libs\V1.23\3DEngine.dll
..\MyProgram\Terrain\Map.dll
..\Common\TimingController.dll
  • 이런 구성이 제대로 작동하려면 응용 프로그램이 CLR에게 기준 폴더 바깥에 있는 어셈블리들을 찾는데 필요한 정보를 제공해야 한다. 가장 간단한 방법은 AssemblyResolve 이벤트를 처리하는 것이다.
  • 다음 예제는 모든 추가 어셈브릴가 c:\ExtraAsemblies에 있다고 가정한다.
class Loader
{
  static void Main()
  {
    AppDomain.CurrentDomain.AssemblyResolve += FindAssembly;

    // c:\ExtraAssemblies에 있는 임의의 형식을 사용하려면 먼저 이 클래스 바깥으로 나가야 한다.
    Program.Go();
  }
  
  static Assembly FindAssembly(object sender, ResolveEventArgs args)
  {
    string simpleName = new AssemblyName(args.Name).Name;
    string path = @"c:\ExtraAssemblies\" + simpleName + ".dll";

    if (!File.Exists(path)) return null;  // 파일이 있는지 점검하고
    return Assembly.LoadFrom(path);  // 적재한다.
  }
}

class Program
{
  internal static void Go()
  {
    // 이제 c:\ExtraAssemblies에 정의된 형식들을 참조할 수 있다.
  }
}
  • c:\ExtraAssemblies의 형식들을 Loader 클래스 안에서 직접 참조(이를테면 속성 접근 등) 하지 않는 것이 대단히 중요하다. Loader 클래스에서 그런 형식들을 참조하면 CLR은 Main()에 도달하기 전에 형식을 해소하려 들 것이기 때문이다.
  • 이 예제는 LoadFrom을 사용했지만, LoadFile을 사용해도 결과에는 차이가 없다. 어떤 경우이든 CLR은 프로그램이 제시한 위치에 있는 어셈블리의 신원이 요청된 어셈블리의 신원과 같은지 점검한다. 이 덕분분에 강력한 이름 참조의 무결성이 유지된다.

단일 실행 파일 만들기

  • 어떤 응용 프로그램이 어셈블리 10개로 구성된다고 하자. 하나는 주 실행 파일이고 나머지 9개는 참조 어셈블리 DLL이다.
    • 이런 식으로 파일들을 나누면 설계와 디버깅이 편하지만, 때에 따라서는 사용자가 어떤 설치 과정이나 파일 추출 과정을 수행할 필요 없이 그냥 ‘클릭하면 실행되는(click and run)’ 하나의 실행 파일로 모든 것을 합치는 것이 바람직할 떄도 있다.
    • 그런 실행 파일을 만드는 방법은 컴파일된 어셈블리 DLL들을 모두 주 실행 파일 프로젝트에 자원으로서 내장하고, 각 어셈블리 이진 이미지를 필요에 따라 적재하는 AssemblyResolve 이벤트 처리부를 주 프로그램에 추가하는 것이다.
    • 다음이 그러한 주 실행 파일의 예이다.
class Loader
{
  static Dictionary<string, Assembly> _libs = new Ditionary<string, Assembly>();

  static void Main()
  {
    AppDomain.CurrentDomain.AssemblyResolve += FindAssembly;
    Program.Go();
  }
  
  static Assembly FindAssembly(object sender, ResolveEventArgs args)
  {
    string shortName = new AssemblyName(args.Name).Name;
    if (_libs.ContainsKey(shortName)) return _libs[shortName];

    using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourcesStream("Libs." + shortName + ".dll"));
    {
      byte[] data = new BinaryReader(s).ReadBytes((int)s.Length);
      Assembly a = Assembly.Load(data);
      _libs[shortName] = a;
      return a;
    }
  }
}

public class Program
{
  public static void Go()
  {
    // 주 프로그램을 실행한다..
  }
}
  • Loader 클래스가 주 실행 파일에 정의되어 있으므로 Assembly.GetExecutingAssembly 호출은 항상 주 실행 파일 어셈블리를 돌려준다. 그리고 그 주 시랳ㅇ 파일 어셈블리에는 컴파일된 DLL들이 자원으로서 내장되어 있다.
    • 이 예제에서는 내장된 각 자원 어셈블리의 이름에 “Libs.”라는 접두사를 붙였다. Visual Studio IDE를 사용하는 경우에는 “Libs.”를 프로젝트의 기본 이름공간으로 바꾸어야 할 것이다.
    • 또한 IDE에서 주 프로젝트에 내장하는 각 DLL 파일의 ‘빌드 작업’을 ‘포함 리소스’로 설정해야 한다.
  • 이 예제는 요청된 어셈블리들을 사전에 캐싱하는데, 이는 CLR이 같은 어셈블리를 다시 요청하는 경우 정확히 같은 어셈블리 객체를 돌려주기 위한 것이다.
    • 그렇게 하지 않으면 어셈블리의 형식들이 이전에 적재된 어셈블리의 같은 형식들과 호환되지 않는다
  • 이와는 조금 다른 방식으로 컴파일시 참조 어셈블리들을 압축하고, 어셈블리 적재시 FindAssembly에서 DeflateStream을 이용해서 압축을 해제할 수도 있다.

선택적 패치 적용

  • 이번에는 실행 파일이 자동으로 자신을 갱신하는 기능을 구현한다고 하자(이를테면 네트워크 서버나 웹사이트에서 새 버전을 내려받아서)
    • 실행 파일을 직접 새 버전으로 교체하는 것은 까다롭고 위험할 뿐만 아니라, 추가적인 파일 입출력 권한이 필요할 수도 있다(특히 프로그램을 Program Files 폴더에 설치한 경우)
    • 멋진 우회책 하나는, 갱신된 라이브러리들을 격리된 저상소에 내려받고(각 라이브러리를 개별적인 DLL로서) FindAssembly에서 주 실행 파일의 자원으로부터 어셈블리를 적재하기 전에 먼저 격리된 저장소에 새 버전이 있는지 점검해서 새 버전이 있으면 그것을 적재하는 것이다.
    • 이렇게 하면 원래의 실행 파일은 갱신할 필요가 없으며, 사용자의 컴퓨터에 불쾌한 찌꺼기 파일들을 남기지 않아도 된다.
    • 어셈블리들에 강력한 이름을 부여했다면 보안 구명도 생기지 않는다(그 어셈블리들을 컴파일 시 참조한다고 가정할 때)
    • 그리고 뭔가가 잘못되었을 때 응용 프로그램을 즉시 원래의 상태로 되돌릴 수 있다. 그냥 격리된 저장소의 모든 파일을 삭제하면 된다.

참조되지 않은 어셈블리 다루기

  • 컴파일 시 참조하지 않은 .NET 어셈블리를 명시적으로 적재하는 것이 유용한 때도 있다.
    • 실행 파일 어셈블리를 그런 식으로 적재해서 실행하는 경우엔느 그냥 현재 응용 프로그램 도메인에서 ExecuteAssembly를 호출하면 그만이다.
    • ExecuteAssembly는 LoadFrom 의미론을 이용해서 실행 파일을 적재한 후 진입점 메서드를 호출한다(필요하다면 명령줄 인수들과 함께). 예컨대 다음과 같다.
string dir = AppDomain.CurrentDomain.BaseDirectory;
AppDomain.CurrentDoamin.ExecuteAssembly(Path.Combine(dir, "test.exe"));
  • ExecuteAssembly는 동기적으로 작동한다. 즉, 호출한 메서드는 호출된 어셈블리가 종료될 때까지 차단된다. 어셈블리를 비동기적으로 실행하려면 다른 스레드나 작업 객체에서 ExecuteAssembly를 호출해야 한다.
  • 그러나 대부분의 경우 적재하고자 하는 어셈블리는 라이브러리 어셈블리이다. 그런 경우에는 LoadFrom을 호출해서 어셈블리 객체를 얻을 후 반영 기능을 이용해서 원하는 형식을 사용하면 된다. 다음이 그러한 예이다.
string outDir = AppDomain.CurrentDomain.BaseDirectory;
string plugInDir = Path.Combine(outDir, "plugins");
Assembly a = Assembly.LoadFrom(Path.Combine(plugInDir, "widget.dll"));
Type t = a.GetType("Namespace.TypeName");
object widget = Activator.CreateInstace(t);
...
  • 이 예는 LoadFile 대신 LoadFrom을 사용하는데, 이는 widget.dll이 참조하는 그리고 widget.dll과 같은 폴더에 있는 다른 전용 어셈블리들도 적재되게 하기 위한 것이다. 어셈블리를 적재한 후에는 형식 이름을 이용해서 어셈블리의 형식을 인스턴스화 한다.
  • widget 인스턴스를 얻었다면 그 속성과 메서드를 호출하고 싶을 것이다. 이 역시 반영 기능을 이용하면 되는데, 자세한 방법은 다음 장에서 설명한다. 그보다 더 쉽고 빠른 접근방식은 그 인스턴스를 주 어셈블리와 적재된 어셈블리가 모두 이해하는 형식으로 캐스팅하는 것이다. 이를 위해 공통어셈블리에 그런 용도의 인터페이스를 정의해 두는 방법이 흔히 쓰인다.
public interface IPluggable
{
  void ShowAboutBox();
  ...
}
  • 이런 인터페이스가 있으면 다음과 같은 코드가 가능하다.
Type t = a.GetType("Namespace.TypeName");
IPluggable widget = (IPluggable)Activator.CreateInstace(t);
widget.ShowAboutBox();
  • WCF나 Remoting 서버에서 서비스를 동적으로 게시할 때도 이와 비슷한 시스템을 사용할 수 있다. 다음 예제는 노출하고자 하는 라이브러리들의 이름이 ‘Server’로 끝난다고 가정한다.
string dir = AppDomain.CurrentDomain.BaseDirectory;
foreach (string assFile in Directory.GetFiles(dir, "*Server.dll"))
{
  Assembly a = Assembly.LoadFrom(assFile);
  foreach (type t in a.GetTypes())
  {
    if (typeof (MyBaseServerType).IsAssignableFrom(t))
    {
      // 형식 t를 노출한다.
    }
  }
}
  • 그러나 이러한 소박한 구현에서는 누군가가 실수로 또는 불순한 의도로 잘못된 어셈블리를 추가하기가 아주 쉽다.
    • 컴파일 시점에서 참조한 걱이 없는 어셈블리의 경우 실행시점에서 그 어셈블리의 신원을 점검하는데 사용할 정보가 CLR에게 전혀 주어지지 않는다.
    • 만일 프로그램이 적재하는 모든 어셈블리를 하나의 알려진 공개키로 서명했다면, 해결책(잘못된 어셈블리 추가를 막는)은 어셈블리 적재 전에 그 공개키를 명시적으로 점검하는 것이다.
    • 다음 예제는 모든 라이브러리가 실행 파일 어셈블리와 같은 키 쌍으로 서명되었다고 가정한다.
byte[] ourPK = Assembly.GetExecutingAssembly().GetName().GetPublicKey();

foreach (string assFile in Directory.GetFiles(dir, "*Server.dll"))
{
  byte[] targetPK = AssemblyName.GetAssemblyName(assFile).GetPublicKey();
  if (Enumerable.SequenceEqual(ourPK, targetPK))
  {
    Assembly a = Assembly.LoadFrom(assFile);
    ...
  • AssemblyName을 이용하면 어셈블리를 실제로 적재하기 전에 그 공개키를 점검할 수 있음을 주목하기 바란다. 이진 배열들의 상등 비교에는 LINQ의 SequenceEqual 메서드를 사용했다.
[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.