실전! 지속적인 통합 11편: MSBuild 고급편

이 글은 월간 마이크로소프트웨어(일명 마소) 2008년 11월호에 기고한 글입니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.

MSBuild는 쓸모가 많다. 그리고 XML 스크립트이므로 부주의하게 다루지만 않으면 가독성이 좋은 편이다. 사실 윈도우 플랫폼에서 개발한다면 MSBuild로 못할 일은 거의 없다. 파이썬과 같은 범용 스크립트 언어를 사용해 문제를 해결해도 좋지만 MSBuild를 도입한다면 빌드 문제에 한해선 손 가는 일이 많이 줄어들 것이다.

최재훈 | SK 아이미디어의 게임 서버 팀에서 일한다. 요즘은 스크립트 엔진을 개발하는 데 전념하며, 새로운 도전을 즐긴다. 직업 외적인 측면에선 배철수의 음악 캠프를 15년째 즐겨 듣고, U2가 최고의 밴드라 생각한다.

지난 주엔 간단한 C# 프로젝트를 예제 삼아 실제로 쓸 수 있는 msbuild 스크립트를 만들었다. 그리고 11월엔 조금 더 재미있는 내용을 다루겠다고 약속했다. 오늘은 그 약속을 지킬 생각이다. MSBuildTasks라는 확장기능을 통해 레지스트리를 마음대로 조작하고 C#으로 짠 스크립트를 내장하는 방법도 살펴볼 생각이다. 사실 이 정도까지 MSBuild를 다룰 수 있다면 여러분이 못할 일은 없다. 안 해본 일이라고 해봐야 MSBuild용 태스크를 직접 만드는 것 정도뿐이기 때문이다.

그러나 MSBuildTasks를 다루기에 앞서 멀티 프로세서 환경에서의 Rebuild 문제를 다룰 생각이다. 10월에 Build와 Clean 그리고 Rebuild를 다뤘기 때문에 그 연장선에 있는 문제를 마저 다룰 생각이다.

멀티 프로세서 또는 멀티 코어에서의 Rebuild

프로그래밍 환경이 참 복잡해진다.

초등학교 5학년 때 컴퓨터 학원을 다녔다. 어떤 계기가 있어 학원에 나갔는지 기억나지 않지만 부모님의 권유가 있었던 듯 하다. 그 전엔 집에 컴퓨터가 없었고 컴퓨터가 뭔지 몰랐으니 말이다. 아마도 은행 직원이던 아버지가 컴퓨터를 알아두면 미래에 쓸모 있으리란 생각을 하게 되지 않았을까? 컴퓨터가 가장 먼저 도입된 곳이 통신과 은행이었으니 말이다. 어쨌거나 당시엔 16비트 컴퓨터가 대세였고 학원 방 한곳엔 게임기 팩이 들어가는 8비트 컴퓨터가 20대쯤 있었다. 학생인 우리는 GW-Basic, Pascal 등을 배웠는데, 멀티 스레드 같은 건 전혀 신경 쓸 이유가 없었다. 실은 멀티 스레드란 말을 알게 된 건 그로부터 10년이 지나서였다.

그에 비해 요즘은 어떤가? 가정용 컴퓨터에도 멀티 코어가 도입됐다. 멀티 스레드가 문제가 아니라 멀티 코어가 문제인 시대다. 이런 변화는 개발자에게도 영향을 준다. 가끔 재미있는 드라마가 나오면 휴대용 기기에 넣어두곤 하는데 이때 곰 인코더를 쓴다. 그런데 곰 인코더가 최근 멀티 코어를 지원하게 됐다. 집 컴퓨터는 듀얼 코어인데 코어 하나가 동영상 파일 하나를 맡아 처리한다. 그러니 종전보다 두 배 정도 빨리 인코딩이 끝나 좋다.

지난 10월 원고를 쓸 때까진 몰랐는데 이런 변화는 빌드 마스터(회사에 이런 직함이 있던 말던 이 일을 도맡아 하는 여러분이 빌드 마스터다)에게까지 영향을 미친다. 그것도 예기치 않게 말이다.

