새로운 .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 실행 사례도 추가 아티클 상에서 살펴보겠습니다.

IDisposable 패턴의 올바른 구현 방법

.NET Framework에서 새로운 클래스를 만들 때 여러가지 메모리 관리 디자인 패턴과 기법을 적용할 수 있지만, 한시적으로 사용해야 할 필요가 있는 자원들을 묶어서 관리할 때에는 IDisposable 패턴을 적극적으로 활용하는 것이 매우 유용합니다.

하지만 생각보다 IDisposable 패턴을 제대로 구현해서 사용하는 것은 쉽지 않으며 잘못 구현하기 쉽습니다.

이 아티클에서는 IDisposable 패턴을 구현하는 몇 가지 일반적인 전략들을 소개합니다. 잘못 설명된 부분이 있거나 보충이 필요한 부분은 댓글로 피드백을 자세히 남겨주시면 적극 반영하겠습니다.

IDisposable 인터페이스에 대한 이해

IDisposable 인터페이스가 제공하는 Dispose 메서드는 명시적이고 코드 작성자가 직접 호출할 수 있는 finalizer로, 이 메서드가 불리면 가비지 컬렉터에 의하여 나중에 호출되는 finalizer의 역할을 대체하도록 되어있습니다. 물론, 메모리 상에 할당된 메모리 블록의 해제까지 건너뛴다는 의미는 아닙니다.

그리고 무엇을 Dispose 메서드에서 제거해야 하는지 기준을 세운다면 객체에 대한 소유 권한을 정의하는 것이 필요합니다. 적어도 다음의 경우에는 확실히 Dispose 메서드 내에서 정리가 되어야 합니다.

  • 해당 객체를 Dispose 하게 되면 외부에서 더 이상 사용하는 것이 의미가 없는 객체 (예를 들어 클래스 내부에서 사용하던 파일 입출력 관련 객체, 비동기 작업을 위하여 만들어 놓은 스레드 관련 객체)
  • 외부에서 전달받은 객체이지만 객체의 생명 주기를 위탁하여 관리하도록 지정한 객체 (예를 들어 StreamReader나 StreamWriter가 객체 생성 시 인자로 Stream을 받는 사례)

가장 기본이 되는 IDisposable 구현 패턴

.NET 은 finalizer에 해당되는 멤버를 재정의할 수 있습니다. 하지만 finalizer가 언제 호출이 될 것인지 기약할 수 없으므로 이 finalizer를 대신하여 좀 더 이른 시기에 명시적으로 소멸자와 동등한 효과를 낼 수 있도록 만든 것이 바로 IDisposable.Dispose 메서드가 되겠습니다.

IDisposable 패턴을 처음 구현할 때에는 다음의 사항들이 핵심이 됩니다.

  • protected virtual void Dispose(bool disposing) 메서드를 추가합니다. sealed 클래스에 대해서는 private void Dispose(bool disposing)으로 바꾸어 정의합니다.
  • 객체가 dispose 처리가 이루어진 상태인지를 관리할 수 있는 boolean 필드를 하나 추가하고 이 필드는 기본값을 false로 설정합니다.
  • Dispose(bool disposing) 호출 시 다음의 로직을 구현합니다.
    • 만약 객체가 dispose 상태인 경우에는 함수를 종료합니다.
    • 객체가 dispose 상태임을 필드에 지정합니다. (true로 설정)
    • disposing 매개 변수의 상태와 관계없이 P/Invoke 등을 활용하여 메모리 할당을 받은 나머지 모든 리소스들에 대해 할당을 해제하는 코드를 추가합니다.
    • disposing 매개 변수가 true로 전달되는 경우는 명시적으로 Dispose 메서드를 호출한 경우이며, 이 때에 다른 모든 IDisposable을 구현하는 객체들을 Dispose 처리합니다.
  • IDisposable.Dispose 메서드 구현 시 Dispose(true)를 호출하고 finalizer 호출을 건너뛰기 위하여 GC.SuppressFinalize(this)를 호출합니다.
  • 소멸자에서는 Dispose(false)를 호출합니다.

다음은 코드 예시입니다.

public class DisposableSample : IDisposable
{
    public DisposableSample()
    { }

    ~DisposableSample()
    {
        this.Dispose(false);
    }
    
    private bool disposed;

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (this.disposed) return;
        if (disposing)
        {
            // IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
        }
        // .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다.
        this.disposed = true;
    }
}

IDisposable 객체의 컬렉션에 대한 소거

