'Windows + .NET'에 해당되는 글 121건

  1. 2014.08.17 .NET에서의 String과 Null Character에 대한 이야기
  2. 2014.04.11 C#에서 HTML 문서 분석 때문에 고민하지 마세요 – HtmlAgilityPack
  3. 2014.04.05 System.Net.Http.HttpClient에 대한 경험담
  4. 2013.10.25 NUnit Runner를 대신하는 간편한 Self Runner 구현하기
  5. 2013.10.18 Practical Code Writing Tips in C#
  6. 2013.08.06 메서드 하나로 .NET 비동기 패턴 연습하기
  7. 2013.07.16 NuGet 패키지 관리자를 이용한 TDD 초기 환경 구축하기
  8. 2013.04.20 IronPython으로 Windows NT 서비스 만들어서 띄우기
  9. 2013.02.03 C#과 VB.NET의 using 구문을 간편하게 쓰는 방법
  10. 2013.02.01 Windows Forms MDI Best Practice
  11. 2013.01.02 AutoResetEvent? ManualResetEvent? 뭐가 다를까?
  12. 2012.09.15 Visual Studio 2012 Express 출시 (1)
  13. 2012.07.28 [Tip] MySQL의 password Hash 함수와 동일한 .NET 구현
  14. 2012.04.17 ASP.NET Universal Provider 소개
  15. 2012.02.26 Phalanger에서 기존 PHP 모듈 활성화하기
  16. 2012.02.10 Phalanger와 PHP의 차이점들
  17. 2012.02.08 PHP x C# x VB.NET = ASP.NET
  18. 2012.02.06 Phalanger와 WebMatrix의 완벽한 만남 (1)
  19. 2012.02.04 PHP와 .NET의 완벽한 만남 - Phalanger
  20. 2011.12.26 유용한 멀티 타기팅 팩: Portable Library Tools (1)
  21. 2010.11.25 C# 소수점 반올림 관련 보조 라이브러리
  22. 2010.11.01 디자인 타임 컬렉션을 관리하는 데에 어려움이 있으십니까?
  23. 2010.07.22 [Memo] Java의 >>> 연산자와 <<< 연산자를 C#으로 옮긴다면 이렇습니다.
  24. 2010.07.15 [필독] Visual Studio 2010과 Crystal Report
  25. 2010.06.25 .NET 4에서의 ISerializable 인터페이스에 대한 주의 사항
  26. 2010.05.29 .NET Framework 4에서 새로워진 핵심 기술들
  27. 2010.01.31 String.Format으로 할 수 있는 일들
  28. 2010.01.19 C# Image Viewer Control 샘플 (2)
  29. 2010.01.18 Sandcastle 관련 리소스 링크
  30. 2010.01.18 AppDomain 프로그래밍에 대한 이야기 (2)
Windows + .NET2014. 8. 17. 09:36

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

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

 

 


문자열은 값이 텍스트인 String 형식의 개체입니다. 내부적으로 텍스트는 Char 개체의 순차적 읽기 전용 컬렉션으로 저장됩니다. C# 문자열 끝에는 null 종결 문자가 없습니다. 따라서 C# 문자열은 포함된 null 문자(‘\0′)를 제한 없이 포함할 수 있습니다. 문자열의 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('\'', '\0');
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('\'', '\0').Trim();
Console.WriteLine("d: {0} (Length: {1})", d, d.Length);


 

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


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

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2014. 4. 11. 09:28

