Visual Studio로 Azure Function 개발하기

Azure Function은 Azure App Service에 포함된 기능 중 하나인 Azure Web Job을 별도의 상품으로 분리하여 출시하였습니다. 하지만 아쉽게도 Visual Studio의 풍부한 IDE 지원을 아직까지도 직접 받을 수 있는 상태는 아닙니다. 또한 Microsoft Docs 등에 공개된 방법도 간접적으로 Azure Function의 설정을 이용한다거나, Node용 CLI를 활용하는 정도에서 언급되는 것이 전부입니다.

소개하려는 내용은 Visual Studio의 콘솔 프로젝트와 Azure Storage Emulator를 이용하여 C# Azure Function을 C# Script가 아닌 통상적인 C# Compiler 기반 프로젝트로 개발과 테스트를 진행하고, 이것을 C# Azure Function으로 마이그레이션하는 방법에 관한 것입니다. 만약 LINQPAD Premium Version을 구입하여 사용 중이라면, 같은 작업을 LINQPAD에서도 실행할 수 있으니 더 적극적으로 Azure Function을 개발하실 수 있을 것입니다.

시작하기

Azure Web Job 기반이기 때문에, 기존에 .NET용으로 출시한 Web Job SDK와 각종 Extension을 Azure Function 사이에는 어느 정도 호환성이 있습니다. 다시 말해서 C# 스크립트로 무언가 새로운 코드를 작성한다기 보다, 기존의 SDK를 C# 스크립트에서 사용할 수 있도록 포장한 것이 Azure Function의 본질에 가깝습니다. 아쉽게도 완전히 같은 코드 베이스는 아니지만, 호환성이 있기 때문에 취할 수 있는 이점이 있고, 그 부분을 활용하는 것입니다.

시작을 위하여 다음의 소프트웨어 스택이 설치되어있는지 점검합니다.

  • Azure Storage Emulator (Azure Cloud Service SDK에 포함되어있습니다.)
  • Visual Studio 2015 Community Edition 이상의 IDE
  • .NET Framework 4.6 이상

만약 Windows 개발 환경이 아닌 경우 Azure Storage Emulator는 제공되지 않기 때문에 어쩔 수 없이 실제 Azure Storage 계정을 만들어 연결해야 합니다. IDE의 경우 Visual Studio Code, Visual Studio for Mac, Rider를 대신 활용할 수 있습니다. 그리고 Mono를 설치하여 개발을 진행할 수 있습니다. 아쉽게도 .NET Core는 2017년 4월 현재 지원되지 않습니다.

선호하는 IDE로 콘솔 프로젝트를 만든 다음, 다음의 NuGet 패키지들을 설치합니다.

  • Microsoft.Azure.WebJobs (2.0.0 이상)
  • Microsoft.Azure.WebJobs.Extensions (2.0.0 이상)

그 다음 Main 메서드를 다음과 같이 코딩합니다.

var jobHostConfig = new JobHostConfiguration("UseDevelopmentStorage=true");
jobHostConfig.UseCore();
jobHostConfig.UseFiles();
jobHostConfig.UseTimers();
jobHostConfig.UseDevelopmentSettings();

using (var cts = new CancellationTokenSource())
using (var jobHost = new JobHost(jobHostConfig))
{
    jobHost.StartAsync(cts.Token);
    Console.WriteLine("Press Ctrl + C to stop the service.");
    Console.CancelKeyPress += (s, e) => cts.Cancel();
    cts.Token.WaitHandle.WaitOne(Timeout.Infinite);
}

Local Azure Storage Emulator를 사용할 수 있는 Windows 환경에서만 UseDevelopmentStorage=true 연결 문자열을 지정하고, 그 외 환경에서는 실제 Azure Storage Account의 연결 문자열을 해당 속성 블레이드에서 찾아 대입해야 합니다.

그리고 Azure Function에 호스팅하려는 함수를 다음과 같이 코딩합니다.

public static void Run(
    [TimerTrigger("* * * * * *", UseMonitor = true)]
    TimerInfo myTimer,
    TraceWriter log)
{
    log.Info($"C# Timer trigger function executed at: {DateTime.Now}");
}

TimerTrigger가 TimerInfo 메서드 인자에 지정되는 것에 유의하여 위와 같이 코딩합니다. TimerTrigger에 지정되는 첫 인자는 타이머의 실행 간격을 나타냅니다. Crontab에 사용되는 반복 간격 표시 문법을 참조하여 값을 지정하도록 구성하는 것이 Azure Function으로 마이그레이션 할 때 편리하므로 해당 문법을 익히는 것을 권장합니다.

그리고 실행이 잘 되는지 확인하기 위하여, Azure Storage Emulator를 시작하고, F5 키를 눌러 샘플 프로그램을 실행합니다. 다음과 비슷하게 출력되면 정상적으로 실행되는 것입니다.

Press Ctrl + C to stop the service.
Development settings applied
Found the following functions:
TimerSample.Run
Singleton lock acquired (1ce1ebaf1e584866b90488a9e1b5d19f/TimerSample.Run.Listener)
The next 5 occurrences of the schedule will be:
2017-04-24 오전 12:16:59
2017-04-24 오전 12:17:00
2017-04-24 오전 12:17:01
2017-04-24 오전 12:17:02
2017-04-24 오전 12:17:03
Job host started
Executing 'TimerSample.Run' (Reason='Timer fired at 2017-04-24T00:16:59.0273081+09:00', Id=aa02dc0a-5a89-4ebd-bf08-8182cce53a0c)
C# Timer trigger function executed at: 2017-04-24 오전 12:16:59
Executed 'TimerSample.Run' (Succeeded, Id=aa02dc0a-5a89-4ebd-bf08-8182cce53a0c)
Executing 'TimerSample.Run' (Reason='Timer fired at 2017-04-24T00:17:00.0061625+09:00', Id=f8161e5d-c989-4d2d-9a49-cb5d9d269134)
C# Timer trigger function executed at: 2017-04-24 오전 12:17:00
Executed 'TimerSample.Run' (Succeeded, Id=f8161e5d-c989-4d2d-9a49-cb5d9d269134)
...

마이그레이션

이렇게 만들어진 Azure Function이 정말 잘 수행되는지 점검할 때 활용할 수 있는 유용한 서비스가 하나 있습니다. Try Azure App Service를 이용하면 실제 Microsoft Azure 구독과 무관하게, Microsoft 계정 이외에도 Google (GMAIL), Facebook, Github 계정으로 로그인하여 1시간짜리 테스트 Azure Function 계정을 발급받을 수 있습니다.

https://azure.microsoft.com/ko-kr/try/app-service/ 에 방문하여 새로운 계정을 하나 생성합니다.

그 다음, 위의 Run 메서드의 코드를 복사합니다. 단, 몇 가지 복사 전에 수정하거나 확인해야 할 부분이 있습니다.

  • 개발 중에 참조한 NuGet 패키지의 참조를 지정해야 합니다. project.json 파일은 기본적으로 만들어지지 않으므로 다음과 같은 뼈대를 만들고, 현재 개발한 프로젝트 내의 package.config 파일의 내용을 여기로 복사해서 넣어야 합니다. 종속 관계에 따라 자동으로 설치되는 패키지들은 제외하고, 실제로 추가했던 패키지만 지정해서 넣으면 됩니다.
{
  "frameworks": {
    "net46":{
      "dependencies": {
        "Microsoft.ProjectOxford.Face": "1.1.0"
      }
    }
   }
}
  • NuGet 패키지가 아닌 BCL 내 어셈블리 또는 개별 .NET DLL 파일을 참조했을 경우에는 C# 스크립트만의 고유 문법인 #r 지시자를 사용하여 참조를 지정합니다.
    • GAC에 설치했거나 별도로 수동 참조한 .NET DLL 파일은 bin 폴더로 직접 업로드해야 합니다.
    • x86용으로 명시하여 빌드한 DLL이거나, 설치 준비 및 사용 과정에서 시스템 레지스트리 변경 등의 작업이 필요한 경우에는 사용할 수 없습니다.
  • 함수를 옮겨 담기 전에는 메서드 이름과 시그니처가 처음 Azure Function을 만들었을 때와 동일한지 점검합니다. 만약 정상적으로 실행되지 않는다면 function.json 파일의 내용을 참고합니다.
{
  "disabled": false,
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 */5 * * * *"
    }
  ]
}
  • 마지막으로 앞에서 TimerTrigger나 BlobTrigger, 혹은 ServiceBusTrigger와 같이 트리거에 지정한 인자의 값을 확인하여 function.json 파일을 수정하도록 합니다. 위의 예제의 경우 매 초 마다 실행되도록 하였으므로, function.json의 schedule 프로퍼티를 “* * * * * *”으로 바꾸어야 합니다.

마무리

이렇게 해서 만들어진 최종 버전의 CSX 파일을 실제 Azure Function 서비스로 배포하는 것은 자유롭게 할 수 있습니다. 연속성 있는 개발을 위해서, 버전 관리 저장소를 통하여 배포하도록 설정해두면 더욱 편리할 것입니다.

이 글을 작성하면서 좀 더 고민해볼 만한 주제가 있다면, 아래와 같은 부분들이 있을 것 같습니다.

  • HTTP Trigger, Web Hook Trigger는 Web Job과 사실 직접적인 상관이 없으며, ASP.NET Web API의 서브셋에 가깝습니다. 다만 TraceWriter 클래스를 사용하는 부분만이 온전히 Web Job에 관련이 있는 부분입니다. 이 부분을 감안하여 DummyTraceWriter 클래스를 만들어 단위 테스트를 하도록 할 수 있을 듯 합니다.
class DummyTraceWriter : TraceWriter
{
    public DummyTraceWriter() : base(default(TraceLevel)) { }
    public override void Trace(TraceEvent traceEvent) => Console.WriteLine(traceEvent);
}
  • LINQPAD용 스크립트 템플릿을 만들어 공유한다면 정식 SDK가 출시되기 전에 더 많이 Azure Function을 개발하고 테스트할 수 있을 것이라고 생각합니다.
  • 일부 네이티브 코드를 포함하는 NuGet 패키지는 아마도 64비트용으로 빌드된 패키지를 사용하는 것이 실행에 문제가 없을 것으로 예상합니다. 32비트 버전의 패키지도 별도 EXE 파일로 실행하는 경우에는 Windows-on-Windows 호환성 기능으로 실행은 보장될 수 있을 것입니다.

더 세부적인 사항, 보충할 부분, 혹은 수정해야 할 부분에 대한 의견을 주시면 큰 도움이 될 것 같습니다.

새로운 .NET AOP 프레임워크 NConcern

Java와는 다르게 .NET은 AOP에 대한 논의나 실제 적용 사례를 찾기 쉽지 않았는데, 개인적으로는 가장 큰 이유가 .NET은 AOP 관점을 실제 런타임에 불어넣기 위한 Weaving 기법을 적용하기 매우 어렵기 때문이라고 생각합니다.

Java의 경우, 별도의 제약을 가하지 않는 한 클래스를 상속하여 필요한 구현체 클래스의 메서드를 자유롭게 재정의할 수 있지만, .NET의 경우 virtual method, 대리자, 혹은 데코레이터 패턴을 사전에 고려하지 않는 한 IL 수준이나 개발 도구 수준에서 미리 대비해야만 원하는 AOP 컨셉을 만들어낼 수 있습니다.

Java, Spring, AspectJ를 배워가면서 틈나는대로 AOP에 관한 다른 언어나 닷넷의 대응 구현체를 찾다가 뜻있는 라이브러리가 있어 간단한 아티클을 써봅니다. 바로 NConcern이라는 라이브러리입니다.

NConcern은 Java의 AOP 프레임워크와 거의 비슷하게 동작합니다. 그리고 PostSharp의 Compile Time Weaving과 Runtime Weaving과 동일한 기능을 제공합니다. 특히 Compile Time Weaving은 Mono의 Cecil 라이브러리를 기반으로 구현한 CNeptune 라이브러리의 도움을 받습니다.

아쉽게도 2017년 4월 현재 .NET Core는 지원하지 않고, .NET Framework 4.0 이상의 프로젝트에 대해서만 지원하는 상태입니다.

NConcern 시험해보기

NConcern이 어떻게 동작하는지 확인해보기 위하여 Visual Studio로 간단한 Console Application 프로젝트를 생성합니다. 프로젝트를 생성한 다음, NuGet Package 관리자로 다음의 두 패키지를 추가합니다.

  • CNeptune
  • NConcern

참고로 Compile Time Weaving을 사용하지 않고 Runtime Weaving만 사용하는 경우에는 NConcern만 설치해도 됩니다. 다만 이 아티클에서 이야기하려는 것은 Compile Time Weaving에 관한 것이므로 CNeptune까지 설치해서 테스트하는 것이 필요합니다.

정상적으로 패키지를 설치한 다음에 packages.config 파일에 다음과 같이 변경되어있는지 확인합니다.