내가 속한 팀은 코어 4개짜리(어쩌면 CPU 2개 X Core 2개일지도 모른다) 서버 두 대를 빌드용으로 쓴다. 그에 비해 개발자용 로컬 컴퓨터는 대체로 코어 2개짜리다. 코어 개수가 다르긴 하지만 어쨌거나 로컬 컴퓨터도 멀티 코어를 쓴다. 그래서 로컬에서 문제가 발생하지 않으면 빌드 서버도 괜찮으리라 생각했는데 그게 아니었다.

어떤 문제가 있었는지 설명하기 전에 지난 주에 만든 msbuild 스크립트부터 확인하자.

[목록 1] 지난달 만든 빌드 스크립트
<Target Name="Build">
	<Message Text="타겟: Build" />
	<Message Text="빌드 조건: '$(BuildCondition)'" Importance="high" />

	<MSBuild Projects="@(ProjectReferences)" Properties="Configuration=%(ProjectReferences.Configuration);Platform=%(ProjectReferences.Platform)" StopOnFirstFailure="true" />
</Target>

<Target Name="Rebuild" DependsOnTargets="Clean; Build">
	<Message Text="타겟: Rebuild" />
</Target>
		

결론부터 말해 이 코드는 문제가 없다. 여기서 주목할 곳은 타겟 Rebuild인데, 이 타겟은 Clean과 Build를 순서대로 적용한다. 다시 말해, 빌드 산출물을 모두 지우고(Clean), 빌드한다. 그에 비해 회사에서 쓰던 이전 빌드 스크립트는 이런 식이었다.

[목록 2] 멀티 코어에서 문제가 되는 Rebuild 방식
<Target Name="Build">
	<Message Text="타겟: Build" />
	<Message Text="빌드 조건: '$(BuildCondition)'" Importance="high" />

	<MSBuild Projects="@(ProjectReferences)" Properties="Configuration=%(ProjectReferences.Configuration);Platform=%(ProjectReferences.Platform)" StopOnFirstFailure="true" />
</Target>

<Target Name="Rebuild" DependsOnTargets="Clean;">
	<Message Text="타겟: Rebuild" />
	<Message Text="빌드 조건: '$(BuildCondition)'" Importance="high" />

<MSBuild Projects="@(ProjectReferences)" Properties="Configuration=%(ProjectReferences.Configuration);Platform=%(ProjectReferences.Platform)" Targets="Rebuild" StopOnFirstFailure="true" />
</Target>

		

이 코드에서 문제가 되는 부분은 Clean을 한 다음 Build가 아닌 Rebuild 옵션으로 솔루션 파일을 빌드하는 것이다. 아니다. 좀더 구체적으로 말해 솔루션 파일을 Rebuild하는 것 자체가 문제다. 사실 이제부터 설명할 문제는 MSBuild의 오류가 아닌 비주얼 스튜디오의 버그라 봐도 좋다. 적어도 비주얼 스튜디오 2008까진 이런 문제가 발생한다. 즉, 비주얼 스튜디오에서 해당 솔루션을 Rebuild하면 빌드가 깨진다.

자, 그럼 도대체 어떤 경우가 문제인가?

빌드 서버에선 거의 항상 Rebuild를 한다. 그런데 이게 문제다. 우선 해당 솔루션은 A, B, C, D, DependsOnA_1, DependsOnA_2 이렇게 6개의 프로젝트로 구성된다. 코어 4개를 이용해 A, B, C, D 네 개의 프로젝트를 동시에 빌드하는데, 이 각각의 프로젝트마다 Clean, Build를 한다. 여기까진 문제가 없다.

[목록 3] 프로젝트 간 의존성

    A - DependsOnA_1, DependsOnA_2
    B
    C
    D
		

