Tech/닷넷 일반2014. 6. 7. 19:58

Visual Studio에서 제공하는 테스트 도구에 NUNIT을 결합해서 사용할 수 있다면 정말 유용할 것입니다. 이번 웹 캐스트는 Visual Studio에서 제공하는 테스트 도구와 NUNIT 간의 연동을 도와주는 NUNIT Test Adapter의 사용 방법과 함께, Windows Forms 기반 응용프로그램에서 Visual Studio와 NUNIT Test Adapter를 결합하여 테스트를 진행할 수 있는 방법을 소개합니다.

아래 이미지를 클릭하시면 웹 캐스트가 시작됩니다.


링크 바로 가기: http://cms.rkttu.com/em/5392cf5a56846

This web cast powered by XINICS SilverStream & Commons

Posted by 비회원

댓글을 달아 주세요

Tech/닷넷 일반2013. 10. 25. 22:00

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 키를 누르면 프로그램이 완료됩니다.

Posted by 비회원

댓글을 달아 주세요

Tech/닷넷 일반2013. 10. 16. 22:00

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 = '\0';

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 메서드가 호출된 상태가 됩니다.

Posted by 비회원
TAG c#

댓글을 달아 주세요

Tech/닷넷 일반2013. 10. 2. 18:10

응용프로그램을 배포하다보면 특정 조건 때문에 파일 하나로 만들어서 배포해야 할 경우가 있습니다. 사실 응용프로그램중에 EXE 파일 하나만으로 구성되는 경우가 드물지 않을까 싶은데, 지역화된 리소스만 집어넣어도 기본적으로 위성 어셈블리가 생성되기 때문이죠.


이번에 각종 DLL을 EXE에 임베드시키고 AssemblyResolve 이벤트를 사용해서 로드하는 방식을 사용해보았기에 정리해보려고 합니다.


실행시키면 아래와 같이 덩그라니 창이 뜨는 아주 단순한 WPF 응용프로그램입니다. (.NET 4 Client Profile 기반)


프로젝트 구조는 왼쪽 처럼 되어 있습니다. RESX 를 사용해서 문자열 리소스(여기서는 윈도우 제목 하나뿐이기 하지만...)를 지역화하고 있습니다. 리소스 어셈블리는 별도 프로젝트로 분리되어 있죠. 중립 언어는 en-US 로 AssemblyInfo.cs 에 설정이 되어 있고요.


기본적으로 이 프로젝트를 빌드해서 배포하려면 아래와 같은 파일들을 배포해야 합니다.

  • WpfSingSangSung.exe
  • WpfSingSangSung.Resources.dll
  • ko-KR/WpfSingSangSung.Resources.resources.dll
파일 하나로 만들어서 배포하려면 아래쪽 두 개의 리소스 DLL을 EXE 에 몰아넣어야 겠네요.


우선 빌드된 DLL 을 embed 시킬 것이므로, WpfSingSangSung 프로젝트 빌드 시에 자동으로 WpfSingSangSung.Resources 프로젝트의 빌드 결과물을 복사할 필요가 없습니다. 아래 그림과 같이 프로젝트 참조에서 Copy Local 을 False 로 바꿔줍니다.

빌드가 성공하려면 어셈블리 참조 자체는 유지해주어야 합니다.


다음 단계는 빌드된 리소스 DLL을 둘 곳을 정하는 건데요, 개발 중에 Debug 로 빌드했다가 Release 로 빌드했다가 할텐데, 그 때 빌드된 리소스 DLL 이 업데이트 되어야 하니까, 빌드된 리소스 DLL 들을 정해진 장소로 복사해주도록 하겠습니다.

프로젝트 폴더 구조는 위와 같습니다. WpfSingSangSung.Resources 프로젝트의 빌드 결과물을 bin/Result 폴더에 복사하도록 하죠. Post-build event 에 아래와 같이 설정해 줍니다.

xcopy "$(TargetPath)" "$(ProjectDir)bin\Result\" /Y
xcopy "$(TargetDir)ko-KR\$(ProjectName).resources.dll" "$(ProjectDir)bin\Result\ko-KR\" /Y

만약 다른 언어가 하나 더 추가된다면 ko-KR 과 동일한 패턴으로 복사하는 코드를 한 줄 넣어야겠죠.


이제 복사된 위치에 있는 리소스 DLL 들을 EXE 에 리소스로 임베드 시킵니다. 리소스로 임베드된 DLL 들만을 위해서 별도의 네임스페이스를 두는 것이 좋으니까, WpfSingSangSung 프로젝트에 폴더를 하나 추가하고, 리소스 DLL 들을 그냥 프로젝트에 추가하는 것이 아니라 Link 로 추가합니다.

바로 위 이미지는 DLL 을 추가하고, 리소스로 임베드시키는 작업이 다 된 상태입니다. 각 DLL 파일들의 Build Action 을 "Embedded Resource" 로 설정해준 것이 보이시죠? 그리고 또 하나 눈에 띄는 점은 ko-KR 폴더를 만들어서 한국어 리소스 DLL을 넣었다는 점이겠네요.


