Assembly 라고 하면 맨 처음 생각 나는게 뭔가요? 저는 Assembly 언어가 생각이 납니다. 기계어와 Assembly
언어. 요사이는 Assembly 언어로 프로그램을 작성하는 분이 거의 없을 거라고 보지만 예전에 PC 사양이 많이 안 좋았던
시절에는 컴파일러의 최적화 성능도 그리 좋지 않았기 때문에 최적화된 코드로 빠른 속도를 내기 위해서 많은 부분을 직접
Assembly 언어로 구현했다는 이야기를 많이 들었었지요. 참 대단합니다. ㅎㅎ
아.. 이야기가 딴 길로 샙니다. 지난 주에 Metadata를 까보면서 넘어갔던 부분이 몇 개 있었는데요. 그 중의 하나가 Assembly 였지요. 오늘은 이 Assembly에 대해 공부해 보려고 합니다.
Assembly?
Assembly 는 뭔가요? 지난 주에 저희가 만들었던 hi.exe 도 어셈블리입니다. 그럼 어셈블리는 실행 파일인가요? 틀린 말은 아니지만 좀 더 정확히 정의하자면
Assembly는 타입이나 리소스를 포함한 하나 이상의 파일들이 모인 파일의 컬렉션 이라고 CLR Via c# 2nd 한글판에서는 이야기하고 있습니다. 물론 일반적으론 하나의 파일로 구성되는 경우가 대부분입니다. Assembly를 논리적인 DLL 또는 EXE 파일로 이해하면 될 것 같습니다.
이 Assembly 파일이 갖고 있는 정보 중의 하나는 Manifest라고 하는데 이 Manifest에는 Assembly의
버전과 컬처, 배포자, public 형태의 타입, Assembly를 구성하는 모든 파일의 정보를 갖고 있습니다. CLR은 이
Assembly를 로드하여 지난주에 살펴본 Metadata 테이블을 읽은 후, Manifest에 있는 다른 파일들의 정보를 읽게
됩니다.
좀 어렵네요. 왜 이런 Assembly를 사용하는가. 를 먼저 살펴봅시다. Assembly는 다음과 같은 특징을 갖습니다.
* 개발된 코드를 여러 파일로 분산하여 실행 시 최소한의 파일로 구성한 다음, 필요한 파일이 생길 경우 인터넷 상으로
다운로드하는 식의 효과적인 배포가 가능합니다. (어플리케이션 설정 파일의 codebase 항목에서 설정된 곳을 통해 필요한
Assembly를 다운로드 할 수 있도록 설정이 가능합니다.)
* Assembly에 리소스나 데이터 파일을 추가할 수 있습니다. 이 데이터 파일의 형식은 어플리케이션이 파싱하여 이해할 수 있는 파일이라면 제약이 없습니다.(예 : 오피스 워드 파일, 엑셀 파일 등등)
* 서로 다른 언어로 구현된 타입(클래스)의 구성으로 Assembly를 생성하는 것이 가능합니다. 전체 모듈 중 일부는 C#으로
다른 일부는 Visual Basic으로 구현하는 것이 가능합니다. 어떤 언어로 작성되었던 컴파일 하게 되면 같은 IL코드로
생성이 되어 있으며 Assembly 테이블을 참조하면 해당 모듈을 재사용하는데 필요한 모든 정보를 얻을 수 있기 때문입니다.
Assembly는 Manifest Metadata 테이블이라는 것을 가지고 있습니다. 테이블 리스트는 다음과 같습니다.
테이블 이름 |
설명 |
Assembly |
Assembly를 식별하는 단일 엔트리를 가집니다. 이 엔트리에는 어셈블리의 이름, 버전, 컬처, 플래그, 해시 알고리즘, 발행자의 공용키를 포함합니다. |
File |
Assembly의
부분인 각 PE 파일과 리소스 파일을 위한 항목을 갖고 있습니다. 엔트리에는 파일의 이름, 확장자, 해시, 플래그를 포함합니다.
만일 Assembly가 오직 자신 하나의 파일만으로 구성된다면 이 테이블이 갖고 있는 엔트리는 없습니다. |
ManifestResource |
각 리소스를 위한 항목을 가집니다. 엔트리는 리소스 이름, 플래그(public, private)를 포함합니다. |
ExportedTypes |
Assembly의 PE 모듈로부터 노출된 각 공용 타입을 위한 항목을 가집니다. 엔트리는 타입 이름, File 테이블의 인덱스, 그리고 TypeDef 테이블의 인덱스를 포함합니다. |
이러한 manifest를 정의함으로써 Assembly의 파일 구성에 대한 정보를 제공하는 것이 가능하고 어셈블리 스스로 설명하는 것이 가능해집니다.
컴파일러 옵션에서 /t 의 값으로 /t[arget]:exe, /t[arget]:winexe, /t[arget]:library 를
주게 되면 컴파일러는 manifest정보를 포함한 단일 파일의 Assembly를 생성하게 됩니다. 이는 각각 CUI 실행 파일,
GUI 실행 파일, DLL 라이브러리 입니다.
이외에도 /t[arget]:module 이라는 옵션도 있습니다. 이 옵션을 사용하게 되면 컴파일러는 manifest와
metadata를 포함하지 않은 PE파일을 생성하게 됩니다. 이 파일은 기본적으로 .netmodule 의 확장자를 가지게 되며
다중 파일로 구성된 Assembly를 구성할 때 사용하게 됩니다. 이 타입의 모듈을 CLR이 이용하기 위해서는 항상 이 해당
모듈이 Assembly에 포함되어 있어야 합니다. 이렇게 .netmodule 형식의 파일을 생성해 놓음으로써 자주 사용하는
메소드와 그렇지 않은 메소드를 구분하여 분리해서 파일 구성을 할 수도 있겠군요.
다음과 같은 코드를 컴파일 하려고 합니다.
코드 1) Hellovb.vb
NameSpace Hello
Public Class HelloWorld
Overloads Shared Sub PrintHello()
System.Console.WriteLine("HELLO!!")
End Sub
End Class
End NameSpace
코드 2) Test.cs
using Hello;
public sealed class Program
{
public static void Main()
{
System.Console.WriteLine("Hi");
HelloWorld.PrintHello();
}
}
코드 1은 VB.NET용 소스 파일이며 코드 2는 C#용 소스 파일입니다.
코드 1은 .netModule로 컴파일을 해 봅시다.
코드 2 는 코드 1에서 정의된 HelloWorld 클래스의 PrintHello 메소드를 이용하고 있습니다. 방금 생성한
hellovb.netModule을 끌어와야 정상적으로 컴파일이 되겠지요. 생성하려는 Assembly에 모듈을 추가하려고 할 때는
/addmodule 이라는 옵션을 사용하면 됩니다.
test.exe라는 Assembly를 만들고 실행을 시키자 코드대로 잘 실행이 됩니다. Assembly의 특징 중 하나인 서로 다른 언어로 구현된 타입(클래스)의 구성으로 Assembly를 생성하는 것이 정말 가능하네요. :)
자 이제 우리는 생성된 Hellovb.netmodule 과 test.exe 파일을 열어봐서 앞에서 살펴본 내용을 확인해 보면 되겠습니다.
먼저 Hellovb.netmodule의 Metadata 엔트리를 봅니다.
앞에서 /t[arget]:module 이라는 옵션을 이용하여 PE파일을 생성하게 되면 컴파일러는 Manifest 정보를 포함하지 않는 PE 파일을 생성한다고 했습니다. 정말 manifest 정보가 없네요.
아 여기에 보이는 AssemblyRef 테이블은 현재 파일이 참조한 모든 참조 어셈블리 파일의 목록을 갖고 있습니다. 이 목록을
이용하여 Assembly의 다른 파일들을 열지 않고도 참조된 어셈블리가 어떤 것들인지 확인 할 수가 있는 것입니다. 이 파일은
기본 모듈인 mscorlib과 Visual Basic으로 만들어진 모듈이기에 Microsoft.VisualBasic이라는
Assembly를 참조하고 있다고 표현하고 있네요.
자 이제는 test.exe을 살펴봅시다.
테이블이 많이 보이네요. 앞에서 언급한 Assembly, FIle, ExportedTypes 테이블이 보입니다. ManifestResource는 없네요. 이 파일엔 리소스가 없거든요 :(
하나하나 살펴봅니다. 먼저 Assembly 테이블을 보니 이름(Name), 버전(Major, Minor, Build,
Revision), Culture, Flag, 해시 알고리즘(hashAlgId), 발행자의 공용키(PublicKey) 값이
보입니다.
이번에는 File 테이블입니다. Test.exe Assembly의 부분인 다른 PE 파일의 정보가 들어있네요. 이름(Name :
Hellovb.netmodule), 플래그(Flags : 이 파일은 리소스 파일이 아님을 알 수 있습니다.) 등을 확인할 수
있습니다.
마지막으로 ExportedType 테이블입니다. 타입의 이름(TypeName : HelloWorld), File테이블의
인덱스(Implementation)등을 확인 할 수 있습니다. TypeDefID 값이 0200002라고 적혀 있네요.
Ildsam.exe 를 이용해서 hellovb.netmodule을 찾아보니(TypeName이 HelloWorld고
Namespace가 Hello니까요) 다음과 같이 ID를 확인할 수가 있군요.
아 복잡하네요. 간단히 말하자면 컴파일러는 컴파일을 하면서 Assembly를 생성하게 되며 이 Assembly들의 내부
Manifest정보와 Metadata정보 덕분에 Assembly 스스로 자신에 대한 설명이 가능하므로 어디서든지 쉽게 재 사용이
가능하구나! 라고 이해하면 될 것 같습니다.
Assembly 구성에 대해 좀 더 알아봅시다. 다음의 코드를 컴파일하여 Assembly 구성을 하되 DLL 라이브러리로 구성합니다.
코드 3) Test2.cs
using Hello;
public class GoodbyeWorld
{
public static void PrintGoodbye()
{
System.Console.WriteLine("Goodbye World");
}
}
public sealed class Program
{
public static void Main()
{
System.Console.WriteLine("Hi");
HelloWorld.PrintHello();
GoodbyeWorld.PrintGoodbye();
}
}
컴파일을 하니 test2.dll 이라는 라이브러리 모듈이 생성되었네요.
이 라이브러리는 그렇다면 다른 곳에서 어떻게 사용하면 될까요? 물론 지금 해왔던 것 처럼 컴파일 시mscorlib.dll을
/r[eference] 옵션을 이용하여 추가해 컴파일 했던 것 처럼 /r[eference] 옵션을 이용하여 이 모듈을 사용할 수
있을 것입니다.
앞으로 우리가 개발할 때 사용할 툴인 Visual Studio IDE에서는 어떻게 사용하나요? 물론 다 알고 계시겠지만 프로젝트에서 참조 추가(Add Referece) 메뉴를 이용하면 쉽게 이 라이브러리를 사용할 수 있습니다.
컴파일러말고도 Assembly를 생성할 수 있는 방법이 또 있습니다. Assembly Linker(AL.exe)를 이용해서
생성할 수 있다고 합니다. Assembly Linker는 서로 다른 컴파일러에 의해서 빌드된 모듈들로 구성된 어셈블리를 만들 때
사용됩니다.
test3.cs 에서 Hellovb에서 사용되었던 코드를 제외하여 test4.cs를 만들고 이걸 netmodule로 컴파일을 하겠습니다.
public class GoodbyeWorld
{
public static void PrintGoodbye()
{
System.Console.WriteLine("Goodbye World");
}
}
public sealed class Program
{
public static void Main()
{
System.Console.WriteLine("Hi");
GoodbyeWorld.PrintGoodbye();
}
}
그리고 앞에서 생성했던 VB.NET의 Hellovb.netmodule과 지금 생성한 C#으로 만든 test3.netmodule을 AL.exe를 이용하여 Assembly로 만들어 봅니다.
잘 생성되었습니다. :)
AL.exe를 이용해서 Assembly에 리소스를 추가할 수가 있습니다. 리소스를 추가하게 되면 앞에서 살펴본 ManifestResource 테이블이 생성되겠지요.
다음과 같이 AL.exe를 이용하여 DLL을 생성합니다.
그리고 테이블을 살펴보니
ManifestResource 테이블에 kara.jpg 이미지 리소스가 추가되어 있네요!! 물론 csc.exe나 vbc.exe 같은 각 언어의 컴파일러에서도 /resource 옵션을 이용하여 리소스 추가가 가능합니다.
휴- 정신없네요. 한숨 돌립시다. @_@
우리가 생성한 test.dll 파일을 보니까 뭔가 썰렁합니다. 파일 정보를 보니 버전 리소스 정보가 하나도 없네요!
물론 Visual Studio IDE를 이용하여 프로젝트를 생성하면 C#의 경우는 AssemblyInfo.cs 라는 파일이
자동으로 추가되면서 이 파일을 통해 생성하려는 Assembly의 파일 정보를 구성할 수 있습니다. 그렇다면 AL.exe나
csc.exe, vbs.exe의 컴파일러를 이용하여 Assembly를 생성하려는 경우 파일 정보는 어떻게 추가 할 수 있을까요?
using System.Reflection;
[assembly: AssemblyTitle("Test Library")]
[assembly: AssemblyDescription("Test")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("VSTS2010")]
[assembly: AssemblyProduct("Test Library")]
[assembly: AssemblyCopyright("Copyleft © 2010")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
public class GoodbyeWorld
{
public static void PrintGoodbye()
{
System.Console.WriteLine("Goodbye World");
}
}
public sealed class Program
{
public static void Main()
{
System.Console.WriteLine("Hi");
GoodbyeWorld.PrintGoodbye();
}
}
위의 코드를 다음과 같이 컴파일 합니다.
그러면 생성된 test.dll 에는 파일 정보가 포함되게 됩니다.
오늘 너무 많은 것을 하네요. 하지만 다음 주 진도를 위해 몇 가지를 더 짚고 넘어가야 합니다. 앞에서 버전 번호를 보면 알겠지만 버전 번호는 다음과 같은 형식을 갖고 있습니다.
주 번호(Major) / 부 버전 번호(Minor) / 빌드 번호(Build) / 리비전 번호(Revision)
Test.dll의 버전은 1.0.0.0 이라고 되어 있습니다. 앞의 1.0은 공식적으로 사용되는 버전의 숫자입니다. 보통 .NET FrameWork 2.0 , 3.5 이런 식으로 이야기 할 때의 버전 번호입니다.
세번 째의 빌드 번호는 말 그대로 빌드 할 때마다 증가하는 번호를 의미합니다. 만약 Test.dll이 32번째 빌드된 버전이라면
2.0.32.0 으로 버전 번호가 매겨지겠지요. 마지막 번호는 빌드의 수정을 의미합니다. 만약 32번째 빌드된 버전에 문제가
있어서 그날 다시 빌드를 해야 한다면 마지막 번호가 증가를 하게 됩니다.
이 버전 구성 방식은 꼭 지켜야 하는 규칙은 아닙니다. 하지만 Microsoft를 포함한 일반적인 소프트웨어 회사에서는 위와 같은 버전 구성 방식을 사용하고 있습니다.
그런데 위의 소스에서 보면 Assembly와 관련된 버전 정보가 하나가 아님을 알 수 있습니다.
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
이 두 개 말고도 [assembly:AssemblyInformationVersion] 이라는 항목도 있습니다. 결국 Version과 관련된 항목이 3개가 있는데 CLR에서 사용하는 버전 정보는 [assembly: AssemblyVersion] 입니다.
이 버전 정보는 CLR에서 굉장히 중요합니다. 뒤에서 배우게 될 Strongly named assemblies를 바인딩 할 때
사용되는 버전 정보입니다. 이 버전 번호가 Assembly의 metadata 테이블 정보 중 AssemblyRef 테이블 항목에
기록이 됩니다.
버전 정보와 함께 Assembly는 Culture(언어) 라는 항목을 가집니다. 이 항목의 값에 따라 사용자는 영어 버전,
일본어 버전, 한국어 버전등의 Assembly를 가질 수 있게 됩니다. Culture에 들어가는 문자열은 주태그 - 부가태그의
형식으로(표준 RFC1766) 표현됩니다.
de-AT Austrian German
en-GB British English
en-US U.S. English
일반적으로는 코드가 포함된 Assembly에는 Culture를 지정하지 않습니다. 코드가 언어에 종속적으로 실행되어야 할 이유가
없기 때문이지요. 대신에 다국어를 지원하기 위한 어플리케이션을 만들 경우 Culture와 관련된 전용 리소스만을 포함한
Assembly를 만들어서 빌드를 하게 됩니다.(이를 satellite assembly - 위성 어셈블리 라고 합니다.) 이렇게
포함된 리소스는 System.Resources.ResourceManager 클래스를 이용하여 사용하게 됩니다.
오늘은 여기까지! 다음주에 계속됩니다!! :)