자, 이제 A 프로젝트와 B 프로젝트가 시차를 두고 순서대로 빌드를 마쳤고, DependsOnA_1과 DependsOnA_2가 그 자리를 순서대로 차지한다고 해보자. 그런데 Rebuild시 비주얼 스튜디오는 각 프로젝트마다 Clean과 Build를 반복한다. 즉, DependsOnA_1이 A를 지웠다 다시 빌드하고 자기 자신을 빌드할 시점에 DependOnA_2가 빌드를 시작하면 문제가 된다. DependsOnA_2가 A를 Clean했다 Build할 텐데, DependsOnA_1이 빌드할 때 필요한 A의 산출물이 지워졌으니 빌드가 성공할 리 없다.

해결책은 간단하다. Visual Studio로 병렬 빌드를 할 땐 Rebuild하지 않으면 된다. 우선 전체 솔루션에 대해 Clean을 한 후, Build하면 된다.

MSBuildTasks 도입하기

설명에 앞서 예제는 웹에서 구하면 된다는 점을 밝힌다.

MSBuildTasks를 살짝 소개한 적이 있다. MSBuildTasks란 별게 아니다. 단순히 MSBuild에 여러 가지 기능을 추가하는 오픈 소스 프로젝트(BSD 라이선스)이다. 관리자에게 빌드 정보를 이메일로 알린다던가 레지스트리를 조작한다던가 할 때 아주 유용하다. MSBuildTasks가 제공하는 태스크는 30여 가지나 되므로 일일이 설명하진 않겠다. 나중에 예제를 통해 이 프로젝트가 왜 유용한지 체험해보는 시간을 갖게 될 테니 말이다.

MSBuildTasks는 인스톨러(.msi)와 소스 코드 형태로 배포된다. 귀찮으니 인스톨러를 다운로드 받아서 설치한다. 설치 경로는 기본적으로 “C:\Program Files\MSBuild\MSBuildCommunityTasks\”가 된다.

일단 인스톨러로 설치하긴 했지만, 개발에 참여하는 모든 사람에게 MSBuildTasks를 설치하라고 할 수는 없다. 마이크로소프트의 파워포인트 팀에 입사한 친구의 말로는 그 팀에선 빌드 마스터를 번갈아 가며 맡는단다. 그리고 빌드 마스터를 맡은 날은 다른 일엔 신경 쓰지 않아도 된다는데, 이런 제도가 있다면 빌드에 필요한 구성요소, 즉 MSBuildTasks를 알아서 설치할지도 모르겠다. 그러나 일반적인 환경에선, 대부분의 사람은 자신이 할 일도 충분히 많기 때문에 빌드엔 신경 쓰지 않으려 한다. 그렇다 해서 누구를 비난할 수 없는 문제다. 사실 소스 버전 관리 시스템에서 소스 코드를 내려 받으면 모든 게 설치되어 있어서 MSBuildTasks를 도입했다는 사실조차 모르게 해야 맞다. 그게 빌드 자동화의 취지니까 말이다.

이제 MSBuildCommunityTasks(“C:\Program Files\MSBuild\MSBuildCommunityTasks\”) 폴더를 복사해서 소스 코드 트리에 놓는다. 예를 들어 이런 식이다.

[목록 4] 소스 트리

\trunk
 \src
  \AIML
 \vendor
  \BuildTool
   \MSBuildCommunityTasks
    \Installer
		

보통 MSBuildCommunityTasks 같은 외부 라이브러리는 vendor라는 폴더에 넣는 관례가 있다. 그리고 MSBuildCommunityTasks 폴더 아래에 Installer 폴더를 만들었다. 이 폴더엔 MSBuild.Community.Tasks.msi 파일을 넣는데, 항상 이렇게 원본 파일을 어딘가 저장해두는 습관을 들이자. 오픈 소스 프로젝트가 어느 날 갑자기 망할 수도 있고, 예전 바이너리를 어느 시점부턴 제공하지 않을 수도 있기 때문이다.

마지막으로 MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets 파일을 메모장으로 열어서 [목록 5]와 같이 수정한다.