여기까지 해서 솔루션을 빌드하면, WpfSingSangSung.exe 에는 DLL들이 모두 리소스로 들어가 있는 상태입니다. 리소스 DLL을 찾아서 사용할 수 있게 해주는 기능이 없으니 실행은 아직 안되지만, ILSpy 로 열어보면 두 개의 리소스 DLL 이 리소스로서 포함되어 있는 것을 확인할 수가 있습니다.

이렇게 ILSpy 로 DLL의 리소스 이름을 확인해두면 뒤에 AssemblyResolve 이벤트 핸들러에서 어셈블리 로드하는 코드를 작성할 때 도움이 됩니다.


가장 중요한 내용으로 들어가기 전에, 기본적인 WPF 응용프로그램의 구조를 조금 바꿔서 코드 작성을 좀 쉽게 만들려고 합니다. 템플릿으로 WPF 응용프로그램을 생성하면 App.xaml 을 이용하는 형태로 만들어 주는데요, 이걸 지워버리고 Program.cs 를 만들어서 진입점으로 만들어주는거죠. 아래와 같이 Program.cs 를 만들어서 넣어줍니다.

    public class Program : Application
    {
        [STAThread]
        public static void Main(string[] args)
        {
            Program app = new Program();
            MainWindow mainWin = new MainWindow();
            app.Run(mainWin);
        }
    }


이제 AssemblyResolve 이벤트 핸들러만 만들어주면 끝납니다! 위에서 만들 Program.cs 에 코드 몇 줄만 추가하면 됩니다.

        public static void Main(string[] args)
        {
            // 이벤트 핸들러 연결
            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

            Program app = new Program();
            MainWindow mainWin = new MainWindow();
            app.Run(mainWin);
        }

        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            AssemblyName missingAssemblyName = new AssemblyName(args.Name);
            CultureInfo ci = missingAssemblyName.CultureInfo;

            // ILSpy 에서 확인한 형태로 리소스 이름을 만들어줍니다.
            string prefix "WpfSingSangSung.Embedded.";
            string culturePart = "ci.Name.Replace("-", "_") + ".";
            string resourceName = prefix + culturePart + missingAssemblyName.Name + ".dll";
            // 중립 리소스 요청인 경우에는 Culture 이름이 비어있습니다.
            if (ci.Name == string.Empty)
                resourceName = prefix + missingAssemblyName.Name + ".dll";

            // 리소스에서 읽은 바이너리를 Assembly로 전환하여 리턴합니다.
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
            {
                byte[] bytes = new BinaryReader(stream).ReadBytes((int)stream.Length);
                return Assembly.Load(bytes);
            }
        }


이렇게 만들어서 빌드한 EXE 를 각각 영어 환경과 한국어 환경에 EXE만 복사해서 실행해보면 환경에 맞게 윈도우 제목이 영어와 한국어로 표시되는 것을 확인할 수 있습니다. 성공이네요!


Posted by wafe

댓글을 달아 주세요

  1. 혹시, ILMerge를 사용해도 되지 않을까요? ^^

    ILMerge
    ; https://www.nuget.org/packages/ilmerge

    2013.10.05 00:37 [ ADDR : EDIT/ DEL : REPLY ]
    • 별도의 도구를 사용하지 않고 Visual Studio 내에서 되는 게 좋겠다 싶어서 ILMerge 에 대해서는 자세히 알아보지 않았는데요, 말씀해주신 덕에 조금 구글링을 해보니 사용하기 어렵지 않아보이고, 이미 많이들 사용하고 있는 모양이네요. 언제 ILMerge 로도 해봐야겠습니다. ^^

      2013.10.08 11:14 신고 [ ADDR : EDIT/ DEL ]

Tech/닷넷 일반2013. 9. 17. 20:00

 

C#이나 .NET Framework를 이용하여 프로그램 코드를 작성할 때, 실제로 실행 시간이 오래 걸리는 코드 뿐 아니라 입출력 작업이나 여러가지 외부적인 요인 (데이터베이스, 네트워크를 통한 원격 시스탬 액세스 등)에 의하여 요즈음은 언제 끝나는지 정확한 실행 시간을 측정할 수 없는, 실행 시간에 변수가 생기는 코드를 작성하는 일이 많습니다. 이러한 프로그래밍 코드는 늘 그렇듯이 사용자 인터페이스와는 친화적이지 않은 경우가 많습니다. (잘 아시다시피 큐를 기반으로 하는 사용자 인터페이스 메시징 처리에 방해가 되기 때문입니다.)

이러한 문제를 해결하기 위해서, Windows Forms에서는 BackgroundWorkerThread를 사용할 수 있고, Timer나 직접 Thread를 이용하는 경우도 있습니다. 하지만 잘못된 관점을 가지고 프로그램을 작성할 경우 관리하기 어려운 코드가 되는 문제가 있습니다. 좀 더 간결하고 알기 쉬운 비동기 코드를 만드려는 노력이 필요한 이유가 여기에 있습니다.

이번 글에서는 어렵게 스레드를 만들고 생명 주기를 관리하려는 노력 대신, 실제 로직에 집중해서 메서드의 실행을 비동기화하고, 원격에서 통제할 수 있는 명쾌한 방법을 하나 소개해볼까 합니다.

다음과 같은 임의의 메서드가 하나 있다고 가정하겠습니다. 컨셉을 이해하기 위함이므로 메서드 본문 안의 내용은 중요하지 않음을 미리 언급해두겠습니다.