소켓, 스레드 풀, 혹은 커넥션 풀 같은 컬렉션을 객체 내부에 보관해야 하는 경우도 있습니다. 이러한 경우에도 컬렉션 내의 모든 IDisposable 객체에 대해서 정리를 하는 것이 필요합니다.

  • 컬 렉션 내의 모든 요소가 IDisposable 인터페이스를 구현하고 있다면 Dispose(bool disposing) 메서드에서 disposing이 true일 때 정리를 하면 됩니다. 그렇지 않은 경우, disposing 매개 변수의 상태에 무관하게 정리합니다.
  • Dispose 작업 도중 새로운 항목이 추가되는 것을 방지하기 위하여 disposed 필드를 확인하도록 하는 코드를 추가해야 할 수 있습니다.
  • 컬렉션 내의 모든 요소를 배열에 복사하여 Immutable Collection으로 변환한 다음 하나씩 방문하여 Dispose를 진행하고, 최종적으로 컬렉션의 요소들을 모두 제거합니다.

다음은 코드 예시입니다.

public class DisposableSample : IDisposable
{
    public DisposableSample()
    {
        this.items = new List<IDisposable>();
    }

    ~DisposableSample()
    {
        this.Dispose(false);
    }
    
    private bool disposed;
    private List<IDipsosable> items;

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (this.disposed) return;
        if (disposing)
        {
            // IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
            IDisposable[] targetList = new IDisposable[this.items.Count];
            this.items.CopyTo(targetList);
            foreach (IDisposable eachItem in targetList)
            {
                eachItem.Dispose();
            }
            this.items.Clear();
        }
        // .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다.
        this.disposed = true;
    }
}

Dispose 메서드 내의 예외 처리

만약 Dispose 메서드를 실행하는 도중에 예외가 발생한다면, CA1065의 지침에 따라 명시적인 Dispose 호출이었든 아니었든 예외를 전파하지 않도록 처리하는 것이 필요합니다. 다만 명시적으로 Dispose를 호출하면서 예외가 발생했다면 예외를 전파하지 않는 대신 적절한 예외 처리는 필요합니다.

이 부분에 대한 자세한 내용은 https://msdn.microsoft.com/ko-kr/library/bb386039.aspx 페이지의 내용을 참고하시면 도움이 될 것입니다.

컬렉션 내의 모든 요소들을 Dispose 하는 코드를 조금 더 보강하면 다음과 같이 고쳐쓸 수 있겠습니다.

public class DisposableSample : IDisposable
{
    public DisposableSample()
    {
        this.items = new List<IDisposable>();
    }

    ~DisposableSample()
    {
        this.Dispose(false);
    }

    private bool disposed;
    private List<IDisposable> items;

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (this.disposed) return;
        try
        {
            if (disposing)
            {
                // IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
                IDisposable[] targetList = new IDisposable[this.items.Count];
                this.items.CopyTo(targetList);
                foreach (IDisposable eachItem in targetList)
                {
                    try { eachItem.Dispose(); }
                    catch (Exception ex) { /* 예외 처리를 수행합니다. */ }
                    finally { /* 정리 작업을 수행합니다. */ }
                }
                this.items.Clear();
            }
            try { /* .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다. */ }
            catch { /* 예외 처리를 수행합니다. */ }
            finally
            {
                /* 정리 작업을 수행합니다.  */
                this.disposed = true;
            }
        }
        finally { /* 정리 작업을 수행합니다. */ }
    }
}

소유하고 있는 객체들에 대한 Dispose 또는 정리 작업들을 각각 try, catch, finally 블록안에 두어 예외가 발생하면 적절한 예외 처리를 할 수 있게 하고, Dispose 메서드 전체에 대해서 try, finally 블록 안에 두어 예외가 전파되지 않도록 하였습니다.

결론

.NET Framework 기반의 응용프로그램이 안정적으로 장시간 실행될 수 있게 만들어야 할 때 고려해야 할 요소들 가운데에서 가장 비중있게 다루어야 할 부분이 바로 메모리 관리입니다. IDisposable 인터페이스를 통한 명시적인 finalizer 호출은 적절하게 활용하면 응용프로그램의 메모리 관리를 단순하게 만드는데 큰 도움을 줍니다.

.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로 치환하는 작업도 필요할 수 있다고 볼 수 있겠습니다.


 

ubuntu 14.04에서 asp.net vnext 설치하고 사용하기

업데이트: mono 3.8이 9월 초에 새로 릴리즈되었으며 이 내용을 기초로 새로 업데이트한 아티클을 올렸습니다.


