본문 바로가기

IT 살이/04. 기술 - 프로그래밍

개발 프레임워크 만들기 대장정 17 - Unity 컨테이너 확장예제 II부

지난 포스트에 계속 이어진다. 혹시 지난 포스트를 읽지 않았다면 먼저 체크해보고 이번 포스트를 읽어가길 바란다. 이번 포스트 다 써놓고 보니 무지 길다. 실시간으로 이해를 하면서 쓰는 글이다 보니 이 모양이다. 다시 읽어보고 싶지 않다. 쓰으...틀린곳 또는 이해가 되지 않은 부분 있다면 코멘트 부탁한다.

■ 예제 시나리오

이 예제에서는 두 개의 Unity 컨테이너를 생성한다. 하나는 표준 컨테이너 stdContainer이고 하나는 확장된 기능을 갖는 customContainer이다. stdContainer는 .NET 애플리케이션이 실행되면 자동으로 생성되는 기본 AppDomain으로 어셈블리를 로딩한다. 그리고 customContainer는 기본 AppDomain에서 별도의 AppDomain을 하나 더 생성해서 그쪽으로 어셈블리를 로딩한다. 이때 추가로 생성되는 AppDomain은 섀도우 복사를 지원하는 것으로서 로딩할 어셈블리를 ShadowFiles 속성에 설정된 위치에서 검색한다. 그런 다음 임시 복사 위치로 복사한다. 가릿? OK! 아래 그림을 보라.

양쪽 AppDomain으로 각각의 어셈블리를 로딩하고 그곳에서 특정 타입( StdClass, SatClass)의 객체를 생성해서 각각의 Speak() 메소드를 호출한다. 근데, 이전 포스트에서 각각의 AppDomain은 독립적으로 작동한다고 해 놓고서는 그림을 보면 기본 AppDomain에 있는 customContainer객체가 추가 생성된 AppDomain의 객체B의 메소드를 호출하고 있다. 쓰으. 필자도 예상 못했던 부분이다. 예제를 대충 훓어봤을때는 AppDomain의 기본 개념내에서 예제가 작성되었을 줄 알았다. 이렇게 AppDomain간에 통신이 가능하다는 것을 예제로 바로 보여줄 줄은 몰랐다. 뭐 그렇다고 크게 당황하지 말자. 분명 AppDomain은 static 멤버조차도 서로 공유하지 않는 독립적인 공간이다. 그러나 두 공간간의 통신도 가능한데 물론 조건이 있어야 한다. 뒤에 예제 소스 코드를 보면 알겠지만,  객체B의 타입 정의에서처럼 MarshalByRefObject 타입을 상속받아야 한다. MarshalByRefObject에 대해서는 여기까지!

추가로 생성된 AppDomain이 타입 SatClass가 AssemblyB.dll에 포함되어 있다는 것을 알고( 어떻게 알까? config에 설정해놨다) 그 어셈블리를 찾는다. 뒤에서 추가로 AppDomain이 생성되는 부분( MyExtension의 CreateDomain() 부분)을 살펴보면 알겠지만 추가 AppDomain의 기본 디렉토리는 ShadowFiles 로 설정한 곳이다. 그러나 추가 AppDomain은 이곳에 있는 어셈블리를 직접 로딩하지 않는다. 추가 AppDomain은 섀도우 복사를 하는 도메인으로 설정되어 있기때문에 일단 임시 복사본 폴더로 어셈블리를 복사를 한다. 그런 다음 그 복사본을 AppDomain으로 로딩한다. 외부의 개발자는 이제 언제든지 AssemblyB.dll 파일의 내용을 변경해서 다시 ShadowFiles 폴더로 원본을 덮어쓸 수 있다. 그럼 다음에 생성되는 추가 AppDomain은 새로운 버전의 것을 다시 섀도우 복사해서 사용한다. 이 예제에서는 ShadowFiles 폴더로 새로운 버전의 어셈블리를 복사한 다음, 추가 AppDomain을 내렸다가 다시 올리는 작업은 수동으로 하고 있다.

FileSystemWatcher같은 녀석들을 사용해서 ShadowFiles 폴더의 파일이 변경되면 자동으로 추가 AppDomain을 내리고 다시 생성하는 작업을 하도록 해주면 좀 더 멋있는 예제가 될 것이다. 이것이 ASP.NET의 시스템의 중단없는 애플리케이션 업데이트 내부 구조이다(맞나?).

