오늘은 .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>

 

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

닷넷 프레임워크를 이용하는 동안, 의외로 곳곳에 훌륭한 기술들이 많이 숨어있습니다. 특히 문자열 처리에 관한 부분은 특별한 경우가 아니면, 기본으로 내장되어있는 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진수 표기, 소문자

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

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

 

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

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

닷넷 프레임워크 기반 프로그래밍에서 회자되는 내용 중에 "어려운 범주"에 속하는 주제들이 몇 가지 있는데, 그 중 하나가 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의 숨겨진 이면을 충분히 분석하여 활용하는 것이 중요할 것입니다. 응용프로그램 도메인에 관하여 복잡하게 고민 중이신 분들께 도움이 되었으면 합니다.

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

네이버 오픈 API를 데이터 소스로 사용하여 Windows Forms에 Binding하는 샘플을 간단히 만들어보았습니다. 지난번에 예를 들었던 난수 생성을 위한 데이터 소스에서 언급했던 IListSource 인터페이스의 사용법을 기초로 발전시킨 샘플입니다. :-)

 

using System;
using System.IO;
using System.Net;
using System.Xml;
using System.Data;
using System.Text;
using System.Collections;
using System.Globalization;
using System.ComponentModel;

namespace OpenApiBinding
{
    [ToolboxItem(true)]
    [DesignTimeVisible(true)]
    public partial class NaverSearchBinding : Component, IListSource
    {
        public NaverSearchBinding()
            : this(null)
        {
        }

        public NaverSearchBinding(Container container)
            : base()
        {
            if (container != null)
                container.Add(this);

            this.InitializeComponent();

            this.key = String.Empty;
            this.target = NaverSearchTarget.WebDocuments;
            this.query = String.Empty;
            this.display = 100;
            this.start = 1;
            this.sort = NaverSortMode.Similarity;
        }

        private string key;
        private NaverSearchTarget target;
        private string query;
        private int display;
        private int start;
        private NaverSortMode sort;

        [NonSerialized]
        private const string RequestUrl = "http://openapi.naver.com/search";