public int CalcXYZ(int x, int y, int z) {
    return x * y * z;
}

.NET Framework 3.5부터 새로 추가된 LINQ는 Lambda Expression과 함께 좀 더 적극적인 Type Inference를 가능하게 하기 위하여, Action과 Func 대리자들의 가짓수가 매우 다양해졌습니다. 이전 버전의 .NET Framework에서는 위의 메서드를 포장하기 위하여 아래와 같이 대리자 형식을 따로 정의해야 했습니다.

public delegate int CalcXYZDelegate(int x, int y, int z);
...
private CalcXYZDelegate f;
...
f = this.CalcXYZ;

물론 위와 같이 대리자를 따로 정의해서 담아두어도 좋습니다. 하지만, 타이핑하고 관리해야 할 코드의 분량을 조금이라도 줄일 방법을 찾는게 좋을 것입니다. 이를 위해서, 반환 형식이 있는 메서드이므로 Func<T1, T2, T3, TResult> 대리자를 사용할 수 있을 것입니다.

private Func<int, int, int, int> f;
...
f  = this.CalcXYZ;

여기서 한 가지 기억해야 할 것은, 비동기 메서드를 만들기 위해서 비동기 작업을 발행하고 관리하는 주체로 대리자를 사용한다는 점입니다. 따라서, 대리자 인스턴스를 하나로 고정하기 위하여, private 멤버 변수로 선언하는 것이 중요합니다. 즉, 위의 코드를 전개하면 다음과 같은 클래스 선언이 나타난다고 할 수 있습니다.

public class MyClass {
    public MyClass() : base() { f = this.CalcXYZ; }
    private Func<int, int, int, int> f;
    public int CalcXYZ(int x, int y, int z) { return x * y * z; }
}

이제 위의 MyClass 정의에 Begin/End 짝 메서드를 정의해보겠습니다. Begin/End 메서드에 대해서 설명하면, BeginXXXX 메서드는 메서드의 호출을 시도하고 비동기 작업을 예약하는 역할을 하며, EndXXXX 메서드는 메서드 호출이 완료되었을 때 결과를 회수하는 역할을 합니다. 이 때, BeginXXXX 메서드에 전달하는 콜백 함수의 호출을 활용하거나, BeginXXXX 메서드가 반환하는 IAsyncResult 및 그 안에 들어있는 스레드 이벤트 동기화 객체가 실행 흐름을 관리하는데 매우 중요한 역할을 담당하게 됩니다.

위의 정의에 내용을 좀 더 추가하면 다음과 같습니다. 굵게 강조 표시한 부분을 확인해 주세요.

public class MyClass {
    public MyClass() : base() { f = this.CalcXYZ; }
    private Func<int, int, int, int> f;
    public int CalcXYZ(int x, int y, int z) { return x * y * z; }
    public IAsyncResult BeginCalcXYZ(int x, int y, int z, AsyncCallback callback, object result) {
        return f.BeginInvoke(x, y, z, callback, result);
    }
    public int EndCalcXYZ(IAsyncResult asyncResult) { return f.EndInvoke(asyncResult); }
}

규칙이 있음을 알 수 있습니다. 다시 살펴보면,

public <반환 형식> <메서드 이름>(<인수들>);

위의 메서드가 원형이라고 하면,

public IAsyncResult Begin<메서드 이름>(<인수들>, AsyncCallback callback, object result) { ... }
public <반환 형식> End<메서드 이름>(IAsyncResult asyncResult) { ... }

이와 같이 함수 호출의 시작과 끝을 관리하는 메서드로 분할하여 정의하고 있으며, 여기에 대한 실질적인 구현은 .NET Framework가 제공하는 대리자를 이용하여 처리하므로 어렵지 않게 비동기 메커니즘을 구현할 수 있게 되는 것입니다. 여기서 조금 더 응용하면, 만약 반환 형식이 void라면 그대로 적어도 무방하며 이 때에는 Func 대리자 대신 Action 대리자로 바꿔주면 됩니다. 인수들이 없다면? 당연히 생략하고 비동기 호출에서 필수적인 인자들만 맞추어 서술하면 끝납니다.

여기서 한 가지 더 이야기할 것은, 최근에 Windows 8 출시와 함께 WinRT가 등장하면서 급부상한 C# 5.0의 async 키워드와 TPL (Task Parallel Library)와의 상관 관계입니다. async 키워드를 직접 사용하지는 않는다 할지라도, WinRT를 자연스럽게 지원할 수 있는 방법이 바로 TPL에 대한 지원을 추가하는 것입니다. 바꾸어 말하면, async 키워드는 컴파일러의 서비스이므로, TPL에 대한 지원만 정확히 하고 있다면, C# 5.0 컴파일러가 TPL 기반의 비동기 코드를 손쉽게 작성할 수 있도록 도움을 준다는 의미도 됩니다.

위의 내용까지 구현했다면, TPL로 가는 길은 바로 코앞에 있는 셈입니다.

using System.Threading.Tasks;
...
public Task<int> CalcXYZAsync(int x, int y, int z) {
    return Task<int>.Factory.FromAsync(this.BeginCalcXYZ, this.EndCalcXYZ, x, y, z, this);
}

