SimpleIsBest.NET

유경상의 닷넷 블로그

친절한 .NET 씨

by 블로그쥔장 | 작성일자: 9/22/2005 12:47:00 AM
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.
와이프가 배신을 때리고 친구와 함께 "친절한 금자씨" 영화를 봐 버리는 바람에... 아직까지 보지 못한 영화 "친절한 금자씨"... 뭐 듣기엔 굉장히 친절하다고 들었습니다. 그래서 몇 자 씨부려 봅니다...

친절한 .NET 씨

필자는 최근 ActiveX 컨트롤 코딩을 하고 있다. 뭐 별건 아니고... 몇몇 DLL이 컴퓨터에 존재하는가 확인하고 닷넷 CAS(Code Access Security) 설정이 어플리케이션이 요구하는 설정을 따르고 있는가를 확인하는 컨트롤이다. 이 코드에서 DLL이 존재하는가를 살펴보는 것 뿐만 아니라 이 DLL의 버전을 확인하여 필요로 하는 버전 이상을 제공하는가 확인해야 했다. 뭐 별거 아니겠거니 생각하고 코딩을 시작했다.

별거 아니겠거니 생각한 결정적인 이유는 닷넷 프레임워크 상에서 파일의 버전 정보(물론 버전 정보를 담을 수 있는 .DLL, .EXE, .SCR 파일이어야 할 것이다)를 알아내는 것은 대단히, 매우, 정말, 진짜로, really, certainly 쉽기 때문이다. 파일 버전 정보를 알아내기 위해서는 System.Diagnostics 네임스페이스의 FileVersionInfo 클래스를 사용하면 된다.

FileVersionInfo 클래스의 스태틱(static) 메쏘드인 GetVersionInfo 메쏘드에 알아내고자 하는 파일의 경로만 던져주면 파일 버전 정보가 들어있는 FileVersionInfo 클래스의 인스턴스를 반환해 준다. 이 클래스에는 파일 버전부터 시작해서 작성자, 회사, 저작권 정보 등, 파일 속성 대화상자에서 보여주는 파일의 정보를 모두 보여준다.

파일 버전 정보는 전체 버전이 통합되어 문자열 형태로도 존재하며, 버전의 각 Major, Minor, Build, Revision 부분을 별도로 알아낼 수도 있다. 대충 다음과 C# 코드가 파일 버전을 알아내는 코드가 되겠다.

FileVersionInfo info FileVersionInfo.GetVersionInfo("C:\\Windows\\Notepad.exe");
Console.WriteLine("Notepad.exe version is {0}", info.FileVersion);
Console.WriteLine("\tMajor : {0}, Minor : {1}, Build : {2}, Revision {3}",
                            info.FileMajorPart, info.FileMinorPart,
                            info.FileBuildPart, info.FilePrivatePart)
;

불친절한  WIN32 API 씨

뭐... 이따구의 간단한 코드를 작성해본 경험이 있으니... ActiveX 를 제작할 때도 이와 상응하는 WIN32 API가 있겠지 생각하며 MSDN을 뒤지기 시작했다. 뭐 십 수년을 도움말을 뒤지고 살았으니 그닥 어렵지 않게 WIN32 API를 찾아 냈다. GetFileVersionInfo 함수가 바로 이 녀석인데... 매개변수가 영 심상치 않다. 뭔가가 있을 듯한 불길한 예감이 들었다.

아니나 다를까. 이 녀석은 파일의 버전정보를 모두 담는 메모리 블록을 반환할 뿐이며, 이 메모리 블록에서 필요한 정보를 추출하는 함수는 별도로 존재했다. 즉, 추가적으로 VerQueryValue 라는 함수를 호출해 주어야만 원하는 정보를 뽑을 수 있었던 것이다. 쓰봉... 게다가 이 함수를 달랑 한번 호출하는 것이 아니라 필요한 정보 마다 이 함수를 호출해 주어야 한다. 뭐... 이 함수를 상세히 설명하기 위한 글이 아니므로 대략 앞서의 닷넷 코드와 동등한 C/C++ 코드를 적어보면 다음과 같다. -_- C/C++ 및 기타 WIN32 프로그래밍을 해보지 않은 독자라면 아래 코드를 이해하려고 하지 마라. 머리만 터지므로 대충 쭈욱 살펴보기 바란다.

BOOL bExist ::PathFileExists(pszDllPath);

// 파일이 존재하지 않는다면 FALSE를 반환하고 곧바로 종료한다.
if (bExist == FALSE) {
    
return ERROR_DLL_NOT_FOUND;
}