이 블로그 포스트의 내용은 아래 두 블로그 포스트의 내용을 기초로 작성한 것임을 말씀드립니다.
•http://graemechristie.github.io/graemechristie/blog/2014/05/26/asp-dot-net-vnext-on-osx-and-linux/
•http://www.rocko.me/install-mono-3-4-ubuntu/


또한 이 블로그 포스트는 MS Azure Virtual Machine과 Ubuntu Server 14.04 버전을 최초 설치했을 때의 상태를 기준으로 작성된 것이며, 이 블로그 글을 작성하는 2014년 8월 현재 ASP.NET vNext가 정식 출시 전임을 말씀드립니다.


주의: 실제 배포 환경에서 이 블로그 포스트의 내용을 활용하시는 것은 매우 위험합니다.


 


 


ASP.NET vNext는 기존의 System.Web 기반의 레거시 웹 개발 프레임워크에서 탈피하고자 하는 MS의 강력한 의지의 결과물인듯 합니다. 이전에는 상상하기 어려웠고, MS의 손이 아닌 오픈 소스 그룹 (Mono의 System.Web 구현)이나 써드 파티 회사 (Grasshoper 같은)에 의한 제한적인 수준의 작업 결과물일 뿐이었던 ASP.NET의 이식성이 이제서야 완벽함을 기할 수 있게 되었습니다.


이 블로그 포스트에서는 ASP.NET vNext를 우분투 서버 14.04에서 설치해본 과정을 기록하여 그것을 토대로 작성하였습니다. ASP.NET vNext의 발전 가능성을 살펴보시고, 여러 이야기를 나눌 수 있지 않을까 하여 기록해봅니다.


사전 준비 작업


ASP.NET vNext는 Windows 서버 환경에서는 손수 기존에 설치된 .NET Framework를 대체하는 K Runtime을 사용하여, 어느 버전의 K Runtime을 사용할 것인지 패키지 레벨에서 정의할 수 있는 것이 특징이었는데, 리눅스의 경우 기본 실행 엔진은 현재는 Mono를 기반으로 하고 있는 것이 특징입니다. 그럼에도 불구하고 K Runtime이 가지는 영역이 엄연히 있고, 아마 핵심 실행 엔진만 현재는 Mono를 기반으로 실행되는 것 같습니다.


그런 이유로 Mono의 최신 버전을 시스템에 설치해야 하는데, 안타깝게도 Ubuntu 14.04에 등록된 Mono 패키지의 최신 버전은 ASP.NET vNext를 실행하기 위해 필요한 버전과 격차가 상당히 크고, 또한 지원되지 않습니다. 그래서 제일 먼저 해야 할 일은 github에 올라와있는 Mono 소스 코드를 내려 받아 컴파일하고 새 버전으로 바꾸는 작업입니다.


우선은 기존에 Mono 런타임을 설치했던 이력이 있을 경우를 고려하여 Mono와 관련된 모든 패키지를 제거해야 하는데, 아래 명령어로 간단히 제거할 수 있습니다.



sudo apt-get -y purge mono-*


그 다음, Mono를 설치하기 위하여 필요한 이미징 라이브러리 관련 종속성을 해결해주어야 하는데, 필요한 패키지들중 상당수는 Ubuntu 14.04에서 직접 지원하지 않거나 오래된 버전으로 취급하여 apt-get으로 직접 설치가 어려운 패키지들입니다. 따라서, 이들 패키지들을 수동으로 내려 받아 설치하는 작업이 필요한데, 아래 명령어를 복사하여 하나씩 실행하시면 되겠습니다.