위와 같은 코드만 작성하면 Begin/End 패턴을 즉시 TPL 기반의 비동기 패턴으로 변환할 수 있습니다. 여기에서도 규칙을 찾을 수 있는데, 다음과 같습니다.

return Task<<반환 형식>>.Factory.FromAsync(Begin<메서드 이름>, End<메서드 이름>, <인수들>, <this 또는 null>);

여기서 Task 클래스에 제네릭 인자를 지정하는 이유는, 메서드의 반환 형식에 대한 형식 안정성을 지키기 위함입니다. 만약 반환 형식이 void라면 다음과 같이 제네릭 인자 자체를 생략하면 됩니다.

public Task SomeMethod() {
    return Task.Factory.FromAsync(Begin<메서드 이름>, End<메서드 이름>, <this 또는 null>);
}

위의 내용까지 포함한 수정된 MyClass 코드는 다음과 같습니다.

public class MyClass {
    public MyClass() : base() { f = this.CalcXYZ; }
    private Func<int, int, int, int> f;
    public int CalcXYZ(int x, int y, int z) { return x * y * z; }
    public IAsyncResult BeginCalcXYZ(int x, int y, int z, AsyncCallback callback, object result) {
        return f.BeginInvoke(x, y, z, callback, result);
    }
    public int EndCalcXYZ(IAsyncResult asyncResult) { return f.EndInvoke(asyncResult); }
    public Task<int> CalcXYZAsync(int x, int y, int z) {
        return Task<int>.Factory.FromAsync(this.BeginCalcXYZ, this.EndCalcXYZ, x, y, z, this);
    }
}

여기서 한 가지 염두에 두어야 할 것은, 기본 메서드의 매개 변수 갯수가 3개보다 많을 경우, FromAsync 메서드에서는 3개까지만 매개 변수를 전달할 수 있도록 선언이 되어 있기 때문에, IAsyncResult 형식의 객체를 받아서 Wrapping하거나, 템플릿 인자를 확장하거나, 중요도가 낮은 매개 변수들을 Dictionary 또는 별도의 POCO (Plain-old-CLR-object) 형식으로 정의하여 매개 변수로 사용하도록 하는 부수적인 절차가 필요합니다.

또한, 메서드 오버로딩을 비동기 메서드에도 적용하고자 하는 경우, 오버로딩에서 가장 핵심이 되는 메서드에 대해서 위의 패턴을 적용하고, 나머지는 핵심 메서드를 호출하는 과정을 동일하게 맞추어야 합니다. 예를 들어, 위의 CalcXYZ 메서드의 오버로딩으로 short 형식을 사용한다고 가정하면 다음과 같이 사용해야 함을 뜻합니다.

public class MyClass {
    // ...
    public int CalcXYZ(short x, short y, short z) { return CalcXYZ((int)x, (int)y, (int)z); }
    public IAsyncResult BeginCalcXYZ(short x, short y, short z, AsyncCallback callback, object result) {
        return
BeginCalcXYZ((int)x, (int)y, (int)z, callback, result);
    }
    // EndCalcXYZ는 따로 정의하지 않습니다.
    public Task<int> CalcXYZ(short x, short y, short z) { return CalcXYZAsync((int)x, (int)y, (int)z); }
    // ...
}

이제 위와 같이 정의했으니, 호출하고 사용하는 방법을 알아보도록 하겠습니다.

비동기 메서드는 기본적으로 실행을 예약하고, 콜백 메서드를 통하여 실행 완료 통지를 받았을 때 따로 실행하도록 하는 것이 기본입니다. 하지만, 비동기로 실행하는 것과는 별개로 실행 흐름을 동기화해야 하는 경우도 있을 수 있는데, 일반적인 메서드 호출과는 다른 특별한 기능을 하나 더 이용할 수 있습니다. 바로 Time out 개념을 사용할 수 있다는 것인데, 이것은 Thread를 직접 제어할 때와는 또 다른 이점입니다.

MyClass inst = new MyClass();
IAsyncResult res = inst.BeginCalcXYZ(1, 2, 3, null, null);
res.WaitHandle.WaitOne();
if (res.IsCompleted) {
    int val = inst.EndCalcXYZ(res);
    Console.WriteLine(val);
} else {
    throw new TimeoutException();
}

Begin/End 메서드는 위와 같이 사용합니다. 여기서 재미있는 부분은 콜백과 상태 관리 객체를 지정하지 않고 실행이 끝날 때 까지 기다리게 했다는 부분입니다. 그런데 한 가지 궁금증이 생깁니다. 이렇게 하면 비동기의 이점이 없는 것이 아닌가 하는 부분입니다.

그런데 WaitOne 메서드에 매개 변수를 하나 지정할 수 있습니다. 바로 밀리초 단위의 time out 대기 시간입니다. 이 값을 생략할 경우 자동으로 System.Threading.TimeOut.Infinite가 지정된 것과 같이 실행되며, 이 상수 필드의 값은 (-1)입니다. 즉, 실행이 끝날 때까지 이 코드를 실행하는 스레드의 실행을 동결한다는 것입니다. 이 값 대신 1000을 지정하면 1초 이내에 실행이 끝나지 않을 때 그 다음 코드로 바로 실행이 이어지는 것입니다. 여기서 IsCompleted 속성을 사용하여 실행이 정말 완료가 되었다면 결과를 회수하고, 그렇지 않으면 Timeout으로 처리할 수 있게 됩니다.