//
// 주어진 DLL의 버전 정보를 읽는다. (이 주석을 쓸때만 해도 이렇게 길어지리라 생각 못했다... 쓰봉)
//
DWORD Dummy = NULL;
BOOL bResult = FALSE;
HRESULT hResult S_OK;
DWORD dwLen 0;
DWORD dwError 0;
PVOID pVersionData = NULL;
PVOID pVersionInfo = NULL;
LANGANDCODEPAGE *lpTranslate;
TCHAR szFieldName[MAX_FIELD_SIZE];

// 버전정보의 크기를 알아 내고 데이터 버퍼를 할당한다.
// 파일의 버전정보의 크기가 파일마다 제각기 다르기 땜시...
dwLen ::GetFileVersionInfoSize(pszDllPath, &Dummy);
if 
(dwLen == 0) {
    
return GetLastError();
}
// 버전 정보를 담을 버퍼를 할당한다. (수천 바이트가 필요하다)
pVersionData ::HeapAlloc(::GetProcessHeap(), 0, dwLen);
ZeroMemory(pVersionData, dwLen);

// 버전 정보 데이터를 읽는다.
bResult ::GetFileVersionInfo(pszDllPath, NULL, dwLen, pVersionData);
if 
(bResult == FALSE) {
    dwError 
GetLastError();
    
::HeapFree(::GetProcessHeap(), 0, pVersionData);
    return 
dwError;
}

// VS_FIXEDFILEINFO 로 부터 버전정보를 읽는다.
// (숫자로 된 버전정보는 이 구조체 안에 정보가 담긴다)
bResult ::VerQueryValue(pVersionData, TEXT("\\"), &pVersionInfo, (PUINT)&dwLen);
if 
(bResult == FALSE) {
    dwError 
GetLastError();
    
::HeapFree(::GetProcessHeap(), 0, pVersionData);
    return 
dwError;
}
VS_FIXEDFILEINFO *pFileInfo 
(VS_FIXEDFILEINFO *)pVersionInfo;
// 숫자로된 버전 정보를 알아냈으니 필요한 대로 쓰문 되겠다.
DWORD wMajor HIWORD(pFileInfo->dwFileVersionMS);
DWORD wMinor LOWORD(pFileInfo->dwFileVersionMS);
// 중략...

// 버전 데이터의 언어 설정을 읽는다.
// 버전 정보 중 문자열 정보는 문자열 리소스 형태로 기록되므로 이 문자열 리소스의 언어설정이
// 무엇인가 먼저 알아내야 한다.
bResult ::VerQueryValue(pVersionData, TEXT("\\VarFileInfo\\Translation"),
          (LPVOID*)&lpTranslate, (PUINT)&dwLen)
;
if 
(bResult == FALSE) {
    dwError 
GetLastError();
    
::HeapFree(::GetProcessHeap(), 0, pVersionData);
    return 
dwError;
}

// 버전 데이터에서 파일 버전(문자열)을 추출해 낸다.
hResult ::StringCchPrintf(szFieldName, MAX_FIELD_SIZE,
            TEXT(
"\\StringFileInfo\\%04x%04x\\FileVersion"), 
            lpTranslate->wLanguage, lpTranslate->wCodePage)
;
if 
(hResult !S_OK) {
    ::HeapFree(::GetProcessHeap(), 
0, pVersionData);
    return 
hResult;
}
bResult 
::VerQueryValue(pVersionData, szFieldName, &pVersionInfo, (PUINT)&dwLen);
if 
(bResult == FALSE) {
    dwError 
GetLastError();
    
::HeapFree(::GetProcessHeap(), 0, pVersionData);
    return 
dwError;
}
// pVersionInfo 변수가 문자열 버전정보를 갖고 있다.
// 쓰고 잡은 대로 쓰문 되긋다.


// 필요로 하다명 추가적인 파일 버전 정보(저작권, 제품명, 기타 등등)를 읽고자 하면
// 바로 위 코드를 반복적으로 사용하되 FileVersion 대신 ProductName, CompanyName 등을
// 사용하면 되겠다.

// 사용 다한 메모리는 해제해 줘야 함다...

::HeapFree(::GetProcessHeap(), 
0, pVersionData);
 

친절한 .NET 씨

좀 길다. 아니 졸라 길다. 왜 이렇게 긴 것일까? 필자는 고민하기 시작했다. 내 코드가 삽질은 아닐까? 불안해진 필자는 Reflector 를 꺼내들고 닷넷의 FileVersionInfo 클래스를 까보았다. 아니나 다를까... 필자가 호출했던 WIN32 API들을 줄줄이 호출하는 닷넷 코드를 볼 수 있었다. 뭐... Windows 상에서 파일 버전을 알아내는 API는 GetFileVersionInfo 함수와 VerQueryValue 함수 둘 뿐이니 닷넷이라고 해봤자 지가 용빼는 재주는 없었을 것이다.