흔히 소프트웨어 개발 과정에서 나타나는 요구 사항들 중에서, 시간 투자 대비 효율이 가장 떨어지는 요구 사항으로 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("https://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에 관한 이야기는 이 블로그 포스트 범위 밖에 있는 이야기이므로 존재에 대해서만 언급을 하는 것으로 갈무리하겠습니다.

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2014. 4. 5. 09:27

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

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

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

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 10. 25. 23: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 Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 10. 18. 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 Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 8. 6. 12:43

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 Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 7. 16. 00:00

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

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

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

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

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

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

Visual Studio를 시작합니다.

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

 

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

 

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

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

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

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

 

 

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

 

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

 

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

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

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

 

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

 

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

 

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

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

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

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

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

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

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

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

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

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

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

 

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

결론

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

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


Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 4. 20. 00:54

안녕하세요. Windows Azure MVP 남정현입니다.

IronPython은 이제 다른 Python Implementation과 어깨를 나란히 할 수 있는 수준까지 완성도가 개선되었습니다. Python을 이용해서 흔히 기대하는 NumPy, SciPy 같은 수학 및 공학용 라이브러리는 당연히 쉽게 처리할 수 있고, 이를 기반으로 하는 NLTK (Natual Language Tool Kit) 라이브러리도 약간의 불편함이 따르기는 하나 종속성을 충족하면 자연어 처리도 원활하게 수행합니다.

그리고 당연하다면 당연한 이야기이지만 Windows NT 서비스를 IronPython으로 구현하는 것도 생각해볼 수 있습니다. Windows NT 서비스를 관리 언어를 이용하여 개발하는 것 자체는 그렇게 새로울 것이 없습니다만, 한 가지 많이 오해를 하는 부분이 있는데 Visual Studio가 제공하는 템플릿이 아니면 만들 방법이 없는 것 처럼 여겨지는 것 같습니다. 그러나 실제로는 전혀 그렇지 않으며, .NET Framework의 실행 모델을 처리할 수 있는 프로그래밍 언어는 모두 간편하게 System.ServiceProcess.dll 어셈블리가 제공하는 SCM 상호 작용 컴포넌트를 쉽게 이용할 수 있습니다.

그렇다면 IronPython을 이용해서 Windows NT 서비스를 개발하려면 어떻게 해야 할까요? 생각보다 단순합니다. 준비물은 이 글을 쓰는 현 시점에서 최신 버전인 IronPython 2.7 패키지와 관리자 권한만 있으면 됩니다. 만약 sc.exe 유틸리티가 없다면 이 유틸리티가 이번 강좌에서는 꼭 필요하므로 Windows SDK를 찾아보시기 바랍니다.

초간단 IronPython 기반 Windows NT 서비스 코드 살펴보기

글을 쓰는 것이 무안할 정도로 정말 초간단합니다.

import clr
clr.AddReference('System.ServiceProcess')
from System.ServiceProcess import ServiceBase

class MySvc(ServiceBase):
  def OnStart(self, args):
    ServiceBase.OnStart(self, args)
    print args
  def OnStop(self):
    ServiceBase.OnStop(self)

svc1 = MySvc()
ServiceBase.Run(svc1)

코드의 각각의 줄을 하나씩 살펴보도록 하겠습니다.

우선 처음의 세 줄은 CLR 모듈을 로드해서 System.ServiceProcess 어셈블리를 현재 IronPython의 스코프에서 사용할 수 있도록 준비하는 과정입니다. 관리되는 언어로 Windows NT 서비스를 등록하고 SCM과 상호 작용하기 위해서 필요한 모든 코드가 이 어셈블리에 들어있다고 보시면 되겠습니다.

그리고 Windows NT 서비스의 API를 객체 지향 방식으로 모델링한 ServiceBase 클래스를 상속받는 새로운 클래스를 하나 만들어야 합니다.

Python 문법에 익숙하지 않은 분들을 위하여 부연 설명을 더하면, 현재 스코프에 ServiceBase라는 클래스가 있고, 이 클래스를 상속받는 MySvc 클래스를 정의하고 있습니다. 그리고 따로 지시자는 없지만 이름이 같게 설정된 메서드는 Python 세계에서는 자동으로 메서드를 재정의한 것이 됩니다. 이에 따라, OnStart과 OnStop 메서드가 SCM에 의해서 자동으로 호출되는 메서드가 되는데, 서비스 시작 시 서비스가 잘 작동하고 있음을 알리기 위해 부모 클래스인 ServiceBase 클래스의 OnStart 메서드를, 그리고 프로세스 종료를 해도 괜찮음을 알리기 위해 OnStop 메서드를 호출합니다. 그리고 self라는 인자는 인스턴스 메서드임을 설명하는 것이며 동시에 여기에 인스턴스 참조가 전달됩니다.

그리고 따로 시작점이 있는 것이 아니라 Python 코드는 그 자체가 즉시 실행 가능한 Main 메서드 역할을 합니다. 그래서 곧바로 MySvc 클래스를 인스턴스로 만들어 ServiceBase.Run 메서드를 호출합니다.

SCM을 이용하지 않고 실행할 경우

ServiceBase.Run 메서드는 기본적으로 SCM과 상호 작용을 시작하기 위해서 필요한 기능들을 미리 제공합니다. 그런데 위의 코드를 일상적으로 사용하는 IronPython 콘솔에서 실행하면 어떻게 결과가 나타날까요? 아래와 같은 메시지가 나타납니다.

"명령줄 또는 디버거에서 서비스를 시작할 수 없습니다. 먼저 installutil.exe를 사용하여 Windows 서비스를 설치한 다음 서버 탐색기, Windows 서비스 관리 도구 또는 NET START 명령을 사용하여 시작해야 합니다."

이런 오류 메시지가 나타납니다. 그런데 제가 하고 싶은 이야기는 정말 installutil.exe를 사용해야만 관리 언어로 만든 NT 서비스를 등록할 수 있는가에 대한 부분입니다. 결론부터 말하면 "아니오"입니다. installutil.exe를 사용하여 NT 서비스를 설치하기 위해서는 위의 코드 말고도 사실 설치 관리자 컴포넌트를 따로 구현해야 하는데, 이것을 Visual Studio Professional 이상의 버전에서는 템플릿으로 제공하고 있고 설치 프로젝트와 연계해서 만들 수 있도록 해주는 것입니다. 그러나 개인적으로는 이러한 설정을 마음에 들지 않습니다. 좀 더 간단하게 갈 수 있는 방법도 많으니까요.

그러면 installutil.exe를 대신할 도구가 있을까요? 바로 sc.exe입니다. sc.exe 자체는 개발된지 오래된 유틸리티이지만, 관리 언어로 만든 Windows NT 서비스까지도 매우 유연하게 지원합니다. 바로 EXE 파일로 만들어지는 NT 서비스에 한해서 그 진가가 십분 발휘됩니다.

SC.EXE 유틸리티를 사용하여 NT 서비스 등록하기

이제 SC.EXE 유틸리티를 사용하여 NT 서비스를 등록해보겠습니다. 그러나 SC.EXE 유틸리티로 서비스를 등록하려면 시스템 관리자 권한이 필요하므로 명령 프롬프트를 관리자 권한 또는 권한 상승 모드에서 시작해주셔야 합니다. 만약 설치 프로그램에서 구동한다면 설치 프로그램 초입에 권한 상승이 동반되므로 따로 신경쓸 것은 없습니다.

SC.EXE 유틸리티의 문법은 사소한 부분에서 실수하기 쉬우므로 문자그대로 아래와 같이 정확하게 입력해야 함을 유의하기 바랍니다.

sc create <서비스 이름> binPath= "<ipyw64.exe의 절대 경로> <IronPython 스크립트 파일의 절대 경로>"

sc create mysvc binPath= "C:\Program Files\IronPython 2.7\ipyw64.exe c:\users\rkttu_000\desktop\service.py"

다른 것보다도 binPath= 부분에 유의합니다. binPath = "~" 도 아니고 binPath ="~"도 아니며 오로지 binPath= "~" 로 해야만 올바른 문법으로 인지됩니다. binPath 옵션에 IronPython Non-Console 인터프리터의 경로와 함께 실제 구현 코드를 포함하는 IronPython 스크립트 파일을 지정했습니다.

주의할 것은 여기에 서술하는 모든 경로는 절대 경로로 서술해야 합니다.

서비스 테스트하기

정상적으로 서비스가 설치되었다면 "[SC] CreateService 성공" 이라는 메시지를 볼 수 있습니다. 그리고 서비스의 시작을 위해서 아래와 같이 명령어를 실행하거나 서비스 관리자에서 여러분이 등록한 서비스를 찾아 시작시킵니다.

C:\Windows\system32>net start mysvc
mysvc 서비스를 시작합니다..
mysvc 서비스가 잘 시작되었습니다.

그리고 서비스 중지도 잘 되는지 확인합니다.

C:\Windows\system32>net stop mysvc
mysvc 서비스를 멈춥니다..
mysvc 서비스를 잘 멈추었습니다.

팁 한가지

IronPython을 이용하여 Windows NT 서비스를 만들 수 있으므로, 복잡한 템플릿에 의존하거나 유틸리티를 사용하는 일 없이 기존 서버 로직을 얼마든지 NT 서비스로 옮길 수 있는 것은 참 바람직합니다. 하지만 NT 서비스는 디버깅하기 매우 불리한 구조를 가지고 있는데, 이를 극복할 방법이 없을지 고민을 해보게 됩니다.

Microsoft의 기술 자료 (http://msdn.microsoft.com/ko-kr/library/7a50syb3(v=vs.90).aspx) 의 내용을 참고하여 직접 서비스를 디버깅하는 것이 널리 알려진 방법입니다. 이 방법은 정말 살아있는 실제 서비스를 대상으로 디버깅을 하는 것이므로 보안 권한을 포함한 모든 사항이 가장 실제 운영 환경에 근접한 것입니다.

그러나 좀 더 단순하게 서비스 로직 자체에 대한 디버깅이나 검증이 필요하다면 다른 접근 방법이 더 좋을 수 있습니다. NT 서비스로 만들기에 앞서 보통의 콘솔 응용프로그램으로 띄울 수 있도록 핵심 로직만 발췌해서 가지고 오도록 하면, NT 서비스로 배포하기에 앞서서 핵심 로직 자체만 따로 평가할 수 있으므로 더 유용할 것입니다.

그리고 프로그램에 전달된 인자를 활용하여 -service 같은 문자열이 전달된 것을 확인할 때에만 ServiceBase.Run 메서드를 호출한다면 NT 서비스가 아닌 상태와 NT 서비스인 상태를 동시에 지원할 수 있어 더욱 유용할 것입니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 2. 3. 21:28

C#과 VB.NET을 이용해서 IDisposable 객체를 다룰 경우, 할당과 해제를 짧은 기간 내에 완벽하게 관리하기 위해서 using 구문을 활용하는 일이 자주 있습니다. 그러나 using 구문의 경우 스코프를 정확하게 다루기 위해서 중괄호나 구문 구조 상 Indent를 하게 되어있습니다. 쓰는 것 자체는 일은 아니지만 using 문이 여러번 사용되다보면 코드에 지나치게 많은 Indent가 쓰이게되서 보기 어려운 코드가 될 수 있습니다. 이를 해결하는 방법을 소개합니다.

Note: 아래에서 소개하는 예제 형식들은 IDisposable 인터페이스를 구현하는 임의의 객체라고 가정하겠습니다.

C#에서 using 구문을 간단하게 사용하는 방법

using (Sample a = new Sample())
{
    using (Sample b = new Sample())
    {
        using (Sample c = new Sample())
        {
            // Sample code goes here...
        }
    }
}

위와 같은 코드가 있습니다. 변수 세 개를 선언했을 뿐이지만 중괄호가 세 번이나 열리고 닫힙니다. 당연히 보기 좋을리 없습니다. 어떻게 정리하면 좋을까요?

using (Sample a = new Sample())
using (Sample b = new Sample())
using (Sample c = new Sample())
{
    // Sample code goes here...
}

앞의 코드보다 훨씬 간결해졌습니다. 연달아오는 세 개의 using 문 안에 선언된 변수는 같은 Scope로 분류되어 뒤이어 오는 중괄호 섹션 한 번 안에 한꺼번에 변수의 수명 주기가 관리됩니다.

그리고 또 다른 방법이 있습니다. 만약, 다루어야 될 IDisposable 객체가 모두 같은 형식이거나 상속 관계 상 활용 가능한 수준의 공통의 부모 클래스나 인터페이스를 가지는 경우 아래와 같은 기술법도 가능합니다.

using (Sample
    a = new Sample(),
    b = new Sample(),
    c = new Sample())
{
    // Sample code goes here...
}

위의 예제는 특히 System.IO.Stream 클래스를 기반으로 할 경우 매우 유용합니다. 입출력 작업에 관련된 기본적인 동작이 이미 System.IO.Stream 클래스에서 구현되어있으므로 특수한 경우를 제외하면 위의 구문을 이용하여 간편하게 코드를 정리할 수 있기 때문입니다.

VB.NET에서 Using 구문을 간단하게 사용하는 방법

C#과 마찬가지로 VB.NET 또한 Using 구문을 간단하게 정리할 방법이 있습니다. 아래와 같은 코드가 있다고 가정하겠습니다.

Using A As Sample = New Sample()
    Using B As Sample = New Sample()
        Using C As Sample = New Sample()
            ' Sample code goes here...
        End Using
    End Using
End Using

Indent가 세 번이나 들어갑니다. 거기에다 C#처럼 같은 줄에 Using 구문을 몰아서 쓸 수 있는 것도 아닌것 같습니다. 방법이 있을까요? 아래와 같이 정리하면 됩니다.

Using A As Sample = New Sample(), _
    B As Sample = New Sample(), _
    C As Sample = New Sample()
    ' Sample code goes here...
End Using

Comma 기호를 이용하여 서로 다른 여러 IDisposable 구현 객체를 한 번에 초기화할 수 있습니다. 여기서 주의하실 것은, C#과 달리 VB.NET은 개행 문자에 대해 이스케이프 처리가 필요하며, 언더스코어 (_) 기호 앞에는 하나 이상의 Whitespace (0x20) 문자가 들어와야하고, 언더스코어 기호 뒤에는 다른 문자를 절대 쓰지 말고 바로 Enter 키를 눌러 개행 문자를 지정하도록 해야 올바르게 줄 이어쓰기로 해석됩니다.

정적 메서드를 이용하여 초기화 과정을 캡슐화하기

위와 같은 구문을 어디에서나 사용할 수 있다면 편리하겠지만, 불가피하게 생성자 이외에 부수적인 초기화 작업이 따르는 경우도 많습니다. 대표적으로 ADO.NET이 그러한 예인데, 여러분만의 독자적인 Helper Library Class를 만들어서 호출하는 방법으로 객체를 생성한다면 위의 코드를 응용할 수 있으므로 여러 IDisposable 인터페이스 구현체를 같은 스코프 안에서 한번에 할당하고 해제하는 구성을 시도할 수 있습니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 2. 1. 14:44

안녕하세요. Windows Azure MVP 남정현입니다.

Windows 8 Metro Style App에 대한 이야기가 요즈음 많이 회자되고 있습니다만, 그럼에도 불구하고 전문적인 도구나 프로그램에서의 Windows Forms에 대한 수요는 끊이지 않습니다. 오늘 살펴보려고 하는 내용은 Windows Forms 환경에서 MDI를 구현할 때 어떻게 시작을 하는 것이 가장 바람직한 모양을 만들 수 있는지 간단하게 정리한 예제 코드와 함께 살펴볼까 합니다.

예제 다운로드: https://skydrive.live.com/redir?resid=318484C5AAD6B73D!2684&authkey=!AO5fx9Fp4Xr0fog

둘러보기

아래 세 장의 스크린 샷과 같은 기능을 제공하는 MDI UI를 제공하는 Desktop App이 가장 이상적일 것입니다. 이 예제는 처음 시작하자마자 몇 개의 창이 자식 창으로 등록된 상태에서 시작됩니다.

자식 창을 전체 화면으로 바꾸었을 때, 아래와 같이 MenuStrip 제일 앞에 자식 창의 아이콘이, 제일 뒤에 최소화, 최대화, 닫기 버튼이 자동으로 병합됩니다.

창 메뉴를 클릭하면 현재 이 창 안에 등록된 모든 자식 창들이 열거되고, 선택 상태가 자동으로 관리됨은 물론, 메뉴를 선택하면 창 전환까지 가능합니다.

그리고 왼쪽편에는 트리 뷰 컨트롤을 도킹시켜 MDI 창과 조화롭게 레이아웃이 구성됩니다. 보이지는 않지만, 트리뷰 바로 다음에는 Splitter 컨트롤을 도킹시켜 창의 크기도 자유자재로 조절이 가능하게 만들었습니다. 제일 하단에는 상태 표시줄까지 추가하였습니다.

깔끔한 구성이지만 사실 이렇게 만드는 것이 기본 제공되는 사항만으로는 알아내기 다소 어려운 점이 있습니다. 위와 같이 만들기 위해서 무엇을 어떻게 해야하는지 단계별로 살펴보도록 하겠습니다.

MDI 부모로 창을 만들기

MDI는 부모 창과 자식 창으로 구성되며 부모 창 1개에 자식 창 여러개가 들어갈 수 있습니다. 이렇게 하기 위해서 부모 창으로 사용할 Form을 열고 디자인 타임에서 아래와 같이 설정하거나 IsMdiContainer 속성을 코드에서 적당한 시점 (생성자나 Load 이벤트 실행 시점)에서 True를 설정합니다.

위와 같이 설정하면 디자인 타임이나 런타임 상의 폼 화면의 클라이언트 영역이 회색 배경에 움푹 파인 모양으로 바뀌는 것을 볼 수 있습니다.

MenuStrip, ToolStrip, StatusStrip 붙이기

MDI 인터페이스에서 일상적으로 쓰이는 메뉴, 도구 모음, 상태 표시줄을 차례대로 가져다 높습니다. 아래 스크린 샷에서 빨간색 사각형으로 강조한 세 가지 항목을 폼에 가져다 놓습니다.

폼에 다른 메뉴 스트립 컨트롤들이 없다면, 폼의 속성 창을 다시 살펴보았을 때 아래와 같이 자동으로 MainMenuStrip에 방금 추가한 메뉴가 포함됩니다. MDI에 관련된 다른 여러 가지 설정들은 지금 이 속성에 연결된 컨트롤 앞으로 전달되거나 응용 동작에 연결되므로 이 설정이 중요합니다.

이렇게 해서 MDI 부모 창의 기본 구성이 완료되었습니다.

자식 창 관리 기능을 만들기

방금 추가한 메뉴 스트립 컨트롤에 MDI 창의 목록을 자동으로 보이고 현재 활성화된 MDI 창 항목을 강조 표시하며 메뉴 항목을 클릭하면 자동으로 창이 앞에 나타나는 기능을 구현해보도록 하겠습니다. 직접 코드를 작성해도 상관은 없지만, 빠르고 간편하게 완성도 높은 기능을 구현하기 위함이라면 지금 소개하는 방법이 가장 편리할 것입니다.

앞서 추가한 메뉴 스트립 컨트롤을 선택하고 아래 그림과 같이 MDI 목록을 표시할 대 메뉴 항목을 하나 추가합니다.

이번엔 메뉴 스트립 컨트롤을 선택한 상태에서 속성을 살펴봅니다. 속성 중에 MdiWindowListItem 속성이 비어있는 상태로 되어있을 것인데, 이것을 방금 추가한 대 메뉴 항목의 것으로 바꿔넣습니다.

좀 더 기능을 넣을 수 있는데, 이렇게 설정된 창 메뉴에 여러분이 임의로 새로운 메뉴를 더 넣을 수 있습니다. 개발자가 추가한 메뉴보다 뒤에 이러한 기능들이 오도록 구현되므로, 계단식 정렬이나 그리드 정렬 등의 창 관리 기능, 창 모두 닫기, 현재 활성화된 창 이외에 전부 닫기 같은 기능을 마음껏 구현하실 수 있습니다. 그리고 이러한 커스텀 메뉴들 제일 끝에 구분선 메뉴를 하나 더 넣어주시면 깔끔하게 구현됩니다.

참고로, 위의 예제와 같이 자동으로 메인 메뉴 항목을 완성하기를 원한다면 메뉴 스트립 컨트롤을 디자인 타임에서 오른쪽 버튼으로 클릭하고, 나타나는 메뉴 중에서 표준 항목 삽입이라는 메뉴를 아래 그림과 같이 클릭하면 간편하게 Mock Up UI 구성을 위한 항목이 자동으로 채워집니다. 표준 Windows UI 가이드 라인에 따라 필요한 항목들을 얼추 완성해주므로 이걸 활용하면 작업이 좀 더 빨라질 수 있습니다.

메인 화면의 상/하/좌/우 활용하기

Windows 탐색기, Microsoft Office 등 우리가 잘 아는 소프트웨어들은 공통적으로 이러한 MDI 화면의 상/하/좌/우에 적당한 패널을 배치하여 사용자의 작업을 돕는 대시보드를 넣어 프로그램을 돋보이게 만듭니다. 다행스럽게도 지금 만드는 MDI 부모 창도 이러한 구성이 가능합니다. 이 예제에서는 트리 뷰를 배치하려고 합니다. 트리뷰를 부모 창의 아무 곳에서 가져다 놓고, 컨트롤을 선택한 다음 아래와 같이 Dock 속성을 원하는 위치로 설정합니다. 예제에서는 Left로 설정했습니다.

적당한 크기를 Size 속성이나 디자인 타임에서 시각적으로 설정한 다음, Splitter (SplitContainer가 아닙니다.)를 추가하고 Splitter의 Dock 속성도 위와 동일한 값으로 설정합니다. 이렇게 하면 크기를 조절하려고 하는 컨트롤이 먼저 Stack에 올라가고, 그 다음에 크기 조절을 위한 Splitter 컨트롤이 올라가는 모양이 됩니다.

자식 창을 만들고 추가하기

이제 자식 창을 만들고 추가하는 코드를 넣을 차례입니다. 자식 창의 경우에는 임의로 새로운 폼을 몇 종류 만듭니다. 다른 어떠한 설정도 필요 없으며, 구분을 위해서 간단하게 라벨을 추가하여 서로 구분될 수 있게만 꾸며주기 바랍니다. 이 과정을 거쳐 예제에서는 ChildForm1, ChildForm2, ChildForm3라는 세 개의 클래스를 프로젝트에 아래 그림과 같이 추가하였습니다.

그 후, Parent Form의 디자인 타임 영역 상의 제목 표시줄을 더블 클릭하면 Load 이벤트에 대한 이벤트 처리기가 자동으로 만들어집니다. 그리고 아래와 같이 코드를 작성합니다.

C#

    private void MainForm_Load(object sender, EventArgs e) {
        new ChildForm1() { MdiParent = this, Visible = true };
        new ChildForm2() { MdiParent = this, Visible = true };
        new ChildForm3() { MdiParent = this, Visible = true };
    }

VB.NET

    Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Dim Window1 As New ChildForm1()
        Window1.MdiParent = Me
        Window1.Visible = True

        Dim Window2 As New ChildForm2()
        Window2.MdiParent = Me
        Window2.Visible = True

        Dim Window3 As New ChildForm3()
        Window3.MdiParent = Me
        Window3.Visible = True
    End Sub

그리고 앞서 추가한 ToolStrip 컨트롤에 버튼을 세 개 추가하고 각각 세 종류의 창을 띄우도록 위의 코드를 응용하여 만들어 봅니다.

자식 창을 최대화했을 때의 문제점

이제 거의 다 되었습니다. 프로그램을 실행해보고 이것저것 테스트해보면 잘 됩니다. 그런데 한 가지 눈에 거슬리는 점이 보이네요. 자식 창을 최대화했더니 아래처럼 메뉴 스트립과 자식 창의 아이콘이 서로 다른 줄에 그려집니다. 왜 그럴까요?

이 문제를 해결하기 위해 엄청나게 복잡한 코드를 쓰는 경우가 있습니다. 자식 창이 최대화 되면 자식 창의 ShowIcon 속성을 끈다거나 제목 표시줄을 없앤다거나 최대화를 흉내내도록 상태를 바꾸지 않고 창 크기만 업데이트한다던가 여러가지가 있지요. 그러나 이런 방법들은 거의 열 중 아홉 이상이 사소한 문제들을 많이 일으킵니다.

위의 문제를 해결하기 위해서는 아래 사항을 확인해야 합니다.

  • 앞서 이야기한 폼의 속성 중 MainMenuStrip 속성이 정확히 메인 메뉴를 가리키고 있는지 확인해야 합니다.
  • 필요한 경우 MainMenuStrip의 RenderMode 속성을 ManagedRenderMode가 아닌 System이나 Professional로 변경해 봅니다.

올바르게 적용되었다면 앞의 그림과 같은 단순하지만 튼튼한 토대를 갖춘 MDI UI를 가진 프로그램이 완성됩니다.

사족

이러한 특성을 극복할 수 없어서 다시 Windows Forms 초기에 제공되던 MainMenu 같은 컨트롤로 롤백하시는 경우도 종종 있을 수 있습니다. 그러나 최신 버전의 .NET Framework로 업그레이드하거나 개발 도구를 변경하게 될 경우에는 가능한 스트립 계열 컨트롤로 업그레이드하시는 것이 좋습니다. 지금 소개한 아티클을 참고하여 다시 시도해보시면 좋은 결과가 있을 것입니다. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2013. 1. 2. 14:44

안녕하세요. Windows Azure MVP 남정현입니다. 이번에 살펴볼 내용은 비동기 프로그래밍에서 중요한 컨셉 중 하나인 스레드 동기화에 약방의 감초처럼 쓰이는 AutoResetEvent와 ManualResetEvent에 대해 간단한 포스팅을 준비해보았습니다. 알아두시면 유용하게 활용할 수 있을 것입니다.

StackOverflow에 있는 글을 읽어보다 재미있는 내용이 있어서 Facebook에도 포스팅을 했었습니다.

정말 알기쉽고 명료하게 비유한 글이군요. ManualResetEvent는 수동문이고 AutoResetEvent는 지하철 개찰구 내지는 자동문과 같지요. :-)

Yes. It's like the difference between a tollbooth and a door. The ManualResetEvent is the door, which needs to be closed (reset). The AutoResetEvent is a tollbooth, allowing one car to go by and automatically closing before the next one can get through.

출처: http://stackoverflow.com/questions/153877/what-is-the-difference-between-manualresetevent-and-autoresetevent-in-net

그런데 정말로 그런지 알아보고 싶어서 샘플 코드를 멋대로 만들어보았습니다. 다중 스레드나 이런게 아니라 그냥 단일 스레드 상에서도 차이점이 나타나는지 알아보고 싶었는데, 확실히 특징이 보였습니다. 아래의 샘플 코드가 그렇습니다.

키보드의 M키를 눌러서 시작하면 ManualResetEvent를 사용하여 로직을 실행하는데, 이러한 경우 처음에 Set Flag가 켜진 상태 (Set 호출)에서 WaitOne 메서드를 4번 호출하게 되는데 Flag가 켜진 상태이므로 대기하지 않고 매번 호출이 빠져 나가게 됩니다. 그러나 Reset을 호출하여 Set Flag를 끈 상태에서는 WaitOne 메서드에 지정한 대기 시간만큼 실제로 대기하게됩니다.

반면 AutoResetEvent는 Set을 처음 한번 호출했을 때만 WaitOne 메서드가 무시되고 그다음부터는 계속 WaitOne 메서드에 지정한 대기 시간만큼 실제로 대기하게 됩니다. 앞서 설명했던 것 처럼 Set 메서드를 부르고 나서 이후에 벌어지는 일에 차이가 있는 셈입니다.

AutoResetEvent의 Auto란 즉, 자동으로 Reset을 호출한다는 의미이므로 비유에서처럼 톨 부스, 지하철 개찰구같은 아날로지에 대응이 가능한 것이고, ManualResetEvent의 Manual이란 방문을 열어놓고 직접 밀어서 닫지 않는 한 문이 계속 열려있는 상태의 아날로지에 대응이 가능한 것입니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2012. 9. 15. 20:37

안녕하세요. Windows Azure MVP 남정현입니다. 오늘은 Visual Studio 2012 출시와 더불어서 Express Edition의 업그레이드에 대한 이야기를 전해드리려고 합니다.

개인적으로 매우 좋아하고 아끼는 Visual Studio 제품 라인 업 중에 Express Edition이 있습니다. 무료로 제공되는 개발 도구임에도 기능에서나 활용 면에서 부족함이 전혀 없고, 제 스스로에게 있어서 작업 시간을 줄여주고 시행 착오를 최소화하는데에 지대한 공헌을 하는 멋진 개발 툴입니다.

이번 2012 라인 업에서는 아래와 같이 구성이 변경되었습니다.

  • Visual Studio 2012 Express for Web: 기존의 Visual Web Developer 2010을 이어서 업그레이드된 버전으로 ASP.NET 4.5와 Windows Azure 최신 개발 툴킷, Silverlight 개발 환경등을 포함하고 있습니다.
  • Visual Studio 2012 Express for Desktop: 기존의 Visual C# Express 2010, Visual Basic .NET Express 2010, Visual C++ Express 2010을 통합하여 데스크탑 응용프로그램 개발에 최적화된 버전으로 업그레이드되었습니다.
  • Visual Studio 2012 Express for Windows 8: Windows 8에서 새로 추가된 Windows Store 앱을 만들기 위하여 필요한 개발 도구로 동시에 Expression Blend for Windows 8이 같이 설치됩니다.
  • Visual Studio Team Foundation Server 2012 Express: 고가의 상용 버전 제품으로만 알려져있었던 Team Foundation Server도 Express 버전을 새로 출시하게 되었습니다. 소규모 개발 팀을 운영 중인 경우 한 번즈음 고려해볼 수 있는 형상 관리, 작업 관리, 빌드 자동화를 제공합니다.

언어 별로 나누어져 있었기 때문에 장점이자 단점으로 동시에 작용하던 작고 가벼움은 아쉽게도 더 이상 존재하지는 않습니다. 그렇지만 Express Edition 특유의 한계를 극복하고 더 강력한 기능을 더하고 단순히 학습용이 아닌 소규모 개발 팀을 위한 배려를 모두 포함하고 있다는 것은 매우 기쁜 일입니다.

그리고 실제로 Visual Studio 2012 전체 버전을 구입하시기 전에 제품의 표면적인 기능만을 제한된 시간 내에 평가해야 하는 트라이얼 버전 대신, 실제 업무에도 자연스럽게 반영해볼 수 있는 Express Edition을 통해서 충분한 시간을 가지면서 활용하시면 좀 더 좋은 부분을 많이 보실 수 있을 것이라고 기대합니다.

Web과 Desktop 버전은 Windows 운영 체제 버전과 관계없이 사용이 가능하며, Windows 8 버전의 경우에는 실제로 Windows 8 운영 체제 위에서 실행되어야만 개발 환경 구축이 가능합니다. 그리고 이전과 마찬가지로, 무료로 사용이 가능하고 상용으로도 활용이 가능하지만 설치 후 기간 내에 Microsoft 등록 페이지에 가서 제품에 대한 25자리 등록 번호를 받아서 등록해야 하는 절차는 변함 없습니다.

2012 Express Edition과 2010 Express Edition을 다운로드하시려면 http://www.microsoft.com/visualstudio/kor/downloads 페이지로 가시면 됩니다. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

  1. 현재 네이버 메인에 나오고 있습니다.

    2013.01.28 15:38 신고 [ ADDR : EDIT/ DEL : REPLY ]

Windows + .NET2012. 7. 28. 00:25

[2014.03.18 Update] XpressEngine 1.5 버전에서 사용하는 구형 패스워드 해시 함수에 대한 내용을 업데이트하였습니다.

asp.net 기반의 프로젝트를 진행하면서 PHP나 MySQL 기반의 데이터베이스와 상호작용해야 하는 상황이 자주 있는데, MySQL의 password 함수를 사용하여 만든 Hash 기반의 Opaque Single Way 암호화 토큰을 비교해야 하는 때가 있습니다. SHA1을 사용하여 만드는 해시 결과 값이지만 명확한 결과와 구현 과정을 이해하기에는 쉽지 않아 같은 기능을 .NET으로 구현하는데에는 약간의 노력이 필요합니다. 정보 공유를 위해서, 그리고 개인적인 편의를 위해서 블로그 글로 간단히 팁을 올려봅니다.

출처: http://stackoverflow.com/questions/868482/simulating-mysqls-password-encryption-using-net-or-ms-sql

using System;
using System.Security.Cryptography;
using System.Text;

// 중략

public string CalculateHash(string key)
{
    byte[] keyArray = Encoding.UTF8.GetBytes(key);

    using (SHA1Managed enc = new SHA1Managed())
    {
        byte[] encodedKey = enc.ComputeHash(enc.ComputeHash(keyArray));
        StringBuilder myBuilder = new StringBuilder(encodedKey.Length);

        for (int i = 0; i < encodedKey.Length; i++)
            myBuilder.Append(encodedKey[i].ToString("X2"));

        return String.Concat("*", myBuilder.ToString());
    }
}

MySQL의 새 password API는 간단히 요약하면 .NET의 SHA1Managed와 거의 동일하며, 앞에 Asterisk를 붙이는 Behavior는 독자적인 것입니다. 이 기능을 사용하여 XpressEngine이나 다른 MySQL 기반 응용프로그램과 안전하게 크레덴셜을 공유하고 Single Membership 체제를 유지할 수 있습니다.

[2014.03.18 Update]

만약 구 버전의 XE 1.5의 Password Hash 함수를 필요로 하는 경우 다음의 코드를 대신 사용합니다.

public static string CalculateHash(string key)

        {

            byte[] keyArray = Encoding.UTF8.GetBytes(key);


            using (HashAlgorithm enc = new MD5Cng())

            {

                byte[] encodedKey = enc.ComputeHash(keyArray);

                StringBuilder myBuilder = new StringBuilder(encodedKey.Length);


                for (int i = 0; i < encodedKey.Length; i++)

                    myBuilder.Append(encodedKey[i].ToString("x2"));


                return String.Concat(myBuilder.ToString());

            }

        }

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2012. 4. 17. 11:55

ASP.NET 환경에서 사용자 멤버십, 프로파일, 역할과 세션 상태 관련 기능은 별도의 공급자 클래스에 의하여 구현되는 경우가 많은데, 이러한 공급자 클래스들은 개발자 스스로 원한다면 별도로 정의하여 각자의 서버 및 네트워크 인프라에 맞추어 다시 작성할 수 있습니다. 하지만 이렇게 작업하기에는 녹록치 않은 면들이 많고, 또 실제로 많은 테스트와 검증 과정이 뒷받침되어야 할 필요가 있습니다.

최근에는 SQL Server Compact Edition이나 SQL Server 2012에서 소개된 SqlExpress 또는 SQL Azure와 같이 전통적인 SQL Server 환경이 아닌 곳을 기반으로 택해야 하는 일도 자주 있습니다. 이러한 경우 많은 시행 착오와 오류를 경험할 수 밖에 없는데, 이러한 문제를 크게 덜어줄 유용한 기술이 하나 있습니다. 바로 ASP.NET Universal Provider이며, 향후 Microsoft가 언급하는 Hybrid Cloud Computing 환경에서의 단일 프로그래밍 모델을 구현하기 위한 초석으로 자리매김할 것으로 예상됩니다. :-)

