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 알 수 없는 사용자
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