[목록 5] MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets
<PropertyGroup>
    <MSBuildCommunityTasksPath >.</MSBuildCommunityTasksPath>
    <MSBuildCommunityTasksLib>MSBuild.Community.Tasks.dll</MSBuildCommunityTasksLib>
</PropertyGroup>
		

그러고 나서 빌드 스크립트 msbuildscript.xml를 손보면 모든 게 끝이다. 문서를 보면 기존 빌드 스크립트에 <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>를 추가하면 MSBuildTasks의 기능을 쓸 수 있다고 한다. 하지만 이 경우에는 <Import Project="BuildTool\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>라고 상대 경로를 적어주면 된다.

끝이다. 이렇게만 하면 별도의 설치 과정 없이도 MSBuildTasks의 기능을 누구나 사용할 수 있다.

MSBuildTasks로 ODBC DSN 만들기

자, MSBuildTask도 설치했겠다 이제 실전에 돌입해볼까?

어떤 사례를 살펴볼까 하다가 ODBC DSN을 만드는 걸 골랐다. 요즘은 ADO든 ODBC든 보통 연결 문자열을 쓰기 때문에 DSN을 만들지 않는다. 그러나 예전에 개발된 소프트웨어 중 상당수가 ODBC DSN 방식을 쓴다. 유지보수 때문에 이런 프로그램을 손보게 될지 모를 일이니 꽤 실용적인 예제라 생각한다.

보통 [제어판-관리도구- 데이터 원본 (ODBC)]에 가서 ODBC DSN을 추가한다. 하지만 어떤 이유가 있어 DSN 값이 바뀐다면, 그래서 그럴 때마다 사람이 일일이 DSN 값을 고친다면 짜증이 날 것이다. 그러니 빌드 스크립트가 ODBC DSN을 자동으로 만들면 좋겠다. 그러려면 우선 DSN에 대해 알아야 한다. Understanding ODBC DSN Creation이란 문서는 이런 자동화에 필요한 정보를 제공하는데 여기선 간단하게 알아보자.

그림 1. ODBC DSN 레지스트리 값

그림 1. ODBC DSN 레지스트리 값

[그림 1]은 ODBC DSN이 어떤 값으로 구성되는지 보여준다. 여기선 MSSQL 2000을 기준 삼았는데 Database는 데이터베이스의 이름, Server는 데이터베이스 인스턴스가 위치한 서버 주소, Driver는 ODBC 드라이버 등을 나타낸다. 다시 말해 ODBC DSN을 자동으로 만들 때 우리가 할 일은 레지스트리를 조작하는 것뿐이다.

자, 이제 MSBuildTasks를 이용해 레지스트리를 마음대로 만들어보자.

[목록 6] MSBuildTasks로 ODBC DSN 만들기
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<Import Project="..\..\vendor\BuildTool\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

<!-- 테스트용 DSN 정보 -->
<ItemGroup>
    <DSNToCreate Include="Item">
        <Name>DSN_NAME1</Name>
        <Server>DatabaseServer1</Server>
        <Database>InitialCatalog1</Database>
        <Description>ODBC DSN for the Database: DatabaseServer1</Description>
        <Driver>SQL Native Client</Driver>
        <LastUser>sa</LastUser>
        <Trusted_Connection>Yes</Trusted_Connection>
    </DSNToCreate>
    <DSNToCreate Include="Item">
        <Name>DSN_NAME2</Name>
        <Server>DatabaseServer2</Server>
        <Database>InitialCatalog2</Database>
        <Description>ODBC DSN for the Database: DatabaseServer2</Description>
        <Driver>SQL Native Client</Driver>
        <LastUser>sa</LastUser>
        <Trusted_Connection>Yes</Trusted_Connection>
    </DSNToCreate>
</ItemGroup>

<Target Name="CreateDSN">
	<RegistryRead