ASP.NET Universal Provider의 구성

ASP.NET Universal Provider는 다음과 같은 구성을 가지고 있습니다.

  • System.Web.Providers.DefaultMembershipProvider
    (기존의 System.Web.Security.SqlMembershipProvider에 대응)
  • System.Web.Providers.DefaultProfileProvider
    (기존의 System.Web.Profile.SqlProfileProvider에 대응)
  • System.Web.Providers.DefaultRoleProvider
    (기존의 System.Web.Security.SqlRoleProvider에 대응)
  • System.Web.Providers.DefaultSessionStateProvider
    (내장된 세션 상태 관리 공급자를 대체)

ASP.NET Universal Provider를 Visual Studio의 확장 패키지 갤러리 (NuGet 갤러리)를 통하여 설치하게되면 위의 각 공급자에 대한 설정을 현재 ASP.NET 프로젝트 상의 web.config 파일에 지정하게되고, 데이터 소스를 어떻게 선택하는지에 따라서 미리 구성된 데이터 스키마에 맞추어 관련된 서비스 기능을 수행할 수 있도록 맞추게 됩니다.

ASP.NET Universal Provider 설치하기

ASP.NET Universal Provider는 Nuget Package Install Site에서 손쉽게 asp.net 프로젝트에 추가할 수 있습니다. Visual Studio나 Visual Web Developer를 설치한 경우 Nuget Console에서 아래 패키지 이름을 검색하여 기존 프로젝트에 추가하시면 됩니다. 

패키지 설치가 끝나면 web.config 파일에 아래와 같이 Membership, Role, Profile, Session State에 대한 설정을 Universal Provider로 업데이트합니다.

<configuration>
    <connectionStrings>
        <add name="DefaultConnection" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\aspnet.mdf;Initial Catalog=aspnet;Integrated Security=True;User Instance=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient"/>
    </connectionStrings>
    <system.web>
      <profile defaultProvider="DefaultProfileProvider" >
        <providers>
          <add name="DefaultProfileProvider" type="System.Web.Providers.DefaultProfileProvider" connectionStringName="DefaultConnection" applicationName="/"/>
        </providers>
      </profile>
      <membership defaultProvider="DefaultMembershipProvider">
        <providers>
           <add name="DefaultMembershipProvider" type="System.Web.Providers.DefaultMembershipProvider" connectionStringName="DefaultConnection"
             enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
             maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
             applicationName="/" />
        </providers>
      </membership>
      <roleManager defaultProvider="DefaultRoleProvider">
        <providers>
           <add name="DefaultRoleProvider" type="System.Web.Providers.DefaultRoleProvider" connectionStringName="DefaultConnection" applicationName="/" />
        </providers>
      </roleManager>
      <sessionState mode="Custom" customProvider="DefaultSessionProvider">
        <providers>
          <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider" connectionStringName="DefaultConnection" applicationName="/"/>
        </providers>
      </sessionState>
    </system.web>

위의 설정에서 DefaultConnection에 적절한 연결 문자열과 함께 정확한 providerName을 기재하면 Universal Provider가 정확한 ADO.NET Connection Driver를 사용하여 필요한 서비스들을 제공하게 됩니다. 이렇게 만듦으로서 SQL Azure는 물론 기존 SQL Server, SQL Server CE를 데이터 원본으로 선택할 수도 있습니다.

좀 더 자세한 정보 알아보기

패키지의 최신 업데이트 정보와 설치 방법은 아래 웹 사이트에서 확인할 것을 권합니다.

http://nuget.org/packages/System.Web.Providers/1.0.1

구체적인 사용 방법과 시나리오에 대해서는 Scott Hanselman의 블로그 아티클을 참고하시기 바랍니다.

http://www.hanselman.com/blog/IntroducingSystemWebProvidersASPNETUniversalProvidersForSessionMembershipRolesAndUserProfileOnSQLCompactAndSQLAzure.aspx

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2012. 2. 26. 01:00

PHP 4.x와 PHP 5.x에서 널리 사용되던 모듈 중에는 유용한 것들이 많이 있습니다. 이미지 처리, 데이터베이스 연동, 암호화, MIME TYPE 관련 연동 등 종류도 매우 많으며, 실질적인 웹 기술에 직접 영향을 끼치는 모듈들도 많이 있어서 웹 관련 개발에 큰 생산성 향상을 가져다주기도 합니다.

Phalanger가 문법적으로는 PHP와 상당한 호환성을 보여주지만 결정적으로 궁금한 것은 과연 기존에 사용하던 Native PHP 모듈을 모두 사용할 수 있는가에 대한 부분일 것입니다. 얼핏 보기에는 Phalanger가 그저 호환성 보존 차원에서 API 계층만 제공하는게 아니냐는 생각이 들 수 있는데 사실 Phalanger의 절반을 이루는 것은 바로 이 상호운용성에 대한 내용입니다.

이번 아티클에서는 Phalanger가 지원하는 대표적인 Native PHP Module들을 활성화하는 방법을 소개해보겠습니다. 이 방법을 사용하면 기존의 php.ini와는 달리 웹 서버 전역이 아닌 특정 웹 응용프로그램 풀 안에서만 적용되는 모듈 구성이 가능합니다.

Custom Configuration Hanlder 등록하기

ASP.NET 위에서 호스팅되는 모든 Web App들은 web.config이라고 하는 사전에 약속된 설정 파일을 사용하여 모든 구성을 지정할 수 있으며 이는 Apache에서 사용하는 httpd.conf와 같은 성격의 파일이기도 하지만, 동시에 완벽한 XML을 사용하기 때문에 다양한 관리 도구와 충돌 없이 상호작용할 수 있습니다.

우선 <configuration> 요소 아래의 제일 첫 번째 노드로 와야 하는 내용이 있습니다. 

<configSections>
...
    <section name="phpNet" type="PHP.Core.ConfigurationSectionHandler, PhpNetCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=0a8e8c4c76728c71" />
...
</configSections>

바로 <configSections> 요소인데, 이 요소 아래에 <phpNet> 요소에 대한 처리기를 위와 같이 지정해야 합니다. 이렇게 설정하여 각 웹 사이트 별로 독립적인 PHP 설정을 가지게할 수 있습니다.

<phpNet> 요소 설정하기

아래의 XML 코드 조각은 보통의 php.ini 파일에서 활용할 수 있는 거의 대부분의 Native Module들을 열거한 것입니다. 아래와 같이 지정하였을 때 실제로 해당 Module들을 .NET Runtime 위로 불러오게 됩니다.

<phpNet>
    <classLibrary>
        <add assembly="System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
        <add assembly="System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
  <add assembly="PhpNetClassLibrary, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4af37afe3cde05fb" section="bcl" />
  <add assembly="PhpNetMsSql, Version=3.0.0.0, Culture=neutral, PublicKeyToken=2771987119c16a03" section="mssql" />
  <add assembly="php_bcmath.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="bcmath" />
  <add assembly="php_com.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="com" />
  <add assembly="php_image.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="image" />
  <add assembly="php_zlib.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="zlib" />
  <add assembly="php_odbc.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="odbc" />
  <add assembly="php_ftp.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="ftp" />
  <add assembly="php_calendar.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="calendar" />
  <add assembly="php_bz2.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="bz2" />
  <add assembly="php_cpdf.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="cpdf" />
  <add assembly="php_crack.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="crack" />
  <add assembly="php_curl.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="curl" />
  <add assembly="php_db.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="db" />
  <add assembly="php_dba.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="dba" />
  <add assembly="php_dbase.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="dbase" />
  <add assembly="php_dbx.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="dbx" />
  <add assembly="php_domxml.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="domxml" />
  <add assembly="php_exif.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="exif" />
  <add assembly="php_filepro.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="filepro" />
  <add assembly="php_gd2.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="gd2" />
  <add assembly="php_gettext.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="gettext" />
  <add assembly="php_hyperwave.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="hyperwave" />
  <add assembly="php_iconv.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="iconv" />
  <add assembly="php_imap.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="imap" />
  <add assembly="php_java.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="java" />
  <add assembly="php_ldap.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="ldap" />
  <add assembly="php_mbstring.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="mbstring" />
  <add assembly="php_mcrypt.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="mcrypt" />
  <add assembly="php_mhash.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="mhash" />
  <add assembly="php_mime_magic.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="mime-magic" />
  <add assembly="php_ming.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="ming" />
  <add assembly="php_msql.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="msql" />
        <add assembly="php_mysql.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="mysql" />
  <add assembly="php_openssl.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="openssl" />
  <add assembly="php_pdf.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="pdf" />
  <add assembly="php_pgsql.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="pgsql" />
  <add assembly="php_shmop.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="shmop" />
  <add assembly="php_snmp.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="snmp" />
  <add assembly="php_sockets.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="sockets" />
  <add assembly="php_xml.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="xml" />
  <add assembly="php_xmlrpc.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="xmlrpc" />
  <add assembly="php_xslt.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="xslt" />
  <add assembly="php_yaz.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="yaz" />
  <add assembly="php_zip.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="zip" />
  <add assembly="php_big_int.mng, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4ef6ed87c53048a3" section="big_int" />
 </classLibrary>
</phpNet>

gd2, xml, zip과 같이 널리 사용하는 모듈들을 불러오고 있습니다만 조금 독특한 부분이 있습니다. Native DLL들을 사용하기로 하였지만 DLL에 대한 시그니처는 닷넷 프레임워크에서 사용하는 정규화된 Full Name을 사용하고 있는데, 사실 Phalanger는 Native DLL들을 as-is로 사용하기보다는 별도의 Managed Wrapper를 만들어서 사용하기 때문에 이와 같은 형태가 됩니다. Phalanger 3.0에 들어와서 64비트를 적극적으로 지원할 수 있게되었다고 이야기할 수 있는 것은 이와 같은 Native DLL에 대한 Wrapper를 제공하기 때문입니다.

더 나아가기

Phalanger는 이제 상당한 수준의 웹 개발 프레임워크로 자리 잡았습니다. 그렇지만 이것을 어떻게 활용할 것인가에 대한 고민은 아직 끝나지 않았고, 또한 ASP.NET MVC와의 연계나 기존 PHP 코드와의 호환성 개선 등 서로 다른 양쪽의 환경을 자연스럽게 이어줄 수 있는 다리 역할을 얼마나 잘 할 수 있을지도 사실 아직은 시험대 위에 놓여있는 상태입니다. 그렇지만 무궁무진한 발전이 기대되는 프레임워크인 것은 사실인듯 합니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2012. 2. 10. 01:30

지난 아티클에서는 Phalanger와 PHP 사이에 차이점들이 있다고 말씀드렸습니다. 구체적으로 어떤 차이점들이 있을까요? 여러 프로그래밍 언어를 지원한다는 사실이 가장 큰 차이점이고 닷넷 기반 위에서 실행된다는 것이 구분되는 점이겠지만 이런 점을 차치하고 PHP 관점에서 차이점을 살펴본다면 지금 이야기하려는 토픽들에 대한 이야기가 빠질 수 없을 것입니다.

App_Code 폴더의 사용

Phalanger가 ASP.NET을 기반으로 하고 있기 때문에 자동으로 이어받는 특성으로, App_Code 폴더의 사용에 관한 부분이 있습니다. ASP.NET에서는 App_Code 폴더 안에 낱개 코드 파일들을 넣어두면 이것을 자동으로 웹 페이지의 서버 런타임에서 자유롭게 가져다쓸 수 있다고 하였는데, Phalanger도 마찬가지입니다. 웹 페이지를 렌더링하기 위한 목적이 아닌 공통이 되는 PHP 코드를 이 폴더에 넣어두기만 하면 자동으로 이 폴더에 속한 모든 PHP 코드들이 글로벌 문맥 상에서 사용 가능하게 활성화됩니다.

단, 조심해야 할 부작용이 하나 있다면 여기에 지나치게 많은 코드를 배치할 경우 컴파일 시간이 늘어나서 처음 사이트를 시작할 때 시간이 오래 걸리게 될 가능성이 있습니다. 안타깝게도 C나 C++처럼 병렬 컴파일은 아직 지원되지 않기 때문에 컴파일 시간이 오래 걸릴 경우 다양한 문제를 야기할 가능성이 있습니다.

php.ini와 같은 Global Configuration이 아닌 web.config에 의한 설정

PHP의 경우 설정을 변경하기 위해서는 PHP 전체의 설정을 주관하는 php.ini 파일을 업데이트하거나, PHP를 다시 컴파일하여 설치하는 번거로운 과정을 거쳐야만 모듈에 대한 설정이나 추가/제거가 가능했습니다. 하지만 Phalanger의 경우 현재 만들어진 응용프로그램 풀마다 다른 설정을 가지도록 구성할 수 있으므로 좀 더 자유도 높은 설정이 가능합니다. 이를 위해서 web.config 파일을 수정하고 저장하기만 하면 됩니다.

이러한 설정을 다루기 위해서는 phpNet이라는 XML 요소를 web.config에 지정해야 하는데, 그냥 지정할 수는 없고 반드시 적절한 처리기를 연결해주어야 합니다. web.config은 단순한 XML 파일이 아니라 닷넷 프레임워크가 직접 내용을 검사하고 분석하는 프로그램 코드의 일부이기 때문에 규칙을 준수하는 것이 매우 중요합니다.

phpNet 요소를 추가하려면 web.config에서 <configuration> 요소의 제일 첫 번째 노드로 아래 XML 조각이 배치되어야 합니다.

<configSections>
<section name="phpNet" type="PHP.Core.ConfigurationSectionHandler, PhpNetCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=0a8e8c4c76728c71" />
</configSections>

그 다음, 보기에 편리한 위치에 phpNet 요소를 추가합니다. 보통 아래의 코드 조각으로 최초 설정을 시작하면 무난합니다.

<phpNet>
<classLibrary>
  <add assembly="PhpNetClassLibrary, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4af37afe3cde05fb" section="bcl" />
  <add assembly="PhpNetXmlDom, Version=3.0.0.0, Culture=neutral, PublicKeyToken=2771987119c16a03" section="dom"/>
</classLibrary>   
</phpNet>