이러한 시나리오가 유용하게 쓰일 수 있는 곳은 매우 많습니다. 단순한 테스트 유닛 실행에서부터 네트워크 및 입출력 관련 처리에 이르기까지 매우 다양한데, 프로그램을 그저 응답 없음 상태로 내버려두는 것이 아니라 좀 더 주도적으로 실행 흐름을 관리할 수 있게 되는 것입니다.

Begin/End 대신 Async 메서드를 사용하는 경우, C# 5.0 이전의 언어를 사용할 경우 다음과 같이 코드를 작성할 수 있을 것입니다.

MyClass inst = new MyClass();
var res = inst.CalcXYZAsync(1,2,3);
res.Wait();
if (res.IsCompleted) {
    int val = res.Result;
    Console.WriteLine(val);
} else {
    throw new TimeoutException();
}

그리고 C# 5.0부터는 좀 더 간결하게 아래와 같이 코드를 작성할 수 있을 것입니다.

public async void Method1() {
    MyClass inst = new MyClass();
    int val = await inst.CalcXYZAsync(1,2,3);
    Console.WriteLine(val);
}

그런데 한 가지 남는 의문은, 전통적인 Event Driven 방식의 Windows 응용프로그램 개발 환경에서 단순히 이벤트 처리기를 사용하여 위의 코드를 직접 실행하면 프로그램이 굉장히 부자연스럽게 동작한다는 점입니다. 왜 그럴까요?

그 원인은 틀림없이 메시지를 처리하는 스레드에서 위의 코드를 실행하기 때문일 것입니다. 즉, 엄밀한 의미에서 모든 작업들은 비동기화 되어야하고, 비동기화된 스레드들의 상태를 모니터링하면서 사용자 인터페이스에 적절한 신호를 주어야 하는 셈입니다. 이렇게 본다면 GUI 프로그래밍은 매우 어려운 작업 중 하나가 될 수 있습니다. 그래서 기준이 하나 있어야 하는데, 바로 I/O Bound Operation에 한하여 위와 같이 작업을 비동기로 분할하여 상태 보고를 할 수 있도록 구성하는 방법을 적절히 사용해야 하는 것입니다. 그러한 방면으로 잘 포장된 것이 바로 BackgroundWorker 컴포넌트입니다.

어떤 방법을 사용하는가에 관계없이, 프로그래밍을 할 때에는 항상 실행 시간과 흐름 관리에 최선을 다해야 합니다. 디자인 패턴 이외에도, 시간이 오래 걸릴 수 있는 불확정성에 기대는 작업을 다룰 때 이러한 세밀한 노력이 얼마나 들어갔는지에 따라서 얼마나 완성도 높은 코드를 생산할 수 있는가가 결정될 것입니다.

Posted by 비회원

댓글을 달아 주세요

Tech/닷넷 일반2013. 7. 16. 00:00

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

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

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

첫 번째는, NuGet 패키지 관리자를 이용하여 NUnit Framework는 물론 NUnit Runner를 MSI 패키지 설치 방식이 아닌 솔루션 단위의 패키지로 설치하여 버전 관리 시스템에 같이 포함하여 배포할 수 있는 방법에 관한 것입니다두 번째는, 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 관련 패키지들이 나타나게 됩니다.

 


이 중에서 우리가 필요로 하는 것은 NUnit과 NUnit.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 1은 decimal이 정확한 덧셈을 처리하고 있음을 증명하며, Case 2는 Windows 환경에서 언제나 성공합니다그러나 Case 3는 Windows 환경에서 언제나 실패하며, Case 4는 1글자이지만 String과 Char가 분명히 다른 형식임을 확인해주고 있습니다.



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

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

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

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

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



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

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

 

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

결론

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

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

Posted by 비회원

댓글을 달아 주세요

Tech/닷넷 일반2010. 9. 19. 20:28

Visual C++로 데스크톱용 제품을 만들 때에는 주로 MFC 기반으로 작업을 하게 됩니다. 이 때는 모듈화를 할 때 MFC ActiveX를 주로 사용합니다. 이것은 Visual C++ 6.0 시절부터 개발해왔던 습관 때문이라고 볼 수 있겠습니다. MFC 응용프로그램에서든, Internet Explorer에서든 모듈을 사용하는 면에서든 COM 모듈을 쓰는 것과 차이가 없고 만들기는 더 편하니까요.

그런데 서비스는 UI가 제공되지 않는 세션(Session 0)에서 실행됩니다. 따라서 ActiveX 같이 윈도우가 없이는 생성조차 되지 않는 모듈은 사용할 수 없습니다. 서비스가 실행되는 세션에서는 데스크톱 윈도우도 없기 때문에 데스크톱 DC를 이용해서 이미지를 조작한다거나 하는 것도 불가능할 정도이거든요.

하지만 이미 만들어져 있는 수많은 ActiveX 모듈을 생각하면 그냥 그렇구나하며 넘어갈 수는 없죠. 일단 검색을 좀 해보니 윈폼을 쓰면 될 거라는 얘기가 있습니다. 이 정보 하나를 가지고 1차 시도를 해봅니다.