KeyName="HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBCINST.INI\%(DSNToCreate.Driver)"
		ValueName="Driver">
		<Output TaskParameter="Value" PropertyName="DriverPath" />
	</RegistryRead>
	<Message Text="DriverPath: $(DriverPath)" />

	<RegistryWrite
		KeyName="HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\%(DSNToCreate.Name)"
		ValueName="Server"
		Value="%(DSNToCreate.Server)" />
	<RegistryWrite
		KeyName="HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\%(DSNToCreate.Name)"
		ValueName="Database"
		Value="%(DSNToCreate.Database)" />
	<RegistryWrite
		KeyName="HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\%(DSNToCreate.Name)"
		ValueName="Description"
		Value="%(DSNToCreate.Description)" />
	<RegistryWrite
		KeyName="HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\%(DSNToCreate.Name)"
		ValueName="Driver"
		Value="$(DriverPath)" />
	<RegistryWrite
		KeyName="HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\%(DSNToCreate.Name)"
		ValueName="LastUser"
		Value="%(DSNToCreate.LastUser)" />
	<RegistryWrite
		KeyName="HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\%(DSNToCreate.Name)"
		ValueName="Trusted_Connection"
		Value="%(DSNToCreate.Trusted_Connection)" />
	<RegistryWrite
		KeyName="HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources\"
		ValueName="%(DSNToCreate.Name)"
		Value="%(DSNToCreate.Driver)" />
</Target>

</Project>

[목록 6]의 소스 코드는 상당히 길다. 하지만 그 내용은 아주 간단하다. 우선<Import Project= … />에서 MSBuildTasks를 불러들인다. 이 코드 한 줄만 있으면 MSBuildTasks의 모든 기능을 쓸 수 있다.

ItemGroup은 앞으로 만들 ODBC DSN의 정보를 담는다. 특히 서버 애플리케이션의 경우, 여러 대의 데이터베이스와 상호작용할 때가 많은데 그렇기 때문에 이 예제에선 아이템 그룹을 사용했다. 잊었을까 다시 설명하자면 아이템 그룹은 일종의 배열이라 생각하면 된다.

CreateDSN 태스크에선 단순히 아이템 그룹에 배정된 값에 따라 레지스트리를 생성할 뿐이다. RegistryWrite는 MSBuild가 기본 제공하는 기능이 아니라 MSBuildTasks가 제공하는 기능이다. 여기서 주목할 부분이 있다면 %(DSNToCreate.Name) 정도다. %(DSNToCreate.X)는 DSNToCreate란 배열에 든 요소를 차례대로 돌면서 그 요소의 X 값을 찍는다는 의미인데, C# 개발자라면 이 기호를 이렇게 생각하면 이해하기 쉽다.