wget http://security.ubuntu.com/ubuntu/pool/main/j/jbigkit/libjbig0_2.0-2ubuntu1.13.10.1_amd64.deb
 wget http://security.ubuntu.com/ubuntu/pool/main/libj/libjpeg-turbo/libjpeg-turbo8_1.3.0-0ubuntu1.1_amd64.deb
 wget http://mirrors.kernel.org/ubuntu/pool/main/libj/libjpeg8-empty/libjpeg8_8c-2ubuntu8_amd64.deb
 wget http://mirrors.kernel.org/ubuntu/pool/universe/t/tiff3/libtiff4_3.9.7-2ubuntu1_amd64.deb
 wget http://mirrors.kernel.org/ubuntu/pool/universe/t/tiff3/libtiffxx0c2_3.9.7-2ubuntu1_amd64.deb
 wget http://mirrors.kernel.org/ubuntu/pool/main/libj/libjpeg8-empty/libjpeg-dev_8c-2ubuntu8_amd64.deb
 wget http://security.ubuntu.com/ubuntu/pool/main/j/jbigkit/libjbig-dev_2.0-2ubuntu1.13.10.1_amd64.deb
 wget http://security.ubuntu.com/ubuntu/pool/main/libj/libjpeg-turbo/libjpeg-turbo8-dev_1.3.0-0ubuntu1.1_amd64.deb
 wget http://mirrors.kernel.org/ubuntu/pool/main/libj/libjpeg8-empty/libjpeg8-dev_8c-2ubuntu8_amd64.deb
 wget http://mirrors.kernel.org/ubuntu/pool/main/libj/libjpeg8-empty/libjpeg-dev_8c-2ubuntu8_amd64.deb
 wget http://mirrors.kernel.org/ubuntu/pool/universe/t/tiff3/libtiff4-dev_3.9.7-2ubuntu1_amd64.deb


sudo dpkg -i libjbig0_2.0-2ubuntu1.13.10.1_amd64.deb
 sudo dpkg -i libjpeg-turbo8_1.3.0-0ubuntu1.1_amd64.deb
 sudo dpkg -i libjpeg8_8c-2ubuntu8_amd64.deb
 sudo dpkg -i libtiff4_3.9.7-2ubuntu1_amd64.deb
 sudo dpkg -i libtiffxx0c2_3.9.7-2ubuntu1_amd64.deb
 sudo dpkg -i libjbig-dev_2.0-2ubuntu1.13.10.1_amd64.deb
 sudo dpkg -i libjpeg-turbo8-dev_1.3.0-0ubuntu1.1_amd64.deb
 sudo dpkg -i libjpeg8-dev_8c-2ubuntu8_amd64.deb
 sudo dpkg -i libjpeg-dev_8c-2ubuntu8_amd64.deb
 sudo dpkg -i libtiff4-dev_3.9.7-2ubuntu1_amd64.deb


Mono 최신 버전 설치하기


이제 기본 준비 작업은 끝났고, 필요한 패키지들을 한꺼번에 설치할 차례입니다. 아래 명령어를 입력하도록 합니다.



sudo apt-get -y install libpng3 libpng3-dev libtool libexif12 libexif-dev libgif4 libgif-dev libpango1.0-dev libatk1.0-dev libgtk-3-0 libgtk-3-dev bison automake autoconf make gcc gtk-sharp2 build-essential xorg-dev libfreetype6 libfontconfig libfontconfig-dev gettext libglib2.0-dev git libjpeg-dev libjpeg8-dev libjpeg-turbo8-dev g++ unzip


쉬운 설명을 위하여, 사용자 프로필 디렉터리에서 설치를 진행한다고 가정하겠습니다.



cd ~


설치가 모두 되고 나면, mono git 리포지터리에서 libgdiplus 소스를 복사합니다.



git clone https://github.com/mono/libgdiplus.git


받은 소스 디렉터리로 이동합니다.



cd ~/libgdiplus


그리고 각종 설정 검사 및 헤더 구성을 진행합니다. 주의할 것은 공식 가이드에서는 –prefix=/usr/local로 소개하고 있으나 우분투의 경우 아래와 같이 /usr을 기준으로 잡아야 합니다.



./autogen.sh –prefix=/usr


구성이 끝나면 컴파일을 하도록 합니다.



make


컴파일 중 특별한 오류 메시지가 없었다면 시스템에 설치하도록 합니다.



sudo make install


이제 다시 홈 디렉터리로 이동합니다.



cd ~


mono 소스를 컴파일하는 과정 중에는 재귀적으로 mcs 컴파일러가 필요합니다. 이를 위하여 mono-gmcs 패키지를 구 버전이지만 우선 설치해야 합니다.



sudo apt-get -y install mono-gmcs


설치가 끝나면, 이제 mono 소스를 복사하도록 합니다.



git clone git://github.com/mono/mono.git
 cd mono


libgdiplus 때와 마찬가지로 prefix 설정에 유의하여 자동 구성을 진행합니다. 자동 구성 중에 다른 git 리포지터리에서 추가로 관련된 소스를 내려받기도 합니다.



./autogen.sh –prefix=/usr


모든 구성이 끝나면 컴파일하고 설치하도록 합니다.



make
 sudo make install


모든 설치가 다 끝났다면, 새 버전 (2014년 8월 현재 3.8)으로 업데이트가 잘 되었는지 확인해보도록 합니다.