예제로 사용할 ActiveX 모듈은 GDI+를 이용해서 이미지를 JPEG 형식으로 저장해주는 간단한 모듈입니다. TheActiveOne이라는 예제 모듈에는 SaveAsJpg 라는 메서드가 하나 있는데, 아래와 같이 이미지를 저장하는 단순한 메서드입니다. from은 원본 이미지 파일 경로이고, to는 새로 생성될 jpg 이미지 파일의 경로입니다. 

InitializeGdiPlus();

 

Image image(from);

CLSID jpegClsid;

GetEncoderClsid(_T("image/jpeg"), &jpegClsid);

 

image.Save(to, &jpegClsid, NULL);

 

FinalizeGdiPlus();

이제 서비스 프로젝트를 하나 만듭니다. 이름은 TheAxService라고 지었습니다. 서비스를 만드는 방법은 이전의 포스트를 참고하시기 바랍니다. 윈폼을 쓰면 될 것이라 했으니, TheActiveOneForm이라는 윈폼을 하나만들고 TheActiveOne 모듈을 넣습니다. 그리고 서비스 클래스 쪽에서 이미지 저장 기능을 사용할 수 있도록 메서드를 하나 추가 했습니다.

public long SaveAsJPG(string srcImgPath, string outJpgPath)

{

    return this.axTheActiveOne1.SaveAsJpg(srcImgPath, outJpgPath);

}

서비스 진입점에서 윈폼을 만듭니다. 그리고 UI는 Single Thread Apartment에서 생성되어야 할 것이므로 서비스 진입점에 STAThread Attribute를 붙여줬습니다. 그 결과 Program.cs 의 Main 메서드 내용은 아래와 같이 바뀌었습니다.

[STAThread]

static void Main()

{

    //System.Diagnostics.Debugger.Launch();

 

    ServiceBase[] ServicesToRun;

    ServicesToRun = new ServiceBase[]

       {

             new TheAxService(new TheActiveOneForm())

       };

    ServiceBase.Run(ServicesToRun);

}

TheAxService 클래스의 생성자에 폼을 생성해서 넘겨주고 있는 것을 위의 코드에서 볼 수 있습니다.

자, 그러면 핵심인 TheAxService 클래스의 코드를 보겠습니다.

public partial class TheAxService : ServiceBase

{

    TheActiveOneForm axForm;

 

    public TheAxService(TheActiveOneForm form)

    {

        InitializeComponent();

 

        this.axForm = form;

    }

 

    protected override void OnStart(string[] args)

    {

        this.axForm.Load += new EventHandler(axForm_Load);

        this.axForm.Show();

    }

 

    void axForm_Load(object sender, EventArgs e)

    {

        this.axForm.SaveAsJPG(@"C:\Users\wafe\Pictures\office live 2.PNG", @"C:\Users\wafe\Pictures\office live 2.jpg");

    }

}

OnStart 메서드에서 폼을 Show 하고 있고, Load 이벤트 핸들러에서 이미지 파일을 저장하는 기능을 호출하고 있습니다.

이제 서비스를 빌드해서 설치하고 실행시키면!

네, 잘 안됩니다. Windows 7에서 개발하고 있는 제 경우에는 윈도우 관리자가 이상해져서 Windows Server 2008 처럼 클래식 테마로 바뀌는 등 난리도 아닙니다. -_-;;

지금까지 만든 코드에는 제대로 동작하지 않을만한 문제가 있습니다. 디버거를 통해서 확인해보면, Main 메서드가 호출되는 메인 쓰레드와 OnStart 메서드가 호출되는 쓰레드는 서로 다른 쓰레드입니다. ActiveX를 생성하지 않은 쓰레드에서 ActiveX의 메서드를 호출하는 것은 문제가 있죠.

그렇다고 OnStart 메서드에서 ActiveX를 포함하는 윈폼을 생성하도록 하는 것은 불가능합니다. STA 쓰레드가 되도록 제어할 수 없는 쓰레드에서 OnStart 메서드가 호출되기 때문입니다.

그렇다면 별도의 쓰레드를 생성해서 윈폼을 만들고 이미지 저장을 하도록 하면 어떨까 하는 생각을 하게 되었습니다. 그 내용은 다음 포스트에서 다뤄보도록 하겠습니다.

 

Posted by wafe

댓글을 달아 주세요