<?xml version=”1.0″ encoding=”utf-8″?>
<packages>
<package id=”CNeptune” version=”1.0.6″ targetFramework=”net452″ />
<package id=”NConcern” version=”4.0.2″ targetFramework=”net452″ />
</packages>

이제 테스트용 클래스를 하나 만듭니다.

public sealed class Sample
{
public void Test()
{
Console.WriteLine(“Hello, World!”);
}
}

보시다시피 sealed 키워드로 선언되어있어 상속이 불가한 클래스입니다. 뿐만 아니라 Test 메서드는 virtual 메서드가 아니므로 직접적인 재정의가 불가합니다.

그리고 로그 기록을 목적으로 하는 Logging Aspect를 하나 추가하겠습니다.

public class Logging : IAspect
{
public IEnumerable<IAdvice> Advise(MethodBase method)
{
yield return Advice.Basic.Before((instance, arguments) =>
{
Console.WriteLine($”Before {method.Name}({String.Join(“, “, arguments)})”);
});
yield return Advice.Basic.After((instance, arguments) =>
{
Console.WriteLine($”After {method.Name}({String.Join(“, “, arguments)})”);
});
}
}

이제 Logging Aspect를 Weaving 하는 코드를 추가하겠습니다. 어트리뷰트나 인터페이스에 매칭되지 않지만 단지 메서드 이름이 “Test”인 메서드에 대해 Logging Aspect를 Weaving 하도록 지시하고, Sample 클래스를 인스턴스화하여 Test 메서드를 호출하는 코드입니다.

class Program
{
static void Main(string[] args)
{
Aspect.Weave<Logging>(x => x.Name == “Test”);
var test = new Sample();
test.Test();
}
}

그리고 이 코드를 컴파일하여 실행하면 다음과 같이 기대한 결과가 나타납니다.

Before Test()
Hello, World!
After Test()
Press any key to continue . . .

무엇이 달라졌는가

CNeptune 패키지를 설치하면 해당 프로젝트가 생성하는 어셈블리를 Compile Time Weaving을 할 수 있도록 어셈블리를 재작성하는 절차가 MSBUILD 프로젝트의 일부가 됩니다. 패키지를 설치한 프로젝트의 CSPROJ 파일을 열어보면 다음과 같은 부분이 추가된 것을 볼 수 있습니다.

<Import Project=”..\packages\CNeptune.1.0.6\build\CNeptune.targets” Condition=”Exists(‘..\packages\CNeptune.1.0.6\build\CNeptune.targets’)” />
<Target Name=”EnsureNuGetPackageBuildImports” BeforeTargets=”PrepareForBuild”>
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition=”!Exists(‘..\packages\CNeptune.1.0.6\build\CNeptune.targets’)” Text=”$([System.String]::Format(‘$(ErrorText)’, ‘..\packages\CNeptune.1.0.6\build\CNeptune.targets’))” />
</Target>

이에 따라 만들어지는 IL 코드는 컴파일러에 의하여 만들어내는 코드와는 다르게 코드를 재정의하기 손쉬운 형태로 변경하여 내보내게 됩니다. Weaving을 실제로 호출하든 하지 않든 MSBUILD를 통해 CNeptune을 호출하도록 되어있으므로 어셈블리의 결과물은 CNeptune 패키지 적용 이전과 달라지게 됩니다.

마무리

컴파일 타임에서의 처리이지만 Weaving의 적용과 해제가 자유롭도록 되어있습니다. 위의 샘플 코드 중 Main 메서드에 아래의 코드를 추가로 더 넣어 실행해보면 역시 의도한대로 결과가 나타나게 됩니다.

Console.WriteLine();
Aspect.Release<Logging>(x => x.Name == “Test”);
test.Test();

Before Test()
Hello, World!
After Test()

Hello, World!

이와 같이 .NET에서도 오픈 소스화된 AOP 프레임워크를 찾아볼 수 있게 되었습니다. 아울러 NConcern와 CNeptune은 모두 MIT 라이선스이므로 상용 프로젝트에도 라이선스 걱정 없이 적용할 수 있습니다.

앞으로 발전이 기대되는 라이브러리입니다. 🙂

이미지 출처: https://commons.wikimedia.org/wiki/File:Effects_of_aspect_on_vegetation-_SW_Idaho.JPG

LINQPad로 Facebook Graph API 빠르게 테스트하기

C#을 사용하면서 알아두면 여러모로 유용하게 활용할 수 있는 도구로 LINQPad가 있습니다. LINQPad Developer Edition부터는 NUGET 패키지를 Query에 포함시킬 수 있는 기능도 제공이 되는데, 이 기능을 활용하여 Outercurve Foundation이 관리하는 Facebook .NET SDK NuGet 패키지를 추가하여 빠르게 Facebook Graph API를 호출할 수 있습니다.

LINQPad에서 NuGet 패키지를 추가하려면, 쿼리 창에서 오른쪽 버튼을 클릭한 다음, NuGet Package Manager 메뉴를 선택합니다.

그 다음, 검색어에 Facebook을 입력하여 검색하면, Facebook이라는 이름의 NuGet 패키지가 검색 결과 제일 처음에 나타납니다. Add To Query 버튼을 눌러 패키지 캐시에 추가한 다음, 설치가 완료되면 Add Namespace 링크를 클릭하여 쿼리에서 편하게 쓸 수 있도록 합니다.

아래 코드 조각을 테스트하기 위해서는 App ID, App Secret, Access Token을 사전에 Facebook Developer 페이지를 통하여 획득하셔야 합니다. 아래 샘플 코드에서는 관리 권한이 있는 Facebook Page에 대해 간단하게 포스팅하고, 해당 포스트의 정보를 가져오는 코드이므로 대상 Page ID도 획득해야 합니다.

string appId = "";
string appSecret = "";
string accessToken = "";
string pageId = "";

FacebookClient client = new FacebookClient(accessToken)
{
  AppId = appId,
  AppSecret = appSecret
};
dynamic result = null;

result = client.Post($"/{page_id}/feed", new
{
  message = $"Random Message {DateTime.UtcNow.Ticks.ToString()}"
});
((object)result).Dump();

result = client.Get($"/{result.id}", new
{
  fields = new string[] { "id" }
});
((object)result).Dump();

Facebook의 Graph API가 반환하는 JSON 응답 객체를 Newtonsoft JSON 라이브러리를 이용하여 C# 객체로 변환하면, 이것을 DLR 바인딩에 연결하여 필요한 프로퍼티에 액세스할 수 있습니다. 그리고 이렇게 얻어온 응답 결과를 LINQPad의 내장 Extension 메서드인 Dump 메서드로 시각적으로 잘 정리된 형태의 표로 볼 수 있습니다.

한 가지 아쉬운 점은, DLR 컨텍스트에서는 LINQPad의 Dump 확장 메서드가 제대로 작동하지 않아서, object 타입으로 캐스팅하여 DLR 컨텍스트를 해제한 다음 Dump 메서드를 호출해주어야 합니다. 실행 결과는 아래와 같습니다.

Facebook이 아니어도, Newtonsoft JSON과 HttpClient를 활용하면 비슷한 방법으로 REST API들을 테스트해볼 수 있습니다.

 

Ubuntu on Windows 10으로 GTK# 응용프로그램 개발해보기

Windows 10의 대규모 업데이트에서 개발자들에게 주목을 받고 있는 기능들이 여러가지가 있습니다. 그중에서도 Ubuntu on Windows 10에 대한 관심이 많이들 있으실텐데, 이번 아티클에서는 저 나름대로 찾아본 Ubuntu on Windows 10을 이용한 GTK# 기반의 클라이언트 응용프로그램 개발 방법을 소개해보려고 합니다.

왜 Ubuntu on Windows 10인가?

VM을 사용할 때의 이점은 완전히 독립된 환경을 만들 수 있다는 것이지만, 달리 표현하면 관리해야 할 컴퓨터의 숫자가 늘어난다는 것을 의미하기도 합니다. Ubuntu on Windows 10이 돋보이는 이유는 바로 간편성에 있습니다.

Ubuntu on Windows 10은 Subsystem for Linux 라는 새로운 기능 위에서 작동합니다. 과거에 서버용으로는 Subsystem for Unix라는 기술이 있었는데, 이 때와는 다르게 Windows 운영 체제의 환경과는 분리된 샌드박스된 환경 위에서 실행되고, 바이너리 수준의 호환성을 보장한다는 것이 차이점입니다. 그렇기에 실용적으로 기능을 활용할 수 있고, 호스트로 실행되는 Windows 운영 체제의 안정성을 해칠 만한 상황을 최소화할 수 있어 마음놓고 사용할 수 있습니다.

Ubuntu on Windows 10으로 할 수 있는 일

Ubuntu on Windows 10은 실제 Ubuntu Linux와는 다릅니다. 어디까지나 User Mode에서 실행되는 바이너리 파일에 대한 실행 환경만을 보장할 뿐, 프로덕션 환경에서 실행될 서버를 띄우거나, Windows Shell을 대체하기 위한 목적, 혹은 커널 드라이버 개발 등의 목표를 가지고서는 사용이 쉽지 않고 불편합니다.

VM 없이 Ubuntu Linux에서 실행해볼 필요가 있는 응용프로그램을 사용하거나 테스트하기 위한 용도로만 초점이 맞추어져야 하며, 아티클에서 다루는 모든 내용은 “개발과 테스트”를 전제로 합니다.

준비할 사항

Ubuntu on Windows 10을 시스템에 이미 설치하셨다는 것을 전제로 이 아티클을 참조하여 주십시오. 자세한 설치 방법은 이곳을 참조하여 주십시오.

이 글을 작성한 2016년 8월 현재, Windows 10의 1주년 업데이트가 정식으로 나온 현 시점까지도 Ubuntu on Windows 10과 서브 시스템은 베타 상태에 있습니다. 그리고 이와 관하여 한국어 지원 역시 완전하지 않습니다.

Subsystem을 설치한 후에 Ubuntu on Windows 10을 lxrun으로 처음 설치하실 때에는 Windows 사용자 계정 이름이 영어로 되어있는 것을 춴합니다. 또한 콘솔에서 한국어 출력이 자연스럽지 않은데, 개인적으로는 로캘 설정을 시스템 기본 설정 대신 영어와 UTF-8 인코딩 셋으로 변경하시는 것을 권해드립니다. 변경하려면 bash 셸을 실행한 다음 아래 명령어를 입력합니다.

sudo update-locale LANG=en_US.UTF8

입력한 다음 bash 셸을 종료하고 다시 시작하면 모든 표시 언어가 미국 영어로 변경되어 나타나게 될 것입니다.

Mono 설치하기

.NET Core의 런타임이 RTM이 출시되었지만 그동안 크로스플랫폼 기반 .NET 개발의 중추적인 역할을 담당하고 있었던 것은 Mono 프레임워크였습니다. 현재까지도 우리가 필요로 하는 대다수의 기능은 .NET Core가 아니라 여전히 Mono 기반이며, .NET Core에서도 자체 런타임 대신 전체 버전의 .NET Framework가 필요한 경우 Mono을 사용하므로 여기서는 Mono의 설치를 먼저 다루도록 하겠습니다.

Ubuntu on Windows 10에 제공되는 Ubuntu Linux는 14.04 (trusty) 버전이며, 버전을 확인해보면 다음과 같습니다.

root@DESKTOP-R1OP5CI:/mnt/c/Users/rkttu# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.4 LTS
Release:        14.04
Codename:       trusty

14.04 버전을 기준으로 Mono를 설치하는 과정을 진행해보도록 하겠습니다. Mono 최신 버전은 운영 체제가 제공하는 패키지 저장소가 아닌 자체 저장소를 통해서 다운로드해야 설치가 가능한데, Ubuntu 계열 운영 체제를 위한 저장소를 사용한 설치 방법 안내 페이지는 이곳을 참조합니다.

아래 명령어를 실행하여 외부 패키지 저장소를 시스템에 등록합니다.

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
sudo echo "deb http://download.mono-project.com/repo/debian wheezy main" | sudo tee /etc/apt/sources.list.d/mono-xamarin.list
sudo echo "deb http://download.mono-project.com/repo/debian wheezy-apache24-compat main" | sudo tee -a /etc/apt/sources.list.d/mono-xamarin.list
sudo apt-get update

그 다음, mono-complete, referenceassemblies-pcl, monodevelop 그리고 xinit 패키지를 아래 명령어를 실행하여 설치합니다. X-Windows 환경도 이 과정에서 종속성 체인에 의하여 모두 설치가 이루어지므로 디스크 공간 사용량이 크게 늘어나니 주의해야 합니다.

sudo apt-get install mono-complete referenceassemblies-pcl monodevelop xinit -y

긴 설치 과정이 모두 끝나면 다음 명령어를 실행하여 mono와 mcs 컴파일러가 최신 버전으로 설치되었는지 확인합니다.

rkttu@DESKTOP-P834HKI:/mnt/c/Users/rkttu$ mono --version
Mono JIT compiler version 4.4.2 (Stable 4.4.2.11/f72fe45 Fri Jul 29 09:58:49 UTC 2016)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com
        TLS:           __thread
        SIGSEGV:       altstack
        Notifications: epoll
        Architecture:  amd64
        Disabled:      none
        Misc:          softdebug
        LLVM:          supported, not enabled.
        GC:            sgen