mono –version
 mcs –version


위의 명령어에서 새 버전으로 표시가 된다면 ASP.NET vNext를 설치할 준비가 다 끝난 것입니다. 이제 다시 홈 디렉터리로 이동합니다.



cd ~


계속 하기 전에, 라이브러리 경로에 관련된 환경 변수를 하나 설정해주는 것이 좋습니다. 아래 명령어를 실행하여 LD_LIBRARY_PATH 환경 변수를 설정하도록 합니다.



export LD_LIBRARY_PATH=/usr/lib:/usr/local/lib:$LD_LIBRARY_PATH


K Runtime과 ASP.NET vNext 설치하기


이제 중요한 부분이 남았습니다. K Runtime과 ASP.NET vNext를 설치하는 것이 남았는데, 앞의 과정보다 시간도 짧게 걸리고 비교적 쉽습니다.


ASP.NET vNext의 전체 소스 코드를 복사하지 않고 필요한 셸 스크립트 파일인 kvminstall.sh 파일만 가져오도록 합니다. 아래 명령어를 홈 디렉터리에서 실행합니다.



curl https://raw.githubusercontent.com/aspnet/Home/master/kvminstall.sh | sh


경로 설정을 맞추기 위하여, 아래 명령어를 실행합니다.



source ~/.kre/kvm/kvm.sh


이제 KVM을 사용자 프로필 디렉터리 아래의 .kre 폴더에 설치하기 위해, 다음 명령어를 실행합니다.



kvm upgrade


기본적인 실행 환경이 준비되었고, K Package Manager (달리 표현하면 K Package Manager의 실행을 담당하는 Mono)가 통신해야 할 사이트들의 HTTPS 인증서를 추가한 다음, 시스템에 설치된 루트 인증서를 가져올 수 있도록 하기 위하여 아래 명령어들을 실행합니다. 확인 프롬프트가 나타나면 여러번 yes를 입력하여 모든 필요한 인증서 및 인증서 체인을 가져오도록 합니다.



sudo certmgr -ssl -m https://go.microsoft.com
 sudo certmgr -ssl -m https://nugetgallery.blob.core.windows.net
 sudo certmgr -ssl -m https://nuget.org
 sudo certmgr -ssl -m https://myget.org
 mozroots –import –sync


Hello, World! 찍어보기


모든 설치가 끝났습니다. 예제 소스 코드를 가져와서 실행하기 위하여, David Fowler님의 github 리포지터리에 올라와있는 ASP.NET vNext 샘플을 이용하도록 하겠습니다. 공식 웹 사이트에 있는 샘플은 HTTPAPI를 기반으로 하는 것이어서 Nowin Factory로 교체하여 실행할 수 있지만 쉬운 설명을 위해 David Fowler님의 예제를 가져와서 대신 설명함을 말씀드립니다.


홈 디렉터리로 이동합니다.



cd ~


그리고 아래 명령어를 실행하여 콘솔 프로젝트 샘플 소스를 복사합니다.



git clone https://github.com/davidfowl/HelloWorldVNext.git


해당 디렉터리로 이동하여 다음 순서대로 명령어를 입력하여 Hello World! 메시지가 나타나는지 확인합니다.



cd ~/HelloWorldVNext/src/helloworld
 kpm restore
 k run


여기서 kpm restore 명령은 해당 예제를 실행하기 위하여 필요하다고 project.json에서 명시한 NuGet 패키지들을 전부 시스템에 설치하는 과정을 포함하며, 최초 한 번만 실행하면 됩니다. 그리고 k run 명령은 project.json 또는 그 상위에 정의되어있는 run 명령어를 실행한다는 의미이며, 보통 run 명령어는 재정의하지 않는 한 Main 메서드를 찾아 실행하는 것과 의미가 같습니다.


받은 프로젝트 디렉터리 상의 파일을 보면 흥미로운 것이, 이전처럼 mcs (gmcs)를 호출하여 exe 파일을 만들지 않았는데도 소스 상태에서 바로 k run이라는 명령어를 넣으면 프로그램이 시작된다는 점입니다. 이런 방식의 닷넷 응용프로그램은 웹 환경에서 큰 강점을 발휘하게 될 것입니다.


ASP.NET vNext 샘플 웹 프로젝트 띄워보기


이제 핵심입니다. ASP.NET vNext 샘플 웹 프로젝트를 띄워볼 차례인데, 다음과 같이 명령어를 입력하도록 합니다. 물론, 진행의 편의를 위해 홈 디렉터리에서 실행하는 것이 좋겠습니다.