Tech/닷넷 일반2010. 9. 1. 17:46

    빠른 공유를 위해 내용보다는 이미지를 주로 이용하여 기본적인 사항만을 전해드리도록 하겠습니다.

    서비스 프로젝트 만들기

  • Visual C# > Windows > Windows 서비스를 선택.

  • 솔루션 탐색기에서 Service1.cs 이름을 적절한 이름으로 변경.
  • 솔루션 탐색기에서 Service1.cs 를 오른쪽 클릭하여 코드 보기를 한다. 다음과 같은 메서드에 필요한 작업을 넣으면 된다.
    • 서비스를 시작할 때에는 그리 길지 않은 시간의 타임아웃 제한이 있으므로, 오래 걸리는 작업은 OnStart 에 넣지 않는 것이 좋다.

  • 이벤트 로그는 다음과 같이 기록할 수 있다.

    image

    서비스 설치 기능 추가

  • Service1.cs의 디자인 모드에서 오른쪽 클릭하여 "설치 관리자 추가"를 선택한다.

  • 그러면 ProjectInstaller.cs 가 추가되어 디자인 모드로 표시된다.
  • serviceProcessInstaller1 을 선택하고 속성 패널에서 속성을 설정한다.
    • Account의 기본값은 User로 되어 있는데, 보통 서비스는 LocalSystem을 사용한다.

  • serviceInstaller1 을 선택하고 속성 패널에서 속성을 설정한다.
    • Description : 서비스 관리 목록에서 표시되는 서비스 설명
    • ServiceName : 서비스 관리 목록에서 표시되는 서비스 이름
    • StartType : Atomatic - 시스템 부팅 시 서비스가 시작되도록 설정

    서비스 설치 및 제거

  • 서비스를 빌드한 후, Visual Studio Command Prompt 에서 다음과 같은 명령을 실행한다.
    • installutil "서비스 EXE 경로"

  • 서비스 관리 도구에 추가된 것을 볼 수 있다.

  • 서비스 제거 시에는 /u 옵션을 추가하면 된다.
    • installutil /u "서비스 EXE 경로"

Posted by wafe

댓글을 달아 주세요

  1. installutil 이 있는 폴더 위치는
    C:\Windows\Microsoft.NET\Framework\v2.0.50727
    입니다.

    Visual Studio Command line 이 없는 경우에는 직접 저 경로에 있는 것을 사용하면 됩니다.

    2010.09.05 19:00 신고 [ ADDR : EDIT/ DEL : REPLY ]

Tech/닷넷 일반2010. 3. 16. 19:23


우엉 코드 주석 문서화 컴파일 작업 한 번 하려는데 이렇게 힘들 줄이야! 피눈물 쥘쥘입니다...

일단 =ㅅ=) 코드 주석 문서화에 필요한 모든 유틸들을 최신버전으로 받는 것이 좋습니다.
최신버전들끼리 호환이 되고 있기 때문에 직접 프로그램을 제공하는 사이트를 찾아서 다운로드 받는 것이 제일 현명합니다.

이제부터 차근차근 프로그램들을 설치해보아요.

 주의 사항 : 설치 폴더는 항상 C:\Programfiles\로 설정하세요.



1. HTML Help Workshop 설치
HTML Help Workshop 다운로드

위의 경로를 따라 다운로드 페이지에 가셔서 "Download Htmlhelp.exe"를 클릭하셔서 설치파일을 저장하고 설치하도록 하세요.



위의 사진처럼 핑크색으로 박스쳐진 부분을 클릭하시면 됩니다. ^-^)v


2. SandCastle 설치
SandCastle 다운로드

위의 경로를 따라 다운로드 페이지에 가셔서 우측의 "Download"라고 쓰여진 녹색 버튼을 클릭하셔서 설치파일을 저장하고 설치하도록 하세요.
(구 버전의 SandCastle은 언어 속성 중에 Korea가 없을 수도 있으므로, 확인해주세요. 최신 버전은 이미 한국어가 추가되어 있어 따로 설정이 필요없습니다.)


3. SandCastle Help File Builder 설치
SandCastle Help File Builder 다운로드

위의 경로를 따라 다운로드 페이지에 가셔서 우측의 "Download"라고 쓰여진 녹색 버튼을 클릭하셔서 설치파일을 저장하고 설치하도록 하세요.

여기까지 하셨다면 .chm포맷으로 Help file을 생성하는 필요 프로그램들이 준비된 것입니다.

이제 SHFB가 설치된 폴더로 가셔서 "SandcastleBuilderGUI.exe" 를 실행하신 후, 몇 가지 설정들을 해주시면 됩니다.
프로그램이 시작되면 프로젝트를 생성하신 후, "Project Properties"에서 설정들을 수정해주세요.



위의 사진에서처럼 핑크색 박스로 쳐진 탭을 확인하시면 됩니다.

Help file을 .chm형식으로 내보내고 싶은 경우 아래처럼 HelpFileFormat을 HtmlHelp1으로 설정하시면 됩니다.



언어를 한국어로 조정해주시고, VisualStudio2005나 2008버전을 사용하시는 분들은 PresentationStyle을 vs2005로 설정해주시면 됩니다.



임의대로 결과물 출력 경로를 설정해주고 싶은 경우 아래처럼 OutputPath를 본인 마음대로 설정하시면 됩니다.




자! 이제 ProjectProperties 설정은 마쳤습니다. ^ㅁ^)/
이제 우측으로 시선을 돌려주세요!

Project Explorer에 Help file을 뽑아내기 위한 희생양을 추가시킬 시간이 왔습니다.



"Documentation Source"를 우클릭하시면 파일 열기 다이얼로그가 뜨는데요.
유심히 Open file format을 살펴봐주세요.



.dll과 .xml뿐만 아니라 .exe, .sln, .csproj까지 제공합니다!
우왕ㅋ굳ㅋ 구버전에는 없었던지라 이런 서비스까지도 감격에 빠지게 만드네요.
일일이 참조 .dll들을 넣어주는 작업들이 여간 귀찮은 일이 아니었거든요.