rkttu@DESKTOP-P834HKI:/mnt/c/Users/rkttu$ mcs --version
Mono C# compiler version 4.4.2.0

MonoDevelop 실행을 위한 설정

이제 개발 환경의 준비는 모두 마무리했고, X Window를 실행하기 위한 준비를 해야 합니다. Windows OS에서는 Xming 서버라는 X Window 호환 서버를 설치할 수 있으며 Ubuntu on Windows에서 쉽게 연결할 수 있습니다.

https://sourceforge.net/projects/xming/ 에서 최신 버전의 Xming 서버를 우선 설치하도록 합니다. 설치 후에는 직접 Xming 서버를 한 번 실행하여 아래 그림과 같이 트레이에 실행 중인 모습을 확인해야 합니다.

Xming 서버와 연결하는 설정을 현재 세션에서만 사용하려면 터미널에서 다음 명령어를 입력하면 됩니다.

export DISPLAY:=0

그리고 위의 명령어를 현재 로그인한 사용자에 대해서만 항상 적용하려면 아래 명령어를 실행하여 bashrc 파일에 내용을 저장하고 bash 셸을 다시 시작합니다.

nano ~/.bashrc

설정을 적용한 다음에는 아래 명령어를 실행합니다.

monodevelop

잠시 기다리면 아래 그림과 같이 Windows 10에서 리눅스 버전으로 실행되는 MonoDevelop의 화면이 보입니다.

계속하기 전에, 사용 상의 편의를 위하여 단축 키 바인딩과 글꼴을 Visual Studio에 가깝게 변경해보겠습니다. Edit – Preference 메뉴를 선택한 다음, Environment – Key Binding에 가서 Visual Studio 템플릿을 선택하고, Font에서 원하는 서체와 크기를 지정합니다.

만약 나눔고딕코딩 같은 서체가 필요하다면 다음의 명령어를 실행하고 MonoDevelop을 다시 실행하여 서체 목록을 보면 목록에 표시될 것입니다. D2Coding과 같은 서체는 수동으로 OTF나 TTF 파일을 설치하는 방법을 설명하는 가이드를 참고하시면 되겠습니다.

sudo apt-get install fonts-nanum fonts-nanum-coding -y

첫 GTK# 프로젝트 만들고 실행해보기

MonoDevelop이 실행되었으니 GTK# 프로젝트를 한 번 만들어보겠습니다. Visual Studio를 사용하시는 것이 어느정도 익숙하다는 전제에서 출발하겠습니다.

새 솔루션 만들기 메뉴를 선택하면 다음과 같이 화면이 나타날 것입니다. GTK# 2.0 프로젝트 템플릿을 선택합니다. (VB.NET 언어는 제대로 지원되지 않습니다.)

프로젝트 생성 시 Location에서 사용자 홈 디렉터리 아래로 경로를 한 번 선택해주는 것이 좋습니다.

프로젝트를 만든 다음에는 User Interface 트리 아래의 MainWindow를 더블 클릭하여 엽니다.

그러면 디자이너가 열립니다. 여기서 화면 오른쪽의 Toolbox 탭을 더블 클릭하면 우리가 아는 Windows Forms나 WPF 디자이너와 마찬가지로 팔레트가 나타납니다.

GTK#은 Windows Forms와는 다르게, 혹은 WPF처럼 컨테이너라는 부모가 있는 것을 전제로 하며, Windows Forms 처럼 디자인하려면 우선 Fixed 컨테이너가 배치되야 합니다. Fixed 컨테이너를 배치하고 그 위에 버튼을 올려보면 위의 그림과 비슷하게 됩니다.

버튼을 선택하고, Properties 탭을 더블 클릭한 후, Signals 탭을 선택하면 버튼에 대해 지정할 수 있는 이벤트들이 표시됩니다. Button Signals를 펼치고 Clicked 항목을 더블 클릭하면 코드 비하인드에 자동으로 지금 선택한 버튼에 대한 처리기 메서드가 추가됩니다.

이제 디자이너 하단의 Sources 버튼을 클릭하고, 방금 추가했던 이벤트 처리기에 코드를 작성합니다. (메서드 이름은 상황에 따라 다를 수 있습니다.)

  protected void OnButton2Clicked (object sender, EventArgs e)
    {
        MessageDialog md = new MessageDialog (null, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, "test");
        md.Run ();
        md.Destroy ();
    }

이제 F5키를 눌러 프로젝트를 빌드하고 디버거에 연결합니다. 화면이 나타나면 버튼을 클릭했을 때 다음과 같이 표시될 것입니다.

덤으로, 지금 이 상태가 작업 관리자에서는 어떻게 표현되고 있을까요? Mono 4.x 이후부터 공식적으로 채택된 SGEN을 런타임으로 사용하기 때문에 macOS에서 mono 기반 응용프로그램을 돌릴 때와 마찬가지로 mono-sgen 프로세스가 실행 중인 상태인 것을 볼 수 있습니다.

결론

Windows 10에 Linux 서브 시스템이 도입되었다는 것은 시사하는 바가 무척 많으며, 보신 것과 같이, GTK#을 Windows OS로 포팅하지 않고, 네이티브 리눅스와 유사한 환경에서, MonoDevelop으로 쉽게 개발할 수 있는 상태가 되었습니다.

MonoDevelop은 GTK# 만이 아니라 콘솔 프로그램, 그리고 ASP.NET 개발 환경도 지원합니다. 애석하게도 XSP4는 지금 이 아티클을 작성하는 시점에서 제대로 작동하지 않고 있지만 곧 업데이트가 이루어질 것이라고 생각합니다.

또한 MonoDevelop은 NuGet 패키지 설치도 지원하므로 필요한 패키지가 있으면 쉽게 설치할 수 있습니다.

이후에 TCP Listener를 기반으로 하는 서버 애플리케이션이나 ASP.NET Core 실행 사례도 추가 아티클 상에서 살펴보겠습니다.

Electron으로 크로스 플랫폼 데스크톱 앱 만들기 #1 – 프로젝트 스캐폴딩

요즈음 모바일 앱은 Xamarin, Cordova 등 다양한 프레임워크를 이용하여 개발하는 사례가 매우 많습니다. 하지만 상대적으로 Desktop App은 이제 막 크로스플랫폼 앱 개발에 대한 논의가 시작되는 단계여서 상대적으로 정보가 많이 적고, 사용할 수 있는 리소스가 모바일 크로스플랫폼 개발에 비해서는 적은 편입니다.

연속되는 아티클 시리즈를 통하여 데스크톱 앱을 크로스플랫폼으로 개발하려고 할 때 시작할 수 있는 유용한 선택지인 Electron과 EdgeJS의 결합 시나리오에 대해 살펴보려고 합니다. 이번에는 그 중 가장 기초가 되는 처음 Electron 프로젝트를 만드는 과정을 살펴보려고 합니다.

Electron에 대하여

Electron은 Github의 크로스플랫폼 에디터인 Atom을 위하여 처음 개발되었고, 그 이후로 많은 발전을 거쳐 여러 애플리케이션의 기반 프레임워크로 현재는 널리 사용되고 있습니다. 최근 큰 주목을 받고 있는 Visual Studio Code는 물론, Slack, 잔디 등 국내외 여러 최신 소프트웨어들이 Electron을 기반으로 하이브리드 + 크로스플랫폼 소프트웨어를 개발하고 있습니다.

Electron의 생태계

Electron은 기술적으로 NodeJS의 기능과 Chromium의 렌더링 엔진을 모두 사용할 수 있습니다. 이 덕분에 Node Package Manager (NPM)과 Bower의 기능을 모두 사용할 수 있어 UI는 익숙하게 사용할 수 있는 jQuery, jQuery UI 및 jQuery Mobile이나 최근 유행하고 있는 Angular 2, React 등을 택할 수 있고, 비즈니스 로직 부분은 TypeScript나 CoffeeScript 등을 활용하여 개발할 수 있습니다.

Electron의 확장성은 여기서 그치지 않고 .NET과의 상호 운용 기능을 제공하는 EdgeJS와도 연동도 제공하고 있습니다. EdgeJS는 Windows에서는 .NET Framework 4.5 이상, Linux와 macOS에서는 Mono 4.x 이상의 프레임워크와 연동되며, C#, F# 등 다양한 언어와의 바인딩과 기존 클래스 라이브러리의 재사용을 모두 소화할 수 있습니다. 기존에 개발했던 Windows Forms나 WPF 기반의 Desktop App을 크로스플랫폼으로 전환하는 것을 염두에 두고 계시다면 현실적인 선택지 중 하나가 될 수 있습니다.

마지막으로, 데스크톱 앱을 수익화하기 위한 방법에서도 Electron은 이제 완벽한 선택지라고 할 수 있습니다. Windows의 경우 이 글을 작성하는 현재 2016년 여름에 Windows 10 Anniversary Update를 통하여 Native App을 스토어에 공식적으로 게시할 수 있게 됨에 따라 Electron App을 스토어에 등록할 수 있고, Linux의 경우 다양한 경우의 수가 있겠지만 데스크톱에서 가장 널리 쓰이는 Ubuntu Software Store에 등록할 수 있으며, Mac App Store로의 Electron App 등록은 이전부터 계속 가능했습니다.

이러한 생태계 아래에서 기존의 Native Application이나 .NET Managed Application을 Electron을 기반으로 리뉴얼을 고려해보실 수 있습니다.

첫 Electron 프로그램 만들기

Electron 기반의 개발 환경은 Windows, Linux, macOS 플랫폼이면 쉽게 구축이 가능합니다. 그 중에서도 이번 아티클에서는 Visual Studio Code를 기반으로 구축하는 예를 들어보려고 합니다.

Visual Studio Code, NodeJS, NPM, Bower를 시스템에 이미 설치한 상태임을 가정하고 진행해보도록 하겠습니다.

작업 디렉터리를 새로 하나 만들고, 해당 디렉터리에서 아래 명령을 실행하여 package.json과 bower.json을 만들어 프로젝트 기준점을 잡도록 합니다.

npm init
bower init

그 다음, electron-prebuilt 패키지를 개발 종속성 패키지로 설치하여 개발 과정 중에 Electron에 연동하여 사용할 수 있게 합니다.

npm install --save-dev electron-prebuilt

Bower를 이용하여 jQuery Mobile 패키지를 설치하겠습니다. 패키지 이름이 jquery-mobile-bower인 것을 설치해야 즉시 사용 가능한 jQuery Mobile 패키지가 설치되니 이름에 주의하여 주십시오.

bower install --save jquery-mobile-bower

이제 Electron 엔진을 사용하여 창을 띄우고 첫 UI를 표시하도록 코드를 조금 추가해보겠습니다.

package.json 파일을 만들 때, 시작점이 되는 JavaScript 파일의 이름을 따서 새 JavaScript 파일을 만듭니다. 예를 들어, package.json 파일이 아래와 같이 되어있다면 index.js 파일을 새로 만듭니다.

{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron-prebuilt": "^1.2.2"
  },
  "dependencies": {
    "electron-edge": "^5.0.3-pre1"
  }
}

index.js 파일을 다음과 같이 만듭니다.

const electron = require('electron');
// Module to control application life.
const {app} = electron;
// Module to create native browser window.
const {BrowserWindow} = electron;

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

function createWindow() {  
  // Create the browser window.
  win = new BrowserWindow({width: 800, height: 600});

  // and load the index.html of the app.
  win.loadURL(`file://${__dirname}/index.html`);

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null;
  });
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow();
  }
});
위의 코드에서 index.html 파일을 로드하도록 하였으므로, index.html 파일을 아래 코드와 같이 만듭니다. 아래 코드의 자세한 내용은 jQuery Mobile 튜토리얼을 참고하시면 이해하기 쉽습니다. (맥락을 정확하게 유지하기 위하여 jQuery Mobile에 대한 내용은 다루지 않겠습니다.)
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="Stylesheet" media="all" type="text/css" href="bower_components/jquery-mobile-bower/css/jquery.mobile-1.4.5.css" />
    <style type="text/css" media="all">
    body { cursor: default; }
    </style>
    <script type="text/javascript" src="compat.js"></script>
    <script type="text/javascript" src="bower_components/jquery/jquery.min.js"></script>
    <script type="text/javascript" src="bower_components/jquery-mobile-bower/js/jquery.mobile-1.4.5.min.js"></script>