php.ini 파일의 샘플 사본을 복사하여 php.ini 파일로 사용하던 것과 비슷한 접근 방법이지만 각 사이트 혹은 도메인 별로 따로 사용하는 web.config 파일 안에서 이러한 설정을 다루는 것이 중요한 차이점입니다. 그리고 무엇보다도 안심해도 좋은 것은 INI 파일처럼 프로그램이 잘못 다루게 될 가능성이 있는 파일이 아니라, XML의 형태로 설정 파일이 관리되므로 web.config 파일을 건드리는 다른 써드 파티 어플리케이션 때문에 Phalanger의 설정이 깨지거나 변형될 일이 거의 없다는 점입니다.

위의 기본 설정을 지정하면 Phalanger에서 기본적인 PHP API를 사용할 수 있으며, PHP5부터 기본으로 제공되는 SimpleXMLElement도 위의 설정으로 기본으로 활성화됩니다.

PHP/CLR의 사용

이제 위의 설정을 토대로 PHP/CLR을 활성화하여 닷넷 프레임워크의 기본 API를 Phalanger에서 즉시 호출하여 사용할 수 있습니다. 위의 <phpNet> 요소 아래에 다음의 XML 요소를 추가하면 됩니다.

<compiler>
  <set name="LanguageFeatures">
    <add value="PhpClr" />
  </set>
</compiler>

그리고 <classLibrary> 요소 아래에 .NET Framework 기본 어셈블리에 대한 레퍼런스를 추가합니다.

<classLibrary>
  <add assembly="mscorlib" />
  <add assembly="System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  <add assembly="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
  <add assembly="PhpNetClassLibrary, Version=3.0.0.0, Culture=neutral, PublicKeyToken=4af37afe3cde05fb" section="bcl" />
  <add assembly="PhpNetXmlDom, Version=3.0.0.0, Culture=neutral, PublicKeyToken=2771987119c16a03" section="dom"/>
</classLibrary>

이제 새로 추가한 코드가 의도한대로 잘 작동하는지 살펴보기 위하여, 이미지 리사이징을 수행하는 샘플 코드를 Phalanger 위에서 실행하도록 PHP/CLR 기반으로 코드를 만들어보도록 하겠습니다. 이 코드는 http://wiki.php-compiler.net/Code_Samples/Resize_image 의 예제를 발췌하여 조금 변형한 것입니다.

<?php
use System\Drawing\Bitmap;
use System\Drawing\Graphics;
use System\Drawing\GraphicsUnit;
use System\Drawing\Rectangle;
use System\Web\HttpContext;

function resize_imageSysDraw($from,$wid,$hgt)
{
  $bmp = Bitmap::FromFile($from);
  $fmt = $bmp->RawFormat;
  $new = new Bitmap($wid, $hgt);
  $gr = Graphics::FromImage($new);
  $gr->DrawImage($bmp,
    new Rectangle(0,0,$wid,$hgt),
    new Rectangle(0,0,$bmp->Width, $bmp->Height),
    GraphicsUnit::Pixel);
  $gr->Dispose();
  $new->Save(HttpContext::$Current->Response->OutputStream, $fmt);
  $new->Dispose();
}

resize_imageSysDraw(realpath('Penguins.jpg'), 320, 240);
?>

PHP/CLR의 경우 여느 닷넷 언어들과 마찬가지로 네임스페이스에 속한 클래스에 대한 참조를 use 명령어로 지정하고 있으며, 정적 멤버에 대해서는 :: 연산자를, 객체 생성은 new 연산자를 사용하였습니다. resize_imageSysDraw 함수에서는 ASP.NET의 HttpContext를 가져와서 기본 출력 대신 비트맵 이미지를 내보내도록 만들었고 그 결과 아래와 같이 축소된 이미지가 렌더링되서 나타나게 됩니다.

Phalanger의 LINQ 지원

이제 마지막으로 PHP/CLR의 하이라이트라고 할 수 있는 LINQ 지원에 대해서 살펴보겠습니다. LINQ는 Microsoft Research에서 C# 언어의 확장 사양인 C-omega 언어의 일부로 개발 중이던 사양을 정규화하여 Production Spec으로 만든 것으로, C# 이외에 VB.NET에도 영향을 주었으며 Prism이나 지금 소개하는 Phalanger에서도 개념을 적극 채택하여 정규 사양으로 활용 중입니다. 그리고 F#은 이러한 접근을 더욱 드라마틱하게 활용하여 함수형 언어로 발전시키기도 하였습니다.

LINQ에 대해서 이야기하려면 책을 한 권 따로 만들어야할 만큼 방대합니다. 그래서 자세한 이야기는 하지 않고, LINQ 자체에 대해서 진지하게 학습하기 원한다면 LINQ 관련 국내외 도서들을 검토하기 바랍니다. 개인적으로는 "생각하는 C# LINQ"라는 책을 추천합니다. :-)

http://kangcom.com/sub/view.asp?sku=200809180001&mcd=571

LINQ는 한 마디로 이야기하면, 프로그래밍 코드를 한 방향에서만 바라보도록 뷰 포인트의 시각을 고정한 것과 같습니다. 본디, 어떤 연관성이 있는 데이터 집합을 접근하는 방법에는 여러 가지 방법이 있을 수 있지만 LINQ는 데이터가 어떤 순서로 들어있든, 어떤 형태로 연결되어있든 관계없이 데이터를 꺼내올 수 있도록 도와주는 Iterator 패턴의 한 형태인 Enumerator를 조금 독특하게 해석하였습니다.

Enumerator가 열거할 대상을 미리 정할 수 있도록 만들고, 열거할 때 조건을 지정하여 필요없는 데이터는 건너뛸 수 있게 해준다던지 이런 취지에서 해석을 한 것이 LINQ입니다. 그리고 Enumerator를 수정하게 되는 시점이 이미 메모리 상에 저장된 데이터 셋에 대한 작업인지, 아니면 아직 수신되지 않은 미지의 데이터 셋에 대한 작업인지에 따라서도 지연 실행이냐 즉시 실행이냐 이렇게 구분하기도 하구요. 그러면서도 항상 잃지 않는 것은 핵심은 Enumerator라는 사실이며, 이에 입각하여 배열이나 리스트같은 정규화된 자료 구조로 변환할 수 있는 길을 항상 열어놓아 최대한의 유연성을 부여하기도 합니다.

어렵게 들릴 수도 있지만 Enumerator를 수정할 수 있게 해준다는 컨셉은 생각보다 활용 폭이 넓은데, 가장 가까이 있는 예로는 SQL 쿼리가 될 수 있습니다. 처음의 아이디어는 SQL 쿼리를 이용하여 전체 데이터 셋보다 가능한 적게 데이터를 반환하여 네트워크 트래픽을 줄이고 빠르게 데이터를 검색할 수 있도록 최적화하자는 것에 있었을 것이며, 이것을 좀 더 프로그래밍 언어와 친화적으로 만들 방법을 모색한 끝에 LINQ to SQL이 나타나게 된 셈입니다. 그리고 이를 필두로 접근할 수 있는 모든 유형의 컬렉션에 대해서 이런 아이디어를 대입하여 현재는 오픈 소스를 찾아보면 정말 엄청나게 많은 LINQ provider들을 발견할 수 있을 정도입니다.

이렇게 독창적이고 전례없던 기술을 Phalanger에서도 이용할 수 있다는 것은 매우 좋은 일입니다. <phpNet> 요소에 대해 PHP-CLR을 활성화하도록 설정을 수정한 후 아래 코드를 테스트해보기 바랍니다.

<?php
$myarray = json_decode('[
    {"label":"foo","name":"baz"},
    {"label":"boop","name":"beep2"},
    {"label":"foo","name":"baz1"},
    {"label":"boop","name":"beep3"},
    {"label":"foo","name":"baz2"},
    {"label":"boop","name":"beep1"}
]', true);

$result =
from $myarray as $x
where $x['label'] == 'foo'
select $x['name'];

foreach ($result as $x) {
    print($x.'<br />');
}

print_r($result);

?>

json_decode라는 기본 PHP 함수를 이용하여 JSON을 PHP 연관 배열로 바꾸고, 이것을 LINQ로 조회한 다음, 그 결과를 foreach 문을 통해서 출력하도록 만들었습니다. C#이나 VB.NET의 LINQ와 약간 다른 점은, from 절에서 in 연산자 대신 as 연산자를 사용하고 in 연산자와는 도치되는 좌/우항 관계를 가집니다. 즉, [나열 변수] in [데이터 소스] 에서 [데이터 소스] as [나열 변수]로 바뀝니다. 그리고 이것은 foreach 문에도 동일하게 적용됩니다. 아래는 실행 결과입니다.

PHP, JSON, 그리고 LINQ가 한 자리에 모여 매우 재미있는 상호 작용을 이룬 것을 볼 수 있습니다. 이 정도면 닷넷에서의 웹 프로그래밍이 이전과는 제법 많이 달라질 수 있다는 것을 체감할 수 있을 것입니다.

다음번에는 Phalanger가 기존 PHP의 모듈들을 어떻게 다루고 관리하는지에 대한 상세한 내용을 살펴보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2012. 2. 8. 02:00

지난 아티클에 이어 오늘은 Phalanger를 이용하여 C#과 VB.NET 코드를 동시에 활용하는 예를 한 번 조명해보려고 합니다. 사실 이것이 Phalanger의 하이라이트라고 하여도 무방하지 않습니다. 기존의 C#과 VB.NET 로직을 재사용하면서도 Phalanger로 빠르고 가볍게 웹 페이지를 풀어나가는 방식은 여러모로 웹 개발에 가속을 붙여줄 것입니다.

계속 설명하기 전에, Phalanger가 ASP.NET 위에서 실행되기 때문에 적용되는 한 가지 특수한 규칙을 이야기할 것이 있습니다. 바로 미리 정해진 이름의 폴더인데, Bin, App_Code 폴더입니다. 그 외 많은 폴더들이 있지만 지금은 두 가지만 살펴보겠습니다.

앞에서 이야기한대로 ASP.NET은 닷넷 프레임워크를 기반으로 작동한다고 하였고, 닷넷 프레임워크는 언어의 종류와 무관하게 MSIL이라는 코드로 컴파일되서 나오는 DLL 파일들을 취급할 수 있습니다. 그렇게 만들어지는 DLL 파일들은 Bin 폴더에 파일을 넣어주면면 자동으로 참조가 활성화되서 해당 웹 사이트 안의 모든 닷넷 코드에서 클래스 라이브러리를 사용할 수 있게 됩니다. 단, 주의할 것은 이 폴더 안에 아무 DLL이나 넣을 수 있는 것은 아니며 반드시 닷넷으로 컴파일된 DLL만 포함시킬 수 있습니다.

그리고 이번 아티클에서 가장 핵심이 되는 기능을 제공하는 폴더인 App_Code 폴더는, 컴파일을 하려는 모듈에 대한 코드를 여기에 넣어서 모든 페이지에서 공유할 수 있습니다. 기본적으로 이 폴더에는 한 종류의 언어에 해당되는 파일들만 넣을 수 있고, 그외 다른 언어의 파일이 포함되면 아래 이미지와 같은 오류가 발생합니다. 하지만 제가 이전에 소개했던 약간의 추가 설정을 적용하면 App_Code 폴더에서 여러 프로그래밍 언어를 동시에 사용할 수 있습니다.

위와 같이 나타나는 문제를 해결하기 위하여, App_Code 폴더 자체에서 사용할 언어는 Phalanger로 정하고, App_Code 폴더 안의 두 번째 수준의 디렉터리들에 C#이나 VB.NET 같은 언어를 사용하기로 결정합니다. 그리고 web.config 파일에 다음과 같이 <system.web> 요소 아래에 하위 디렉터리가 존재한다는 것을 알려줍니다.

<system.web>
<globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8" />
<compilation>
  <codeSubDirectories>
    <add directoryName="Cs" />
    <add directoryName="Vb" />
  </codeSubDirectories>
</compilation>
</system.web>

그리고 App_Code 폴더에 각각 Cs 폴더와 Vb 폴더를 만듭니다. 폴더 이름에서 바로 알 수 있듯이 각각 C#과 VB.NET 코드를 따로 보관할 수 있도록 하고, 위의 그림처럼 오류가 발생하지 않으면서 자동으로 C#과 VB.NET으로 만든 코드를 Phalanger에서 액세스할 수 있도록 하게 할 것입니다. 이것으로 준비는 모두 끝났으며 우리가 원하는대로 C#과 VB.NET 코드를 추가하기만 하면됩니다.

C# 코드

using System;
using System.Collections.Generic;
using System.Web;

/// <summary>
/// Summary description for ClassName
/// </summary>
public class ClassName
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString() {
        return String.Format("Name: {0} / Age: {1:D3}", Name, Age);
    }
}

VB.NET 코드

Imports Microsoft.VisualBasic

Public Class ClassName2
    Public Content As String
End Class

그리고 중요한 부분이 있습니다. Phalanger가 서브 디렉터리에 있는 코드들을 컴파일하였을 때 각각 따로 만들게 될 코드 조각들을 인식할 수 있도록 최소한 1개 이상의 임의의 PHP 파일이 필요합니다. 이후 아티클에서 따로 설명하겠지만 Phalanger는 App_Code 폴더 안에 있는 PHP 파일을 모두 자동으로 include하는 동작을 가지고 있습니다. (이것이 이전의 Original PHP와는 다른 부분입니다.)

지극히 일상적이고도 당연한 클래스 선언을 담고 있는 파일을 각 디렉터리에 추가하였습니다. 이제 웹 매트릭스의 폴더 레이아웃은 아래 그림과 같은 형태가 되면 됩니다.

이제 마지막으로 index.php 코드를 아래와 같이 작성합니다. 아래와 같이 작성하면 정말 PHP에서 C#과 VB.NET 코드를 자동으로 불러올 수 있을까요? 기대됩니다. :-)

<?php       
// C#
$test1 = new ClassName();
$test1->Name = "남정현";
$test1->Age = 2012-1987;
print $test1->ToString().'<br />';
print get_class($test1).'<br />';

// VB.NET
$test2 = new ClassName2();
$test2->Content = '<strong>안녕하세요!!!</strong>'.'<br />';
print $test2->Content;
print get_class($test2).'<br />';

phpinfo();
?>

ClassName은 C#으로, ClassName2는 VB.NET으로 만든 코드입니다. PHP/CLR 확장의 도움으로 Name, Age, Content 프로퍼티에 문자열을 대입하고 있으며, print 문을 이용하여 웹 페이지 상에 문자열을 출력합니다. 그리고 get_class라는 기본 PHP 함수를 사용하여 클래스 형식명을 가져오는 일도 하려고 하는군요. 마지막으로는 phpinfo() 함수를 호출하여 Phalanger 및 서버 시스템에 대한 정보도 덤프로 출력합니다. 이제 이 페이지를 실행해보면 아래와 같이 나타나게 될 것입니다. :-)

훌륭합니다! C#과 VB.NET 컴파일러를 따로 부르는 일 없이 앉은 자리에서 한 번에 코드를 컴파일하고, 이것을 PHP에서 보기 좋게 가져다 쓰는 일까지 해냈습니다.

다음 아티클에서는 마지막으로 Phalanger만의 고유한 기능이라고 할 수 있는 자동 include에 대해서 마지막으로 이야기해볼까 합니다. Original PHP에서는 include나 include_once 같은 함수를 어떤 인과 관계에 따라서 포함하게 될 것인지, 그리고 순서에 대해서도 많은 고민을 해야 했지만 Phalanger는 이러한 부분에 대한 고민을 많이 덜어낼 수 있습니다. 그리고 그 중심에는 App_Code 폴더의 역할이 크다고 했는데요, 이 부분에 대해서 집중적으로 살펴보려고 합니다.

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2012. 2. 6. 01:00

Phalanger 공식 웹 사이트에서 따로 소개된 적은 없지만, Phalanger와 웹 매트릭스는 아주 이상적인 궁합을 보여주고 있으며 Phalanger가 보여주고자 하는 모습을 가감없이 완벽하게 보여줍니다. 이 아티클을 다 읽고 나면 지금 말하는 것이 어떤 의미인지 알게 될 것입니다.

Phalanger 설치하기

우선 컴퓨터에 Phalanger를 설치해야 합니다. 이전 아티클에서 이야기한대로 Phalanger는 .NET Framework 4.0을 필요로 합니다. 그리고 지금 우리가 실습하려는 WebMatrix도 설치해야 하는데, 결론적으로 지금 이야기하는 도구와 프레임워크들은 한 번에 Microsoft /web 홈페이지에서 제공하는 Web Platform Installer를 이용하여 한 번에 설치할 수 있습니다. http://www.microsoft.com/web 에서 WPI를 설치하고 WebMatrix와 .NET Framework 4.0을 선택하여 설치를 진행하도록 합니다. Phalanger를 설명하는 과정에서는 필요없지만, C#이나 VB.NET을 같이 사용하려고 한다면 Visual Web Developer 2010 Express도 설치합니다.

기본 구성 요소 설치가 끝나면 http://www.codeplex.com/phalanger에서 최신 버전의 Phalanger 설치 프로그램을 다운로드하여 설치를 시작합니다. Phalanger 설치 프로그램 안에는 런타임과 Visual Studio 도구가 모두 들어있습니다. 웹 개발에는 Visual Studio 도구가 따로 필요하지 않으며, Phalanger를 이용해서 PHP/PEAR와 같은 응용프로그램 개발을 하기 원한다면 Visual Studio 도구를 사용하는 것이 편합니다.

WebMatrix에서 Phalanger 사용하기