        [Browsable(true)]
        [DefaultValue("")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string Key
        {
            get { return this.key; }
            set { this.key = (value ?? String.Empty).Trim(); }
        }

        [Browsable(true)]
        [DefaultValue(NaverSearchTarget.WebDocuments)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public NaverSearchTarget Target
        {
            get { return this.target; }
            set { this.target = value; }
        }

        [Browsable(true)]
        [DefaultValue("")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string Query
        {
            get { return this.query; }
            set { this.query = (value ?? String.Empty).Trim(); }
        }

        [Browsable(true)]
        [DefaultValue(100)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int Display
        {
            get { return this.display; }
            set { this.display = value; }
        }

        [Browsable(true)]
        [DefaultValue(1)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int Start
        {
            get { return this.start; }
            set { this.start = value; }
        }

        [Browsable(true)]
        [DefaultValue(NaverSortMode.Similarity)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public NaverSortMode Sort
        {
            get { return this.sort; }
            set { this.sort = value; }
        }

        public override string ToString()
        {
            return String.Format(
                CultureInfo.CurrentCulture,
                "{{ Target: '{0}', Query: '{1}' }}",
                this.target,
                this.query);
        }

        private Uri CreateUri()
        {
            if (String.IsNullOrEmpty(this.key))
                return null;

            StringBuilder buffer = new StringBuilder();
            buffer.AppendFormat("key={0}&", this.key);

            switch (this.target)
            {
                case NaverSearchTarget.Blog:
                    buffer.Append("target=blog&");
                    break;

                case NaverSearchTarget.Cafe:
                    buffer.Append("target=cafe&");
                    break;

                case NaverSearchTarget.CafeArticles:
                    buffer.Append("target=cafearticle&");
                    break;

                case NaverSearchTarget.News:
                    buffer.Append("target=news&");
                    break;

                default:
                case NaverSearchTarget.WebDocuments:
                    buffer.Append("target=webkr&");
                    break;
            }

            buffer.AppendFormat("query={0}&", this.query);
            buffer.AppendFormat("display={0}&", this.display);
            buffer.AppendFormat("start={0}&", this.start);

            switch (this.sort)
            {
                default:
                case NaverSortMode.Similarity:
                    buffer.Append("sort=sim");
                    break;

                case NaverSortMode.Date:
                    buffer.Append("sort=date");
                    break;

                case NaverSortMode.Member:
                    buffer.Append("sort=member");
                    break;

                case NaverSortMode.NewArticles:
                    buffer.Append("sort=newArticles");
                    break;

                case NaverSortMode.Rank:
                    buffer.Append("sort=rank");
                    break;
            }

            return new Uri(String.Concat(RequestUrl, '?', buffer.ToString()));
        }

        public IList GetList()
        {
            Uri requestUri = this.CreateUri();

            if (requestUri == null)
                return null;

            WebRequest webRequest = WebRequest.Create(requestUri);

            using (WebResponse webResponse = webRequest.GetResponse())
            {
                using (Stream webResponseStream = webResponse.GetResponseStream())
                {
                    XmlDocument xmlDocument = new XmlDocument();
                    xmlDocument.Load(webResponseStream);

                    DataTable inferencedTable = new DataTable(this.target.ToString());

                    switch (this.target)
                    {
                        case NaverSearchTarget.Blog:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("bloggername", typeof(string));
                            inferencedTable.Columns.Add("bloggerlink", typeof(string));
                            break;

                        case NaverSearchTarget.Cafe:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("ranking", typeof(string));
                            inferencedTable.Columns.Add("member", typeof(string));
                            inferencedTable.Columns.Add("totalarticles", typeof(string));
                            inferencedTable.Columns.Add("newarticles", typeof(string));
                            break;

                        case NaverSearchTarget.CafeArticles:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("cafename", typeof(string));
                            inferencedTable.Columns.Add("cafeurl", typeof(string));
                            break;

                        case NaverSearchTarget.News:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("originallink", typeof(string));
                            inferencedTable.Columns.Add("pubDate", typeof(string));
                            break;

                        default:
                        case NaverSearchTarget.WebDocuments:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            break;
                    }

                    foreach (XmlNode eachNode in xmlDocument.SelectNodes("/rss/channel/item"))
                    {
                        XmlElement eachElement = eachNode as XmlElement;

                        if (eachElement == null)
                            continue;

                        int i = 0;
                        object[] values = new object[inferencedTable.Columns.Count];

                        foreach (DataColumn eachColumn in inferencedTable.Columns)
                            values[i++] = eachElement.SelectSingleNode(eachColumn.ColumnName).InnerXml;

                        inferencedTable.Rows.Add(values);
                    }

                    return inferencedTable.DefaultView;
                }
            }
        }

        public bool ContainsListCollection
        {
            get { return true; }
        }
    }

    [Serializable]
    public enum NaverSearchTarget : int
    {
        Blog,
        Cafe,
        CafeArticles,
        WebDocuments,
        News
    }

    [Serializable]
    public enum NaverSortMode : int
    {
        Similarity,
        Date,
        Member,
        NewArticles,
        Rank
    }
}

 

위의 소스 코드를 프로젝트에 추가하여 Windows Forms – 또는 – ASP.NET 프로젝트에 연결하거나, 독립적으로 객체를 만들어서 사용하는 것이 가능합니다. 다음은 Windows Forms 디자인 타임에서의 사용 예시입니다.

 

 

컴포넌트에서 API Key를 Key 프로퍼티에 설정하고, 검색어를 Query 프로퍼티에 설정하여 검색 결과를 수신할 수 있습니다. 데이터 그리드를 이용하여 수신한 데이터를 곧바로 보여줄 수도 있습니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

닷넷 프레임워크를 통하여 응용프로그램을 개발할 때, 특히 Windows Forms 기반의 다중 스레드 프로그램을 구성할 때 자주 적용되는 패턴이 하나 있다면, 단일 메서드를 가지고 PreCondition/PostCondition으로 양분하는 패턴이 있을 것입니다. 예를 들면 다음과 같습니다.

 

private void MyForm_Load(object sender, EventArgs e)
{

    if (this.InvokeRequired)

    {

         this.Invoke(new EventHandler(MyForm_Load), new object[] { sender, e });

         return;

    }

    // 실제 메서드 코딩
}

 

위와 같이 조건에 따라서, 같은 메서드를 상태에 변화를 가하여 다시 호출하는 기법은 Windows Forms 뿐만 아니라 일반적인 응용프로그램에서도 자주 활용될 수 있는 기법이 될 것입니다. 사실, 위와 같은 코드는 어려울 것이 없겠습니다만 저는 이를 일반화할 수 있는 방법을 고민하던 중에, Reflection과 Interop을 이용할 수 있다는 것을 발견하였습니다.

 

using System;
using System.Reflection;

namespace dotForex.Test
{
    class Program
    {
        static bool needInvoke = true;

        static void Main(string[] args)
        {
            if (needInvoke)
            {
                Action<string[]> func = Delegate.CreateDelegate(
                    typeof(Action<string[]>),
                    MethodInfo.GetCurrentMethod() as MethodInfo) as Action<string[]>;
                needInvoke = false;
                func(args);
                return;
            }
            else
            {
                Console.WriteLine("Test");
            }

            return;
        }
    }
}

 

위의 코드에서 강조표시된 부분이 일반화에서 핵심이 되는 부분들입니다. Delegate 클래스의 CreateDelegate 메서드를 통하여, 생성할 대리자의 형식을 지정하고, 현재 메서드의 정보를 가져와서 이를 대입하는 방법입니다.

 

위와 같은 기법을 통하여 아래와 같이 Windows Forms의 스레드 안정성을 고려한 이벤트 핸들러 처리를 일반화하는 것도 가능합니다.

 

using System;
using System.Reflection;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace dotForex.Application
{
    public sealed partial class ErrorReportForm : Form
    {
        public ErrorReportForm()
            : base()
        {
            this.InitializeComponent();
        }

        private object EnsureThreadSafe(MethodInfo targetMethod, Type targetDelegateType, params object[] arguments)
        {
            if (!this.InvokeRequired)
                throw new InvalidOperationException("EnsureThreadSafe is not required at this time.");

            if (targetMethod == null)
                throw new ArgumentNullException("targetMethod");

            if (targetDelegateType == null)
                throw new ArgumentNullException("targetDelegateType");

            if (!targetDelegateType.IsSubclassOf(typeof(Delegate)))
                throw new ArgumentException("Selected type is not a delegate type.", "targetDelegateType");

            Delegate methodDelegate = Delegate.CreateDelegate(
                targetDelegateType,
                targetMethod);

            if (methodDelegate == null)
                throw new Exception("Method cannot converted as delegate.");

            return this.Invoke(methodDelegate, arguments);
        }

        private void ErrorReportForm_Load(object sender, EventArgs e)
        {
            if (this.InvokeRequired)
            {
                this.EnsureThreadSafe(MethodInfo.GetCurrentMethod() as MethodInfo, typeof(EventHandler), sender, e);
                return;
            }

            // Method Coding Here
        }
    }
}

 

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

MSDN 온라인 버전 2.0에서 개인적으로 많이 활용하는 팁이 한 가지 있는데 이 팁을 이용하면 컴퓨터에 .NET Framework SDK 2.0이나 MSDN Express Edition을 힘들게 설치하지 않아도 언제 어디서나 다양한 언어 버전의 MSDN Library를 만날 수 있습니다. 지금 이야기하는 것은 .NET Framework 레퍼런스에만 해당됩니다.

  • http://msdn.microsoft.com/지역코드/네임스페이스.aspx
  • http://msdn.microsoft.com/지역코드/네임스페이스.클래스이름.aspx
  • http://msdn.microsoft.com/지역코드/네임스페이스.클래스이름_members.aspx

지역 코드는 ko-kr, en-us와 같이 표기할 수 있으며 이 설정에 따라서 언어를 선택할 수 있습니다. 한글 MSDN를 보시려면 ko-kr을 지정하시면 됩니다. 만약, 문서의 내용 자체가 특정 언어로 기술되어있을 경우 문서 내용은 해당 언어 그대로 출력되고, 인터페이스 요소 (메뉴, 문맥)만 선택한 언어로 나타나게 됩니다.

네임스페이스와 클래스 이름은 모두 소문자로 표기하는 것을 원칙으로 합니다. 가령 System.Windows.Forms 네임스페이스의 한글 도움말을 찾으려면 아래와 같이 합니다.

http://msdn.microsoft.com/ko-kr/system.windows.forms.aspx

그리고 Form 클래스의 기본 정보를 보려면 아래와 같이 합니다.

http://msdn.microsoft.com/ko-kr/system.windows.forms.form.aspx

만약 Form의 멤버들 (필드, 메서드, 이벤트, 프로퍼티, 상수 등)을 보려면 아래와 같이 클래스 이름 뒤에 _members 접미어를 붙여서 URL을 만들면 됩니다.

http://msdn.microsoft.com/ko-kr/system.windows.forms.form_members.aspx

2009년 9월 28일 업데이트: 만약 닷넷 프레임워크별로 버전을 선택적으로 확인하고자 하신다면 아래와 같은 버전 예약어를 사용하여 직접 확인하는 것도 가능하니 참고하시기 바랍니다. (devia님 정보 감사합니다. ^^)

  • VS.71: Visual Studio 2003 (내부 버전 7.1, Codename: Everett) + .NET Framework 1.1에 대응되는 문서로 내용 보기
  • VS.80: Visual Studio 2005 (내부 버전 8.0, Codename: Whidbey) + .NET Framework 2.0에 대응되는 문서로 내용 보기
  • VS.85: Visual Studio 2005 (내부 버전 8.0, Codename: Whidbey) + .NET Framework 3.0 (Codename: WinFX)에 대응되는 문서로 내용 보기
  • VS.90: Visual Studio 2008 (내부 버전 9.0, Codename: Orcas) + .NET Framework 3.5에 대응되는 문서로 내용 보기
  • VS.95: Visual Studio 2008 (내부 버전 9.0, Codename: Orcas) + Silverlight v2.0 / v3.0에 대응되는 문서로 내용 보기 (실버라이트 개발 환경에 해당)
  • VS.100: Visual Studio 2010 (내부 버전 10.0, Codename: Hawaii / DEV10) + .NET Framework 4.0에 대응되는 문서로 내용 보기

사용 방법 1 - Silverlight 도움말 중 한국어 UI를 사용하고, 한국어로 번역된 문서를 기본으로 하여 System.Windows 네임스페이스의 문서 내용을 살펴 봅니다: http://msdn.microsoft.com/ko-kr/system.windows(VS.95).aspx

사용 방법 2 - Silverlight 도움말 중 한국어 UI를 사용하고, 영어로 번역된 문서를 기본으로 하여 System.Windows 네임스페이스의 문서 내용을 살펴봅니다.: http://msdn.microsoft.com/ko-kr/system.windows(en-us,VS.95).aspx

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

2009년 7월 6일에, Microsoft가 기존의 Windows Related Protocol에 대하여 한정적으로 시행해왔던 Community Promise를 ECMA-334와 ECMA-335에 대해서도 적용을 시작한다는 공식 발표가 있었습니다. ECMA-334는 C# 프로그래밍 언어에 대한 사양을, ECMA-335는 Common Language Infrastructure (CLI)에 대한 사양을 정의하는 표준안입니다.

이 발표를 통하여, 이전까지는 자생적인 차원에서 진행되었던 오픈 소스 프로젝트들이, ECMA-334와 ECMA-335 사양을 구현하기 위하여 직접 Microsoft와 의사 교류를 진행할 수 있음은 물론 라이선스 계약서 등에 동의해야 할 필요가 없습니다. 즉, Community Promise 정책 아래에 놓여있는 표준 사양에 대한 모든 활동이 자유롭게 진행될 수 있음을 뜻합니다.

좀 더 자세한 내용은 아래 링크들을 참조하시면 편리합니다.

앞으로는 현재보다 더 많은 수의 닷넷 호환 프레임워크들이 시중에 등장할 수 있는 여건이 마련될 것으로 보입니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

노트: 이 아티클은 Microsoft Visual C# 컴파일러 버전 3.0을 기준으로 작성되었습니다. Mono C# 컴파일러에서는 아직 확인되지 않은 기능입니다.

Extension Method는 대체로 특정 형식과 함께 그 형식을 부모 형식으로 사용하는 다른 형식들에 일괄적으로 영향을 주도록 선언이 설정됩니다. 상속 관계에 있어서는 편리하게 사용할 수 있지만, 비슷한 인터페이스들을 하나 이상 동시에 구현하는 여러 종류의 형식을 선택하는 방법이 잘 보이지 않았습니다.

구현하고자 하는 인터페이스의 수가 늘어나게 되면 일정한 패턴을 형성하게 되는데, 이러한 패턴을 Generic에 대입하고, 이러한 형식 패턴에 대하여 Extension Method를 추가하는 방법을 이번 Article에서 소개하고자 합니다.

다음의 예제 코드를 살펴보기로 합니다.

public static class SpecialExtensions
{
    public static string SpecialMethod<T>(this T target)
        where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
    {
        return target.ToString();
    }
}

위의 코드에서 제네릭 형식 T는 where 절의 서술에 따라 다음의 조건을 만족해야 하는 특수성을 가지게 됩니다.

  • struct 키워드가 사용되었기 때문에 값 형식에 속해야 한다.
  • IComparable 인터페이스를 구현하고 있어야 한다.
  • IFormattable 인터페이스를 구현하고 있어야 한다.
  • IConvertible 인터페이스를 구현하고 있어야 한다.
  • IComparable<T> 인터페이스를 구현하고 있어야 한다. 이 때 SpecialMethod<T> 메서드의 인자 T를 그대로 전달하게 된다.
  • IEquatable<T> 인터페이스를 구현하고 있어야 한다. 이 때 SpecialMethod<T> 메서드의 인자 T를 그대로 전달하게 된다.

위의 where 절에서 언급한 이와 같은 인터페이스 구현 패턴은 Primitive Type에 대한 설정으로, 아래와 같이 작동여부가 선택적으로 바뀌게 됩니다. 즉, 구조체이거나 나열 상수라서 모두 SpecialMethod 메서드를 사용할 수 있는 것이 아니고 위의 인터페이스 구현 패턴을 따르는 Primitive Type에 대해서만 SpecialMethod 메서드의 호출이 허용되게 됩니다.

true.SpecialMethod(); // 실패: IFormattable 인터페이스가 구현되지 않음
char.MaxValue.SpecialMethod(); // 실패: IFormattable 인터페이스가 구현되지 않음
Guid.Empty.SpecialMethod(); // 실패: IConvertible 인터페이스가 구현되지 않음
UriFormat.SafeUnescaped.SpecialMethod(); // 실패: IComparable<T>, IEnumerable<T> 인터페이스가 구현되지 않음

byte.MaxValue.SpecialMethod(); // 성공
sbyte.MaxValue.SpecialMethod(); // 성공
short.MaxValue.SpecialMethod(); // 성공
ushort.MaxValue.SpecialMethod(); // 성공
int.MaxValue.SpecialMethod(); // 성공
uint.MaxValue.SpecialMethod(); // 성공
long.MaxValue.SpecialMethod(); // 성공
ulong.MaxValue.SpecialMethod(); // 성공
float.MaxValue.SpecialMethod(); // 성공
double.MaxValue.SpecialMethod(); // 성공
decimal.MaxValue.SpecialMethod(); // 성공

또 다른 방면으로 where 절의 특수 조건 절을 적절히 활용하면, 추상 클래스와 일반 클래스 사이를 선택하여 Extension Method가 달리 선언될 수 있도록 제어하는 것도 가능합니다. 이와 같이 상속 방향 이외의 진행 방향으로 다양하게 Extension Method의 지정 범위를 택하는 기술을 이용하면 좀 더 구체적이고 깊이있는 요구 사항을 처리할 수 있습니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

app.config 파일이나 web.config 파일은 닷넷 기반 응용프로그램에서 응용프로그램의 동작이나 세세한 설정을 제어하는 유용한 파일입니다. 특히 web.config 파일은 ASP.NET 사이트 구축과 활용에 있어서 키 포인트가 되곤 하지요. 하지만 실전에서 web.config 파일은 생각보다 자주 바뀌어야 하고 그 때마다 번번히 동적 컴파일이 일어난다면 생각보다 용이하지 않고 불편할 것입니다. (특히 세션 서버를 외부에 배치하지 않고 In-Process 모드로 이용하여 구축한 경우 동적 컴파일이 발생할 때 마다 모든 사용자가 로그아웃되는 불상사가 발생하기도 합니다.)

이러한 문제점을 해결해주는 것이 configSource 어트리뷰트입니다. configSource 어트리뷰트를 이용하여 특정 Configuration Section의 Subset을 다른 XML 파일로부터 가져오도록 처리할 수 있으며, 해당 XML 파일의 내용이 바뀌더라도 동적 컴파일이 발생하지 않도록 유연성을 확보할 수 있습니다. 다음의 예시를 살펴보기로 하겠습니다. 바꿔야 할 상황이 상대적으로 많은 appSettings 섹션과 connectionStrings 섹션을 예로 들어봅니다.

<appSettings configSource="App_Data\AppSettings.xml"/>
<connectionStrings configSource="App_Data\ConnectionStrings.xml"/>

매우 간단합니다! configSource에 상대 경로만 지정해주면 동적 컴파일에 대한 걱정 없이 편리하게 내용을 바꿀 수 있습니다. 그러면 실제로 App_Data\AppSettings.xml 파일과 ConnectionStrings.xml 파일의 내용을 살펴보도록 하겠습니다.

<!-- AppSettings.xml 파일 예시 -->
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
<appSettings>
    <!-- Session 위조/변조를 막기 위하여 사용합니다. -->
    <add key="SessionValidationKey" value="4BD283757A7F42E3B76A79DB87A6865DB5117E17F20D4B5CADCC953714C9668B4623B2E0E369485A8C7CD06B2B2CC3F4" />

    <!-- 저작권 시작 연도 표기입니다. (종료 연도는 자동으로 현재 서버 시스템 날짜를 기준으로 확장됩니다.) -->
    <add key="CopyrightBeginYear" value="2008" />

    <!-- 저작권자 명칭입니다. -->
    <add key="CopyrightOrgName" value="rkttu" />

    <!-- 저작권자 웹 사이트 주소입니다. -->
    <add key="CopyrightOrgUrl" value="" />
   
    <!-- 관리자 E-MAIL 주소입니다. -->
    <add key="AdminContact" value="" />

    <!-- 첨부 파일 저장 경로입니다. -->
    <add key="ResourceFilePath" value="C:\Contents" />

    <!-- 야후 API 키입니다. -->
    <add key="YahooAPIKey" value="k_LUIAbV34H9SK4UdGAN.LKVC9urWOfxKjXe00eQ2IsO_e4RsoO8djl1gfc-" />

    <!--
        클라이언트가 지원할 경우 GZIP이나 Deflate 압축을 사용할지 여부입니다.
        GZIP과 Deflate 압축을 동시에 지원하는 경우 GZIP을 사용하도록 설계되었습니다.
        NOTE: 서버의 자체적인 GZIP이나 Deflate 압축을 사용하는 경우 아래 설정을 False로 지정하십시오.
        -->
    <add key="CompressEnabled" value="False" />
</appSettings>


<!-- ConnectionStrings.xml 파일 예시 -->
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
<connectionStrings>
    <add
        name="MyConnectionString"
        connectionString="Data Source=localhost;Initial Catalog=pubs;Persist Security Info=True;User ID=sa;Password=tiger"
        providerName="System.Data.SqlClient" />
</connectionStrings>

기본 XML 파일의 형식을 유지한채 해당 Section의 내용을 Root로 삼아 각각의 파일의 내용이 들어있는 것을 확인하실 수 있습니다. standalone 어트리뷰트를 no로 표기한 것은 개인적인 편의를 위하여 사용한 것이므로 실제로는 중요하지 않습니다. (asp.net 처리기에는 영향이 없는 사항입니다. ^^)

이 파일들을 App_Data 폴더 안에 보관한 이유는 단순합니다. 이러한 형태의 XML 파일이 배치되기에 제일 적당하고, 내용이 노출되어서는 안될 사항들이므로 App_Data 폴더 밑에 배치한 것입니다. 실제로 이들 XML 파일을 찾기 위하여 URI를 입력해보면 403 또는 404 오류 코드가 반환되는 것을 보실 수 있습니다.

이 방법에는 한 가지 문제점이 존재하는데, Visual Studio의 웹 프로젝트 - 또는 - Visual Web Developer와의 호환성 문제입니다. Service Pack 1 버전을 이용하면 완화되지만 초기 버전의 경우 이렇게 configSource로 분리 지정하였을 경우 .NET Framework 3.5와 연결되도록 설정하였음에도 이를 인지하지 못하고 강제로 web.config 파일의 내용을 채워버립니다.

configSource 어트리뷰트가 지정된 섹션은 XML 규약 상으로는 올바르지만 asp.net 처리기의 입장에서 수용할 수 없는 설정으로 바뀝니다. 즉, configSource 어트리뷰트와 일반 설정을 동시에 포함할 수 없는 것이 현재의 한계입니다. (좀 더 신경을 썼다면 configSource의 내용을 먼저 읽고 나중에 지정된 설정을 따로 읽어 override하는 것으로 간주하는 것이 좋을것 같은데 아쉬운 부분입니다.) 이로 인하여 asp.net 프로젝트가 꼬이는 경우가 있을 수 있으니 버전이 낮은 개발 도구를 이용하여 웹 사이트 프로젝트를 열지 않도록 신경써주시기 바랍니다.

또한, Custom Tag를 포함하는 내용이나 Namespace Include 설정은 Visual Studio의 인텔리센스 기능을 자주 활용하시는 분이라면 별도로 분리하지 마시고 가급적 web.config 파일 안에 그대로 두시기 바랍니다. 인텔리센스가 다행히 이를 잘 처리해주기는 하나, 속도가 많이 느립니다.

이쯤에서 유용한 리스팅 하나를 공개하고 글을 마치겠습니다. configSource 어트리뷰트를 지원하는 XML 요소들을 나열해 보겠습니다. (완전하지 않을 수 있습니다만 보편적인 사례를 들어서 써봅니다.)

  • /configuration/appSettings
  • /configuration/configProtectedData
  • /configuration/connectionStrings
  • /configuration/system.codedom
  • /configuration/system.net/connectionManagement
  • /configuration/system.net/settings
  • /configuration/system.net/requestCaching
  • /configuration/system.net/authenticationModules
  • /configuration/system.net/defaultProxy
  • /configuration/system.net/webRequestModules
  • /configuration/system.net/mailSettings/smtp
  • /configuration/system.web/anonymousIdentification
  • /configuration/system.transactions/defaultSettings
  • /configuration/system.transactions/machineSettings
  • /configuration/system.web/authentication
  • /configuration/system.web/authorization
  • /configuration/system.web/caching/cache
  • /configuration/system.web/caching/outputCache
  • /configuration/system.web/caching/outputCacheSettings
  • /configuration/system.web/caching/sqlCacheDependency
  • /configuration/system.web/clientTargets
  • /configuration/system.web/compilation
  • /configuration/system.web/customErrors
  • /configuration/system.web/deployment
  • /configuration/system.web/deviceFilters
  • /configuration/system.web/globalization
  • /configuration/system.web/healthMonitoring
  • /configuration/system.web/hostingEnvironment
  • /configuration/system.web/httpCookies
  • /configuration/system.web/httpHandlers
  • /configuration/system.web/httpModules
  • /configuration/system.web/httpRuntime
  • /configuration/system.web/identity
  • /configuration/system.web/machineKey
  • /configuration/system.web/membership
  • /configuration/system.web/mobileControls
  • /configuration/system.web/pages
  • /configuration/system.web/processModel
  • /configuration/system.web/profile
  • /configuration/system.web/roleManager
  • /configuration/system.web/securityPolicy
  • /configuration/system.web/sessionState
  • /configuration/system.web/sessionStatePolicy
  • /configuration/system.web/siteMap
  • /configuration/system.web/trace
  • /configuration/system.web/trust
  • /configuration/system.web/urlMappings
  • /configuration/system.web/webControls
  • /configuration/system.web/webParts
  • /configuration/system.web/webServices
  • /configuration/system.web/xhtmlConformance
  • /configuration/system.web.extensions/scripting/scriptResourceHandler
  • /configuration/system.web.extensions/scripting/webServices/jsonSerialization
  • /configuration/system.web.extensions/scripting/webServices/authenticationService
  • /configuration/system.web.extensions/scripting/webServices/profileService
  • /configuration/system.web.extensions/scripting/webServices/roleService
  • /configuration/system.windows.forms
  • /configuration/system.xml.serialization/dateTimeSerialization
  • /configuration/system.xml.serialization/schemaImporterExtensions
  • /configuration/system.xml.serialization/xmlSerializer

참고로, IIS 7.0에서 사용하는 <system.webServer> 섹션은 configSource 어트리뷰트를 지원하지 않는 IIS 7.0만의 고유한 설정입니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)