</head>
<body>
    <div data-role="page" id="start">
        <div data-role="header">
            <h1>Welcome To My Homepage</h1>
        </div>
        <div data-role="main" class="ui-content">
            <p>I Am Now A Mobile Developer!!</p>
            <a href="#lastStep">Go to Next Page</a>
        </div>
        <div data-role="footer" data-position="fixed">
            <h1>Footer Text</h1>
        </div>
    </div>
    <div data-role="page" id="lastStep">
        <div data-role="header">
            <h1>Welcome To My Homepage</h1>
        </div>
        <div data-role="main" class="ui-content">
            <a href="#confirmDialog">End</a>
        </div>
        <div data-role="footer" data-position="fixed">
            <h1>Footer Text</h1>
        </div>
    </div>
    <div data-role="page" data-dialog="true" id="confirmDialog">
        <div data-role="header">
            <h1>Welcome To My Homepage</h1>
        </div>
        <div data-role="main" class="ui-content">
            <a href="#start">Go to Start page</a>
        </div>
    </div>
</body>
</html>
앞의 단락에서 이야기한 것처럼, Electron은 NodeJS와 통합된 실행 환경을 사용하고 있습니다. 이렇게 하면서 module 프로퍼티가 일반 웹 브라우저와는 다르게 미리 정의가 되는데, 이 때문에 jQuery가 전역 객체로 설정되지 않습니다. (https://blog.outsider.ne.kr/1170 아티클의 내용을 보고 도움을 얻을 수 있었습니다.)
이런 동작은 버그가 아니고 By-Design 사항입니다. 물론 필요에 따라 Chromium의 JavaScript 컨텍스트만 사용하도록 제한할 수 있지만 이렇게 할 경우 Electron의 강력한 기능을 활용할 수 없으므로 이 방법 대신 아래 코드를 compat.js로 저장하여 HTML 페이지에서 먼저 참조하고 그 다음에 jQuery를 로드하도록 수정하여 jQuery와 NodeJS 모듈을 동시에 사용할 수 있게 하려고 합니다.
window.nodeRequire = window.require;
window.nodeExports = window.exports;
window.nodeModule = window.module;
delete window.require;
delete window.exports;
delete window.module;
위의 코드를 사용하여 require, exports, module 프로퍼티를 nodeRequire, nodeExports, nodeModule로 이동시키고, jQuery가 정상적으로 초기화될 수 있게 합니다. 나중에 electron과 연동을 할 때에는 새 이름으로 정의된 객체를 사용하도록 하여 어렵지 않게 연동이 가능하게 됩니다.
이렇게 코드를 구성하고, 아래와 같이 명령을 작업 디렉터리 내에서 실행하면 앱이 실행됩니다.
node_modules/.bin/electron .
001
Production 버전으로 패키지를 만든 것이 아니고, 메뉴 등이 그대로 표시되므로 Developer Tool을 이용할 수 있습니다. Developer Tool을 View – Toggle Developer Tool 메뉴로 켜고 끌 수 있으며, 실행하면 창 하단/우측에 붙어서 나오거나 아래 그림처럼 별도의 창으로 띄울 수도 있습니다.
002

다음 아티클에서는

다음 아티클에서는 Visual Studio Code와 통합 디버깅 환경을 설정하는 방법과 JavaScript 디버깅을 손쉽게 수행할 수 있는 방안을 살펴볼 에정입니다.

.NET에서의 String과 Null Character에 대한 이야기

.NET은 문자열을 다루는 데 있어서 C, C++, 혹은 파스칼과 비슷한 듯 다른 면이 있습니다. 그리고 이번 아티클에서는 사소하지만 큰 오류를 내포하게 될 가능성이 있는 부분을 잠시 소개하려고 합니다.


http://msdn.microsoft.com/ko-kr/library/ms228362.aspx 에서는 .NET의 String에 대해 이렇게 소개하고 있습니다.


 


 



문자열은 값이 텍스트인 String 형식의 개체입니다. 내부적으로 텍스트는 Char 개체의 순차적 읽기 전용 컬렉션으로 저장됩니다. C# 문자열 끝에는 null 종결 문자가 없습니다. 따라서 C# 문자열은 포함된 null 문자(‘′)를 제한 없이 포함할 수 있습니다. 문자열의 Length 속성은 유니코드 문자의 수가 아니라 포함된 Char 개체의 수를 나타냅니다. 문자열에서 개별 유니코드 코드 포인트에 액세스하려면 StringInfo 개체를 사용합니다.


굵게 강조 표시한 부분의 내용에 오늘 아티클의 핵심 내용이 모두 들어있습니다. 하지만 꼼꼼하게 기억해두지 않으면 허술하게 다루어질 가능성도 있는 부분이라고 생각합니다.


위의 내용을 상기하면서, 아래의 코드들이 각각 어떻게 실행될지 예상해보면 흥미롭습니다.
string a = “‘abc'”.Replace(”’, default(char));
Console.WriteLine(“a: {0} (Length: {1})”, a, a.Length);
string b = “‘abc'”.Replace(”’, Char.MinValue);
Console.WriteLine(“b: {0} (Length: {1})”, b, b.Length);
string c = “‘abc'”.Replace(”’, (char)0);
Console.WriteLine(“c: {0} (Length: {1})”, c, c.Length);
string d = “‘abc'”.Replace(”’, ”);
Console.WriteLine(“d: {0} (Length: {1})”, d, d.Length);



 


 


Replace로 한 글자만 제거하고 싶어서 위와 같은 코드를 작성하기 쉬운데, 위의 결과에서 원래 의도는 ‘abc’ 라는 다섯 글자를 abc라는 세 글자로 만드는 것이지만, 실제로는 여전히 다섯 글자가 됩니다. 그런데 여기서 한 가지 더 중요한 것은, Trim() 메서드가 앞 뒤로 붙는 null character를 제거해 주지는 않는다는 점입니다.
string a = “‘abc'”.Replace(”’, default(char)).Trim();
Console.WriteLine(“a: {0} (Length: {1})”, a, a.Length);
string b = “‘abc'”.Replace(”’, Char.MinValue).Trim();
Console.WriteLine(“b: {0} (Length: {1})”, b, b.Length);
string c = “‘abc'”.Replace(”’, (char)0).Trim();
Console.WriteLine(“c: {0} (Length: {1})”, c, c.Length);
string d = “‘abc'”.Replace(”’, ”).Trim();
Console.WriteLine(“d: {0} (Length: {1})”, d, d.Length);



 


앞/뒤로 붙은 null character를 제거하려면 null character를 명시하는 작업이 필요합니다. 그리고 이것은 Replace 메서드에 대해서도 동일하게 적용됩니다.
</pre>
<pre>string a = “‘abc'”.Replace(”’, default(char)).Trim(”);
Console.WriteLine(“a: {0} (Length: {1})”, a, a.Length);
string b = “‘abc'”.Replace(”’, Char.MinValue).Trim(”);
Console.WriteLine(“b: {0} (Length: {1})”, b, b.Length);
string c = “‘abc'”.Replace(”’, (char)0).Trim(”);
Console.WriteLine(“c: {0} (Length: {1})”, c, c.Length);
string d = “‘abc'”.Replace(”’, ”).Trim(”);
Console.WriteLine(“d: {0} (Length: {1})”, d, d.Length);



이런 맥락에서 보았을 때, 외부로부터 들어오는 입력 문자열에 대해 엄격하게 이야기하자면, null character에 대한 것을 String.Empty로 치환하는 작업도 필요할 수 있다고 볼 수 있겠습니다.


 

NUnit Runner를 대신하는 간편한 Self Runner 구현하기

NUnit의 GUI Runner는 여러 개의 테스트 유닛 프로젝트를 로드하여 동시에 테스트 결과를 시각적으로 확인할 수 있는 매우 유용한 유틸리티입니다. 그러나 한 가지 아쉬운 점이 있다면, Visual Studio와 완벽하게 통합되어있지는 않아서 단위 테스트 도중 변수의 상태를 확인하거나 디버깅을 하기에는 불편한 구조로 제작되어있다는 점입니다. 그래서 개인적으로 자주 애용하는 대안으로 Reflection을 사용하여 Test Fixture와 Test Case를 검색하여 자동으로 호출하는 유틸리티 클래스의 소스 코드를 https://github.com/rkttu/nunit-self-runner 에 게시하였습니다.


이 프로그램 코드는 NUnit Framework 어셈블리 외에 특별한 종속성이 없고 어떤 코드에서든 쉽게 붙여넣어 시작할 수 있습니다. 그러나 기능 상의 제약이 있는데 다음과 같은 유형의 Test Fixture나 Test Case에서는 작동하지 않습니다.



  • Test Fixture 생성 시 별도의 생성자 매개 변수가 필요한 경우

  • Test Method 실행 시 별도의 호출 매개 변수가 필요한 경우

  • private이나 protected, internal 멤버

이 소스 코드를 NUnit 클래스 라이브러리 프로젝트에 추가하고, 해당 NUnit 클래스 라이브러리를 컴파일하여 실행하면 다음과 같은 형태로 단위 테스트가 전개될 것입니다.



테스트에 실패하는 케이스, 즉 Exception이 발생하면 위와 같이 적색의 Test case failed 라는 문구가 나타나고 자세한 Stack Trace 결과가 노란색의 텍스트로 표시되어 시각적으로 구분을 쉽게 해줍니다. 그리고 실패했다는 사실을 알리기 위하여 테스트가 일시 중단되고, Enter 키를 누르면 계속 실행됩니다. 이 메시지를 확인하고 적절한 위치에 중단점을 설정하면 디버거가 해당 위치에서 중지되므로 좀 더 쉽게 문제를 진단할 수 있습니다.



반면 예외 없이 정상적으로 실행되는 테스트 케이스는 초록색의 Test case succeed 메시지를 표시하고 중단없이 계속 다음 테스트를 진행합니다. 그리고 한 Test Fixture의 실행이 완료되면 다시 사용자의 입력을 대기하는 상태로 들어가며, Enter 키를 누르면 다음 Test Fixture로 진행할 수 있으므로 인터랙티브하게 단위 테스트 결과를 확인할 수 있습니다. 



모든 테스트 Fixture의 실행이 끝난 이후에도 한 번 더 사용자의 입력을 기다립니다. 콘솔에 표시된 전체 내용을 리뷰하고 마지막으로 Enter 키를 누르면 프로그램이 완료됩니다.

Windows Azure Linux Virtual Machine과 docker를 이용한 Linux 기반의 C# 개발 환경 구축

Windows Azure의 Virtual Machine 서비스는 Windows와 Linux를 Guest OS로 지원하는 전형적인 IaaS 플랫폼입니다. 그리고 Linux는 다양한 오픈 소스 소프트웨어와 결합할 수 있는 매우 이상적인 소프트웨어 개발 환경이기도 합니다.


Linux에서 사용할 수 있는 매력적인 소프트웨어들 중 최근 큰 주목을 받고 있는 프로젝트로 docker 프로젝트가 있는데, 이 프로젝트는 기본적으로 Hypervisor 없이 Linux 실행 환경을 가상화하고 서로 격리된 상태로 실행할 수 있도록 도와주는 Linux Container (LXC)를 조금 더 실용적이고 사용하기 편리하게 만들어주는 컴패니언 소프트웨어로, 이미지를 공유하거나, 이미지로부터 생성하는 컨테이너를 만들기 위해 필요한 명령을 자동으로 등록하거나, 컨테이너를 이미지로 변환하는 등의 작업을 단순화합니다.


docker 프로젝트를 설치하고 사용하는 방법에 대한 가이드는 인터넷 상에 이미 다양한 자료로 게시된 적이 있고, 최근에 열린 NAVER 개발자 행사인 DeVIEW 2013에서도 다루어진 적이 있습니다. (http://www.slideshare.net/modestjude/docker-in-deview-2013)


오늘 살펴보려는 내용은 docker 프로젝트를 Windows Azure Virtual Machine 상에서 설치하고 이용하는 방법과 함께, docker Index에 게시되어있는 최신 버전의 Mono 이미지를 다운로드하고 활용하는 방법을 간단히 살펴보려고 합니다. 지금 소개하는 방법을 통해서 쉽고 빠르게 리눅스 기반의 다중 개발 환경을 손쉽고 빠르게 프로토타이핑할 수 있습니다.


시작하기 전에 – Hypervisor 안에서 또다른 가상 환경을 만드는 것은 불가능하지 않습니까?


docker를 일반적인 PC나 서버 환경이 아닌 곳에서 사용할 때 가장 먼저 드는 의문점이 바로 이것입니다. 기본적으로 Hypervisor 위에서 실행하는 OS는 또 다시 가상 환경을 만들어낼 능력이나 여건이 되지 못합니다. 가능하도록 설정했다고 해도 결국 물리적인 한계에 부딪힐 가능성이 커집니다.


사실, docker는 Hypervisor가 아니기 때문에 Hypervisor 고유의 CPU 기능을 활용하는 일은 거의 없고, 호스트가 되는 리눅스의 커널의 재량에 따라 그 안에서 실행되는 독립적인 프로세스일 뿐입니다. 그러나, 제아무리 docker가 유용하다고 해도, 시스템이나 VM에 할당된 자원의 밀도나 품질을 생각해보았을 때 docker가 실제 처리할 수 있는 작업의 양이 항상 효율적이라고는 할 수 없습니다. 따라서, docker를 사용한 시스템 구축은 전적으로 충분한 시나리오 테스트와 QA를 거쳐야만 함을 염두에 두어야 할 것입니다.


docker로 할 수 있는 일


docker는 단순히 프로세스만을 격리하는 것이 아니라 일정 수준의 가상 환경을 다룹니다. 즉, 자체적으로 이용할 수 있는 파일 시스템, 네트워크 어댑터, 가용 메모리 크기 등이 있고, 상황에 따라 이들 자원의 크기가 동적으로 변화하게 됩니다. 그러나 우리가 일반적으로 사용하는 완전한 Hypervisor와는 다르게 자원을 명시적으로 제한할 수 있는 방법이 2013년 10월 현재 기준으로 특별히 없습니다. 그리고 네트워크는 사설 IP로만 할당되기 때문에, docker가 자체적으로 구성하는 NAT를 이용하여 외부로부터 들어오는 네트워크 요청을 특정 컨테이너 앞으로 도착하도록 연결해주는 것이 꼭 필요합니다.


그럼에도 불구하고, docker는 리눅스 기반의 환경에서 항상 있을 수 있는 파일 시스템, 커널, 잘못된 라이브러리나 패키지 설치로 인한 시스템의 중단으로부터 안전하게 지켜줄 수 있고, 신뢰할 수 있을만한 수준을 제공하면서도, 가볍게 다룰 수 있는 가상 환경을 제공한다는 점에서 큰 주목을 받고 있으며, 심지어 Hypervisor 기반의 실행 환경 위에서 전혀 다른 Linux Guest를 추가 실행할 수 있을만큼 유연합니다.


docker Index (https://index.docker.io/)에 게시된 이미지들을 살펴보면 알겠지만 호스트의 Linux OS와 아무 관련이 없는 busybox 같은 응급 이미지도 존재하고, Ubuntu가 호스트인 시스템에서 CentOS를 컨테이너 OS로도 택하는 것이 가능합니다. 즉, docker 환경에 맞추어 개발된 OS이기만하면 docker와의 상호작용을 전제로 호스트 OS와 완전히 분리된 환경에서 실행이 가능하므로 독립적인 환경 구성이 가능함을 뜻합니다.


docker를 Azure Linux VM 위에 설치하기


docker는 LXC 프로젝트를 기반으로 하기 때문에, 커널 버전에 대한 의존성이 크고, 사용할 수 있는 Host OS의 종류에도 제한이 있습니다. 현 시점에서는 Ubuntu Linux 13.04에서의 실행이 가장 안정적이기 때문에, Windows Azure Virtual Machine 갤러리에서 다음 그림과 같이 Ubuntu Linux 13.04 기반의 VM을 하나 추가해서 시작하셔야 합니다. 만약 Ubuntu Linux 12.04나 12.10 버전을 실행 중인 경우 Secure Shell 터미널 환경을 통하여 원격으로 13.04로 업그레이드하는 명령을 실행할 수도 있지만, libcurse 기반의 UI 실행이 일부 필요하기 때문에 putty 등에서는 정상적으로 보이지 않을 수 있고, 또한 잘못 선택할 경우 VM에 원격 접속이 불가능한 상황이 올 수 있으므로 추천하지 않습니다.


http://manage.windowsazure.com/ 으로 접속하여 새 VM을 갤러리로부터 생성하도록 시작합니다. 아래와 같은 대화 상자가 나타나는지 확인한 후 Ubuntu Linux 13.04를 선택합니다.


 



다음 버튼을 클릭한 다음 기본 VM 설정을 입력합니다.


 



PuttyGen을 이용하여 키 체인을 만들어 업로드하거나, 사용자 암호를 지정하고 다음 버튼을 클릭합니다.


 



클라우드 서비스는 네트워크 연결 및 중재를 위한 기본 단위이자 실행 환경을 정의하는 단위 환경입니다. docker 환경과 연결하려는 클라우드 서비스나 다른 VM이 있을 경우 같이 소속되도록 설정해주시고, 가용성 설정 등을 확인한 다음 다음 버튼을 클릭합니다.


 



기본적으로 SSH 포트가 열려있습니다. 그러나 docker container와의 연결을 허용하고 외부에서 접속을 받아들이려면 Windows Azure의 방화벽 설정도 수정해야 합니다. 외부에서 docker container 서비스로의 접속이 안된다면 이쪽의 방화벽 설정을 꼭 확인하셔야 합니다. 마침 버튼을 클릭하여 VM 생성을 시작합니다.


 



VM의 외부 DNS 주소를 확인하고, putty로 접속을 시작합니다.


 



접속이 완료되었다면 이제부터 명령어 입력을 진행합니다. 설치 가이드의 내용은 http://docs.docker.io/en/latest/installation/ubuntulinux/#ubuntu-raring-13-04-64-bit 에서 발췌하였습니다.


우선 최신 패키지 목록을 가져오기 위하여 다음 명령을 수행합니다. 최신 릴리즈를 사용하여 VM을 만들었다면 별 다른 업데이트는 발생하지 않을 것입니다.


sudo apt-get update


그리고 Ubuntu 13.04 시스템들 중 일부 릴리즈에서 누락되어있을 수 있는 AUFS 파일 시스템에 대한 지원 (LXC 실행을 위해 꼭 필요합니다.)을 추가하기 위하여 아래 명령어를 수행합니다. 참고로 Azure Virtual Machine을 통하여 최신 릴리즈를 사용하도록 VM을 생성했다면 역시 AUFS에 대한 지원이 이미 포함되어있어서 아래 명령어로 시스템에 변화가 발생하지는 않을 것입니다.


sudo apt-get install linux-image-extra-</SPAN>uname -r<SPAN class="sb">


이제 docker 리포지터리에 접근하여 패키지를 설치할 차례입니다. 우선 키 체인을 다운로드하여 시스템에 등록하여 docker 패키지를 인터넷으로부터 다운로드할 수 있도록 허가합니다.


sudo sh -c “wget -qO- https://get.docker.io/gpg | apt-key add -“


OK라는 메시지를 확인하였으면 이어서 리포지터리 목록에 docker 리포지터리를 추가하기 위하여 다음 명령어를 실행합니다.


sudo sh -c “echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list”


이제 패키지 업데이트를 다시 수행하여 사용 가능한 패키지 목록을 갱신합니다.


sudo apt-get update


출력 중에서 다음과 비슷한 메시지가 들어있는지 확인합니다.


Get:3 http://get.docker.io docker/main amd64 Packages [1,395 B]


이제 패키지 목록을 업데이트하였으므로 lxc-docker 패키지가 사용 가능한 상태가 되었을 것입니다. lxc-docker 패키지를 아래 명령으로 설치합니다.


sudo apt-get install lxc-docker


종속 패키지들을 다수 설치해야 함을 알리는 메시지가 나타나면 y 키를 눌러 진행합니다.


설치가 잘 되었는지 확인해보기 위하여 docker Index 사이트로부터 ubuntu 게스트 OS 이미지를 다운로드해보겠습니다. 아래 명령어를 실행합니다.


sudo docker run -i -t ubuntu /bin/bash


아래와 같이 다운로드 메시지가 나타나고 콘솔이 바뀌는 것을 확인하기 바랍니다.


rkttu@dockertest:~$ sudo docker run -i -t ubuntu /bin/bash
Unable to find image ‘ubuntu’ (tag: latest) locally
Pulling repository ubuntu
8dbd9e392a96: Download complete
b750fe79269d: Download complete
27cf78414709: Download complete
root@f45ee37cf476:/#


호스트 이름이 Azure VM의 dockertest가 아니라 임의로 작명한 f45ee37cf476이라는 이름으로 바뀐 것을 볼 수 있습니다. 그리고 IP 주소 대역도 다르다는 것을 쉽게 파악할 수 있는데, ifconfig을 비롯한 네트워킹 도구가 이 버전의 이미지에는 들어있지 않기 때문에 다음 명령어를 실행하여 net-tools 패키지를 설치합니다.


apt-get install net-tools


격리 환경 상에서 자동으로 root 권한을 얻었으므로 sudo를 덧붙일 필요가 없습니다. 설치가 끝난 다음, ifconfig eth0 명령을 실행하면 다음과 같이 나타납니다.


eth0      Link encap:Ethernet  HWaddr fa:b2:4f:b9:15:84
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          inet6 addr: fe80::f8b2:4fff:feb9:1584/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:192 errors:0 dropped:0 overruns:0 frame:0
          TX packets:81 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:267507 (267.5 KB)  TX bytes:5591 (5.5 KB)


굵게 강조 표시한대로 사설 IPv4 주소가 할당되어있습니다. 그리고 패키지 업데이트와 업그레이드를 진행하여 인터넷 연결 상태를 다시 한 번 확인해봅니다.


apt-get update
apt-get upgrade


가상 환경에서 나가기 위하여 exit 명령을 입력하면 다음과 같이 원래의 Host OS로 프롬프트가 바뀌는 것을 볼 수 있습니다.


root@f45ee37cf476:/# exit
exit
rkttu@dockertest:~$


이제 방화벽 설정을 확인합니다. 기본적으로 Ubuntu는 ufw라는 방화벽 프로그램을 이용합니다. 그리고 Windows Azure 기본 구성 이미지에서는 ufw가 비활성화되어있고, Windows Azure의 방화벽이 외부로부터의 연결을 차단하며, Windows Azure 간 네트워크에는 제한이 없습니다. 아래 명령어를 입력하여 방화벽 상태를 확인합니다.


rkttu@dockertest:~$ sudo ufw status
Status: inactive
rkttu@dockertest:~$


그리고 방금 전 테스트를 위하여 받은 이미지와 그 이미지를 기반으로 실행한 컨테이너가 잘 등록되었는지 확인하여 설치 프로세스를 마무리합니다.


rkttu@dockertest:~$ sudo docker images -a
REPOSITORY          TAG                 ID                  CREATED             SIZE
ubuntu              12.04               8dbd9e392a96        6 months ago        131.5 MB (virtual 131.5 MB)
ubuntu              latest              8dbd9e392a96        6 months ago        131.5 MB (virtual 131.5 MB)
ubuntu              precise             8dbd9e392a96        6 months ago        131.5 MB (virtual 131.5 MB)
ubuntu              12.10               b750fe79269d        7 months ago        24.65 kB (virtual 180.1 MB)
ubuntu              quantal             b750fe79269d        7 months ago        24.65 kB (virtual 180.1 MB)
<none>              <none>              27cf78414709        7 months ago        180.1 MB (virtual 180.1 MB)


rkttu@dockertest:~$ sudo docker ps -a
ID                  IMAGE               COMMAND             CREATED             STATUS              PORTS
f45ee37cf476        ubuntu:12.04        /bin/bash           8 minutes ago       Exit 0


예상한 대로 이미지가 다운로드되어있고, 해당 이미지를 기반으로 /bin/bash 앱에 대한 컨테이너가 생성 후 실행되었으며 종료 코드 0으로 종료되었다는 결과 표가 보입니다.


mono 개발 환경 빠르게 구축하기


mono 개발 환경을 입맛에 맞게 구축하는 방법은 여러가지가 있습니다. 방금 전처럼 base image를 받아서 수작업으로 설치하거나, 그 과정을 서술하는 Dockerfile을 만들어 한 번에 일괄 실행하여 시스템의 설치를 전개하는 방식도 있을 수 있습니다. 그렇지만 docker를 개발 환경으로 이용하기 위해서 취할 수 있는 가장 좋은 방법은 docker Index 사이트에 게시된 최신 이미지를 확인하여 해당 이미지를 직접 로컬 시스템으로 Pull 하는 것입니다.


편의를 위하여 잠시 리눅스 콘솔에서 윈도 화면으로 되돌아온 다음 https://index.docker.io/ 로 접속하여 키워드로 mono를 지정하고 검색합니다.




 


 

Ubuntu 이미지를 이용하여 Mono 3.2.3을 설치하여 배포하는 이미지가 Docker Index에 올라와있습니다. 그 외에도, 닷넷 기반의 SOA 실행 및 개발 환경 구축을 편리하게 할 수 있도록 Service Stack과 연계한 리눅스 이미지도 보입니다. 우리가 사용하려는 것은 rwentzel/ubuntu-mono 이미지이므로 이 이미지의 이름을 기록합니다.


이제 다시 리눅스 콘솔로 되돌아가서 해당 이미지를 로컬 리포지터리로 다운로드하겠습니다. 다음 명령어를 실행하여 동기화를 시작합니다. 해당 이미지는 여러 차례 수정을 거쳐 만들어진 것이기 때문에 다운로드에 다소 시간이 걸리니 조금 오래 기다리셔야 합니다.


sudo docker pull rwentzel/ubuntu-mono


이미지 다운로드가 완료되었다면, 이제 이 이미지를 이용하여 컨테이너를 만들고 정말 안에 mono 개발 환경이 들어있는지 확인해볼 차례입니다. 아래 명령어로 이미지를 우선 확인합니다.


sudo docker images -a


설치한 이미지의 갯수가 매우 많아졌습니다. 그런데 주의할 것이 하나 있습니다. docker rmi 명령으로 이미지를 제거하는 것이 가능하지만, 꼭 필요한 경우가 아니라면 가급적 리포지터리나 TAG가 none으로 설정된 이미지를 임의로 삭제하는 일은 피해야 합니다. 콘솔에서는 잘 드러나지 않지만 이미지는 차이점 보관 방식으로 생성되어있고 일종의 트리 계층을 형성하기 때문입니다. 그래서 docker로 이미지를 만들 때 정말 중요하게 관리되어야 하는 이미지는 REPOSITORY나 TAG에 정확한 속성을 지정하여 쉽게 찾을 수 있도록 해주는 것이 중요합니다.


REPOSITORY             TAG                 ID                  CREATED             SIZE
rwentzel/ubuntu-mono   latest              2e8ec476cfd1        3 weeks ago         12.29 kB (virtual 2.627 GB)
<none>                 <none>              866ee2ba174c        3 weeks ago         12.29 kB (virtual 2.627 GB)
<none>                 <none>              9d58fdd1f145        3 weeks ago         28.67 kB (virtual 2.627 GB)
<none>                 <none>              48d313d93277        3 weeks ago         194.1 kB (virtual 2.627 GB)
<none>                 <none>              a6f3f7d033e8        3 weeks ago         1.151 GB (virtual 2.627 GB)
<none>                 <none>              0063533722a2        3 weeks ago         486.2 MB (virtual 1.476 GB)
<none>                 <none>              563cc9ce1df7        3 weeks ago         43.2 MB (virtual 989.9 MB)
<none>                 <none>              d963202bdca8        3 weeks ago         62.24 MB (virtual 946.7 MB)
<none>                 <none>              fa1d9d247e8a        3 weeks ago         27.46 MB (virtual 884.5 MB)
<none>                 <none>              e01eae29dae1        3 weeks ago         32.86 kB (virtual 857 MB)
<none>                 <none>              eb5606044c61        3 weeks ago         70.23 MB (virtual 857 MB)
<none>                 <none>              519b96c9a701        3 weeks ago         29.08 MB (virtual 786.8 MB)
<none>                 <none>              552c68e56d9a        3 weeks ago         46.38 MB (virtual 757.7 MB)
<none>                 <none>              17f3c8064eb6        3 weeks ago         25.59 MB (virtual 711.3 MB)
<none>                 <none>              0cb9cc3fc7b8        3 weeks ago         144.5 MB (virtual 685.7 MB)
<none>                 <none>              898780b65670        3 weeks ago         12.29 kB (virtual 541.2 MB)
<none>                 <none>              de6790a79ec8        3 weeks ago         158.2 MB (virtual 541.2 MB)
<none>                 <none>              29002fa46318        3 weeks ago         24.58 MB (virtual 383 MB)
<none>                 <none>              270acf4d2474        3 weeks ago         96.52 MB (virtual 358.5 MB)
<none>                 <none>              1d4aaea09576        3 weeks ago         81.83 MB (virtual 261.9 MB)
ubuntu                 12.04               8dbd9e392a96        6 months ago        131.5 MB (virtual 131.5 MB)
ubuntu                 latest              8dbd9e392a96        6 months ago        131.5 MB (virtual 131.5 MB)
ubuntu                 precise             8dbd9e392a96        6 months ago        131.5 MB (virtual 131.5 MB)
ubuntu                 12.10               b750fe79269d        7 months ago        24.65 kB (virtual 180.1 MB)
ubuntu                 quantal             b750fe79269d        7 months ago        24.65 kB (virtual 180.1 MB)
<none>                 <none>              27cf78414709        7 months ago        180.1 MB (virtual 180.1 MB)


위의 rwentzel/ubuntu-mono 이미지를 사용하여 내부에 들어있는 /bin/bash를 컨테이너로 실행하기 위하여 아래 명령을 실행합니다.


sudo docker run -i -t rwentzel/ubuntu-mono /bin/bash


그러면 다음과 같이 프롬프트가 변경되는 것을 볼 수 있습니다.


rkttu@dockertest:~$ sudo docker run -i -t rwentzel/ubuntu-mono /bin/bash
root@f739c613d0ae:/#


이제 간단한 C# 소스 코드를 작성하여 프로그램으로 컴파일하고 잘 작동하는지 확인해보겠습니다. vi를 사용해도 좋고, vi 사용에 익숙하지 않은 경우 아래와 같이 명령을 실행하여 pico/nano 에디터를 추가 설치할 수 도 있습니다.


apt-get install nano


Hello.cs 라는 소스 코드를 원하는 에디터로 아래와 같이 작성합니다.


using System;
using System.Linq;
using System.Collections.Generic;


public static class Program {
        [STAThread]
        public static void Main(string[] args) {
                var count = args.Count();
                List<string> options = args.Where(x => x.StartsWith(“-“)).ToList();
                Console.Out.WriteLine(“Hello, World!”);
                Console.Out.WriteLine(“Total Args: {0}, Option Args: {1}”, count, options.Count);
        }
}


Mono 3.2.3은 LINQ와 제네릭을 모두 잘 지원하므로 위의 코드를 아래와 같이 컴파일하였을 때 문제없이 컴파일이 완료될 것입니다.


mcs Hello.cs


그리고 JVM과 마찬가지로 Mono VM을 실행하여 컴파일한 어셈블리 파일을 실행해봅니다.


root@f739c613d0ae:/# mono Hello.exe
Hello, World!
Total Args: 0, Option Args: 0
root@f739c613d0ae:/# mono Hello.exe a b c
Hello, World!
Total Args: 3, Option Args: 0
root@f739c613d0ae:/# mono Hello.exe a b -c
Hello, World!
Total Args: 3, Option Args: 1


의도한 대로 LINQ와 제네릭을 잘 받아서 처리하고 있습니다.


mono 실행 속도 개선하기


최신 버전의 mono는 실행 속도를 개선하기 위하여 가비지 컬렉터를 새롭게 디자인하였고, 전처리 컴파일을 미리 수행하는 방법을 제공합니다. 특히, Microsoft .NET Framework와 마찬가지로 GAC에 대해 ngen을 수행하는 것과 비슷하게 AOT 컴파일을 미리 수행하도록 명령어를 한 번 실행해주면 실행 속도 개선에 큰 도움이 됩니다. 아래 명령어를 실행하여 GAC 내부의 모든 어셈블리에 대해 AOT 컴파일을 실행합니다.


for i in /usr/local/lib/mono//mscorlib.dll; do mono –aot $i; done
for i in /usr/local/lib/mono/gac/
//.dll; do mono –aot $i; done


위의 이미지 환경 내에서의 mono 설치 경로는 /usr/local에 있으므로 경로는 mono 실행 파일의 위치를 which 명령으로 확인하여 적절하게 변경해야 합니다. 참고로 GAC에 대한 AOT 컴파일은 시간이 오래 걸릴 수 있으며, 이 버전의 가상 환경에서는 .NET Framework 1.0이나 1.1 기준으로는 개발이 불가능하므로 버전을 업그레이드 하거나 base image로부터 구 버전의 mono를 설치하도록 수동 구성해야 합니다.


그리고 개별 어셈블리에 대한 AOT 컴파일은 다음과 같이 실행할 수 있으며, 실행 결과로 .so 파일이 생성되므로 AOT 컴파일의 효과를 위하여 항상 같은 위치에 배포될 수 있도록 배포합니다.


mono –aot Hello.exe
mono Hello.exe


마무리


지금까지 Windows Azure Linux VM에서 docker를 이용한 mono 개발 환경의 구축 방법을 살펴보았습니다. 언제든 원하는 때에 즉시 Linux VM을 만들 수 있다는 것 말고도, 한 번 만든 VM을 손상시키지 않으면서 환경을 독립적으로 구성할 수 있도록 하는 환경 상의 완결성을 제공하는 docker를 이용함으로서 최상의 리눅스 개발 환경을 체험할 수 있게 된 것은 참 좋은 일입니다.


그리고 한 가지 더 덧붙이면, docker를 이용하여 컨테이너를 만들 때 -v 스위치를 사용하여 호스트 파일 시스템과 링크를 연결할 수 있으므로, 컨테이너 셸 상에서 만든 파일을 쉽게 호스트로 반출하거나 반입할 수 있습니다. -p 스위치는 가상 NAT를 통하여 포트 리디렉션을 할 수 있는 방법을 제공하며, 양쪽 스위치의 자세한 사용법은 http://blog.docker.io/2013/07/docker-0-5-0-external-volumes-advanced-networking-self-hosted-registry/ 의 내용을 확인하기 바랍니다.

Practical Code Writing Tips in C#

C#에서 프로그램 코드를 전개하는 방법은 상대적으로 다른 언어에 비해 자유도가 높은 편입니다. 그렇지만 이런 기능들을 잘 모를 경우 코드 품질이 낮아질 수도 있고, 이해하기 어려운 코드가 되기 쉽습니다. 이러한 문제점을 극복할 수 있는 실용적 코드 작성 팁 몇 가지를 공유해보도록 하겠습니다.


양보하기 어려운 변수 작명을 만났다면?


코딩을 하다보면 그런 경우가 있습니다. 밖으로 드러내는 것이든, 안에서 사용하는 것이든 코드의 의도를 정확히 설명하기 위해서 양보하기 어려운 변수 작명을 고수해야 할 때가 있습니다. 이럴 때에는 고민하지 말고, 변수명 앞에 @ 기호를 지정해주기만 하면 됩니다. C#의 주요 키워드들 (상황에 따라 예약되는 키워드는 이 문제를 만날 가능성이 적습니다.) 상당수를 이 방법을 사용하여 약간 바꾸어 변수 작명으로 채용하는 것이 얼마든지 가능합니다.


string
    @abstract = string.Empty,    @as = string.Empty,    @base = string.Empty,    @bool = string.Empty,
    @break = string.Empty,    @byte = string.Empty,    @case = string.Empty,    @catch = string.Empty,
    @char = string.Empty,    @checked = string.Empty,    @class = string.Empty,    @const = string.Empty,
    @continue = string.Empty,    @decimal = string.Empty,    @default = string.Empty,    @delegate = string.Empty,
    @do = string.Empty,    @double = string.Empty,    @else = string.Empty,    @enum = string.Empty,
    @event = string.Empty,    @explicit = string.Empty,    @extern = string.Empty,    @false = string.Empty,
    @finally = string.Empty,    @fixed = string.Empty,    @float = string.Empty,    @for = string.Empty,
    @foreach = string.Empty,    @goto = string.Empty,    @if = string.Empty,    @implicit = string.Empty,
    @in = string.Empty,    @int = string.Empty,    @interface = string.Empty,    @internal = string.Empty,
    @is = string.Empty,    @lock = string.Empty,    @long = string.Empty,    @namespace = string.Empty,
    @new = string.Empty,    @null = string.Empty,    @object = string.Empty,    @operator = string.Empty,
    @out = string.Empty,    @override = string.Empty,    @params = string.Empty,    @private = string.Empty,
    @protected = string.Empty,    @public = string.Empty,    @readonly = string.Empty,    @ref = string.Empty,
    @return = string.Empty,    @sbyte = string.Empty,    @sealed = string.Empty,    @short = string.Empty,
    @sizeof = string.Empty,    @stackalloc = string.Empty,    @static = string.Empty,    @string = string.Empty,
    @struct = string.Empty,    @switch = string.Empty,    @this = string.Empty,    @throw = string.Empty,
    @true = string.Empty,    @try = string.Empty,    @typeof = string.Empty,    @uint = string.Empty,
    @ulong = string.Empty,    @unchecked = string.Empty,    @unsafe = string.Empty,    @ushort = string.Empty,
    @using = string.Empty,    @virtual = string.Empty,    @void = string.Empty,    @volatile = string.Empty,
    @while = string.Empty,    @__arglist = string.Empty,    @__refvalue = string.Empty,    @__makeref = string.Empty,
    @__reftype = string.Empty;


위의 코드를 컴파일하였을 때 사용하지 않는 변수라는 경고를 제외하고 컴파일에는 이상이 없음을 확인할 수 있습니다.


String.Join 메서드와 같이 시작과 끝에 구분 기호 (Delimiter)가 붙지 않는 문자열 더하기를 수행하는 방법


간혹 그런 경우가 있습니다. 기존 컬렉션으로부터 새로운 컬렉션을 만들면서 시작이나 끝에는 구분자 기호나 원소를 붙이지 않고 중간에만 원하는 내용을 삽입하고 싶을 때가 있는데, 이런 경우 인덱스를 사용하려고 하거나 굳이 배열로 변환하려는 노력을 하게 될 수 있는데, 이는 별로 바람직하지 않습니다. 대신, IEnumerator 인터페이스와 if 문 한번, while 문 한 번으로 나누어 반복문을 써주기만 하면 쉽게 문제가 해결됩니다. 참고로, C#의 foreach 문은 IEnumerator 인터페이스에 대한 포장입니다.


String.Join 메서드와 같은 기능을 하는 메서드를 만들기 위하여, 아래와 같이 코드를 작성할 수 있을 것입니다.


static string Join<T>(string delim, IEnumerable<T> cols)
{
    StringBuilder buffer = new StringBuilder();
    IEnumerator<T> @enum = cols.GetEnumerator();


    if (@enum.MoveNext())
        buffer.Append(@enum.Current);


    while (@enum.MoveNext())
    {
        buffer.Append(delim);
        buffer.Append(@enum.Current);
    }


    return buffer.ToString();
}


위의 메서드를 이용하여 문자열의 각 문자들 사이에 쉼표를 붙이는 것을 쉽게 처리할 수 있습니다.


string modified = Join<char>(“, “, “Hello guys!”);
Console.WriteLine(modified);


H, e, l, l, o,  , g, u, y, s, !


현재 컴퓨터를 기준으로 언제나 유일한 값을 빠르게 만들어내는 방법


완벽한 의미에서의 유일성은 상당히 많은 Factor를 반영해야만 그 성격을 보장할 수 있습니다. 그러나, 대개의 경우 지구상에서 유일한 값을 만들어내는것 보다는, 현재 실행 중인 컴퓨터나 데이터베이스를 기준으로 유일한 값을 만들어내는 것 정도만으로도 충분히 목표를 달성할 수 있습니다. 이럴 경우에도 매번 GUID를 생성하거나, 데이터베이스의 Identity Seed를 사용하는 것은 비용이 많이 들고, 특히 데이터베이스의 Identity Seed는 데이터베이스마다 커스터마이징 정도의 차이가 있지만 대개는 생성된 값을 클라이언트 측에서 확인하기 어렵기 때문에 Round Trip을 유발합니다.


지금 소개하는 방법은 이러한 문제점을 극복하면서도 매우 빠른 실행 속도를 보장하는 유일 값 생성 방법입니다. 바로, 현재 시스템의 Tick Count를 그대로 이용하는 방법입니다. Tick Count는 100 나노초 단위이므로 일정한 수준에서의 유일성을 보장하기에는 충분한 밀도가 됩니다. 그리고 생성하는 값의 데이터 형식이 64비트 정수이므로 범위 또한 충분히 넓습니다.


long uniqueVal = DateTime.UtcNow.Ticks;


위와 같이 값을 얻어올 수 있고, 위의 값을 데이터베이스에 레코드를 추가할 때 힌트용으로 사용하는 열에 지정하면 삽입 즉시 조회할 수 있는 고유한 값이 되므로 프로그램 로직 개선에 많은 도움이 됩니다.


조건문의 분기를 임의로 결정하도록 만드는 방법


Modular Operator (%)의 기능과 특징을 아신다면 당연하게 받아들일 수 있는 내용이지만, 이런 특이한 상황에 대해서 유용하게 쓰일 수 있습니다. switch나 if/else 등의 조건문의 분기 자체를 임의 결정할 수 있도록 시뮬레이션해야 하는 상황에서 난수 값이 구체적으로 어떤지를 검색하거나 값을 한정하기 위해서 제약하는 것보다 더 손쉽고 이해하기 편한 시뮬레이션 방식을 % 연산자를 이용하여 쉽게 구현할 수 있습니다.


string modified = Join<char>(“, “, “Hello guys!”);
Random random = new Random();
char x = ”;


for (int i = 0; i < 100; i++)
{
    switch (Char.ToUpperInvariant(modified[random.Next() % modified.Length]))
    {
        case ‘H’: x = ‘i’; break;
        case ‘E’: x = ‘f’; break;
        case ‘L’: x = ‘m’; break;
        case ‘O’: x = ‘p’; break;
        case ‘ ‘: x = ‘?’; break;
        case ‘G’: x = ‘h’; break;
        case ‘U’: x = ‘v’; break;
        case ‘Y’: x = ‘z’; break;
        case ‘S’: x = ‘t’; break;
        case ‘!’: x = ‘@’; break;
        case ‘,’: x = ‘.’; break;
        default: x = ‘ ‘; break;
    }
    Console.Write(x);
}
Console.WriteLine();


위와 같이 % 기호 다음에 오는 operand로 컬렉션의 길이나 배열의 길이를 지정해주면, 배열의 요소를 임의로 고를 수 있어서 활용폭이 더 넓어집니다.


소스 코드에 특수문자나 CJK 문자를 안전하게 기록하고 다른 사람과 공유하는 방법


드문 경우이지만, 주석 이외에 프로그램의 실행에 실제로 영향을 줄 가능성이 있는 문자열이 영어나 숫자, 혹은 ASCII 범위의 문자가 아닐 경우 다른 환경이나 언어 구성에서 소스 코드 파일을 편집한 후 되돌려받았을 때 문자열이 깨지는 일이 자주 있습니다. 지금 이야기하는 방법은 사실 실용적이지는 않지만, 정말 중요하게 지켜야 할 리소스라면 지금 소개하는 방법을 이용하여 번거롭지만 확실하게 문자열 데이터를 지키는 것도 가능하니 한 번 고려해보시는 것도 좋을 것 같습니다.


예를 들어, 중국어 문자열 “我国屈指可数的财阀。” (우리나라 굴지의 재벌)이 소스 코드에 문자열로 저장되어있고 이 문자열을 인코딩 문제로부터 보호하기 위해서, 위의 문자열을 복사하여 LINQPAD에 아래의 인라인 식에 치환하여 넣습니다. (LINQPAD는 http://www.linqpad.net 에서 다운로드합니다.)


String.Join(“, “, “paste here“.Select(x => “0x” + ((int)x).ToString(“X4”)))


그러면 다음과 같은 결과가 나타납니다.


0x6211, 0x56FD, 0x5C48, 0x6307, 0x53EF, 0x6570, 0x7684, 0x8D22, 0x9600, 0x3002


이제 위의 내용을 new String(new char[] { 0x6211, 0x56FD, 0x5C48, 0x6307, 0x53EF, 0x6570, 0x7684, 0x8D22, 0x9600, 0x3002
 }); 와 같이 바꾸어서 소스 코드에 저장하면 실행 시 원래 문자열로 복원되면서도, 소스 코드 상의 문자열이 훼손될 걱정을 하지 않아도 됩니다. 단, 이 경우 소스 코드의 내용만으로는 실제로 어떤 문자열인지 파악하기 어려워진다는 장점이자 단점이 동시에 발생합니다. 장점으로는, 일종의 난독처리가 이루어진 셈이며, 단점으로는, 관리가 어려워진 셈이기 때문입니다.


조건문을 어떻게 관리하십니까?


조건문을 어떻게 작성하고 관리하는가에 대한 문제는 개인의 취향과 논리에 따라 매우 다양한 패턴이 존재합니다. 그러나 경험 상, 코드가 간결할 수록 유리하다는 것은 보편적으로 통하는 진리입니다. 개인적인 경험으로 유추해볼 때, 코드의 간결함은, 조건문이나 분기가 얼마나 단일 메서드 내에서 잘 관리되고 있는가에 대한 이야기로 바꾸어 말할 수도 있을 것 같습니다.


이런 방침에 따라, C나 C++ 스타일의 언어들은 중첩해서 사용하는 중괄호의 여닫음 횟수가 늘어날수록 복잡도가 크게 증가합니다. C#도 예외는 아닌데, 이런 이유때문에 저는 스스로 조건문이나 코딩 스타일을 나름의 원칙을 정하여 사용하고 있습니다.


우선, 단위 메서드를 작성하기에 앞서서 조건 검사를 할 때에는 부정적인 시나리오부터 먼저 확인합니다. 다음의 예를 들어보도록 하겠습니다.


public int Divide(int a, int b, out int z)
{
    z = 0;


    if (b != 0)
    {
        z = a % b;
        return a / b;
    }
    else
    {
        throw new DivideByZeroException();
    }
}


무난한 코드입니다. 하지만, 제가 볼 때에는 중괄호를 여닫을 필요가 없어보이는 코드입니다. 아래와 같이 정리하면 어떨까요?


public int Divide(int a, int b, out int z)
{
    z = 0;


    if (b == 0)
        throw new DivideByZeroException();


    z = a % b;
    return a / b;
}


요지는 이렇습니다. 이 메서드에서 우려하는 최악의 상황은 사실 매개 변수 b가 0으로 들어오는 경우입니다. 확실히 문제가 있음을 제기해야 한다면 이 경우를 따로 다루어야 하겠지요. 이를 위해서 b가 0으로 지정되었는지를 검사하여 메서드의 시선으로부터 그런 상황을 제거합니다. 그러면 남는 일은 오로지 나눗셈에 의한 나머지와 몫을 구하는 일이 됩니다. (참고로 z = 0을 서두에 지정한 것은 out 매개 변수에 대한 제약 때문에 그렇습니다. 메서드 본문 밖을 return에 의해서이든 throw에 의해서이든 빠져나가기 전에 반드시 out 매개 변수의 값은 초기화를 해야 합니다.)


그리고 중괄호를 많이 열게 될 개연성이 있는 또 다른 유형은 바로 IDisposable 변수를 다루기 위한 using 블럭입니다. 아래의 경우를 살펴보도록 하겠습니다.


Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);


using (WinFormModule mod = new WinFormModule(args.FirstOrDefault()))
{
    using (StandardKernel kern = new StandardKernel(mod))
    {
        Application.Run(kern.Get<ApplicationContext>());
        mod.FormName = “Form3”;
        Application.Run(kern.Get<ApplicationContext>());
        mod.FormName = “Form2”;
        Application.Run(kern.Get<ApplicationContext>());
    }
}


두 번 열 필요가 없어보이는데도 두 번이나 열었습니다. 위의 코드는 아래와 같이 깔끔하게 정리할 수 있습니다.


Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);


using (WinFormModule mod = new WinFormModule(args.FirstOrDefault()))
using (StandardKernel kern = new StandardKernel(mod))
{
    Application.Run(kern.Get<ApplicationContext>());
    mod.FormName = “Form3”;
    Application.Run(kern.Get<ApplicationContext>());
    mod.FormName = “Form2”;
    Application.Run(kern.Get<ApplicationContext>());
}


IDisposable.Dispose 메서드가 항상 모든 것을 앗아가기만 하는 것은 아니다.


직전에서 다룬 using과 IDisposable에 대한 흔한 오해는, IDisposable 형식의 참조를 using 문과 함께 사용할 때에는 반드시 using 문 내부에서만 선언해야 한다는 것입니다. 그러나 이 경우 문제가 발생하는 일이 있습니다. 아래의 경우를 살펴보도록 하지요.


using (MemoryStream memStream = new MemoryStream())
using (FileStream fileStream = File.OpenRead(@”WinFormDI.exe.config”))
{
    fileStream.CopyTo(memStream, 64000);
}
// memStream에 들어있는 내용은 어디서 찾을 수 있습니까?


주석 처리한 부분에서 memStream 변수를 접근해야 하는 이유는 간단합니다. 혹시 MemoryStream의 구현 상에 있을지 모르는 버퍼링 (물론 실제로는 그럴리 없습니다만)을 모두 끝내고 실제 스트림에 쓰여진 상태를 확보하고 싶은데, 막상 MemoryStream의 존재 자체를 알 수 없는 외곽 블록에서는 실행이 다 끝나고도 데이터에 접근할 수 없는 우스운 상황이 생깁니다. 위의 코드를 아래와 같이 고치면 의도대로 잘 작동합니다.

MemoryStream memStream;
using (memStream = new MemoryStream())
using (FileStream fileStream = File.OpenRead(@”WinFormDI.exe.config”))
{
    fileStream.CopyTo(memStream, 64000);
}
byte[] buffer = memStream.ToArray();
Console.WriteLine(Convert.ToBase64String(buffer));

사실, 위와 같이 memStream 변수를 밖으로 빼내어도 이상이 없습니다.


memStream은 using 블록 밖에서는 당연히 더 이상 데이터를 기록할 수 없도록 파기된 상태입니다. 하지만, 앞에서 이야기했듯이 IDisposable.Dispose 메서드가 모든 것을 소거하지는 않습니다. 즉, MemoryStream 내부의 byte 배열 버퍼는 여전히 유효합니다. 따라서, 그것의 참조를 Dispose 메서드가 불린 이후라도 가져와서 BASE64 인코딩으로 파일 내용을 인코딩하여 문자열로 바꾸려 했던 코드를 잘 실행할 수 있습니다.


바꾸어 말하면, 아래의 코드도 유효합니다.


MemoryStream memStream = new MemoryStream();
using (memStream)
using (FileStream fileStream = File.OpenRead(@”WinFormDI.exe.config”))
{
    fileStream.CopyTo(memStream, 64000);
}
byte[] buffer = memStream.ToArray();
Console.WriteLine(Convert.ToBase64String(buffer));


객체의 생성을 using 문 밖에서 처리하고, 사용하고픈 참조를 담고 있는 변수명을 지칭하기만 해도 같은 의미가 됩니다. using 문 밖으로 나가면 당연히 memStream은 Dispose 메서드가 호출된 상태가 됩니다.

NuGet 패키지 관리자를 이용한 TDD 초기 환경 구축하기

NuGet Visual Studio에 추가하여 사용할 수 있기도 하고, 독립적으로도 사용할 수 있는 패키지 관리 시스템으로 기존의 Windows Forms 응용프로그램에서부터 ASP.NET, 그리고 Windows 8Windows Phone 8에 이르기까지 다양한 종류의 프로젝트를 지원하는 전천후 패키지 관리 시스템이자 또한 NuGet 웹 사이트와 연동하여 최신의 패키지를 자유롭게 활용할 수 있는 멋진 기능입니다.



.NET Framework 관련 소프트웨어 개발을 시작할 때 새로운 프로젝트를 만들고 TDD 초기 환경 구축을 하는 과정은 다양합니다. Visual Studio가 제공하는 Test Project를 만드는 방법이 있을 수도 있고, 나름대로 Test Mockup을 만드는 방법도 있을 수 있지만, 무료로 사용할 수 있으면서도 분명한 효과를 제공하는 도구로는 단연 NUnit이 거론됩니다. 그런데 Visual Studio의 기본 구성 요소도 아니고, 프로젝트에 추가해서 사용하기 번거로운 면도 일부 있습니다. 그리고 테스트 코드를 만들고 프로젝트에 포함시키는데 있어서도 프로젝트의 코드 관리를 어렵게 만드는 면이 있습니다.



이러한 문제를 해결하기 위한 방법으로 두 가지 방안을 소개하려고 합니다.



첫 번째는, NuGet 패키지 관리자를 이용하여 NUnit Framework는 물론 NUnit RunnerMSI 패키지 설치 방식이 아닌 솔루션 단위의 패키지로 설치하여 버전 관리 시스템에 같이 포함하여 배포할 수 있는 방법에 관한 것입니다. 두 번째는, Friend 어셈블리를 이용하여 테스트 어셈블리에 대해서만 독점적인 접근 권한을 부여하여 테스트 논리를 만드는 절차를 간소화하는 방법에 관한 것입니다.



NuGet 패키지 관리자 버전 확인 후 업데이트하기



NuGet 패키지 관리자는 Visual Studio 2010 이후부터 서비스 팩을 설치하면 극 초기의 버전이 자동으로 추가되는 경우가 있습니다. 하지만 제품과 함께 제공되거나 서비스 팩을 이용하여 설치한 패키지 관리자는 버전이 너무 낮고 기능에도 일부 오류가 있어 쓰기 불편합니다. 당연히 여기에 대한 업데이트가 배포 중이며, 다음과 같은 방법으로 업데이트할 수 있습니다. Visual Studio 2010 이후의 버전은 모두 다음과 같은 방법으로 진행하면 됩니다.



Visual Studio를 시작합니다.





도구 메뉴를 선택한 다음 확장 관리자 메뉴를 아래와 같이 선택합니다.



 



나타나는 대화 상자의 왼쪽 편의 항목들 중 온라인 갤러리선택 후 모두를 선택합니다. 그러면 아래와 같이 NuGet Package Manager가 상위권 항목에 나타납니다. 많이들 사용하는 기능이기 때문에 검색할 필요도 없이 금세 발견할 수 있을 것입니다.



 



업그레이드를 할 필요가 없거나 이미 최신 버전이 설치된 경우 위와 같은 화면이 나타나지만, 대개는 업그레이드가 필요함을 알려줄 것입니다. 리스트에서 다운로드 버튼이 보이면 클릭하여 설치나 업데이트를 진행하시면 됩니다.


설치를 완료한 다음에는 Visual Studio를 다시 시작하라는 메시지가 나타나며, 이 메시지에 따라 다시 시작 버튼을 클릭하면 자동으로 다시 실행됩니다.


기존 프로젝트 또는 새 프로젝트에 NUnit 프레임워크와 NUnit Runner 추가하기


이제 NuGet 패키지 관리자를 새로 업그레이드하였으니 이 패키지 관리자를 사용하여 기존 프로젝트 또는 새 프로젝트에 NUnit 프레임워크와 NUnit Runner를 추가할 차례입니다. 단위 테스트 기능을 추가하려는 프로젝트를 열거나 새로운 프로젝트를 만들고, 아래와 같이 솔루션 탐색기에서 해당 프로젝트를 마우스 오른쪽 버튼으로 클릭한 다음, NuGet 패키지 관리 메뉴를 선택합니다. 만약 테스트 코드와 실제 제품 코드를 분리하고자 할 경우에는 별도의 새로운 프로젝트를 만든 다음 그 프로젝트에 아래 그림과 같이 패키지 관리자를 실행하도록 하면 됩니다.


 


 


그러면 NuGet 패키지 관리자가 다음과 같이 나타납니다. 아무것도 설치한 것이 없으므로 처음에는 덩그러니 빈 화면만 나타나는데, 이번에도 좌측편의 항목들 중 온라인을 선택합니다.


 


그 다음, 우측 상단의 검색 창에 NUnit을 입력하고 검색 버튼을 클릭하면 다음과 같이 NuGet 관련 패키지들이 나타나게 됩니다.


 


이 중에서 우리가 필요로 하는 것은 NUnitNUnit.Runners 패키지입니다. NUnit 패키지에서는 NUnit 프레임워크 어셈블리를 포함하고 있으며, NUnit.Runners 패키지는 NUnit 테스트 실행을 위한 프로그램의 GUI, CLI 및 플랫폼 중립, x86 버전의 파일도 같이 들어있습니다. 그러나 Runners 패키지는 실제 프로젝트에 참조로 추가되는 것은 아니며 Windows 탐색기를 사용하여 파일을 별도로 실행하거나 빌드 자동화 시점에서 활용할 수 있는 유틸리티 정도로 생각하면 편합니다.


이제 새로운 Test Fixture 클래스와 Test Case 메서드들을 몇 가지 추가해봅니다. 테스트해 보고픈 임의의 코드를 추가하고 컴파일이 잘 되는지 확인합니다. 여기서는 다음과 같이 코드를 작성했다고 가정해 보겠습니다.



이제 위의 테스트 어셈블리를 포함한 솔루션을 NuGet이 설치한 NUnit Runner를 통하여 열어보도록 하겠습니다. 솔루션 폴더를 찾아서 폴더 창을 열려고 하면 번거롭습니다. 이를 단순하게 하기 위하여, 현재 열려있는 코드 편집기 창의 탭 부분을 오른쪽 버튼으로 클릭하면 상위 폴더 열기 메뉴가 아래 그림과 같이 나타납니다. 이 메뉴를 클릭합니다.


 


그러면 다음과 같이 폴더 창이 정확한 위치를 가리키며 나타나게 됩니다. 이제 이 위치에서 SLN 파일이 있는 위치로 상위 폴더로 몇 번 이동합니다. 그 다음, 해당 폴더 위치를 기준으로 packages 폴더 > NUnit.Runners.x.x.x 폴더 > tools 폴더 순으로 접근합니다. 그리고 아래 그림과 같이 nunit.exe 파일을 찾아 실행합니다.


 


익숙한 화면이 나타납니다. 시스템에 관리자 권한을 이용하여 설치하지 않았어도 NUnit Runner가 즉시 실행되고 사용 가능한 상태로 준비된 것이 보입니다. 이제 여기서 SLN 파일을 열어보겠습니다. File 메뉴의 Open 메뉴를 선택하여 SLN 파일을 찾아 엽니다.


 


만약 솔루션 파일을 열려고 시도하였을 때, 솔루션이 이상 없이 컴파일이 잘 됨에도 불구하고 다음과 같이 오류 메시지가 나타나면 대상 플랫폼 설정이 NUnit의 대상 플랫폼과 일치하지 않기 때문에 오류가 발생하는 것입니다.



이 경우 문제 해결을 위하여 아래 그림과 같이 대상 플랫폼을 Mixed Platform 대신 x86으로 변경하고 nunit-x86.exe Runner를 대신 사용하거나, Any CPU로 맞추어 다시 솔루션을 빌드합니다.



SLN 파일을 열고 난 다음에는 테스트를 진행할 수 있게 화면이 나타납니다. 현재 활성화된 환경 설정을 기준으로 자동으로 포커스가 변경됩니다.



테스트가 잘 실행되는지 살펴봅니다. 예상대로 Case 1decimal이 정확한 덧셈을 처리하고 있음을 증명하며, Case 2Windows 환경에서 언제나 성공합니다. 그러나 Case 3Windows 환경에서 언제나 실패하며, Case 41글자이지만 StringChar가 분명히 다른 형식임을 확인해주고 있습니다.



실제 코드 어셈블리와 테스트 어셈블리를 분할하는 방법


NuGet 패키지 관리자를 사용하여 NUnit을 전보다 더 가깝고 편리하게 사용할 수 있게 된 것은 좋은 일입니다. 그렇지만 한 가지 고민이 남는데, 인프라의 개선과는 별도로 설계와 유지에 있어서 테스트 코드와 실제 제품 코드가 한 배를 타는 것은 별로 좋은 것 같지 않습니다. 테스트 코드가 제품 코드에 자유롭게 접근할 수 있으면서도, 제품 코드가 테스트 코드를 배려하는 별도의 부수적인 옵션 구성 요소들을 추가하는 일 없이, 테스트 코드가 자유롭게 제품의 기능을 접근하여 확인할 수 있는 수단이 필요할 것입니다.


여기에 대한 답을 .NET FrameworkFriend Assembly라는 이름의 개념으로 정의하고 있는데, 기본적으로 Assembly는 그 안에 속한 Module들 간에는 internal로 선언한 멤버들을 자유롭게 제어하고 다룰 수 있게 되어있습니다. 그런데 이 Assembly 간의 관계를 설정해두면 특정 어셈블리 상의 코드에 대해서만 internal로 선언한 멤버들을 자유롭게 제어하고 호출하거나 다룰 수 있게 해주는 특권의 부여가 가능합니다.


이 기능을 사용하면, 제품에 대한 실제 코드를 담고 있는 클래스 라이브러리나 실행 파일 모듈을 가지고 있는 .NET 어셈블리와 각 유형별 테스트 케이스를 따로 모아놓은 테스트 어셈블리들을 분리하여 테스트 어셈블리는 배포하지 않고, 실제 코드 어셈블리만 배포하는 것이 가능합니다. 그러면서도, 실제 코드 어셈블리의 모든 internal 멤버들을 테스트 어셈블리들이 자유롭게 활용할 수 있습니다. 이렇게 하여 둘 사이에 발생할 수 있는 상호 종속적인 관계를 분리할 수 있으니 훨씬 자유로운 테스트 코드 작성이 가능합니다.


위의 예제에서 보인 것처럼 실제 코드와 테스트 코드를 분리한 상태에서, 실제 코드를 가지고 있는 어셈블리에서는 우선 보호하고 싶은 클래스나 멤버에 대해 internal 키워드를 사용하여 선언합니다. 여기까지는 우리가 알고 있는 그대로이며, 다른 어셈블리에서는 internal 키워드를 사용하여 선언한 멤버들을 접근하거나 활용할 수 없습니다. 그러나, 프로젝트 내에 추가할 테스트 어셈블리의 이름을 아래 그림과 같이 확인해둡니다.



접근을 허용하려는 테스트 어셈블리의 이름을 찾아 복사합니다. 그리고 실제 코드를 포함하는 어셈블리의 적당한 위치에 다음과 같이 코드를 작성합니다. 아래와 같이 어셈블리에 대한 특성을 부여하는 코드는 보통 Visual Studio 프로젝트와 함께 자동으로 생성되는 AssemblyInfo.cs 파일에 기술하면 편리합니다.


[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(“<Assembly Name>”)]


 


위와 같이 코드를 작성하고 컴파일 한 다음 경고 메시지가 나타나지 않으면 됩니다. 그 다음, 접근을 허용한 어셈블리에서 테스트하려는 코드를 포함한 어셈블리를 참조에 추가한 다음, internal로 선언한 모든 멤버들을 정상적으로 사용할 수 있는지 확인하여, 테스트 코드를 작성할 수 있는 상태이면 테스트 케이스를 만들어나가기 시작하면 됩니다.


결론


테스트 주도 개발은 이번 아티클에서 살펴본 것과 같이 초기의 개발 환경 구축을 단순화할 수 있다면 얼마든지 쉽게 시작할 수 있는 효율적이고 인상적인 개발 방법론입니다. 그러나 여기에서 염두에 두어야 할 것은 이렇게 구축한 개발 환경을 어떤 관점을 유지하면서 활용해 나아갈 것인가에 대한 전략의 설정과 실천에 있을 것입니다.


테스트 주도 개발에 관한 좀 더 근본적인 내용을 검토하기 위해서는 다양한 자료들을 참조할 수 있지만, 가장 추천해 드릴 만한 자료로는 단연 Kent BeckTest Driven Development (ISBN 978-89-91268-04-3)이라는 도서입니다. 테스트 주도 개발의 원칙과 방향성에 대한 이야기를 자세히 들어볼 수 있으므로 꼭 살펴보실 것을 권합니다.