기본적으로 WebMatrix는 PHP4와 PHP5를 지원합니다. 그러나 WebMatrix가 기본으로 제공하는 PHP 런타임은 사용하지 않고 앞 단계에서 설치한 Phalanger 런타임을 대신 불러오도록 설정을 업데이트할 것입니다. 그러면서도, 기존의 PHP 개발 템플릿을 그대로 이용할 수 있습니다.

WebMatrix에서 Phalanger를 사용하는 방법은 간단합니다. 여러분이 원하는대로 사이트를 하나 새로 만들고, 사이트 탭을 클릭하고 설정 메뉴를 클릭하면 아래와 같이 화면이 나타납니다. 그림에 적은 설명대로, .NET Framework는 버전 4.0을 사용해야 하며, PHP 설정은 사용하지 않고, 필요하다면 index.php가 기본 페이지로 지정되도록 합니다.

이제 파일 탭을 클릭하고 F5키를 눌러 web.config 파일이 있는지 확인합니다. 만약 없다면 web.config 파일을 새 파일로 하나 추가합니다. web.config 템플릿이 WebMatrix에 기본으로 제공되므로 쉽게 추가할 수 있을 것입니다.

참고로 web.config은 기존의 ASP.NET 환경에서도 쓰이지만 WebMatrix가 내부적으로 서버로 사용하는 IIS 7 Express 및 IIS 7의 설정 파일로도 사용되며, IIS 6 이하에서 사용하던 메타베이스 기반 설정과는 달리 Apache HTTP Server의 httpd.conf와 같은 맥락의 디렉터리 단위 설정 파일이라고 보면 되겠습니다.

web.config 파일을 열어서 <system.webServer> XML 요소 아래의 내용을 수정해야 합니다. (다른 부분이 있더라도 여기서는 필요하지 않습니다.) 아래의 내용을 추가하여 PHP 확장자에 대한 지원을 추가하도록 합니다.

<handlers>
  <add name="PhalangerHandler" path="*.php" verb="*" type="PHP.Core.RequestHandler, PhpNetCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=0a8e8c4c76728c71" resourceType="Unspecified" preCondition="integratedMode" />
</handlers>

파일을 저장하고, 잘 작동하는지 테스트하기 위하여 index.php 파일을 만들고 아래와 같이 코드를 작성합니다. 그리고 상단의 실행 버튼을 눌러 잘 나타나는지 확인합니다.

<?php

class myClass {
    var $x;
}

$test = new myClass();
$test->x = "Hello, World! in ";
print $test->x.'<br />';
print get_class($test);

phpinfo();

?>

그러면 아래와 같이 화면이 나타날 것입니다. 클래스를 선언하고, 변수 필드를 선언하고, 여기에 값을 대입하거나 print 문을 사용하여 문자열을 출력하고, get_class 같은 간단한 형식 조회 함수도 씁니다. 아, 그리고 phpinfo 함수는 PHP 세계에서는 매우 기초적이고도 기본적인 함수였죠. :-)

그리고 흔히 사용하는 include_once 같은 API도 잘 작동합니다. 같은 디렉터리 상에 test.php를 만들고 class 선언만 따로 떼어 저장한 다음 include_once 함수를 호출해보기 바랍니다.

test.php

<?php
class myClass {
    var $x;
}

?>

index.php

<?php

include_once('test.php');

$test = new myClass();
$test->x = "Hello, World! in ";
print $test->x.'<br />';
print get_class($test);

phpinfo();

?>

Phalanger에서 한글을 사용하려면

PHP4나 PHP5와 다를바 없는 실행 모습입니다. 그런데 한 가지 점검해봐야 할 것이 있습니다. Phalanger에서 한국어나 일본어같은 2바이트 문자를 정상적으로 취급할 수 있을까요? 기본 설정으로는 그렇지 않을 가능성이 있습니다. 그리고 웹 매트릭스는 모든 파일을 UTF-8로 저장하기 때문에 문제가 됩니다. 이러한 문제를 예방하기 위해서는 반드시 web.config 설정을 변경해야 합니다.

<system.web> 요소 아래에 다음의 코드를 추가하도록 합니다.

<globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8" />

그리고 앞의 코드에서 한국어 문자열을 포함하는 print 명령문을 한 번 더 추가하여 한글이 잘 나오는지 확인합니다.

이제 Phalanger를 WebMatrix에서 개발할 준비는 다 끝났습니다. 다음 아티클에서는 Phalanger만의 고유한 기술적 특징을 살펴보면서, Phalanger를 C#과 VB.NET과 함께 사용하는 방법을 살펴보도록 하겠습니다. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

  1. 웹매트릭스!! 잘 보고 갑니다.

    2012.02.06 11:44 신고 [ ADDR : EDIT/ DEL : REPLY ]

Windows + .NET2012. 2. 4. 23:02

그 동안 .NET은 ASP.NET을 통해서 개발하는 것이 가장 최선이었고 실제로도 많은 개발 프로젝트는 ASP.NET 웹 폼을 통해서 진행되어왔습니다. 그리고 당연한 이야기이지만 PHP와 .NET은 전혀 다른 도메인에 속해있던 분리된 환경이었고, 그저 IIS를 통해서 호스팅 가능한 서로 다른 응용프로그램 도메인 상의 환경일 뿐이었습니다.

그러나 최근에 아주 흥미롭고 참신한 발견을 다시 했는데, 그간 베타 수준에만 머물러있던 Phalanger의 꾸준한 버전 업그레이드를 통해서 올해 1월에 3.0 버전을 발표했습니다. 64비트 시스템에 대한 지원도 충실히 하고 있으며, PHP의 태생적 한계로 자주 지목된 성능 상의 문제도 .NET 런타임을 사용하기로 하였기 때문에 구조적으로 해결하고 있으며, 무엇보다도 중요한 것은 엄격하게 PHP의 문법을 준수한다는 전제 아래에서 PHP 코드를 사용할 수 있다는 점입니다.

ps. Phalanger의 발음은 '팔란저'라고 하면 됩니다. 뜻은 '여우 원숭이'입니다.

Phalanger 프로젝트의 주요 기능은 다음과 같습니다.

PHP를 .NET 환경의 주요 프로그래밍 언어로 사용 가능하게 만듭니다.
* PHP 언어를 .NET CLR에서 사용 가능한 MSIL로 컴파일합니다.
* .NET 객체를 직접 PHP/CLR 언어 확장을 통해서 곧바로 사용할 수 있습니다.
* C#, VB.NET 등의 .NET 프로그래밍 환경에서 기존 PHP 코드 기반 라이브러리를 재사용할 수 있습니다.

기존 PHP 응용프로그램의 실행 속도를 향상시킵니다.
* 기존의 많은 수의 PHP 응용프로그램을 컴파일할 수 있습니다.
* Just-in-Time (JIT) 컴필레이션을 통해서 실행 속도를 개선할 수 있습니다.
* 표준 PHP 라이브러리 함수들과 네이티브 PHP4 확장 플러그인들을 그대로 사용할 수 있습니다.

PHP/CLR 확장을 통해서 PHP의 기능을 확장할 수 있습니다.
* PHP/CLR을 통해서 기존의 .NET CTS 시스템과 완벽하게 연동할 수 있습니다.
* PHP/CLR 프로젝트에 기존의 C#, VB.NET, C++ CLR 등으로 작성한 코드의 네임스페이스를 불러올 수 있습니다.
* PHP 언어로 .NET 제네릭 형식을 불러오거나 제작할 수 있습니다.
* .NET 커스텀 어트리뷰트, 부분 클래스, 프로퍼티 등 주요 기능들을 지원합니다.

PHP 언어로 .NET 라이브러리를 만듭니다.
* .NET/Mono 어셈블리에 맞추어 PHP 스크립트를 직접 DLL로 컴파일할 수 있습니다.
* Pure Mode를 사용하면 네이티브 코드에 의존하지 않는 완전한 .NET 환경에 맞출 수 있습니다.
* Legacy Mode를 사용하면 기존 PHP4/PHP5와의 호환성을 유지하면서도 .NET 환경에 맞출 수 있습니다.

PHP 프로젝트에서 .NET 라이브러리를 사용할 수 있습니다.
* C#이나 VB.NET으로 작성한 비즈니스 로직 위에 PHP를 표현 수단으로 채택할 수 있습니다.
* Phalanger를 통해서 기존에 작성한 어떤 종류의 .NET 객체이더라도 PHP에서 사용할 수 있습니다.
* ASP.NET 2.0 멤버십 API를 Phalanger 기반 PHP와 ASP.NET 모두에 적용할 수 있습니다.

PHP 언어를 Visual Studio에 통합할 수 있습니다.
* 프로젝트 템플릿, 문법 하이라이트, 디버거를 설치할 수 있습니다.
* 이를 통하여 Windows Forms, Console, Simple Win32 App을 만들 수 있습니다.

미리 이야기할 것이 하나 더 있는데 안타깝게도, Phalanger가 지원하는 PHP 문법은 기존의 PHP 문법과 완벽하게 일치하는 것이 아닙니다. 그래서 Phalanger를 기존의 PHP와 완벽하게 동일한 집합이나 기술로 보는 것은 다소 무리가 있으며, 대신 기존에 여러분이 알고 있던 PHP를 .NET 환경에서 그대로 쓸 수 있다는 사실에 집중하는 것이 바람직합니다.

개인적으로 Phalanger를 테스트하기에 가장 이상적인 환경은 WebMatrix인 것 같습니다. 다음 아티클에서는 Phalanger를 설치하고 WebMatrix위에 Phalanger로 PHP 코드를 작성하는 방법을 소개하도록 하겠습니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2011. 12. 26. 02:11

오늘 소개하려는 Visual Studio 확장 기능은 멀티 타기팅 팩의 기능을 매우 적극적으로 활용하는 똑똑한 확장 기능이다. 실무에서 3.5세대 혹은 4세대 닷넷 프레임워크를 적극적으로 사용 중인 개발자들이 국내에도 많이 있을텐데 이 경우 숟한 고민에 부딪히게 되는 것 중에 하나는 다중 플랫폼을 지원해야 한다는 점이다. 대충 열거해봐도 세 가지 이상은 된다. ASP.NET, Windows Presentation Foundation, Silverlight, 경우에 따라서는 XNA Game Studio나 Windows Phone 7.1까지 생각해야 하는 셈이다.

물론 신경써서 세심하게 잘 프로그래밍할 수 있을만큼 노련하다면야 이런 도구가 굳이 필요하지는 않겠지만 사람이 하는 일인지라 신경쓸 것이 많아지면 자연스레 귀찮을 수 밖에 없고 실수도 어디선가는 꼭 나온다. 이런 불분명하고 애매한 상황을 도구를 통하여 정확하게 잡아낼 수 있다면 괜찮지 않을까? Microsoft 개발 팀이 이런 문제를 해결해 줄 명쾌한 답을 Visual Studio Gallery에 올려놓았으니 참고하기 바란다. 아래의 URL에서 다운로드할 수 있다.

http://visualstudiogallery.msdn.microsoft.com/b0e0b5e9-e138-410b-ad10-00cb3caf4981/

Portable Library Tools를 설치하면 Visual Studio에 새로운 프로젝트 템플릿이 C#과 Visual Basic .NET에 대하여 Windows 카테고리 아래에 아래 그림과 같이 나타날 것이다.


시험삼아 새 프로젝트를 한 번 만들어보자. 이름에서 알 수 있듯이 특별할 것이 없는 공통되는 코드를 묶기 위한 클래스 라이브러리 프로젝트로서 만들어지며 일상적으로 여러분의 코드를 추가할 수 있다. 그런데 정말 중요한 것은 프로젝트 속성 안에 들어있다. 프로젝트 속성을 열어보면 아래와 같이 재미있는 구성을 볼 수 있다.

목표로 하는 프레임워크를 이전처럼 하나만 선택하는 것이 아니라 하나 이상을 동시에 지정할 수 있다. 이는 다시 말해서 이들 프레임워크의 공통 분모만을 사용하여 클래스 라이브러리를 정확히 프로그래밍할 수 있도록 보증한다는 의미이다. 그리고 Change 버튼을 클릭하면 더 유용한 쓰임새를 찾을 수 있다.

Visual Studio와 함께 설치된 다른 멀티 타기팅 팩이 있을 경우 여기에 모두 열거되어 여러분이 선택할 수 있도록 할 수 있다. 프로젝트의 종류가 많고 정말 일반적이면서도 널리 쓰여야 할 클래스 라이브러리를 만들어야 한다면 여기서 선택을 어떻게 하는가에 따라서 사용할 수 있는 API가 자동으로 필터링된다. 만약 위에서 선택한 플랫폼에서 사용할 수 없는 API에 대한 내용이 발견되면 곧바로 빌드 오류로 나타나기 때문에 문제를 쉽게 찾아낼 수 있다. 그리고 여기서 선택한 대로 해당 프로젝트에서 레퍼런스로 추가할 수 있으므로 생산성도 높일 수 있다. 즉, 솔루션이나 프로젝트를 여러벌로 나눠 관리할 필요 없이 한 곳에서 모두 관리할 수 있다.

이 도구를 사용하기 위해서는 Visual Studio 2010 전체 버전이 필요하며 SP1이 설치되어있어야 한다. 그리고 다른 타기팅 팩이 필요하다면 아래 URL에서 추가적으로 설치할 수 있다. 타기팅 팩은 그 자체로 설치할 수 있는 것은 아니고 보통은 해당 플랫폼에 대한 SDK 안에 같이 수록된다는 점에 유의해야 한다.

http://go.microsoft.com/fwlink/?LinkID=153626

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

  1. 이런거 있으면 클래스 만들때 호환성걱정은 많이 덜수 있겠네요

    2011.12.31 14:01 신고 [ ADDR : EDIT/ DEL : REPLY ]

Windows + .NET2010. 11. 25. 11:56

응용프로그램 작성 도중 C#의 소수점 반올림 관련 코드에서 몇몇 자주 사용되는 기능들을 보완하는 코드를 작성하여 올려봅니다. 이 코드는 http://www.latiumsoftware.com/en/delphi/00033.php 의 코드를 바탕으로 작성된 것입니다.

namespace System
{
    // Original Source: http://www.latiumsoftware.com/en/delphi/00033.php
    public static class MathExtension
    {
        public static int Sign(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Math.Sign(x);
        }

        public static int Sign(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Math.Sign(x);
        }

        public static int Integer(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return (int)Math.Truncate(x);
        }

        public static int Integer(
#if CS3
            this
#endif // CS3
            double x)
        {
            return (int)Math.Truncate(x);
        }

        public static decimal Fraction(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return x - Math.Truncate(x);
        }

        public static double Fraction(
#if CS3
            this
#endif // CS3
            double x)
        {
            return x - Math.Truncate(x);
        }

        public static decimal RoundUp(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Integer(x) + Sign(Fraction(x));
        }

        public static double RoundUp(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Integer(x) + Sign(Fraction(x));
        }

        public static decimal RoundDown(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Integer(x);
        }

        public static double RoundDown(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Integer(x);
        }

        public static decimal Round(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Integer(x) + Integer(Fraction(x) * 2m);
        }

        public static double Round(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Integer(x) + Integer(Fraction(x) * 2d);
        }

        public static decimal Fix(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            if (x >= 0m || Fraction(x) == 0m)
                return Integer(x);
            else
                return Integer(x) - 1;
        }

        public static double Fix(
#if CS3
            this
#endif // CS3
            double x)
        {
            if (x >= 0d || Fraction(x) == 0d)
                return Integer(x);
            else
                return Integer(x) - 1;
        }

        public static decimal RoundDownFix(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Fix(x);
        }

        public static double RoundDownFix(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Fix(x);
        }

        public static int Absolute(
#if CS3
            this
#endif // CS3
            int x)
        {
            return Math.Abs(x);
        }

        public static decimal RoundUpFix(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Fix(x) + Absolute(Sign(Fraction(x)));
        }

        public static double RoundUpFix(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Fix(x) + Absolute(Sign(Fraction(x)));
        }

        public static decimal RoundFix(
#if CS3
            this
#endif // CS3
            decimal x)
        {
            return Fix(x + 0.5m);
        }

        public static double RoundFix(
#if CS3
            this
#endif // CS3
            double x)
        {
            return Fix(x + 0.5d);
        }

        public static decimal RoundTo(
#if CS3
            this
#endif // CS3
            decimal x, int d)
        {
            decimal n = (decimal)Math.Pow(10, d);
            x *= n;
            return (Integer(x) + Integer(Fraction(x) * 2m)) / n;
        }

        public static double RoundTo(
#if CS3
            this
#endif // CS3
            double x, int d)
        {
            double n = Math.Pow(10, d);
            x *= n;
            return (Integer(x) + Integer(Fraction(x) * 2d)) / n;
        }
    }
}

사용 예시는 다음과 같습니다.

using System;