foreach(DSNToCreate item in DSNToCreateArray)
{
	RegistryWrite(“HKEY_LOCAL_MACHINE\SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources\" , Item.Name);
}
	

이렇게 만든 스크립트를 한번 실행시켜보자.

[목록 7] CreateDSN 실행하기

C:\workspace\src\Aiml>msbuild msbuild.xml /t:createdsn
Build started 2008-10-20 오후 12:24:17.
Project "C:\workspace\src\Aiml\msbuild.xml" on node 0 (createdsn target(s)).
  Read Windows Registry
  DriverPath: c:\WINDOWS\system32\sqlncli.dll
  Write Windows Registry
Write Windows Registry
……
Write Windows Registry
  Write Windows Registry
Done Building Project "C:\workspace\src\Aiml\msbuild.xml" (createdsn target(s)).

Build succeeded.

얼추 [목록 7]과 같은 결과가 출력되면 성공이다. 이제 [제어판-관리도구-데이터 원본 (ODBC)-시스템 DSN]을 확인해보자. 그러면 [그림 2]와 같은 결과가 나온다. MyDSN1과 MyDSN2가 만들어진 모습이다.

그림 2. MSBuildTasks로 만든 ODBC DSN

그림 2. ODBC DSN 만든 결과

MSBuildTasks로 ODBC DSN 지우기

창조가 있으면 종말이 있고, Build가 있으면 Clean이 있는 법! ODBC를 만들었으면 ODBC를 지워야 한다. DSN 설정 값이 달라졌다면 레지스트리 값을 지우고 새로 고쳐 써야 한다. DSN 값이 달라진 경우가 아니라도 매번 다시 생성하는 편이 좋다. 그렇지 않으면 빌드가 깨진 줄도 모르는 수가 생긴다. 누가 레지스트리 값을 고치다 깨졌는데, 내 컴퓨터엔 반영이 안 돼서 그냥 넘어가는 경우가 종종 있다. 이런 일을 방지하고 피드백을 빨리 주려면 매번 Clean, Build를 다시 하는 편이 좋다.

그런 의미에서 ODCB DSN을 지우는 방법도 알아보자.

[목록 8] MSBuildTasks로 레지스트리 지우기
<!-- DSN 삭제 -->
<PropertyGroup>
    <DelDSN>
        <![CDATA[
            public static string ScriptMain() {
                System.Collections.Specialized.StringCollection dsnNames = new System.Collections.Specialized.StringCollection();
                dsnNames.Add("MyDSN1");
                dsnNames.Add("MyDSN2");

                string returnMsg = string.Empty;

                using (Microsoft.Win32.RegistryKey OurKey = Microsoft.Win32.Registry.LocalMachine)
                {
                    foreach (string dsnName in dsnNames)
                    {
                        using (Microsoft.Win32.RegistryKey dataSourcesKey = OurKey.OpenSubKey(@"SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources\", true))
                        {
                            try
                            {
                                dataSourcesKey.DeleteValue(dsnName);
                            }
                            catch (Exception ex)
                            {
                                returnMsg += Environment.NewLine;
                                returnMsg += "While deleting a registry value " + @"SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources\" + dsnName;
                                returnMsg += Environment.NewLine;
                                returnMsg += ex.ToString();
                                returnMsg += Environment.NewLine;
                            }
                        }

                        try
                        {
                            OurKey.DeleteSubKeyTree(@"SOFTWARE\ODBC\ODBC.INI\" + dsnName);
                        }
                        catch (Exception ex)
                        {
                            returnMsg += Environment.NewLine;
                            returnMsg += "While deleting a registry key " + @"SOFTWARE\ODBC\ODBC.INI\" + dsnName;
                            returnMsg += Environment.NewLine;
                            returnMsg += ex.ToString();
                            returnMsg += Environment.NewLine;
                        }
                    }
                }

                return returnMsg;
            }
        ]]>
    </DelDSN>
</PropertyGroup>

<!-- 깔끔하게 정리 -->
<Target Name="CleanDSN">
    <Script Language="C#" Code="$(DelDSN)" Imports="System">
        <Output TaskParameter="ReturnValue" PropertyName="DelDSNResult" />
    </Script>
    <Message Text="DSN deletion: $(DelDSNResult)" />
</Target>

			

[목록 8]에서 CleanDSN 태스크를 보자. 이 태스크는 MSBuildTasks가 제공하는 Script 기능을 쓴다. Script는 말 그대로 닷넷 소스 코드를 동적으로 실행하여 그 결과를 반환한다. 여기선 $(DelDSN)이란 소스 코드를 실행하고 그 결과를 $(DelDSNResult)란 프로퍼티에 받는다. 결국 중요한 곳은 $(DelDSN) 소스 코드다.

이 소스 코드는 “SOFTWARE\ODBC\ODBC.INI\DSN_NAME” 경로를 통째로 지운다. 하위 경로에 있는 모든 값을 지움으로써 DSN을 삭제한다. 아주 간단한 소스 코드인데 dsnNames.Add(“MyDSN2”); 식으로 DSN의 이름을 하드 코딩한 부분만 개선하면 유지보수하기가 쉬워질 것이다.

MSBuildTasks의 Script 기능은 아주 쓸모 있어서 이 기능만 있으면 굳이 MSBuild용 태스크를 직접 만들 필요가 줄어든다. 어지간한 일은 C#이나 VB.NET 등으로 바로 처리할 수 있으니 말이다. 다만, 소스 코드 때문에 빌드 스크립트가 지저분해질 수 있으니 해당 스크립트 코드를 가급적 별도의 빌드 스크립트로 빼낸 후 Import하는 게 바람직하겠다.

참고. 이런 생각이 들지 않는가? 왜 이렇게 힘들게 스크립트까지 만들어가며 레지스트리를 지우는가? 그냥 RegistryWrite 처럼 RegistryDel을 쓰면 되지 않는가? 단순히 예제로 보여주려고 이런 짓까지 하는 건가? 사실 이렇게 문제가 간단하면 좋겠다. 그러나 MSBuildTasks엔 레지스트리를 지우는 기능이 없다. 그래서 이런 무식한 방법으로 문제를 해결해야 한다.

64비트 WOW 시스템에서의 DSN

64비트 WOW 시스템을 처음 썼을 때 32비트 애플리케이션 에뮬레이션이 된다는 사실에 감명 받았다. 어떤 시스템이든 처음엔 멋져 보이기 마련이다. 그러다 익숙하지 않은 시스템에 당황할 때가 오는데 ODBC DSN의 경우가 그랬다. 자, 그럼 여기서 문제 하나!

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\msbuild.exe msbuild.xml /t:createDSN

여러분은 64비트 WOW 시스템을 사용하는데 위와 같이 CreateDSN 타겟을 실행시켰다. 그러고 나서 64비트로 빌드한 애플리케이션을 실행시킨다. 과연 애플리케이션은 제대로 작동할까? 답을 예상했겠지만 당연히 답은 ‘아니오’이다.

그 이유가 무엇일까? 바로 빌드 스크립트를 실행시킬 때 32비트 msbuild 바이너리를 사용했기 때문이다. 앞서 64비트 WOW 시스템은 32비트 에뮬레이션 모드가 있다고 했다. 말인즉, 32비트 애플리케이션(여기선 msbuild)이 레지스트리를 건드리려고 하면 운영체제가 32비트 레지스트리로 리다이렉트시킨다(참고. QA – problems running WScript.Shell on 64bit Windows). 방금 32비트 레지스트리라고 했는가? 그렇다. 64비트 레지스트리와 32비트 레지스트리가 구분되어 있다. MSBuild는 32비트 레지스트리를 조작했는데 64비트 애플리케이션은 64비트를 레지스트리를 뒤진다. 그러니 올바른 결과가 나오기 힘들고 나오더라도 올바른 과정을 거쳤을 리 없다.

다른 것도 마찬가지다. [제어판/관리도구/데이터 원본 (ODBC)]은 64비트용 ODBC 관리도구라서 64비트 레지스트리의 정보를 가져오고 조작한다. 그러니 32비트 레지스트리를 조작했다면 DSN이 보일리 만무하다. 32비트용 ODBC 관리도구는 C:\Windows\SysWOW64\odbcad32.exe인데, 이걸 실행시켜야 비로소 그토록 보고 싶어했던 DSN이 보인다.

짐작했겠지만 이 문제는 쉽게 해결 가능하다. 32비트 애플리케이션을 빌드할 땐 32비트 msbuild를, 64비트 애플리케이션을 빌드할 땐 64비트 msbuild를 쓰면 된다. 간단하지만 처음엔 이런 자질구레한 걸 몰라서 고생하는 법이다. 그래서 파워포인트 팀처럼 빌드 마스터를 번갈아 가며 체험해보는 게 좋을지도 모르겠다.

끝마치는 말

어느덧 “지속적인 통합” 칼럼을 쓴지 11개월째다. 이제 한편만 더 쓰면 1년이 찬다. 아마도 다음 글이 이 칼럼의 마지막이 될 것이다. 아직 다루지 못한 내용이 많아 어떻게 마무리할지 고민된다. 하지만 11월 분량까지만 해도 여러분이 빌드 마스터가 되기엔 충분하다. 이제부턴 응용, 그리고 응용만 남았다.

최 재훈

블로그, 페이스북, 트위터 고성능 서버 엔진, 데이터베이스, 지속적인 통합 등 다양한 주제에 관심이 많다.
Close Menu