닷넷 프레임워크와 FileVersionInfo 와 같은 클래스들이 모여있는 BCL (Base Class Library)은 정말로 친절하게도 개발자의 수고를 줄여주고 있는 것임을 새삼스럽게 느꼈다. FileVersionInfo 클래스 뿐인가? 수많은 WIN32 API, COM 인터페이스, ADSI 관련 API, MSMQ API 등을 모두 닷넷 프레임워크 CLR과 BCL은 친절하게도 wrapping 하고 있다. 덕분에 개발자는 보다 쉽게, 보다 빠르게 코드를 작성할 수 있는 것이다. C/C++ 혹은 VB 6.0으로 이벤트 로그에 로그 한 줄을 남기기 위해 얼마나 많은 코드와 작업이 필요했던가? Windows 서비스를 작성하기 위해 얼마나 많은 C/C++ 코드가 필요했던가는 코드를 작성해 본 사람만이 알 것이다. 닷넷은 어떤가? 이벤트 로그에 로그 남기는 것은 단 한 줄의 코드로도 가능하고, Windows 서비스 역시 VS.NET의 프로젝트 템플릿 도움 없이도 쉽게 코딩할 수 있다.

VB 6.0은 친절했을까?

닷넷이 없던 시절엔 어떠했을까? 파일 버전 정보를 읽어오는 오늘의 예제를 생각해 보자면, C/C++ 개발자라면 죽으나 사나 앞서 보여준 대로 졸라 코딩을 했어야 할 것이고... C/C++ 보담은 보다 4GL에 가까운 VB 6.0은 어떠했을까? VB 6.0은 C/C++에 비교해서 빠르게 프로그램을 개발할 수 있도록 해주긴 해준다. 하지만 VB 6.0이 기본적으로 제공하는 기능은 한계가 뚜렷했다. KB 160042KB 139491 을 살펴보더라도 VB 6.0과 그 이전 버전에서도 상당히 많은 삽질을 해야만 원하는 기능을 구현할 수 있다는 것을 알 수 있다. (링크 클릭하는 것마저 귀찮아 하는 독자를 위해, 위 두 KB는 VB 6.0 에서 앞서 언급한 GetFileVersionInfo 함수, VerQueryValue 함수들을 어떻게 호출하고 사용하는가 예제를 다루고 있다. C/C++ 코드에 만만치 않게 졸라 길다)

닷넷 이전 시절에 그나마 개발 생산성이 좋다고 알려졌던 VB 6.0 이지만, 이런 세세한 기능까지 모두 커버하지 않았다는 것이 명백했으며, 그다지 친절하지 못한 VB 6.0 덕에 개발자들은 MSDN과 구글, 인터넷 등을 헤메였던 것이다. 꺼이 꺼이... 옛날 생각하면 슬프다... T_T

결론(?)

뭐 결론 내리자고 글 쓴 건 아니지만... 닷넷 프레임워크의 CLR 과 BCL 등 많은 컴포넌트들은 개발자를 위해 많은 일을 하고 있다. 조청과 단꿀, 물엿과 엿기름, 버터와 쇼트닝유가 흐르는 "친절한 닷넷의 개발 환경"에 우린 감사해야 할 것이다. (결론 치고 졸라 썰렁하다... 난 왜 블로그를 쓸까? 아아... -_-)

부록. 파일 버전 정보에 대하여

(부록까지... 이젠 별짓을 다한다...)구라만 풀기 좀 뭐하니깐... 파일 버전 정보에 대해서 간략히 언급하고자 한다. 파일 정보는 EXE, DLL 등 PE 포맷의 파일의 리소스로서 존재한다. Visual Studio를 이용하여 C/C++ 프로젝트를 만들면 .rc 파일 내에 항상 VS_VERSION_INFO 라는 버전 자원(resource)이 생성되는 것을 보곤 했을 것이다. 바로 여기에 기록되는 버전 정보가 WIN32 리소스로서 EXE, DLL 내에 표준 위치(위치라기 보다는 표준 리소스 아이디인 1을 사용한다)에 기록된다. 이렇게 표준적으로 기록된 정보를 읽어오는 API가 GetFileVersionInfo 함수인 것이다. 파일 버전 정보가 리소스 형태로 존재하기 때문에 Microsoft가 제정하고 권고하는 표준 정보들(버전, 회사명, 파일설명, 저작권, 제품이름, 제품버전 등)외에도 추가적인 정보를 기록하는 것이 얼마든지 가능하며, 이것이 잘못된 것이거나 이상한 것이 전혀 아니다. 그래서 각 파일마다 이 파일버전 정보의 크기가 다를 수 있는 것이다.