git clone https://github.com/davidfowl/HelloWorldVNext.git
 cd ~/HelloWorldVNext/src/helloworldweb
 kpm restore
 k web


예제에 같이 들어있는 Nowin Factory 프로젝트의 코드를 보면 TCP/5000 포트를 웹 리스너 포트로 사용하고 있습니다. 밖에서 호스트 이름과 함께 5000번 포트로 접속하면 웹 페이지가 나타나는 것을 볼 수 있습니다. 그리고 서버를 종료하려면 콘솔에서 아무 키나 누르면 종료가 됩니다.


만약에 원격에서 좀 더 지속적으로 서버의 성능을 측정해보고 싶으시다면 screen 유틸리티를 사용하여 세션을 분리하신 상태에서 위의 명령어를 입력하고, 서버가 떠 있을 때 Ctrl 키를 누른 상태에서 빠르게 a, a, d 키를 누르면 세션이 분리되어 계속 살아있는 서버가 만들어집니다. 이 상태에서 Apache Bench (AB)등의 유틸리티를 사용하여 부하 테스트 등을 해보시는 것도 의미가 있을 것입니다.


참고로, NAT 환경이나 퍼블릭 클라우드 환경에서는 대표 IP 주소에 대한 외부 방화벽 설정을 열어주셔야 밖에서도 접속이 가능합니다.


마무리


아직 ASP.NET MVC 6나 다른 기술들이 완전히 준비된 것은 아니지만, 이 정도만 하더라도 ASP.NET은 더 이상 윈도 OS 안에서만 사용 가능한 기술이 아니라는 것을 증명하는데에는 손색이 없을 것입니다. 더 많은 가능성과 잠재력을 포함하는 최신 기술이 곧 나타나게 될 것이 무척 기대가 됩니다.


만약 기존에 ASP.NET 웹 사이트를 개발해 놓은 것이 있다면, ASP.NET vNext로 프로젝트를 마이그레이션하면서 플랫폼에 중립적으로 동작하는 코드로 업데이트하는 프로젝트를 한 번 진행함으로서 그리 어렵지 않게 멀티 플랫폼으로 ASP.NET 웹 응용프로그램을 포팅하실 수 있을 것입니다.


앞으로 더 자세한 정보와 상세한 내용들, 그리고 활용 방안들도 블로그 포스트로 전할 수 있도록 하겠습니다.

C#에서 HTML 문서 분석 때문에 고민하지 마세요 – HtmlAgilityPack

흔히 소프트웨어 개발 과정에서 나타나는 요구 사항들 중에서, 시간 투자 대비 효율이 가장 떨어지는 요구 사항으로 HTML 문서 분석에 대한 것이 있습니다. 이 요구 사항을 해결하기 위해서, 보통 택하는 방안으로는 Windows 환경에서 Internet Explorer의 MSHTML을 이용하는 방안을 고민하게 됩니다. 그러나 익히 알려져 있는 바, 메모리 누수나 무거운 런타임 크기, 더 나아가서는 제한된 수준의 해석 등 이점보다는 단점이 더 많은 방식이라는 것이 큰 문제입니다. 물론 이 이후로도 오픈 소스 브라우저를 활용하거나 WebKit 바인딩을 이용하는 방법도 흔히 검토가 가능하긴 하지만 전체 브라우저 스택을 완전히 불러들여야 하고 HTML 문서의 계산 과정이 동반된다는 점이 성능 상의 이슈가 됩니다.



HtmlAgilityPack은 순수하게 .NET Framework의 코드 만으로 HTML 문서를 온전하게 분석하는 것은 기본이고, System.Xml 네임스페이스에서 제공하는 XPATH 식 관련 인터페이스와 해석기를 충실하게 지원하고 있어서, 복잡하고 까다로운 HTML 문서 구조 탐색을 매우 손쉽게 처리해 준다는 큰 장점이 있습니다. 무엇보다도 중요한 것은, NuGet에서 쉽게 설치해서 사용할 수 있으므로 HTML 문서 분석에 관한 고민에 시달리고 계시다면 지금 당장 테스트해보시기를 강력히 권할 수 있을 만큼, 훌륭한 기능을 제공합니다. 강력한 기능 덕분에 몇몇 상용 솔루션에서는 이미 절찬리에 채택되어 사용 중에 있습니다.



패키지/프로젝트 소개


HtmlAgilityPack은 2014년 4월 현재 다음과 같이 두 패키지로 구분되어있습니다.