그러나 다시 한번 말하지만, 이 예제는 예제의 멋을 부릴려고 작성한 것은 아니다. 예제에서 보이고 있는 새로운 strategy는 외부에서 지정한 특정 AppDomain으로 객체를 생성하는 일을 한다.  이 예제에서는 이런 strategy를 만드는 방법 그리고 그것을 Unity 컨테이너에 추가하는 방법을 보이고자 한 것이다.  가릿?

■ 컨테이너 확장 절차

우선 예제에서 strategy를 만들고 Unity 컨테이너를 확장하는 절차에 대해서 전체적으로 알아보도록 하자. 바로 코드로 헤딩하는 것보다는 이렇게 절차에 대한 그림을 염두에 두고 분석해나가는 것이 코드 이해에 도움이 될 것이다.

필요한 타입을 작성하고 config 파일에 등록하는 작업을 반복하고 있다. 물론 어떤 순으로 하든 상관없다. Unity 컨테이너 익스텐션을 하려면 이런 작업들이 있다는 것만 머리에 그리면 되겠다. 각 단계별 작업을 좀 더 알아본다. 그리고 새롭게 작성하는 타입들의 베이스 타입을 보여주고 있다. 이 베이스 타입은 이 예제에서 사용하고 있는 타입이다. 타입 hierarchy상 정확히 이 타입이 아니고 좀더 부모 타입이거나 좀 더 자식 타입이어도 될 것이다. 

단계 하는일 베이스 타입
Strategy 작성 ( MyBuildup ) 객체 빌드/해제하는 동안 하고 싶은 일을 구현하는 곳
예제에서는 외부에서 설정해준 특정 AppDomain으로 객체를 생성하는 일을 한다.
IBuildStrategy
Extension 작성( MyExtension) i) MyBuildup 객체를 생성해서 strategy 체인에 추가한다.
ii) 섀도우 복사용 AppDomain 객체를 생성해서 MyBuildup 객체에 건네준다.
UnityContainerExtension
Config 등록 앞에서 작성한 MyExtension을 config 파일에 등록한다.  
Extension Config 핸들러 작성 i) Extension용 config 설정 정보를 파싱해서 가지고 있다.
ii) Extesion 객체를 config 정보를 이용해서 초기화하거나 필요한 작업을 해준다.
UnityContainerExtensionConfigurationElement
Config 등록 앞에서 작성한 Extension Config 핸들러를 config 파일에 등록한다.  
테스트용 객체 타입 생성 현 예제에서는 두 개의 도메인에 각각 로딩될 StaClass와 SatClass 타입을 정의하고 있다. 그리고 두 개의 타입은 IMyInterface를 구현하고 있다. 이 인터페이스는 간단한 Speak() 메소드만을 정의하고 있다. 그리고 앞의 SatClass는 MarshalByRefObject를 상속하고 있다. 왜?  타입의 객체는 추가생성되는 AppDomain에서 인스턴싱된다. 그러면서 기본 AppDomain에서 호출되고 있다. 그렇게 되려면 원격 객체는 이 타입을 상속받아야 한다고 했다.  
테스트용 객체 타입 등록 앞에서 작성한 테스트용 객체 타입을 config에 등록한다.  

■ 소스 설명

이제 구현되어 있는 예제 소스를 보도록 하자. 블로그에서는 샘플 코드 조각들만을 제시하고 있어서 한번 실행시켜 보기 위해서는 수동으로 개발 구조를 재구성해야 했다. 프로젝트명이 정확히 일치하지는 않는다. 특히 UnityExtension 프로젝트는 원래 AppDomainUnity이었다. 그러나 익스텐션 부분이라는 것을 좀 더 강조하고 싶어서 앞의 이름을 사용하기로 했다. 기분나빠하려나? 쓰으... 

AppDomainUnity.AssemblyA, AppDomainUnity.AssemblyB, AppDomainUnity.Common 세개의 프로젝트는 테스트용 비즈니스 객체들이다.  Common에 인터페이스 IMyInterface가 정의되어 있고 AssemblyA와 AssemblyB에는 이것을 구현하고 있는 StdClass와 SatClass가 있다.

public interface IMyInterface

{

    string Speak();

}

namespace NickField.AppDomainUnity.AssemblyA

{

    public class StdClass : IMyInterface

    {

        public string Speak()

        {

            return "I'm StdClass object";

        }

    }

}