namespace ConsoleApplication1
{
    public static class Program
    {
        [STAThread]
        public static void Main(string[] args)
        {
#if CS3
            Console.WriteLine("{0}", (3.3m).RoundUp() == 4m);
            Console.WriteLine("{0}", (-3.3m).RoundUp() == -4m);
            Console.WriteLine("{0}", (3.7m).RoundDown() == 3m);
            Console.WriteLine("{0}", (-3.7m).RoundDown() == -3m);
            Console.WriteLine("{0}", (3.5m).Round() == 4m);
            Console.WriteLine("{0}", (-3.5m).Round() == -4m);
            Console.WriteLine("{0}", (3.1m).Round() == 3m);
            Console.WriteLine("{0}", (-3.1m).Round() == -3m);
            Console.WriteLine("{0}", (3.7m).Integer() == 3);
            Console.WriteLine("{0}", (-3.7m).Integer() == -3);
            Console.WriteLine("{0}", (3.7m).Fix() == 3m);
            Console.WriteLine("{0}", (-3.7m).Fix() == -4m);
            Console.WriteLine("{0}", (3.7m).RoundDownFix() == 3m);
            Console.WriteLine("{0}", (-3.7m).RoundDownFix() == -4m);
            Console.WriteLine("{0}", (3.1m).RoundDownFix() == 3m);
            Console.WriteLine("{0}", (-3.1m).RoundDownFix() == -4m);
            Console.WriteLine("{0}", (3.1m).RoundUpFix() == 4m);
            Console.WriteLine("{0}", (-3.7m).RoundUpFix() == -3m);
            Console.WriteLine("{0}", (3.5m).RoundFix() == 4m);
            Console.WriteLine("{0}", (-3.5m).RoundFix() == -3m);
            Console.WriteLine("{0}", (123.456m).RoundTo(0) == 123.00m);
            Console.WriteLine("{0}", (123.456m).RoundTo(2) == 123.46m);
            Console.WriteLine("{0}", (123456m).RoundTo(-3) == 123000m);

            Console.WriteLine("{0}", (3.3d).RoundUp() == 4d);
            Console.WriteLine("{0}", (-3.3d).RoundUp() == -4d);
            Console.WriteLine("{0}", (3.7d).RoundDown() == 3d);
            Console.WriteLine("{0}", (-3.7d).RoundDown() == -3d);
            Console.WriteLine("{0}", (3.5d).Round() == 4d);
            Console.WriteLine("{0}", (-3.5d).Round() == -4d);
            Console.WriteLine("{0}", (3.1d).Round() == 3d);
            Console.WriteLine("{0}", (-3.1d).Round() == -3d);
            Console.WriteLine("{0}", (3.7d).Integer() == 3);
            Console.WriteLine("{0}", (-3.7d).Integer() == -3);
            Console.WriteLine("{0}", (3.7d).Fix() == 3d);
            Console.WriteLine("{0}", (-3.7d).Fix() == -4d);
            Console.WriteLine("{0}", (3.7d).RoundDownFix() == 3d);
            Console.WriteLine("{0}", (-3.7d).RoundDownFix() == -4d);
            Console.WriteLine("{0}", (3.1d).RoundDownFix() == 3d);
            Console.WriteLine("{0}", (-3.1d).RoundDownFix() == -4d);
            Console.WriteLine("{0}", (3.1d).RoundUpFix() == 4d);
            Console.WriteLine("{0}", (-3.7d).RoundUpFix() == -3d);
            Console.WriteLine("{0}", (3.5d).RoundFix() == 4d);
            Console.WriteLine("{0}", (-3.5d).RoundFix() == -3d);
            Console.WriteLine("{0}", (123.456d).RoundTo(0) == 123.00d);
            Console.WriteLine("{0}", (123.456d).RoundTo(2) == 123.46d);
            Console.WriteLine("{0}", (123456d).RoundTo(-3) == 123000d);
#else // !CS3
            Console.WriteLine("{0}", MathExtension.RoundUp(3.3m) == 4m);
            Console.WriteLine("{0}", MathExtension.RoundUp(-3.3m) == -4m);
            Console.WriteLine("{0}", MathExtension.RoundDown(3.7m) == 3m);
            Console.WriteLine("{0}", MathExtension.RoundDown(-3.7m) == -3m);
            Console.WriteLine("{0}", MathExtension.Round(3.5m) == 4m);
            Console.WriteLine("{0}", MathExtension.Round(-3.5m) == -4m);
            Console.WriteLine("{0}", MathExtension.Round(3.1m) == 3m);
            Console.WriteLine("{0}", MathExtension.Round(-3.1m) == -3m);
            Console.WriteLine("{0}", MathExtension.Integer(3.7m) == 3);
            Console.WriteLine("{0}", MathExtension.Integer(-3.7m) == -3);
            Console.WriteLine("{0}", MathExtension.Fix(3.7m) == 3);
            Console.WriteLine("{0}", MathExtension.Fix(-3.7m) == -4);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(3.7m) == 3m);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(-3.7m) == -4m);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(3.1m) == 3m);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(-3.1m) == -4m);
            Console.WriteLine("{0}", MathExtension.RoundUpFix(3.1m) == 4m);
            Console.WriteLine("{0}", MathExtension.RoundUpFix(-3.7m) == -3m);
            Console.WriteLine("{0}", MathExtension.RoundFix(3.5m) == 4m);
            Console.WriteLine("{0}", MathExtension.RoundFix(-3.5m) == -3m);
            Console.WriteLine("{0}", MathExtension.RoundTo(123.456m, 0) == 123.00m);
            Console.WriteLine("{0}", MathExtension.RoundTo(123.456m, 2) == 123.46m);
            Console.WriteLine("{0}", MathExtension.RoundTo(123456m, -3) == 123000m);

            Console.WriteLine("{0}", MathExtension.RoundUp(3.3d) == 4d);
            Console.WriteLine("{0}", MathExtension.RoundUp(-3.3d) == -4d);
            Console.WriteLine("{0}", MathExtension.RoundDown(3.7d) == 3d);
            Console.WriteLine("{0}", MathExtension.RoundDown(-3.7d) == -3d);
            Console.WriteLine("{0}", MathExtension.Round(3.5d) == 4d);
            Console.WriteLine("{0}", MathExtension.Round(-3.5d) == -4d);
            Console.WriteLine("{0}", MathExtension.Round(3.1d) == 3d);
            Console.WriteLine("{0}", MathExtension.Round(-3.1d) == -3d);
            Console.WriteLine("{0}", MathExtension.Integer(3.7d) == 3);
            Console.WriteLine("{0}", MathExtension.Integer(-3.7d) == -3);
            Console.WriteLine("{0}", MathExtension.Fix(3.7d) == 3);
            Console.WriteLine("{0}", MathExtension.Fix(-3.7d) == -4);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(3.7d) == 3d);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(-3.7d) == -4d);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(3.1d) == 3d);
            Console.WriteLine("{0}", MathExtension.RoundDownFix(-3.1d) == -4d);
            Console.WriteLine("{0}", MathExtension.RoundUpFix(3.1d) == 4d);
            Console.WriteLine("{0}", MathExtension.RoundUpFix(-3.7d) == -3d);
            Console.WriteLine("{0}", MathExtension.RoundFix(3.5d) == 4d);
            Console.WriteLine("{0}", MathExtension.RoundFix(-3.5d) == -3d);
            Console.WriteLine("{0}", MathExtension.RoundTo(123.456d, 0) == 123.00d);
            Console.WriteLine("{0}", MathExtension.RoundTo(123.456d, 2) == 123.46d);
            Console.WriteLine("{0}", MathExtension.RoundTo(123456d, -3) == 123000d);
#endif // CS3
        }
    }
}

위의 코드에서 CS3 라는 매크로를 프로젝트 내에 설정하면 Extension Method로 사용할 수 있으며, 이 매크로의 선언을 해제하면 일반 정적 메서드로 변경하여 사용할 수 있습니다. 여러 메서드들이 있지만 그 중에서도 RoundTo 메서드는 사용 빈도가 매우 많은 함수일 듯 합니다. :-)

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 11. 1. 17:00

Windows Forms, ASP .NET Web Form 디자이너의 경우, 디자인 타임에 대한 의존도가 상당히 높은 편입니다. 그리고 이러한 디자인 타임 상의 구현을 완료하는데에 있어서 흔히 겪게 되는 문제가 있는데, 바로 컬렉션에 대한 처리입니다.

디자인 타임을 위한 컬렉션의 초기화 방법은 달라야 합니다.

디자인 타임을 위한 컬렉션의 초기화 방법은 런타임때와는 달라야 합니다. 객체를 생성하고 호출하는 방법이 우리가 이해하는 런타임 때와는 다르며, 컬렉션은 이를 준수하기 위해서 지연된 초기화 과정을 거쳐야 합니다. 다음은 디자인 타임용 컬렉션을 초기화하는 프로퍼티의 한 예시입니다.

internal ArrayList internalObjCollection = new ArrayList(); // 나중에 설명할 부분입니다.
private ObjectCollection objCollection = null; // 생성자나 인라인 식에서 초기화하지 않습니다. 대신...