•http://www.nuget.org/packages/HtmlAgilityPack/
•http://www.nuget.org/packages/HtmlAgilityPack-PCL/


 


 


처음의 패키지는 Desktop 버전의 .NET Framework (Mono 포함)에서 이용할 수 있도록 패키징된 버전으로, XPATH 식에 대한 지원을 포함하고 있습니다. 그리고 두 번째 패키지는 Windows Phone, Windows Store App 등의 환경에서도 사용 가능하도록 Portable Class Library 프로필에 맞게 리패키징한 버전입니다. 이 글에서 설명하고자 하는 버전은 첫 번째 버전입니다.



샘플 코드


테스트하려는 프로젝트를 여신 다음, HtmlAgilityPack을 NuGet 패키지 관리자로 프로젝트에 설치하고, 실제로 HTML 페이지 분석이 잘 이루어지는지 확인해보기 위하여, 다음과 같이 코드를 작성해보도록 하겠습니다.
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.XPath;


class Program
{
    static void Main(string[] args)
    {
        Uri targetUri = new Uri(“http://www.youtube.com/watch?v=8YkbeycRa2A“); HttpWebRequest webRequest = HttpWebRequest.Create(targetUri) as HttpWebRequest;
        using (HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse)
        using (Stream webResponseStream = webResponse.GetResponseStream())
        {
            HtmlDocument s = new HtmlDocument();
            Encoding targetEncoding = Encoding.UTF8;


            s.Load(webResponseStream, targetEncoding, true);
            IXPathNavigable nav = s;


            string title = WebUtility.HtmlDecode(nav.CreateNavigator().SelectSingleNode(“/html/head/meta[@property=’og:title’]/@content”).ToString());
            string description = WebUtility.HtmlDecode(nav.CreateNavigator().SelectSingleNode(“/html/head/meta[@property=’og:description’]/@content”).ToString());
            string fullDescription = WebUtility.HtmlDecode(s.GetElementbyId(“eow-description”).InnerHtml);
            fullDescription = Regex.Replace(fullDescription, @”<(br|hr)[^>]>”, Environment.NewLine);
            fullDescription = Regex.Replace(fullDescription, @”<[^>]
>”, String.Empty).Trim();


            Console.WriteLine(title);
            Console.WriteLine(description);
            Console.WriteLine(fullDescription);
        }
    }
}


여기서 설명한 코드는 YouTube의 메타 태그 속성을 이용하여 동영상에 대한 기본적인 정보를 가져오는 코드입니다. 여기서 주목할 것은 XPATH 식을 사용하여 HTML DOM 모델에 정확하게 접근하고 있다는 점입니다. Property 속성이 og:title인 META 태그의 Content 속성을 가져와서 YouTube에 정확하게 등록한 원래 동영상 제목을 추출하거나, 동영상의 설명을 같은 방법으로 가져오고 있습니다. 여기서 그치지 않고, eow-description이라는 ID를 가진 HTML 태그를 찾아 그 태그의 내용을 통째로 가져와서 축약되지 않은 원래 설명도 특별한 API 없이 가져오고 있습니다.



System.Xml.XPath 호환


위의 코드에서 HtmlDocument 클래스를 IXPathNavigable 형식의 변수 s로 캐스팅한 것을 볼 수 있습니다. 이 코드가 의미하는 바는 실로 큰데, IXPathNavigable은 .NET Framework BCL의 System.Xml 안에 들어있는 인터페이스를 실제로 대응하여 구현한 것입니다. 이 기능을 사용하면 다루는 대상이 XML이든 HTML이든 혹은 XHTML이든 일반화하여 다루는 것이 가능하여 의존성 주입의 차원을 고도화하는 것이 가능합니다.



IXPathNavigable 인터페이스는 다른 메서드를 제공하지 않고 오로지 CreateNavigator() 메서드만을 제공하는데, 이 메서드를 이용하여 만드는 객체는 XPathNavigator 클래스입니다. 이 클래스의 인스턴스는 상대적 접근 경로를 인식하여 해당 객체를 만들기 위하여 어떤 DOM 노드 객체를 활용하였는지에 따라 경로가 달라집니다. 이 기능을 사용하면 XPATH 식으로 선택이 어려운 노드를 근처의 형제 노드 (Sibling Node)를 통해서 전/후 탐색 메서드를 사용하여 접근이 가능합니다. XPathNavigator 클래스는 한 번 생성된 이후에는 자신이 스스로 상태를 관리하도록 되어있으므로 일종의 커서처럼 탐색이 가능합니다.



실제 활용 사례


이 즈음되면, 정말 특이한 상황을 제외하고 HTML 페이지를 수집하러 돌아다니는 크롤러를 얼마든지 만들 수도 있겠다는 예상도 해볼 수 있습니다. 실제로 이 라이브러리를 사용하여 크롤링 기술을 구현하고 있는 Arachnode.net이라는 Lucene.net 기반의 상용 검색 엔진 프로젝트도 많은 주목을 받고 있습니다. (http://arachnode.net/)



필자 개인적으로는 HTML 구문 분석이 필요한 프로젝트에서 상당히 큰 도움을 받았으며 해당 프로젝트의 주요 기능으로 적극적으로 채택하는데 큰 도움을 받았습니다.



도입 시 고려할 사항


HTML을 분석할 수 있다는 전무후무한 강력한 장점이 있음에도 불구하고, 한 가지 주의 사항이 있습니다. HtmlAgilityPack으로 처리할 수 있는 HTML은 파일 시스템 상의 고정된 정적 페이지나 네트워크를 통해서 서버가 정적 또는 동적으로 제공하는 HTML 페이지만 정확하게 처리가 가능하며, CSS나 JavaScript, 혹은 다른 Rich Internet Application (Flash나 Silverlight 등)이 나중에 가공하는 HTML 페이지의 변경 사항은 반영되지 않는다는 점입니다. 이것은 라이브러리의 문제가 아니며, 라이브러리가 취하고자 하는 기능의 명확한 한계입니다.



Rich Internet Application에 의한 후 처리를 제외하고 CSS나 JavaScript에 대한 처리가 완료된 웹 페이지에 대한 분석이 필요한 상황인 경우, Headless Browser를 사용하여 계산이 완료된 웹 페이지를 처리하거나 더 복잡한 작업을 수행할 수도 있습니다. 이를 위하여 PhantomJS (http://phantomjs.org/) 를 사용하는 것이 유용합니다. PhantomJS는 NuGet 패키지로도 제공됩니다. (http://www.nuget.org/packages/PhantomJS/)



만약 Rich Internet Application에 의한 후 처리까지 필요한 경우에는 Chrome이나 Internet Explorer를 해석 엔진으로 안정적으로 사용할 수 있도록 도움을 주는 Selenium Web Driver에 의한 간접적 처리도 고려해볼 수 있습니다. (NuGet 패키지: http://www.nuget.org/packages/Selenium.WebDriver/) PhantomJS와 Selenium Web Driver에 관한 이야기는 이 블로그 포스트 범위 밖에 있는 이야기이므로 존재에 대해서만 언급을 하는 것으로 갈무리하겠습니다.


 

System.Net.Http.HttpClient에 대한 경험담

System.Net.Http.HttpClient는 ASP.NET 웹 API의 REST API 호출을 위해서도 요긴하게 사용하지만, 다양한 상황에서 사용 가능한 HTTP 송수신을 담당하는데 특화된 클래스입니다. 최근에 이 클래스를 사용하여 개발한 소프트웨어 프로젝트에서 예기치 않은 문제가 하나 있었는데, TIME_WAIT 상태로 연결 대기가 유지되는 소켓 연결의 수가 급증하여 서버에서 실행 중인 다른 데이터베이스나 웹 서비스의 연결이 WSAENOBUF WinSock 오류 코드를 반환하며 거부되는 일이었습니다.


이 클래스는 기본적으로 IDisposable 인터페이스를 구현하고 있고, 원래의 의도는 사용하지 않을 때 적시에 제거하는 것이 올바른 방식이라고 생각하였습니다. 그러나 실제로 이 클래스를 Dispose() 메서드를 사용하여 소거한다고 하더라도 클라이언트 측 연결이 끊어지지 않고 TIME_WAIT 상태로 변경되는데, 이러한 상황에서 계속 HttpClient 인스턴스를 반복적으로 만들고 연결을 다시 시도하다보면, 접속하는 클라이언트 측의 잔여 TCP 포트의 수가 부족해지는 문제가 발생하게 됩니다.


이 문제를 해결하기 위하여 취한 방법은 해당 인스턴스를 싱글턴 인스턴스로 만들어 사용하는 것이었으며, 실제로 문제를 해결할 수 있었습니다. 하지만 남아있는 문제는 이 인스턴스를 Thread-Safe 인스턴스로 만들어야 하는 것이며 이를 위해서 보강해야 할 것이 있다고 보고 있습니다.

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

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