namespace NickField.AppDomainUnity.AssemblyB

{

    public class SatClass : MarshalByRefObject, IMyInterface

    {

        public string Speak()

        {

            return "I'm SatClass object";

        }

    }

}

StdClass의 객체는 표준 컨테이너 stdContainer에 로딩되어 호출될 것이고, StaClass의 객체는 확장 컨테이너 customContainer에 로딩되어 호출된다. 원격에 생성될 타입 SatClass는 MarshalByRefObject를 상속하고 있다. 원격 AppDomain에 어셈블리가 로딩된다면 클라이언트 AppDomain에서는 어떻게 그것의 타입을 알 수 있을까?  타입을 등록하고 있는 config 파일을 함 보자. 다음은 예제에서 사용하고 있는 전체 config 내용이다.

\par ?? <\cf3 container\cf1 >\par ?? <\cf3 types\cf1 >\par ?? <\cf3 type\cf1 \cf4 type\cf1 =\cf0 "\cf1 IMyInterface\cf0 "\cf1 \cf4 mapTo\cf1 =\cf0 "\cf1 AssemblyAObject\cf0 "\cf1 \cf4 name\cf1 =\cf0 "\cf1 local\cf0 "\cf1 />\par ?? \par ?? \par ?? \par ?? <\cf3 container\cf1 \cf4 name\cf1 =\cf0 "\cf1 customContainer\cf0 "\cf1 >\par ?? <\cf3 types\cf1 >\par ?? <\cf3 type\cf1 \cf4 type\cf1 =\cf0 "\cf1 IMyInterface\cf0 "\cf1 \cf4 mapTo\cf1 =\cf0 "\cf1 AssemblyBObject\cf0 "\cf1 \cf4 name\cf1 =\cf0 "\cf1 remote\cf0 "\cf1 />\par ?? \par ?? <\cf3 extensions\cf1 >\par ?? <\cf3 add\cf1 \cf4 type\cf1 =\cf0 "\cf1 NickField.UnityExtension.MyExtension, UnityExtension\cf0 "\cf1 />\par ?? \par ?? <\cf3 extensionConfig\cf1 >\par ?? <\cf3 add\cf1 \cf4 name\cf1 =\cf0 "\cf1 MyExtensionConfigHandler\cf0 "\cf1 \cf4 type\cf1 =\cf0 "\cf1 NickField.UnityExtension.MyExtensionConfigHandler, UnityExtension\cf0 "\cf1 \par ?? \cf4 domain\cf1 =\cf0 "\cf1 CustomAppDomain\cf0 "\cf1 \cf4 shadowFiles\cf1 =\cf0 "\cf1 Shadow\cf0 "\cf1 />\par ?? \par ?? \par ?? \par ??\par ??} -->

<unity>

  <typeAliases>

    <typeAlias alias="IMyInterface"   

                      type="NickField.AppDomainUnity.Common.IMyInterface, AppDomainUnity.Common" />

    <typeAlias alias="AssemblyAObject" 

                      type="NickField.AppDomainUnity.AssemblyA.StdClass,  AppDomainUnity.AssemblyA" />

    <typeAlias alias="AssemblyBObject" 

                      type="NickField.AppDomainUnity.AssemblyB.SatClass,  AppDomainUnity.AssemblyB" />

  </typeAliases>                             

  <containers>

    <!-- Default Container -->

    <container>

      <types>

        <type type="IMyInterface" mapTo="AssemblyAObject" name="local" />

      </types>

    </container>

    <!-- Dynamic Container -->

    <container name="customContainer">

      <types>

        <type type="IMyInterface" mapTo="AssemblyBObject" name="remote" />

      </types>

      <extensions>

        <add type="NickField.UnityExtension.MyExtension, UnityExtension" />

      </extensions>

      <extensionConfig>

        <add name="MyExtensionConfigHandler" 

                   type="NickField.UnityExtension.MyExtensionConfigHandler, UnityExtension"

            domain="CustomAppDomain" shadowFiles="Shadow" />

      </extensionConfig>

    </container>

  </containers>

</unity>

기본 컨테이너에는 "AssemblyAObject" 즉 "NickField.AppDomainUnity.AssemblyA.StdClass,  AppDomainUnity.AssemblyA"만이 등록되어 있다. 그리고 확장 컨테이너에는 "AssemblyBObject" 즉 "NickField.AppDomainUnity.AssemblyB.SatClass,  AppDomainUnity.AssemblyB"만이 등록되어 있다. 이것은 기본 컨테이너에서는 SatClass 타입에 대해서는 알 수 없다는 것이다. 그럼 어떻게 확장 컨테이너의 객체 B에 대한 참조를 받아올 수 있겠나?