[Browsable(true)]
[Category(ForexRuntime.ForexCategoryName)]
[Editor(typeof(CollectionEditor), typeof(UITypeEditor))]
[Description("속성에 대한 설명을 여기에 지정합니다.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ObjectCollection Collection {
    get {
        if (objCollection == null) objCollection = new ObjectCollection(this);
        return objCollection;
    }
}

위의 예시에서처럼 지연된 초기화를 사용한다는 점을 기억해야 합니다. 또한, 디자인 타임 컬렉션이 갖추어야 할 추가적인 조건이 한 가지 더 있는데, 디자인 타임 컬렉션은 철저히 프록시 역할을 수행해야 한다는 점입니다. 위에서 언급한 ObjectCollection의 생성자 호출 시 부모 객체의 참조를 넘겨받는 다는 점을 유심히 살펴보아야 합니다. 그리고 ObjectCollection은 컬렉션으로서 준수해야 할 기본적인 인터페이스 구현만을 포함해야 하며 실제로 모든 객체 관리는 다시 internalObjCollection 이라는 별도의 컬렉션에서 관리가 이루어지게 된다는 점입니다.

[Serializable]
[DesignTimeVisible(true)]
public class OutputFieldDefineCollection : IList { ... }

컬렉션 클래스 자체에는, 디자인 타임과의 원활한 상호 작용을 위하여 DesignTimeVisibleAttribute 속성이 추가된 것을 확인해 둡니다. 그리고 이 컬렉션의 생성자 형식에서 수용하기로 한 원본 객체의 참조는, 원본 객체 내부의 ArrayList에 접근하기 위한 목적으로 사용되고, 이 클래스가 구현하기로 한 IList 및 다른 인터페이스는 원본 객체 내부의 ArrayList를 기준으로 컬렉션의 기능을 제공하도록 코드를 작성합니다.

생성자와 IContainer 인터페이스의 역할은 매우 중요합니다.

추가하기로 한 컴포넌트에서 각별히 신경써야 할 것은, 바로 컨테이너의 기본 생성자와 더불어서 IContainer 객체의 참조를 받는 생성자로 적어도 2가지 생성자가 항상 제공되어야 합니다. 예를 들어, BookComponent가 있다고 가정해 보겠습니다.

public class BookComponent : Component {
    public BookComponent() : this(null) { }
    public BookComponent(IContainer container) : base() {
        if (container != null) { container.Add(this); }
        // 이곳에 생성자 코드를 지정하거나, this.InitializeComponent() 메서드를 호출합니다.
    }
}

위와 같은 코드가 있다고 하였을 때, 보통 container의 Add 메서드를 별 다른 생각없이 부릅니다. 하지만 여기에 숨겨진 기능이 하나 더 있는데, Add 메서드에는 디자인 타임에서 사용할 변수의 이름을 지정할 수 있는 인자가 제공된다는 점입니다. Add 메서드의 두 번째 오버로드를 사용하면 쉽게 변수의 이름을 다시 정의할 수 있는 것입니다. 단, 중복되는 변수 이름이 발생하지 않도록 정교한 명명 규칙이 필요함을 유의해야합니다.

또한, 별도의 디자이너가 존재하는 경우, 가능하면 컴포넌트의 생성자 메서드의 실행이 끝나기 전에 필요한 모든 초기화 작업을 수행하는 것이 좋으며 여기에는 앞서 설명한 Add 메서드의 활용은 물론 컴포넌트 자체의 설정에 관한 부분들도 포함됩니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 7. 22. 20:24

Java로 작성된 코드를 C#으로 옮길 필요가 있어 살펴보던 중 눈에 익숙하지 않은 Java 고유의 Bitwise Shift 연산자를 발견했습니다. Triple Shift Operator를 어떤 의미로 해석해야 가장 정확할지 확실히 알 수 없어서 인터넷을 검색하던 중 저와 같은 고민을 하는 개발자의 질문이 역시 stackoverflow.com에 있었고 명쾌한 해답을 얻을 수 있었습니다. :-)

 질문: What is the equivalent (in C#) of Java's >>> operator? Just to clarify, I'm not referring to the >> and << operators.
답변: In C#, you can use unsigned integer types, and then the << and >> do what you expect. The MSDN documentation on shift operators gives you the details. Since Java doesn't support unsigned integers (apart from char), this additional operator became necessary.

위의 질문과 답변 내용에 입각하여 코드를 작성한다면 아래와 같습니다.

/* Java Code */
int x = 3;
int y = x >>> 3;

/* Equivalent C# Code */
int x = 3;
int y = (int)((uint)x >> 3);

Java에는 C#과는 달리 Unsigned Integer Type이 따로 정의된 것이 없기 때문에 이와 같은 특수한 연산자를 하나 더 정의하게 된 것이라는 설명도 스레드 하단부에서 찾을 수 있었습니다.

자료 출처: http://stackoverflow.com/questions/1880172/equivalent-of-java-triple-shift-operators-and-in-c

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 7. 15. 00:38

이미 알고 계신분들도 계시겠지만, Visual Studio 2010부터는 더 이상 Crystal Report가 번들링되지 않게 되었습니다. Visual Studio 2010과 함께 제공되는 Microsoft Report Viewer로 마이그레이션하는 것이 가장 좋은 선택이지만, 이미 개발된 많은 수의 레포트를 정해진 기간 내에 모두 마이그레이션할 수 없는 상황이라면, 이 포스트의 내용을 참고하셔서 무료로 제공되는 Visual Studio 2010용 Crystal Report Free Product를 설치하시면 되겠습니다.

http://www.businessobjects.com/jump/xi/crvs2010/default.asp 에서 Crystal Report for Visual Studio 2010을 다운로드받으실 수 있으며 이 글을 작성하는 현 시점에서 이 제품은 베타 버전으로 발표되어있습니다. 페이지에 접속하면 미국 내 사용자와 기타 지역 사용자용으로 다운로드 링크가 나누어져있으므로 해당되는 링크로 접속하면 자동으로 파일 다운로드가 시작됩니다. 파일 크기는 약 200~300MB 정도입니다.

관련된 블로그 및 포럼 스레드

참고로, Crystal Report를 개발하던 회사인 Business Objects는 2007년에 SAP사에 인수 합병되었습니다. 이에 따라서 전통적으로 Microsoft의 Visual Studio에 번들링되어오던 Crystal Report 제품 개발 일정 상에 큰 변동이 있었고 이와 같은 방침이 SAP에 의하여 결정되고 수행된 것으로 보입니다. 개인적인 소견을 덧붙이면, Visual Studio 2010 이후의 버전에서는 더 이상 Crystal Report가 무료로 제공되지 않을 가능성이 높을 것으로 보이며, 가능한한 Microsoft의 Report Viewer를 사용하여 기존의 모든 Crystal Report 기반 레포트 기술을 교체하는 것을 권장합니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 6. 25. 09:11

오늘은 .NET Framework 4 에서 가장 많은 충돌을 야기할 수 있는 변동 사항 중 하나를 오늘 아티클에서 잠시 다루어보기로 하겠습니다. 바로 ISerializable 인터페이스에 관한 것인데요, Serializable Attribute와 NonSerialized Attribute를 이용하여 제어하는 것 만으로는 불충분한 경우 즐겨 사용해오던 인터페이스입니다. 그렇지만 여기에 급격한 변화 (Breaking Changes)가 .NET Framework 4에 더해지게 되었는데, Partial Trust Mode로 실행되는 어셈블리에서 이 인터페이스를 구현하는 로직을 호출하게 되는 경우, ISerializable 인터페이스의 GetObjectData에 추가된 SecurityCritical Attribute에 의해 호출이 거부됩니다.

저 개인적으로는 XML-RPC의 .NET Framework 버전의 Implementation 라이브러리 (http://www.xml-rpc.net/)를 .NET Framework 4로 업그레이드하여 Windows Azure에 Deploy하면서 이러한 현상을 겪었는데, 이 라이브러리에서 제공하는 Custom Exception 클래스 상의 GetObjectData 메서드가 문제의 원인이 되었습니다. .NET Framework 4 환경에서 사용하도록 별도의 프로젝트 파일을 만들어서 Predefine Condition을 부여하여 ISerializable 인터페이스를 이용하지 않도록 코드를 수정한 이후에는 문제가 잘 해결되었습니다.

만약 ISerializable 인터페이스의 기능을 그대로 가져갈 필요가 있다면, http://msdn.microsoft.com/en-us/library/system.runtime.serialization.isafeserializationdata.aspx 의 내용을 참고하시어 ISafeSerializationData 인터페이스를 구현하는 별도의 코드를 작성해야 합니다. 아래는 MSDN Library에서 발췌한 샘플 코드입니다.


using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
using System.Security;

// [assembly: SecurityCritical(SecurityCriticalScope.Everything)]
// Using the SecurityCriticalAttribute prohibits usage of the
// ISafeSerializationData interface.
[assembly: AllowPartiallyTrustedCallers]
namespace ISafeSerializationDataExample
{
    class Test
    {
        public static void Main()
        {
            try
            {
                // This code forces a division by 0 and catches the
                // resulting exception.
                try
                {
                    int zero = 0;
                    int ecks = 1 / zero;
                }
                catch (Exception ex)
                {
                    // Create a new exception to throw.
                    NewException newExcept = new NewException("Divided by", 0);

                    // This FileStream is used for the serialization.
                    FileStream fs =
                        new FileStream("NewException.dat",
                            FileMode.Create);

                    try
                    {
                        // Serialize the exception.
                        BinaryFormatter formatter = new BinaryFormatter();
                        formatter.Serialize(fs, newExcept);

                        // Rewind the stream and deserialize the exception.
                        fs.Position = 0;
                        NewException deserExcept =
                            (NewException)formatter.Deserialize(fs);
                        Console.WriteLine(
                        "Forced a division by 0, caught the resulting exception, \n" +
                        "and created a derived exception with custom data. \n" +
                        "Serialized the exception and deserialized it:\n");
                        Console.WriteLine("StringData: {0}", deserExcept.StringData);
                        Console.WriteLine("intData:   {0}", deserExcept.IntData);
                    }
                    catch (SerializationException se)
                    {
                        Console.WriteLine("Failed to serialize: {0}",
                            se.ToString());
                    }
                    finally
                    {
                        fs.Close();
                        Console.ReadLine();
                    }
                }
            }
            catch (NewException ex)
            {
                Console.WriteLine("StringData: {0}", ex.StringData);
                Console.WriteLine("IntData:   {0}", ex.IntData);
            }
        }
    }

    [Serializable]
    public class NewException : Exception
    {
        // Because we don't want the exception state to be serialized normally,
        // we take care of that in the constructor.
        [NonSerialized]
        private NewExceptionState m_state = new NewExceptionState();

        public NewException(string stringData, int intData)
        {
            // Instance data is stored directly in the exception state object.
            m_state.StringData = stringData;
            m_state.IntData = intData;

            // In response to SerializeObjectState, we need to provide
            // any state to serialize with the exception.  In this
            // case, since our state is already stored in an
            // ISafeSerializationData implementation, we can
            // just provide that.

            SerializeObjectState += delegate(object exception,
                SafeSerializationEventArgs eventArgs)
            {
                eventArgs.AddSerializedState(m_state);
            };
            // An alternate implementation would be to store the state
            // as local member variables, and in response to this
            // method create a new instance of an ISafeSerializationData
            // object and populate it with the local state here before
            // passing it through to AddSerializedState.       

        }
        // There is no need to supply a deserialization constructor
        // (with SerializationInfo and StreamingContext parameters),
        // and no need to supply a GetObjectData implementation.


        // Data access is through the state object (m_State).
        public string StringData
        {
            get { return m_state.StringData; }
        }

        public int IntData
        {
            get { return m_state.IntData; }
        }

        // Implement the ISafeSerializationData interface
        // to contain custom  exception data in a partially trusted
       // assembly. Use this interface to replace the
       // Exception.GetObjectData method,
        // which is now marked with the SecurityCriticalAttribute.
        [Serializable]
        private struct NewExceptionState : ISafeSerializationData
        {
            private string m_stringData;
            private int m_intData;

            public string StringData
            {
                get { return m_stringData; }
                set { m_stringData = value; }
            }

            public int IntData
            {
                get { return m_intData; }
                set { m_intData = value; }
            }

            // This method is called when deserialization of the
            // exception is complete.
            void ISafeSerializationData.CompleteDeserialization
                (object obj)
            {
                // Since the exception simply contains an instance of
                // the exception state object, we can repopulate it
                // here by just setting its instance field to be equal
                // to this deserialized state instance.
                NewException exception = obj as NewException;
                exception.m_state = this;
            }
        }
    }
}

또는, 코드의 수정을 최소화하기 위하여 설정 파일 (app.config이나 web.config)에 LegacyCASMode Property를 사용하도록 설정을 구성할 수 있습니다. 이에 대한 내용은 http://msdn.microsoft.com/ko-kr/library/tkscy493.aspx 의 내용을 참고하시면 됩니다. 아래는 MSDN Library에서 발췌한 사용 예시입니다.

<location allowOverride="false">
  <system.web>
    <securityPolicy>
      <trustLevel name="Full" policyFile="internal" />
      <trustLevel name="High" policyFile="web_hightrust.config" />
      <trustLevel name="Medium" policyFile="web_mediumtrust.config" />
      <trustLevel name="Low"  policyFile="web_lowtrust.config" />
      <trustLevel name="Minimal" policyFile="web_minimaltrust.config"/>
    </securityPolicy>
  </system.web>
</location>

<location allowOverride="false">
  <system.web>
    <trust level="Medium" originUrl="" />
  </system.web>
</location>

<location allowOverride="true" path="Default Web Site/Temp">
  <system.web>
    <trust level="Medium" originUrl="" />
  </system.web>
</location>

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 5. 29. 17:18
닷넷 프로그래밍 커뮤니티에서 요즈음 제일 큰 이슈는 바로 .NET Framework 4와 Visual Studio 2010일 것입니다. 어느새 버전 4에 이르렀고, 정말로 많은 변화와 개선이 있었고, 그 현주소가 바로 지금 시점일 것입니다. 오늘은 .NET Framework 4에 대한, 그 중에서도 핵심 API 및 기술들에 대한 변화를 살펴보는 블로그 아티클을 올려봅니다.

진단 기능 및 성능 측정

.NET Framework 4에 들어서면서부터, CPU 사용량과 메모리 사용량을 프로세스 단위가 아닌, 응용프로그램 도메인 단위에서 측정하는 일이 가능해졌습니다. 응용프로그램 도메인은, 닷넷 프레임워크에서 사용하는 실제 실행 단위이며, 무엇보다도 중요한 것은 응용프로그램 도메인은 프로세스와 달리 닷넷 프레임워크의 관리 아래에서 통제할 수 있는 자원이라는 점입니다. (프로세스는 운영 체제의 커널에 의하여 귀속되고 관리되는 자원입니다. 따라서, 비 관리 영역에 속하는, 기본적으로는 권외 영역인 셈입니다.)

AppDomain 클래스에 추가된 새 멤버인 AppDomain.MonitoringIsEnabled 프로퍼티를 이용하여 (http://msdn.microsoft.com/ko-kr/library/system.appdomain.monitoringisenabled.aspx 참고) 현재 실행 중인 응용프로그램 도메인에 모니터링 기능이 부여되어있는지를 파악할 수 있습니다. 그리고, 이 설정을 바탕으로 CLR ETW 이벤트 (Common Language Runtime Event Tracing for Windows, http://msdn.microsoft.com/ko-kr/library/dd264810.aspx) 기능을 사용하여 오버헤드없이 성능 측정을 면밀하게 수행할 수 있게 되었습니다.

향상된 가비지 컬렉션

.NET Framework 4에 들어서면서부터 백그라운드 가비지 수집 기능이 제공되는데, 이는 이전 버전의 동시 가비지 수집을 대체하고, 좀 더 향상된 가비지 수집 처리 서비스를 제공합니다. 자세한 내용은 http://msdn.microsoft.com/ko-kr/library/ee787088.aspx 페이지의 Background Garbage Collection 섹션을 참고하시기 바랍니다.

병렬 컴퓨팅 패러다임의 지원
 
이제 .NET Framework 4에서도 병렬 컴퓨팅의 패러다임을 손쉽게 가져다 사용할 수 있게 되었습니다. 이제까지 .NET Framework에서 사용하던 비동기 프로그래밍은 Thread 클래스를 직접 사용하거나, Begin/End의 Pair로 구성된 비동기 버전의 연산, 나중에 이벤트로 결과를 통지 받는 패턴, ThreadPool 클래스의 활용과 같이 다중 Threading 작업의 세밀한 부분과 상호 작용을 고려해야 하는 패턴들이었습니다.
 
http://msdn.microsoft.com/ko-kr/library/dd460693.aspx 에서 소개하는 것 처럼 .NET Framework 4는 병렬 프로그래밍에 대한 새로운 개념들을 제공합니다. .NET Framework 4 환경에서 병렬 프로그래밍은 Task Parallel Library (TPL)에 의한 Action과 PLINQ (Parallel LINQ)에 의한 Action으로 구분됩니다.
 
Task Parallel Library의 경우, 데이터를 중심으로 비동기/병렬 연산을 수행하기 위한 시나리오, 작업 자체에 집중하여 작업의 실행 순서를 제어하고 관리하는 시나리오, 기존의 전통적인 비동기 패턴과 TPL을 통합하는 시나리오 등 다양한 시나리오를 MSDN 내에서 제공하고 있습니다. TPL을 어떻게 활용할 지를 고민하고 효율적으로 선택해야 하는 어려움이 있지만, 여러분의 응용프로그램이 Cloud Ready 응용프로그램이 될 수 있도록 도와주는 지름길로도 활용할 수 있을 것입니다.
 
그리고 PLINQ는 이미 여러 닷넷 관련 전문가분들의 블로그에서 수차례 .AsParallel() Extension Method로 소개되었던 LINQ의 Extension입니다. PLINQ에 대한 좀 더 자세한 이해는 http://msdn.microsoft.com/ko-kr/library/dd997425.aspx 에서 참고하실 수 있습니다.
 
향상된 네트워킹과 암호화
 
.NET Framework 4에서는 HttpWebRequest, HttpListener, SmtpClient, SslStream, NegotiateStream 등 여러 클래스에서 Windows 인증의 보안이 강화되었습니다. Windows 7 및 Windows Server 2008 R2에서 응용 프로그램에 대한 확장된 보호 기능을 사용할 수 있습니다. 그리고, IPv6 및 Teredo를 사용한 NAT(Network Address Translation) 통과를 지원합니다. 자세한 내용은 IPv6 및 Teredo를 사용한 NAT 통과를 참조하십시오.

HttpWebRequest 클래스에서 AddRange 메서드에 대한 새 오버로드를 통해 큰 바이트 범위 헤더(64비트 범위)를 사용할 수 있도록 지원합니다. HttpWebRequest 클래스의 새 속성을 통해 응용 프로그램에서 여러 개의 HTTP 헤더를 설정할 수 있습니다. Host 속성을 사용하여 요청 URI에 종속되지 않은 HTTP 요청에 호스트 헤더 값을 설정할 수 있습니다. 그 외에, SmtpClient 및 관련 클래스에 대한 SSL(Secure Sockets Layer) 지원 기능이 추가되었으며, MailMessage 클래스의 메일 헤더 지원 기능이 향상되었습니다.

암호화에 null 암호화를 사용할 수 있도록 지원합니다. ServicePointManager 클래스와 EncryptionPolicy 속성을 사용하여 암호화 정책을 지정할 수 있습니다. SslStream 클래스의 생성자에서 EncryptionPolicy 클래스를 매개 변수로 사용합니다.
 
ASP.NET 4
 
ASP.NET Caching을 확장할 수 있게 하는 새 API, Session State의 압축 지원 및 Application Preload 기능이 추가되어 더욱 개선된 Performance를 제공하게 되었습니다. Web Forms의 경우, 보다 통합된 ASP.NET 라우팅 지원, 향상된 웹 표준 지원, 업데이트된 브라우저 지원, 데이터 컨트롤을 위한 새 기능 및 새로운 뷰 상태 관리 기능 등이 포함됩니다. 그리고, 새로운 차트 컨트롤 등이 포함됩니다.

ASP.NET MVC 2에서는 뷰를 위한 새 도우미 메서드, 분할된 MVC 응용 프로그램에 대한 지원 및 비동기 컨트롤러 등이 포함됩니다. ASP.NET AJAX 라이브러리의 클라이언트 기반 AJAX 응용 프로그램에 대한 추가 지원 등이 포함됩니다.
그 외 변경 사항들
 
WPF 4는 이제 Silverlight 4와 동등한 컨트롤들을 제공합니다. 특히 데이터 그리드, 달력, 날짜 선택 컨트롤이 제공되고 실버라이트 응용프로그램을 거의 그대로 있는 그대로의 상태 (as-is)로 마이그레이션하는 것을 고려해 볼 수 있습니다. 자세한 내용은 http://msdn.microsoft.com/ko-kr/library/bb613588.aspx 를 참고하여 주십시오.
 
ADO.NET의 경우 ADO.NET Entity Framework에 대한 향상이 많이 있었습니다. http://msdn.microsoft.com/ko-kr/library/ex6y04yf.aspx 의 내용을 참고하여 주십시오. WCF의 경우 ASP.NET 호환 모드에서 ASP.NET Routing과 잘 통합됩니다. WCF의 새로운 기능은 http://msdn.microsoft.com/ko-kr/library/dd456789.aspx 에 잘 설명되어있으며, WF의 경우 http://msdn.microsoft.com/ko-kr/library/dd489410.aspx 의 내용을 참고하십시오.
 
지금 시작하기
 
.NET Framework 4.0의 기능을 지금 사용해보기 원하시나요? http://www.microsoft.com/express 에 방문하셔서 최신 버전의 Visual Studio 2010 Express Edition을 설치하고 지금 시작해보세요. 그리고, 2010년 6월 1일에 있을 REMIX'10 행사는 Visual Studio 2010과 함께 최신 웹 기술을 집중적으로 조명합니다. 관심있으신 개발자 여러분들의 적극적인 참여를 기대합니다. :-)
 
REMIX'10 행사 바로가기: http://www.visitmix.co.kr/remix10/index.html
Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 1. 31. 03:18

닷넷 프레임워크를 이용하는 동안, 의외로 곳곳에 훌륭한 기술들이 많이 숨어있습니다. 특히 문자열 처리에 관한 부분은 특별한 경우가 아니면, 기본으로 내장되어있는 API들 (Formatting API와 정규 표현식 API)만으로 쉽게 처리가 가능합니다. 예전 블로그 아티클 중에 정규표현식에 관련된 아티클들이 몇 가지 있으니 이 아티클과 더불어서 보시면 좋을것 같습니다.

 

오늘 살펴보려고 하는 내용은 String Formatting API에 대한 내용입니다.

 

1. 자유자재로 사용하는 치환자

 

대부분의 예제, 대부분의 코드에서는 String Formatting API에 서식 문자열을 지정할 때, 인자 순서대로, 단 한 번씩만 치환자를 사용합니다. 하지만, 같은 치환자를 여러번 중복해서 사용하거나, 인자 순서와는 별개로 지정할 필요는 많습니다. 이럴 때에는, 걱정하지 말고 매개 변수 개수에 유의하면서 치환자를 자유롭게 사용하면 됩니다.

 

String.Format("{0} {1} {1} {4} {3} {2}", 'a', 'b', 'c', 'd', 'e');
// => a b b e d c

 

2. C# 만의 편리한 기능: Verbatim String과 함께 사용하는 강력한 Formatting API

 

여러 줄에 걸친 문자열을 별도의 데이터 파일이 아닌, C# 코드 내에 포함할 수 있다면, 좀 더 String Formatting API를 극적으로 활용할 수 있을 것입니다. 이럴 때, Verbatim String을 활용하면 좋습니다. Verbatim String은 @" 으로 시작하여 " 로 끝이나는 문자열 구문 전체를 말합니다. 이 문자열 블럭 안에는 Back Slash나 CR/LF 등의 문자를 포함할 수 있습니다.

 

String.Format(@"<html>
<head><title>Hello World!</title></head>
<body>
<p>{0}</p>
</body>
</html>", DateTime.Now);

 

다만 유의할 점이 하나 있다면, 문자열이 파일로부터 나타난 것이든, Verbatim String에 의한 것이든, 의미를 다르게 해석할 가능성이 있는 글자 (예: '{' 나 '}' 같은 글자)들은 그 의미를 정확하게 살릴 수 있도록 지정하거나, 같은 문자를 연속으로 두번으로 지정하여 이스케이프 처리해야 합니다. (예: "{{", "}}")

 

String.Format(@"int main(int argc, char **argv) {
return 0;
}");

 

즉, 위의 경우 String.Format 메서드에서 오류를 반환합니다. 치환자 시작 기호 다음에 적절한 수식 대신 다른 문자열들이 열거되기 때문에 치환할 수 없다는 오류가 발생합니다. 위의 경우 중괄호 부분들을 아래와 같이 수정해야 합니다.

 

String.Format(@"int main(int argc, char **argv) {{
return 0;
}}");

 

위의 예시에서는 C 언어 코드에 대한 것을 예로 들었습니다만, 같은 방법으로 C# 프로그래밍 코드를 코드 안에 포함하여 재사용할 수도 있습니다. 또는, IronPython 등의 코드를 Embedding하여 DLR에서 사용할 수 있도록 재구성하는 시나리오도 생각해 볼 수 있을 것입니다.

 

3. 날짜 및 시간 표현을 자유자재로 활용하기

 

아직도 날짜 및 시간 표현을 위하여 문자열을 조립하거나, 알고리즘을 사용하여 오전/오후를 구분하십니까? 그렇게 할 이유가 없습니다. 오히려 좀 더 세밀하고 다양한 기능을 제공하는 기본 기능이 있기 때문입니다.

 

String.Format(@"{0:yyyy-MM-dd}", DateTime.Now); // 2009-01-30
String.Format(@"{0:tt hh:mm:ss}", DateTime.Now); // 오전 01:47:33
String.Format(@"{0:yyyy-MM-dd tt hh:mm:ss}", DateTime.Now); // 2009-01-30 오전 01:48:11

 

만약 오전/오후에 해당하는 문자열을 한글이 아닌 영문 표기 (AM/PM)로 변경하려면 어떻게 해야 할까요? 이럴 때에는 CultureInfo 객체를 Format 메서드에 전달하면 간단해집니다. CultureInfo는 System.Globalization 네임스페이스 안에 있습니다.

 

String.Format(CultureInfo.GetCultureInfo("en-US"), @"{0:tt hh:mm:ss}", DateTime.Now); // AM 04:30:44
String.Format(CultureInfo.GetCultureInfo("ko-KR"), @"{0:tt hh:mm:ss}", DateTime.Now); // 오후 03:03:02
String.Format(CultureInfo.GetCultureInfo("ja-JP"), @"{0:tt hh:mm:ss}", DateTime.Now); // 午後 10:08:03

 

위의 예시에서 주석으로 표시한 것과 같이 현재 시간에서 "오전"과 "오후"에 대한 표현을 해당 국가의 언어의 표기법에 맞추어 표기하고 있습니다. en-US는 언어를 영어로 사용하며 지리적으로 미국을 기준으로 한다는 의미로 해석되며, ko-KR은 언어를 한국어로 사용하고 지리적으로는 대한민국, ja-JP는 언어를 일본어로 사용하고 지리적으로 일본을 기준으로 한다는 의미로 해석됩니다.

 

위와 같이 언어 및 지역 코드를 설정할 때 알아야 할 사항이 두 가지가 있는데, 첫 째는 반드시 언어 코드의 전체 이름을 기재해야 합니다. 예를 들어, en-US에서 en만 지정하면 en에 해당하는 설정은 "중립 문화권"이기 때문에 CultureInfo.GetCultureInfo 메서드로는 받아들일 수 없습니다. 그리고 US만 지정하면 US에 해당하는 코드가 없으므로 역시 오류가 발생합니다. 그리고, 언어 코드가 시간대 설정까지 자동으로 반영하는 것은 아니므로 en-JP (언어는 영어이며 지역은 일본)와 같은 설정은 유효하지 않으며 표준 코드 정의 내역에 없기 때문에 받아들여지지 않습니다. 시간대에 대한 설정은 http://msdn.microsoft.com/ko-kr/library/system.timezone.aspx 에 소개된 TimeZone 클래스 (닷넷 3.5부터 사용 가능합니다)를 활용해야 합니다.

 

4. 통화 금액 표현하기

 

지역 설정을 이용하여 손쉽게 처리할 수 있는 일이 또 한 가지 있는데, 바로 통화 금액 표기에 관한 것입니다. 아래의 코드를 살펴보기로 하겠습니다.

 

String.Format(CultureInfo.GetCultureInfo("es-ES"), "{0:C}", 300); // 300,00 €
String.Format(CultureInfo.GetCultureInfo("ko-KR"), "{0:C}", 300); // ₩300

 

그리고, 좀 더 구체적으로 소수점 자릿수 등을 표현하거나, 음수/양수/영점 표현을 설정하고자 한다면 아래와 같이 활용할 수 있습니다. (Delphi Basic 웹 사이트 http://www.delphibasics.co.uk/RTL.asp?Name=FormatFloat 에서 부분 발췌한 샘플 코드를 올립니다.)

 

// 반올림 예시
String.Format("{0:#####}", 1234.567);
String.Format("{0:00000}", 1234.567);
String.Format("{0:0}", 1234.567);
String.Format("{0:#,##0}", 1234.567);
String.Format("{0:0,0}", 1234.567);

// 소수점 사용 예시
String.Format("{0:0.####}", 1234.567);
String.Format("{0:0.0000}", 1234.567);

// 공학용 표기
String.Format("{0:0.0000000E+00}", 1234.567);
String.Format("{0:0.0000000E-00}", 1234.567);
String.Format("{0:#.#######E-0#}", 1234.567);

// 음수/양수/ZERO 에 따른 표기
String.Format("{0:0.0}", -1234.567);
String.Format("{0:0.0 CR;0.0 DB}", -1234.567);
String.Format("{0:0.0 CR;0.0 DB}", 1234.567);
String.Format("{0:0.0 CR;0.0 DB;Zero}", 0.00);

5. Bonus: 자연스러운 16진수 표기 방법 (String.Format을 사용하지 않습니다.)

 

"0x" + (16).ToString("X8"); // 8자리 16진수 표기, 대문자
"0x" + (33).ToString("x4"); // 4자리 16진수 표기, 소문자

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 1. 19. 19:25

C#을 프로그래밍 언어로 사용하여 Windows Forms를 이용하여 만든 이미지 뷰어 컨트롤입니다. PictureBox 컨트롤이 내부적으로 스크롤 기능을 지원하지 않는점을 고려하여 디자인한 컨트롤이며, 다음의 기능들을 지원합니다.

 

  • 배율에 따른 이미지 확대/축소 기능
  • 이미지 축소 및 확대 시 부드럽게 이미지를 변조하는 기능
  • 키보드 스크롤, 마우스 드래그 스크롤

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

  1. 프레임쪽을 봐야했는데 많은 참조가 되었습니다. 감사합니다.

    2010.01.21 08:55 [ ADDR : EDIT/ DEL : REPLY ]

Windows + .NET2010. 1. 18. 16:47

Sandcastle Compiler를 이용하여 도움말을 빌드하는 데에 필요한 유틸리티 및 관련 리소스들을 종합해보았습니다. Sandcastle Compiler는 C#, Visual Basic .NET 등의 컴파일러가 제공하는 Code Documentation XML 추출 기능으로 만들어진 XML 파일을 XSLT 변환 등을 활용하여 도움말 패키지로 제작하는 빌드 후 처리 도구 중 하나입니다.

 

Sandcastle Compiler: http://www.codeplex.com/Sandcastle

> Sandcastle 컴파일러를 사용하려면 이 패키지를 다운로드하여 설치해야 합니다.

 

GhostDoc Visual Studio Add-In: http://submain.com/products/ghostdoc.aspx

> Visual Studio IDE에 연동되는 플러그인으로 기본 설정인 Ctrl + Shift + D 키를 누르면 영어권 언어를 사용하여 이름 및 코드로부터 XML 주석을 자동으로 유추하여 코드에 붙여넣어주는 도구입니다. (Visual Studio 2005 / 2008 / 2010 지원)

 

Sandcastle Help File Builder: http://www.codeplex.com/SHFB

> Sandcastle 프로젝트를 체계적으로 관리하고 세부적인 설정을 바꿀 수 있도록 돕는 도구입니다. MSBUILD 프로젝트 파일을 만들어주기 때문에, 도움말의 출판 단위를 프로젝트 기준으로 맞추어 구성할 수 있습니다.

 

Sandcastle Styles: http://www.codeplex.com/SandcastleStyles

> Sandcastle 기본 설정 위에 추가할 수 있는 패키지로 XSLT 일부 패치, Sandcastle 관련 보조 도구 등을 다운로드할 수 있습니다.

 

Visual Studio SDK: http://msdn.microsoft.com/ko-kr/vsx/default(en-us).aspx

> MS Help 2 (Visual Studio 2005/2008), Help 3 (Visual Studio 2010)에 연동할 수 있는 도움말 패키지는 Visual Studio SDK 안에 포함되어있습니다. 해당되는 SDK 버전을 다운로드하여 Sandcastle Help File Builder에서 경로를 지정하면 기본으로 제공되는 CHM 및 웹 사이트 컴필레이션 외의 다른 컴필레이션을 생성할 수 있습니다.

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

Windows + .NET2010. 1. 18. 16:10

닷넷 프레임워크 기반 프로그래밍에서 회자되는 내용 중에 "어려운 범주"에 속하는 주제들이 몇 가지 있는데, 그 중 하나가 AppDomain에 관련된 것입니다. 닷넷 프레임워크는 전통적인 프로그래밍 모델인 프로세스와 스레드의 개념 위에 응용프로그램 도메인이라는 개념을 새롭게 제공합니다.

 

닷넷 프레임워크 위에서 실행되는 응용프로그램은 JRE의 경우와 마찬가지로 논리적으로 구획이 나뉘어진 하나의 Virtual Machine 위에서 실행되고, 모든 메모리 관리가 이루어지게 됩니다. 생산성 향상을 위하여 이러한 부분을 내부적으로 모두 감추고, 마치 컴파일러나 IL Assembler가 직접 실행 가능한 EXE 파일을 생산해내는것처럼 묘사되어있지만, 우리가 일상적으로 만들어내는 닷넷 컴파일러 기반의 EXE 파일은 미리 프로그래밍된 스텁 코드입니다.

 

대부분의 경우, 응용프로그램 도메인이 여러 개일 필요 없이, 단순히 프로세스 내에 여러 개의 스레드만을 사용하여 프로그래밍하기 때문에, 스텁 코드의 내용에 대해 고민할 필요가 없습니다. 하지만, 닷넷 프레임워크 실행 환경 안에서 다른 어셈블리를 불러온다는 것은, 기존에 LoadLibrary 같은 Win32 API를 이용하여 DLL을 후기 바인딩하는 것과는 개념적으로 차이가 있습니다.

 

LoadLibrary의 경우는 Win32 API이며, 메모리를 비롯한 모든 자원 관리가 운영 체제 내의 커널에 의하여 처리됩니다. 하지만 닷넷 프레임워크 환경에서 다른 어셈블리를 로드하고 해제하는 일은 닷넷 프레임워크 환경 내에 위치한 개별 VM에서 처리되는 것이고, 로드하고 해제하는 위치 또한 어셈블리를 로드하도록 요구한 코드가 속해있는 응용프로그램 도메인에 고정시키게 됩니다. 그리고, 프로그램의 안정성을 위하여, 한번 로드한 어셈블리를 개별적으로 언로드하는 동작은 Win32 API 때와는 달리 지원되지 않습니다. 이러한 특성 때문에 응용프로그램 도메인을 별도로 분리하여 어셈블리를 필요한만큼 한꺼번에 로드하고, 일괄적으로 해지하는 방식이 나타나게 된 것입니다.

 

이러한 규칙 아래에서 프로그래밍을 해야 하기 때문에 만약 여러 응용프로그램 도메인을 생성하고 해지하는 일이 빈번한 프로그램을 작성한다면, 다음과 같은 사항들을 고려할 필요가 있습니다.

 

  • 컴파일러가 제공하는 표준 Stub 프로그램 대신, 직접 Visual C++ 컴파일러로 작성하여 mscoree.dll과 fusion.dll에 링크하는 커스텀 Stub 프로그램을 작성하여 프레임워크 응용프로그램을 시작하기
  • 스텁 프로그램을 직접 작성하지 않을 경우, 진입점 메서드에 [LoaderOptimization] 어트리뷰트를 사용하여 다중 응용프로그램 도메인을 관리함을 명시하는 방법

 

그리고, 응용프로그램 도메인을 이용하여 프로그래밍할 때에는, Global Assembly Cache에 등록할 어셈블리가 아니라 할지라도, 강력한 이름을 어셈블리 내에 부여하여, 어셈블리에 고유한 Identity를 설정하는 것이 좋습니다.

 

흔히 강력한 이름 (Strong Name)은 어셈블리의 이름, 버전, 언어 및 지역 코드 만으로는 완전하게 식별할 수 없다는 것을 보완하기 위하여, 그리고 다수의 DLL 간의 버전 충돌이 발생하였을 때 DLL을 식별할 여지가 없는 DLL 지옥 (DLL hell)에 빠지는 상황을 예방하기 위하여 도입된 개념입니다만, GAC에 등록을 하던 하지 않던 간에 강력한 이름으로 서명한 어셈블리는 버전 간 충돌에 대한 걱정 없이 "고유한 성격"을 유지할 수 있습니다. 그런데, 이 부분이 왜 중요할까요?

 

간혹 전사적 정책에 따라, Active Directory 등에 연결되어있는 모든 컴퓨터로부터 관리자 권한을 박탈하고, 최소한의 권한만으로 컴퓨터를 액세스할 수 있도록 관리하는 경우가 있는데, 이런 환경에서 GAC는 사용하기 불편한 상태가 됩니다. (GAC의 기본 디렉터리가 %WINDIR%\Assembly에 있으므로 GAC를 변경하는 작업은 "당연하게도" 관리자 권한이 요구됩니다.) 지금 설명할 응용프로그램 도메인이 어셈블리를 찾는 방법은, GAC를 사용할 수 없을 경우, GAC를 대신하여 이용할 수 있는 방법이므로, 강력한 이름을 기초로 식별하는 방법이 유일하기 때문입니다.

 

닷넷 프레임워크에서 어셈블리를 로드하는 기본 방식은, 응용프로그램 도메인이 시작될 때 설정된 BaseDirectory를 기준으로 아랫쪽에 있는 디렉터리나 파일들을 검색하는 방식입니다. 그러나 이 방법은 편리하지만 매우 제약이 심합니다. 그래서 부수적으로 제공하는 API가 하나 더 있는데, 바로 System.Reflection 네임스페이스의 Assembly 클래스가 노출하는 일부 정적 메서드들 중 From 으로 끝이나는 정적 메서드 시리즈들입니다. 그런데 여기서 고민이 하나 더 생깁니다.

 

어셈블리를 특정 도메인 위에서 로드하려면, AppDomain 클래스의 메서드를 이용해야 하지만, 앞서 이야기한 규칙에 따르면 자유롭게 로드하기 위해서는 Assembly 클래스의 메서드들을 이용해야 하는 충돌 상황이 발생합니다. 어떻게 이 상황을 풀면 좋을까요? 이 부분에 대해서 많은 Workaround를 찾아다닌 끝에 제가 찾은 방법은 아래와 같습니다.

 

AppDomain.CreateDomain 메서드를 사용하여 새 응용프로그램 도메인을 만듭니다. 다만, 기존 응용프로그램 도메인과 동일한 Evidence, 유사한 Setting을 상속받을 수 있게 하기 위하여 아래와 같이 매개 변수를 좀 더 구체적으로 써 넣습니다.

 

            AppDomainSetup domainSetup = new AppDomainSetup();
            domainSetup.ApplicationBase = Environment.CurrentDirectory;
            domainSetup.ConfigurationFile = configPath;
            domainSetup.PrivateBinPath = privateBinPath;

            AppDomain domain = AppDomain.CreateDomain(
                String.Concat("Delegate Wrapper #", func.GetHashCode()),
                null, domainSetup);

 

 

만들어진 domain 객체의 SetData 메서드를 사용하여, 응용프로그램 도메인 내에서 참조할 수 있는 데이터를 전달합니다. (함수 수준에서 응용프로그램 도메인 경계를 넘어 직접 매개 변수를 전달할 수 없다는 점에 따른 부분입니다.)

 

             domain.SetData("_ApplicationBasePath", Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "MyData"));

 

반환 형식이 없고, 매개 변수가 없는 일반 함수를 만들고, 본문 안에서 AppDomain.CurrentDomain 속성을 활용하여 새 도메인 기준으로 코드를 작성해 나갑니다. 이 때, AppDomain.CurrentDomain.AssemblyResolve 이벤트를 구독하도록 프로그래밍합니다.

 

            AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

 

AssemblyResolve 이벤트는 보통의 이벤트 핸들러와는 다르게 이벤트 핸들러가 직접 결과를 반환하는 형태로 구성되어있습니다. 이 지점에서, 응용프로그램 도메인에 전달한 기준 디렉터리 경로를 활용하여 닷넷 프레임워크가 기본 정책에 따라 찾지 못한 어셈블리를 Assembly.LoadFrom 메서드로 따로 로드한 뒤 객체를 되돌려줌으로서 경로 문제를 보완하게 됩니다. 이 때, 로드할 대상 어셈블리에 대해 강력한 이름 서명이 되어있어야 DLL 간 충돌 문제를 완벽히 예방할 수 있습니다.

 

        private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            string basePath = AppDomain.CurrentDomain.GetData("_ApplicationBasePath") as string;

            if (basePath == null || !Directory.Exists(basePath))
                return null;

            AssemblyName parsedName = new AssemblyName(args.Name);
            switch (parsedName.Name)
            {
                case "My.Common":
                case "My.Core":
                case "My.Productivity":
                    return Assembly.LoadFrom(Path.Combine(Path.Combine(basePath, "Library"), String.Concat(parsedName.Name, ".dll")));

                default:
                    return null;
            }
        }

 

그리고 새로 만든 함수를 domain.DoCallBack 메서드에 콜백 객체로 전달합니다. 만약, 실행 결과를 되돌려 받기 원한다면, domain.GetData 메서드로 콜백 함수 내에서 설정한 데이터를 넘겨받을 수 있습니다. DoCallBack 메서드는 동기 방식 메서드이므로 콜백 메서드의 실행이 끝나기 전까지는 제어권이 되돌아오지 않습니다.

 

참고로, 콜백 실행 도중 처리되지 않은 예외는 새로 만든 domain의 UnhandledException 이벤트 핸들러를 호출하게 되며, 이 보다 더 직접적으로 예외 처리를 할 필요가 있다면 아래와 같이 TargetInvocationException 형식에 대한 예외를 잡아내어 InnerException을 조사하도록 하면 더 빠르게 예외 처리를 할 수 있습니다.

 

                try
                {
                    domain.DoCallBack(appDelegate);
                }
                catch (TargetInvocationException remoteException)
                {
                    if (remoteException != null &&
                        remoteException.InnerException != null)
                        throw remoteException.InnerException;
                    else
                        throw remoteException;
                }

 

도메인의 사용이 모두 끝나면, domain.Unload 메서드를 사용하여 안전하게 도메인에 로드된 모든 어셈블리를 메모리에서 해지합니다.

 

긴 강좌를 읽어 주셔서 감사합니다. 적절한 상황이나 조건이 그 때 마다 다르겠지만, 프로그램의 규모가 커지고 복잡해 질 수록 .NET Framework의 숨겨진 이면을 충분히 분석하여 활용하는 것이 중요할 것입니다. 응용프로그램 도메인에 관하여 복잡하게 고민 중이신 분들께 도움이 되었으면 합니다.

 

Posted by Cloud Developer 남정현 (rkttu.com)

댓글을 달아 주세요

  1. C#을 배우고 있지만... 눈이 휘둥글~

    2010.01.24 14:49 [ ADDR : EDIT/ DEL : REPLY ]
    • 사실 닷넷의 기본 내용에 충실하게 접근하는 것이 가장 이상적인 프로그래밍 방식이겠지만, 유감스럽게도 닷넷 플랫폼을 있는 그대로 사용할 수 있는 경우는 흔하지 않은것 같습니다. 도움 되시면 좋겠습니다. ㅎㅎ

      2010.01.25 08:49 [ ADDR : EDIT/ DEL ]