.sln을 넣어주면 해결된다는 사실은 매우 감격스런 일이라는 것을 다시 한 번 말씀드려봅니다.

물론 System.Winodws등의 프로젝트 외부에서 제공하는 .dll파일들은 직접 파일을 찾아서 "References"에 추가해주셔야 합니다.


응헝헝헝응앵으아아앙...
이제사 코드 주석 문서화 작업을 시작할 모든 준비가 갖추어졌습니다. ㅠ_ㅠ...

이제 빌드를 시켜주시면 "Build Output"탭이 생기면서 빌드를 시작합니다.
빌드 로그들이 꽈득 채워지는 모습을 보며 흐믓한 표정을 지으시고 계시겠지만, 곧 모니터를 보며 멍한 표정을 짓게 되실 겁니다.
굉 - 장 - 히 오랜 시간동안 빌드가 될 테니까요...
빌드가 되고 있을동안 다른 작업이나 휴식을 가지시는 것이 좋습니다.

오랜시간 견딘 끝에 빌드 완료 메시지가 뜨면 OutputPath로 가셔서 결과물을 확인해보세요!
이쁘장하게 .chm파일이 당신을 향해 미소짓고 있을 것입니다!


요런 눈망울로?


이제 다른 형식으로 Help file 결과물을 만들어보도록 하겠습니다.
.HxS포맷인데요.

이 형식으로 결과물을 만들기 위해선 또 다시 필요한 기능을 설치해야 합니다.

4. Visual Studio 2008 SDK 1.1 설치
Visual Studio 2008 SDK 1.1 다운로드

.chm을 위해서 필요한 것이 HTML Help Workshop이라면, .HxS를 위해서 Visual Studio 2008 SDK 1.1를 설치해 주셔야 합니다.

설치를 다 하셨다면, 다시 SHFB를 실행하셔서 .chm을 생성했을 때와 같은 방식으로 빌드해주시면 됩니다.

단!
"Project Properties"에서 "HelpFileFormat"을 "MSHelp2"로 변경하신 다음에 말입니다.

비교적으로 .chm때보다 .HxS를 생성하기 위한 빌드 시간이 짧은 편입니다.
행복하죠.

.chm은 프로젝트가 크면 클 수록 빌드 시간이 경악스러우니까요.

빌드를 완료한 후, 결과물 폴더를 향해 빠르게 이동하셨겠지만 .HxS파일을 읽을 수가 없습니다. 엉엉앙앙으해해해흐애애앵 ㅠ_ㅠ...
생성한 .HxS파일을 읽기 위해서 Viewer를 다운로드 받으셔야 합니다.

5. H2Viewer 다운로드
H2Viewer 다운로드



위의 사진에 나와있는 "H2Viewer146.zip"을 다운받아 원하는 위치에 압축을 푸시고, "H2Viewer.exe"를 실행하신 후, 적절한 네임스페이스를 선택하시면 아래처럼 실행화면이 뜹니다.



상단에 있는 file탭에서 파일 열기를 클릭하셔서 .HxS파일을 여시면 원하는 결과물을 보실 수 있습니다. ^ㅁ^)/

응어어어어헝헝헝헝 험난한 코드 주석 문서화 작업을 이제 모두 마쳤습니다.

참고로 .chm파일로 배출하시다가 빌드 오류가 발생되면 저처럼 포기하시고 .HxS로 Output하시는 것도 하나의 방법이겠습니다. ㅠ_ㅠ)...

그럼, 이것으로 포스팅을 마칠게요우!
수고하셨습니다! >ㅅ<)/



추신 : Sandcastle을 사용한 간단 문서화 라 하여 공도님의 블로그에도 Sandcastle을 이용한 문서화 방법에 대해 포스팅이 되어있는데요, DocProject라는 것을 설치하여 VisualStudio에서 직접 문서화를 실행할 수 있는 Proejct Template을 제공합니다. 다만, English Only라는 점... 그것도 모르고 설치하고 왜 안되나 끙끙대다 피눈물 쏟았네요.
Posted by 벚꽃손님

댓글을 달아 주세요

  1. 텐샤

    example 안에 xml 형태의 tag 를 넣으니 개행과 들여쓰기가 안되고 일렬로 붙어 보이던데요. 혹 이쁘게 보이게 할 수 있을까요?

    2010.10.07 11:13 [ ADDR : EDIT/ DEL : REPLY ]
  2. HTML Help Workshop 다운로드 링크가 깨졌네용

    http://msdn.microsoft.com/en-us/library/ms669985(v=vs.85).aspx 로 이용하세요

    2011.03.03 10:57 신고 [ ADDR : EDIT/ DEL : REPLY ]
  3. CHM 형식은 빌드 시간이 너무 오래 걸려서 HxS 형식을 사용했는데, 이건 Visual Studio가 설치되지 않은 환경에서 보려면 VS SDK 를 설치해야 되어서 배보다 배꼽이 크네요. 많은 사람과의 공유를 위해서는 Website 형식을 쓰는 게 가장 좋았습니다. 검색 기능도 제공되고 기능 상으로는 동일합니다.

    2011.03.31 20:17 신고 [ ADDR : EDIT/ DEL : REPLY ]