다행히도 config를 보면 SatClass 타입이 구현하고 있는 IMyInterface를 기본 컨테이너에서도 알고 있다는 것이다. 바로 기본 컨테이너에서 별칭으로 해서 사용하고 있는 type="IMyInterface"가 <typeAlias>요소에 선언되어 있는 type="NickField.AppDomainUnity.Common.IMyInterface, AppDomainUnity.Common" 을 가리키고 있는데 그것이 바로 SatClass에서 구현하고 있는 베이스 타입이기도 하다. 이 인터페이스 타입을 통해서 기본 컨테이너에서도 SatClass 객체에 대한 참조를 할당해줄 수 있는 것이다. ( 앞의 그림 참조) 말이 너무 어렵나? 쓰으...

지금 필자가 하고 싶은 말은 그러나 이것이 아니다.  config 스키마에 대한 설명을 하려는 것이다. 즉 <container/>요소별로는 각각 사용하고자 하는 실제(concrete) 타입을 등록하고 그리고 각각의 컨테이너에서 공통으로 사용하는 타입은 이렇게 <container/>상위에 있는 <typeAlias/>를 사용하면 되지 않을까 하는 생각이다. 실제로 <typeAliases> 및 <typeAlias>가 이런 목적으로 정의되었는지는 모르겠지만 말이다. 별걸 아닌것을 힘들게 말했나? 야튼 이 인사, 어렵게 만드는데는 뭔가 있다니까. 이게 중요한 게 아닌데.

이제 컨테이너를 확장하는 프로젝트 UnityExtension 부분을 보도록하자. 

▶ 사용자정의 strategy 작성

namespace NickField.UnityExtension

{

    public class MyBuildup : IBuilderStrategy

    {

        AppDomain _AppDomain;

        public AppDomain AppDomain

        {

            set

            {

                _AppDomain = value;

            }

            get

            {

                return _AppDomain;

            }

        }

        public void PreBuildUp(IBuilderContext context)

        {

            Type targetType = BuildKey.GetType(context.BuildKey);

            if (AppDomain != null)

            {

                context.Existing = AppDomain.CreateInstanceAndUnwrap(targetType.Assembly.FullName, targetType.ToString());

                context.BuildComplete = true;

            }

        }

        public void PostBuildUp(IBuilderContext context) { }

        public void PreTearDown(IBuilderContext context) { }

        public void PostTearDown(IBuilderContext context) { }

    }

}

MyBuildup은 AppDomain 인스턴스에 대한 참조를 내부적으로 가지고 있다. 그리고 이 인스턴스는 외부에서 설정될 것이다(어떻게 아냐고? 내부에서 생성하는 곳이 없으니까-_-;;).  그리고 사용자 정의 strategy 타입 MyBuildup은 IBuilderStrategy를 상속하고 있다. 이 인터페이스는 앞에서 설명한 것처럼 네개의 메소드를 정의하고 있다. 이 중에서는 MyBuildup은 하나의 메소드를 구현하고 있다 : PreBuildUP().

이 메소드에서는 외부에서 생성한 AppDomain 인스턴스의 CreateInstanceAndUnwrap() 이용해서 추가된 AppDomain으로 지정한 타입의 객체를 로딩하는 작업을 한다. 그리고 그것에 대한 참조를 반환한다. 이거 좀 이상하지 않는가? 필자는 처음에 CreateInstanceAndUnwrap() 메소드가 단순히 주어진 타입에 대한 인스턴스를 생성해서 반환하는 것으로만 생각했다. 그래서 좀 더 구글링을 해보니 객체를 동적으로 생성해서 참조를 반환해줄뿐만 아니라 이 메소드를 호출한 AppDomain 인스턴스로 생성한다는 것을 알게 되었다. 나만 이제 알게 되었나? 야튼 정말 targetType이 포함된 어셈블리를 추가 AppDomain으로 로딩해서 그곳에서 객체를 생성했을까? 영문이긴 하지만 궁금하다면 다음 블로그를 참조할 수 있다.