그렇다면 이 리소스가 없는 EXE, DLL은 ? 당근 파일 버전 정보는 없다. DLL 이나 EXE를 오른쪽 클릭하고 속성 메뉴를 선택하면 나타나는 파일 정보 대화 상자에서 버전 탭이 없는 것들이 있는데, 이런 경우가 바로 버전 정보 리소스를 포함하지 않는 DLL, EXE 이기 때문이다. 당연하게 GetFileVersionInfo 함수 호출은 실패하게 된다.

닷넷은 어떤가? 닷넷 컴파일러는 AssemblyTitle, AssemblyDescription, AssemblyCompany, AssemblyVersion 등의 특성(attribute)의 값을 이용하여 DLL, EXE 에 버전 리소스를 알아서 생성해 준다. 대개 AssemblyInfo.cs 파일에서 이러한 특성들의 값을 기록하는 것임을 독자들도 잘 알 것이다. 정말 친절한 닷넷씨가 아닌가?



Comments (read-only)
#re: 친절한 .NET 씨 / 찌유니아빠 / 9/26/2005 11:32:00 AM
결국 만드셨군요!!!
#re: 친절한 .NET 씨 / 이경구 / 9/26/2005 11:57:00 PM
결국 이러한 .Net의 친절함이 아직까지는 C/C++ 및 기타 WIN32 프로그래밍을 오히려 돋보이게 하는 것이 아닐까요..
아직 C/C++ 프로그래밍을 전문적으로 해 보지는 못해서.. ^^;;
이쪽의 경험도 빨리 쌓아야 겠다는 생각이 더 간절해지네요..
항상 재밌고 유익하게 읽고 있습니다. 감사...
#re: 친절한 .NET 씨 / 블로그 쥔장 / 9/27/2005 9:45:00 AM
네... 닷넷의 친절함이 C/C++ 프로그래밍(프로그래머)의 가치를 올려주는 것은 사실입니다만...
동일한 작업을 하는데 소요되는 시간(생산성)의 차이가 매우 심하다는 점은 짚고 넘어가야 할 사항입니다.
점점 C/C++ 개발자를 찾기 힘들어지고 있습니다. 역으로 이것을 노리는 것도 개발자들이 한번쯤
자신의 가치를 올리기 위한 방법으로 생각해 볼 수도 있겠지요.

C/C++ 프로그래밍은 C/C++ 언어 자체를 습득하는 것으론 아무런 도움이 되지 않습니다. (기본 정도라고 할까요?)
C/C++을 이용하여 Win32 API를 얼마나 잘 구사하느냐, 혹은 ActiveX/COM 구현을 얼마나 잘하느냐가
중요하죠. (요즘 MFC에 대한 요구 사항은 그다지 많아 보이지 않습니다만...)
혹시나 C/C++을 공부할 계획이 있으신 분들은 C/C++라는 프로그래밍 언어만을 보지 마시고
Win32 API와 ATL(Active Template Library)를 목표로 하면서 사이드로 프로그래밍 언어에
익숙해지시는 것이 좋다고 생각합니다....
뭐... 갠적인 생각임다... ^^
#re: 친절한 .NET 씨 / 제뤼 / 9/26/2006 5:45:00 PM
친절한 Loner's .NET Blog... ^^
#re: 친절한 .NET 씨 / 남정현 / 4/17/2007 6:23:00 PM
닷넷의 BCL을 보면 단순히 이런 특정 기능 하나를 감싼 클래스 하나하나가 매우 매력적이죠.
그렇지만 이런 친절함에 걸려서 가끔 낚시질을 당할 때도 있습니다. 간단하기 때문에 세세한걸 놓치는 경우도 존재하는데
그것 때문에 주로 P/Invoke와 관련된 부분에서 낚시질당하는 수가 많죠. ㅠ_ㅠ
#re: 친절한 .NET 씨 / 남처리 / 10/30/2008 1:07:00 AM
글 하나하나 볼때마다 미숙한 저로써는 너무 신기할 따름입니다.
언제쯤 이렇게 수학공식 풀이하는듯한 포스트를 작성해 볼수 있을지.... 얼른 얼른 노력해야 겠네요.
오늘도 행복하세요.
#re: 친절한 .NET 씨 / 온조 / 2/23/2010 10:29:00 PM
필요한 정보였는데

잘 정리해놓으셔서 도움 많이 받고 갑니다

감사합니다.