Executing Code in Another AppDomain(http://blogs.msdn.com/suzcook/archive/2003/06/12/57169.aspx)

CreateInstanceAndUnwrap()에서 넘겨 받은 객체에 대한 참조는 현재 빌드 컨텍스트 객체의 Existing 속성에 할당하고 있다. 컨텍스트라는 용어가 나오는데 앞에서도 "애플리케이션 컨텍스트"라고 해서 한번 들어봤다. 필자도 어떻게 이 녀석을 설명하면 좋을지 고민이다. "애플리케이션 컨텍스트"라 하면 애플리케이션 차원에서 필요한 정보가 있다면 이 객체를 통해서 얻을 수 있다. 만약 빌드 컨텍스트라고 하면 Unity 컨테이너 정확히 ObjectBuild가 객체를 생성해서 반환해 주기까지의 과정 즉 객체를 빌드해가는 과정에 대한 정보를 가지고 있는 객체라고 할 수 있겠다. 과정중에서 추가한 정보는 뒤에 오는 단계에서 컨텍스트를 통해서 접근할 수 있다. 도중에 정보를 제거할 수도 있다. 가릿?

생성한 객체를 빌드 컨텍스트 객체의 Existing 속성에 할당하고 나서 BuildComplete 라는 속성을 true로 하고 있다. BuildComplete라는 속성이 뭔지 도움말을 찾아봤더니 현재 빌드하고 있는 객체는 이제 완전하니 더 이상 이 객체에 어떤 작업도 하지 말하는 표시라고 한다. 즉 이 후에 올 strategy 체인에서는 어떤 작업도 이 객체에 하지 않게 된다. 가릿? OK

MyBuildup 타입은 AppDomainUnity 어셈블리의 NickField.UnityExtension 네임스페이스에 포함되어 있다.

▶  Extension 작성

사용자 정의 strategy를 strategy 체인에 추가하는 녀석이 누구라고 했는지 기억하나? 앞의 표를 참고하라. Unity에서는 익스텐션이라고 부르는 넘이다. 이제 MyBuildup strategy를 체인에 등록할 익스텐션 MyExtension을 작성한다.

namespace NickField.UnityExtension

{

    public class MyExtension : Microsoft.Practices.Unity.UnityContainerExtension, IDisposable

    {

        private MyBuildup strategy = new MyBuildup();

        private bool disposed;

        protected override void Initialize()

        {

            Context.Strategies.Add(strategy, UnityBuildStage.PreCreation);

        }

        /// <summary>    /// Build the new appdomain allowing   

        /// </summary>    /// <param name="domain"></param>   

        /// <param name="shadowFiles"></param>   

        public void CreateAppDomain(string domain, string shadowFiles)

        {

            AppDomainSetup appDomainSetup = new AppDomainSetup();

            appDomainSetup.ApplicationBase = Environment.CurrentDirectory + @"\" + shadowFiles;

            appDomainSetup.ShadowCopyFiles = "true";

            strategy.AppDomain = AppDomain.CreateDomain(domain, null, appDomainSetup);

        }

        ...

    }

}

MyExtension에서는 처음 부분을 보면 아예 사용자 정의 strategy MyBuilup 객체를 하나 생성해서 가지고 있다.

        private MyBuildup strategy = new MyBuildup();

이 코드를 보면 컨테이너를 통해서 MyBuildup 객체를 얻는 것이 아니라 직접 new를 사용해서 그 객체를 얻고 있다. MyBuildup이 config에 등록되지 않은 것을 보니, MyExtension의 전용 strategy라는 것을 추측할 수 있다. 왜 그런 추측을 하냐고? 원래 Unity 컨테이너의 목적이 타입의 직접 참조가 아닌 config 등록에 의한 동적 참조를 그 목적으로 하고 있기 때문에. 뭔 소린지 원. 야튼 중요한 것은 strategy 객체가 있다면 이것을 체인에 등록해주는 전용 익스텐션을 두는 것을 확장의 표준 구조로 생각해도 될 것이라는 것이다.

Initialize() 메소드에서 바로 strategy 체인에 등록하는 코드가 나오고 있다.

        protected override void Initialize()

        {

            Context.Strategies.Add(strategy, UnityBuildStage.PreCreation);

        }

이 녀석의 override 되고 있는 것을 보니 상위 타입 또는 그보다 더 상위 타입 어딘가에서 자동 호출되고 있을 것이라는 추측을 해 볼 수 있다. Unity에서 정의한 stage 중에서 preCreation이라는 단계의 strategy 목록에 추가하고 있다. 즉 익스텐션을 초기화하는 과정에서 체인에 등록하는 작업을 해주면 되겠다는 것을 알 수 있다.

그리고 그 아래에 public으로 된 CreateAppDomain() 메소드가 정의되어 있다.

        public void CreateAppDomain(string domain, string shadowFiles)

        {

            AppDomainSetup appDomainSetup = new AppDomainSetup();

            appDomainSetup.ApplicationBase = Environment.CurrentDirectory + @"\" + shadowFiles;

            appDomainSetup.ShadowCopyFiles = "true";

            strategy.AppDomain = AppDomain.CreateDomain(domain, null, appDomainSetup);

        }

이곳에서 바로 MyBuilup이 객체 빌드시 사용하게 될 AppDomain을 생성해서 그 참조를 MyBuildup으로 넘겨주고 있다. 

CreateAppDomain() 메소드를 보면 인자로 domain과 shadowFiles 두개의 문자열을 받고 있다. domain은 AppDomain을 생성할 때 그리고 shadowFiles값은 특히 섀도우용 AppDomain을 생성할때 필요한 값이다.

이 코드 설명을 잠시 하자면, ShadowCopyFiles 속성에 "true"값을 설정해서 현재 생성되는 AppDomain 인스턴스가 섀도우 복사를 지원하도록 하고 있다. 그리고 ApplicationBase 속성을 config에 설정된 shadowFiles 값으로 하고 있다. 즉 기본 AppDomain의 현재 디렉토리에 하위 폴더로 shadowFiles에 설정한 디렉토리가 있어야 하고 이 AppDomain에서 사용할 타입을 포함하고 있는 어셈블리들이 있어야 한다. 이 값들은 변경 가능성이 다분히 있는 것으로서 config 파일에 설정하는 것이 바람직하다.

익스텐션을 작성하면 그것이 필요로하는 정보가 있을 수 있다. 이런 정보는 대부분 그 익스텐션 전용일 것이라는 것이다. 다시 말하면 익스텐션을 제작하게 되면 그 익스텐션 전용 정보를 제공하기 위해서 config 설정을 이용할 수 있으면 편할 것이다. 물론 Unity에서는 그런 시나리오를 지원하고 있다.

▶   MyExtension용 config설정

앞의 전체 config 중에서 익스텐션을 등록하고 그 익스텐션 전용 config 설정을 등록하는 부분이다.

      <extensions>

        <add type="NickField.UnityExtension.MyExtension, UnityExtension" />

      </extensions>

      <extensionConfig>

        <add name="MyExtensionConfigHandler" 

                   type="NickField.UnityExtension.MyExtensionConfigHandler, UnityExtension"

            domain="CustomAppDomain" shadowFiles="Shadow" />

      </extensionConfig> 

사용자 정의 익스텐션 자체는 <extensions>요소에 add 시킬 수 있다. 문법적인 표현으로 하자면 있는 타입은 UnityContainerExtension( 또는 그 베이스 타입인 IUnityContainerExtensionConfigurator)를 상속한 타입이면 이곳에 등록해서 사용할 수 있다.

그리고 사용자 정의 익스텐션에서 사용하는 데이터가 있다면 이것도 config에 설정할 수 있다. <extensionConfig/>요소의 하위 요소 <add/>를 통해서 익스텐션에서 사용하는 데이터를 설정할 수 있다. 이곳의 어트리뷰트 예를 들어 위의 domain, shadowFiles 같은 사용자 정의 어트리뷰트를 필요한대로 추가할 수 있다. 이런 익스텐션을 위한 설정은 Unity 사용자 마음대로 원하는 대로 정의할 수 있는 대신, 이것을 해석할 수 있는 모듈도 Unity 사용자가 제공해줘야 한다. 다시 말하면 익스텐션용 config 설정을 파싱할 수 있는 config 핸들러가 있어야 한다는 것이다. 그 핸들러의 풀네임과 어셈블리가 <add/>의 type 어트리뷰트에서 제공하고 있는 타입의 의미이다. 그 타입은 Unity 컨테이너에 name 어트리뷰트의 이름으로 등록된다. 가릿?  그럼 예제에서의 익스텐션 config 핸들러에 대한 정의를 보도록 한다.

▶   MyExtension용 config 핸들러 작성

     

namespace NickField.UnityExtension

{

    public class MyExtensionConfigHandler : UnityContainerExtensionConfigurationElement

    {

        /// <summary>   

        /// Name of the non-default application domain   

        /// </summary>   

        [ConfigurationProperty("domain")]

        public string Domain

        {

            get { return (string)this["domain"]; }

            set { this["domain"] = value; }

        }

        /// <summary>   

        /// Relative location of replaceable assemblies   

        /// </summary>   

        [ConfigurationProperty("shadowFiles")]

        public string ShadowFiles

        {

            get { return (string)this["shadowFiles"]; }

            set { this["shadowFiles"] = value; }

        }

        /// <summary>   

        /// Assign the new domain name to the custom container   

        /// </summary>   

        /// <param name="container"></param>   

        public override void Configure(Microsoft.Practices.Unity.IUnityContainer container)

        {

            container.Configure<MyExtension>().CreateAppDomain(Domain, ShadowFiles);

        }

    }

}

앞의 표에서 말했지만, 익스텐션 config가 하는 일은 2가지가 있다. 사용자 정의 익스텐션에서 필요로 하는 데이터를 config에서 읽어 가지고 있는다. 그리고 그 데이터를 해당 익스텐션으로 건네주거나 또는 익스텐션을 자동 생성해서 필요한 작업을 해준다.

MyExtensionConfigHandler에 정의된 속성들 Domain, ShadowFiles를 보면 <extensionConfig/> 하위 요소 <add/>의 어트리뷰트 domain, shadowFiles를 나타내고 있다. 그리고 상위 타입의 메소드 Configure()를 오버라이딩하고 있다. 이 메소드에서 MyExtension 타입의 익스텐션 객체를 받아와서 새로운 AppDomain 객체를 생성하는 메소드를 호출하고 있다. 

개발자의 코딩없이,  config에 익스텐션을 등록하고 그리고 익스텐션 config 핸들러를 등록하는 것만으로도 익스텐션 인스턴스를 생성해서 원하는 작업을 해 줄수도 있다는 것이다. 이런 작업을 하는 시기는 언제 ? config 내용을 읽어서 컨테이너 인스턴스를 세팅하는 시기 ! 그런 언제 컨테이너를 세팅하는 것이 가장 좋을까? 컨테이너 인스턴스를 생성하고 나서 바로 !

그럼 컨테이너 인스턴스를 호출하고 바로 config 내용으로 컨테이너 인스턴스를 설정하는 코드를 보겠다. 바로 시작 프로그램에서 하는 작업이다.

▶   시작 프로그램 작성

이제 앞에서 작성한 익스텐션과 익스텐션 config 핸들러를 테스트해 보는 시작 프로그램을 작성한다. MainConsole 이라는 이름으로 콘솔 프로젝트를 하나 추가했다.

다음은 Program 클래스에 대한 정의이다.

class Program

{

    private readonly IUnityContainer stdContainer;

    private IUnityContainer customContainer;

    public Program()

    {

        stdContainer = new UnityContainer();

        customContainer = new UnityContainer();


        // config 정보 읽어들이기

        UnityConfigurationSection section;

        section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");


        //Unity 컨테이너에 config 정보를 설정한다.

        section.Containers.Default.Configure(stdContainer);

        section.Containers["customContainer"].Configure(customContainer);

    }

    public void Unload()

    {

        customContainer.Dispose();

    }

    public void Reload()

    {

        customContainer = new UnityContainer();

        UnityConfigurationSection section;

        section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");

        section.Containers["customContainer"].Configure(customContainer);

    }

    public void WaitForUpdate()

    {

        Console.WriteLine("Container has been unloaded ... waiting for update");

        Console.Read();

    }

    public void Spill()

    {

        Console.WriteLine(stdContainer.Resolve<IMyInterface>("local").Speak());

        Console.WriteLine(customContainer.Resolve<IMyInterface>("remote").Speak() );

    }

    static void Main(string[] args)

    {

        Program p = new Program();

        p.Spill();

        p.Unload();

        p.WaitForUpdate();

        p.Reload();

        p.Spill();

        p.WaitForUpdate();

    }

}

먼저 두개의 unity 컨테이너를 생성하고 있다. customContainer도 stdContainer와 같이 생성되고 있다. 이것은 두 컨테이너 모두 기본 AppDomain에서 생성되었다는 것을 말한다. 그런 다음 config 파일의 "unity" 섹션을 읽어들이고 있다. unity 섹션 하위에 기본 컨테이너와 확장 컨테이너에 대한 설정이 모두 포함되어 있다. 그런 다음 다음 코드가 실행되고 있다.

        //Unity 컨테이너에 config 정보를 설정한다.

        section.Containers.Default.Configure(stdContainer);

        section.Containers["customContainer"].Configure(customContainer);

착각하지 말 것은, section 객체의 Containers 속성은 컨테이너 인스턴스 컬렉션을 말하는 것이 아니라 <Container/>요소 컬렉션을 나타낸다. 코드는 Default <cotainer/> 설정을 stdContainer 인스턴스에 전달하는 작업을 하는 것이고, 두번째 코드는 name 어트리뷰트의 값이 "customContainer"값인 <container/>요소의 설정 내용을 이용해서 customContainer 인스턴스에 전달하는 작업을 한다. 이때 익스텐션 config 핸들러의 가상 함수(override 함수) Configure()가 내부에서 호출된다.

        public override void Configure(Microsoft.Practices.Unity.IUnityContainer container)

        {

            container.Configure<MyExtension>().CreateAppDomain(Domain, ShadowFiles);

        }

시작 프로젝트 MainConsole의 bin/Debug 폴더를 보면 Shadow폴더가 있다. 이 폴더 명은 MyExtension용 확장 config <extensionConfig/>하위에 설정된 ShadowFiles 어트리뷰트의 값이다. 이곳에 두번째 AppDomain에서 사용할 어셈블리들이 있어야 한다.

▶   사용자 경험

이제 테스트를 위해서 간단히 준비를 하자. 우선 프로젝트들을 두 개의 솔루션으로 분리해서 구성하자. 첫번째 솔루션은 계속 실행되고 있는 있을 것이고 두번째 솔루션은 수정해서 다시 빌드하는 작업을 해야 할 것이기때문이다.

시작 프로젝트와 UntiyExtension 프로젝트를 하나의 솔루션으로 묶자.

그리고 테스트용 비즈니스 객체용 프로젝트들을 하나의 솔루션으로 묶자.

그리고 테스트용 비즈니스 프로젝트용 빌드 결과들을 모아놓을 폴더를 하나 만들자.

- 최초 버전의 테스트용 비즈니스 프로젝트의 빌드 결과물들을 우선 "TestBizAssembly" 폴더에 모아두자.

- 비즈니스 프로젝트의 빌드  Output경로를 이 폴더로 두지는 말라. 이제 시작 프로젝트의 참조에 이 비즈니스 프로젝트 결과들을 추가하자.

- 그런 다음 AppDomainUnity.Assembly.dll과 AppDomainUnity.Common.dll을 시작 프로젝트의 bin/Debug/Shadow 폴더로 복사하자.

이제 실행을 시켜보자.

이제 SatClass의 Speak() 메소드 내용을 수정하자. 이때 실행 프로그램은 종료하지 않는다.

public string Speak()

{

    return "I'm SatClass object. version 2.0";

}

- 그런 다음 이 클래스가 포함된 AppDomainUnity.AssemblyB 프로젝트를 빌드해서 결과물을 bin/Debug/Shadow 폴더로 복사하자.

이제 다시 실행을 해 보자.

확장 끝이다 !! 허허..

▶   Unity 컨테이너 익스텐션의 결론

이제 정신을 좀 가다듬고 정리를 해 보자.

Unity 컨테이너 익스텐션을 위해서는 "strategy 정의".... "익스텐션 정의"... "익스텐션 config 핸들러"....! 를 작성해야 한다.

이제 끝내야 겠다. 뭔가 아쉽지만.

하지만 이 예제는 분석하고 보니 간단한 편이다. 이 예제에서는 policy 객체를 제작하지는 않았다. 다음 포스트에서는 완전한 익스텐션 제작에 대한 예제를 분석해볼까. Unity Application Block을 다운받아 설치하면 두개의 예제가 설치된다 : EventBroker, Stoplight. 이 중에서 EventBroker 예제를 보면 Policy를 사용하는 완전한 확장 예제가 있다.
다음에 이것을 할지. 아니면 Enterprise Library 블락 설명을 시작해볼지. 모르겠다. 최종적으로는 마이크로소프트에서 제공하는 모든 블락을 사용해서 개발 프레임워크를 하나 만들어볼까 하는 욕심이 앞선다.  혹시 이 글을 읽는 독자의 요청이 있다면 먼저 고려해보겠다. 그럼, 휘릭~