본문 바로가기

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

개발 프레임워크 만들기 대장정 38 - 화면 정보 로딩

달봉이는 프리젠테이션 레이어를 WPF로 구현할 것이다. ClickOnce로 배포되는 스마트클라이언트 애플리케이션을 염두에 두고 있다.

 

우선 사용자가 보게 될 시스템의 최종 실행 모습을 미리 보도록 하자. WPF용 애플리케이션을 만들겠지만, 아직 이것으로 만들어진 녀석이 없으니 우선 기존의 Window Form으로 만들어진 녀석을 보자.

 

사용자가 로그인을 하게 되면 제일 먼저 이런 유사한 화면을 보게 될 것이다. 업무 개발자가 담당할 부분이 가운데 있고, UI 컨테이너가 업무 화면을 둘러싸고 있다. 애플리케이션을 시작하면 UI 컨테이너의 상단과 좌측단에 메뉴가 로딩되고, 메뉴를 클릭하면 업무화면이 동적으로 생성되어 가운데 부분에 출력된다.

 

UI 컨테이너

 

UI 컨테이너라 함은 말 그대로 Visual을 갖는 컨트롤 또는 Window( WPF의 Window를 말한다 ) 객체들이 출력되는 구조를 잡아주는 역할을 한다. 이 컨테이너의 Visual한 구성은 기업마다 달라질 것이다. 메뉴 구조가 좌측에 트리 모양으로 있게 해 달라는 곳도 있을 것이고, 트리 구조가 아니라 아웃룩 형식으로 해달라는 곳도 있을 것이고, 상단에 드롭 다운 형식으로 있게 해달라는 곳도 있을 것이다. 이 녀석은 프레임워크에 속한다기 보다는 예제를 만들려면 필요한 녀석이라 구성하는 것이다. 달봉이 POC에서는 기업용 애플리케이션에서 흔히 사용하는 탭 컨트롤을 UI 컨테이너로 사용할 것이다.

 

화면 정보 컨테이너

 

화면 정보 컨테이너란 화면 정보(주로 화면 타입 정보)를 관리하는 컨테이너다. Spring.NET은 애플리케이션이 시작되면서 자신이 관리해야 하는 모든 객체들의 타입을 로딩한다. 

 

화면 객체 컨테이너

 

이 녀석은 업무 화면 영역에 로딩될 화면 객체의 참조들의 컨테이너들이다. 이 녀석이 갖는 의미는 이렇다. 한번 인스턴스화되는 화면은 모두 이 컨테이너에 캐싱된다. 이후 코드에서 화면 객체를 요구하면 이 캐싱된 녀석을 반환해줄 것이다.

최종 화면 인스턴스가 UI 컨테어너로 출력되는 절차를 보면 다음과 같다.

 

애플리케이션이 시작되면서 화면 정보는 Spring.NET의 화면 정보 컨테이너로 로딩된다. 사용자가 메뉴를 클릭하면 메뉴로부터 해당 화면의 ID를 전달받은 후 Spring.NET으로 ID를 전달하고, Spring.NET은 화면 정보 컨테이너에서 화면 타입 정보를 얻어서 화면 객체를 생성한다. 그런 다음 화면 객체를 코드로 전달한다. 코드에서는 화면 객체를 UI 컨테이너로 전달해서 업무 화면이 출력되게 된다. Spring.NET에는 다음과 같은 절차로 최종 객체를 생성해서 관리하게 된다.

 

Spring.NET에서 제공하는 XmlObjectFactory를 사용하면 이런 기능 및 컨테이너가 모두 구현되어 있다.  이 녀석이 우리가 말하는 Spring.NET 컨테이너의 실체이다.

달봉이는 XmlObjectFactory를 사용하기로 했다.  달봉이가 이제 시작할 일은 ①번 절차이다. 우선 Spring.NET이 제공하는 기본적인 기능이 기업용 애플리케이션에 적용하는 것이 적합한지를 알아보고 확장/수정해야 한다면 어떻게 해야 할지를 검토해본다.

 

화면 정보 로딩

 

Spring.NET의 컨테이너를 UI 프레임워크단에 적용할 때 가장 고민스러운 부분이 이 부분이다. 기업형 애플리케이션의 경우는 화면 객체에 대한 정보가 모두 메뉴 관리라는 프로그램을 통해서 데이터베이스에 등록된다. Spring.NET이 제공하는 기본적인 기능을 이용하자면 이 화면 객체에 대한 정보를 XML 파일로 관리해야 한다는 얘기가 된다. 기업형 애플리케이션의 화면 객체를 XML 파일로 관리하기에는 그 수가 너무 많다. 그리고 기업형 애플리케이션의 메뉴는 사용자별 권한과도 관련되어 있다.  메뉴를 사용자  그리고 사용자 역할별로 할당하는 작업을 해야 한다. 따라서 데이터베이스로 관리되어야 하는 것이 합리적이다

상황은 이렇다.

UI단에서 Spring.NET의 컨테이너를 사용하고 싶다. 그러나 모든 객체들에 대한 정보들을 XML로 만들 수 없다. 즉 화면 객체들에 대한 정보는 데이터베이스로 관리한다. 이런 상황에서 Spring.NET 컨테이너를 사용하기를 원한다면 어떤 작업이 필요할까.

애플리케이션에서 공통으로 사용하는 객체는 XML로 설정하고 화면 객체에 대한 정보는 프로그램적으로 컨테이너에 등록하면 되는 것이다. 이를 위해서 XmlObjectFactory에서는 다음과 같은 API를 제공하고 있다.

 public void RegisterObjectDefinition(string name, IObjectDefinition objectDefinition)

XmlObjectFactory를 사용하고자 한다면, 화면 객체에 해당하는 IObjectDefinition 객체를 만들어서 화면 ID( name 파라미터)와 함께 이 API를 이용해서 등록하면 된다.

그래서 화면 객체들에 대한 정보를 받아서 XmlObjectFactory의 이 메소드를 호출하는 일을 하는 녀석을 하나 만들기로 했다.

public class Dalbong2XmlObjectFactory : XmlObjectFactory

{

    public Dalbong2XmlObjectFactory(IResource resource ) : base(resource)

    {

        base.InstantiationStrategy = new Dalbong2InstantiationStrategy();

    }

 

    /// <summary>

    /// IDalbong2Element 타입 정보를 등록한다.

    /// </summary>

    /// <param name="elements"></param>

    public void RegisterDalbong2ElementDefinitions(List<Dalbong2ElementInfo> elements)

    {

        MutablePropertyValues pv = null;

        ChildObjectDefinition od = null;

        foreach (Dalbong2ElementInfo element in elements)

        {

            pv = new MutablePropertyValues();

            pv.Add("dalbong2ElementInfo", element);

 

            //singleton으로 등록한다.

            od= new ChildObjectDefinition("dalbong2ControlBase", Type.GetType(element.QulifiedFullTypeName), null, pv );

 

            base.RegisterObjectDefinition( element.ID,od);

 

        }

    }

 

}

XmlObjectFactory 기능을 그대로 이용하기 위해서 이 녀석을 상속받는 Dalbong2XmlObjectFactory를 하나 만들었다. 그리고 화면 객체들에 대한 정보를 리스트로 받아서 XmlObjectFactory에서 제공하는 API, base.RegisterObjectDefinition()을 이용해서 컨테이너로 등록하도록 했다.

Spring.NET에서는 객체 정보를 나타내는 타입이 두 가지 있다 : RootObjectDefinition, ChildObjectDefinition. 이 코드에서는 ChildObjectDefinition을 사용하고 있다. 그럼 루트가 어디에 등록되어 있다는 말인가? 그렇다. 루트 객체 정보는 XML을 통해서 등록된다. 이 녀석은 개발자들이 만들 화면 객체들의 베이스 클래스로서 사전에 XML에 정의해 둘 수 있는 녀석이다. 그 베이스 객체 정보는 “dalbong2ControlBase”라는 이름으로 등록되어 있다는 것이 앞의 코드이다.

Dalbong2WinObjects.xml이라는 파일이 있다. 이 내용을 보면 다음과 같다.

<objects xmlns="http://www.springframework.net"

         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://www.springframework.net http://www.springframework.net/xsd/spring-objects.xsd">

  <object id="iDalbong2Element"

          type="Dalbong2.Win.IDalbong2Element, Dalbong2.Win" abstract="true"/>

 

<object id="dalbong2ControlBase"

          type="Dalbong2.Win.Dalbong2ControlBase, Dalbong2.Win" parent="iDalbong2Element" />

  <object id="dalbong2PageBase"

          type="Dalbong2.Win.Dalbong2PageBase, Dalbong2.Win" parent="iDalbong2Element" />

 

</objects>

앞 설정을 보면 “dalbong2ControlBase”라는 이름으로 등록된 객체를 볼 수 있다( 그 외의 “iDalbong2Element”, “dalbong2PageBase”로 등록된 녀석들은 뒤에 보겠다).

이 설정을 XmlObjectFactory가 읽어들이게 된다. 실제로 이 설정을 XmlObjectFactory가 어떻게 읽어들일 수 있게 되는지 다음 코드를 보자.

 

static void Main(string[] args)

{

    //XML로 설정된 객체 정보 로딩

    Dalbong2XmlObjectFactory dalbong2ObjectFactory

        = new Dalbong2XmlObjectFactory(new ConfigSectionResource("config://spring/objects"));

 

    //프로그램적으로 화면 정보 생성

    List<Dalbong2ElementInfo> elements = new List<Dalbong2ElementInfo>();

 

    Dalbong2ElementInfo el = null;

 

    el = new Dalbong2ElementInfo();

    el.ID = "01";

    el.FileName = "BONG.WIN.CO";

    el.FullyQualifiedTypeName= "BONG.WIN.CO.UserMgmt";

    el.LoadUrl = "http://dalbong2-pc/SmartControls";

    elements.Add(el);

 

 

    el = new Dalbong2ElementInfo();

    el.ID = "02";

    el.FileName = "BONG.WIN.CO";

    el.FullyQualifiedTypeName= "BONG.WIN.CO.UserMgmt02";

    el.LoadUrl = "http://dalbong2-pc/SmartControls";

    elements.Add(el);

 

    //컨테이너에 화면 객체 정보 등록

    dalbong2ObjectFactory.RegisterDalbong2ElementDefinitions(elements);

 

Dalbong2XmlObjectFactory 객체를 생성하면서 인자로 new ConfigSectionResource(“config://spring/object”)) 객체를 건네주고 있다. 이것은 객체 자원들이 애플리케이션의 config 파일( 여기서는 app.config 파일이 되겠다)의 “spring/objects” 하위의 노드에 정의되어 있으니 그것을 읽어들이라는 표시다.

app.config의 이 부분을 보면 다음과 같이 되어 있다.

app.config 내용

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

  <configSections>

    <sectionGroup name="spring" >

      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/>

      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />

    </sectionGroup>

  </configSections>

  <spring>

    <context caseSensitive="false">

    </context>

    <objects xmlns="http://www.springframework.net"   

             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

             xsi:schemaLocation="http://www.springframework.net http://www.springframework.net/xsd/spring-objects.xsd">

 

      <import resource="assembly://Dalbong2.Win/Dalbong2.Win/Dalbong2WinObjects.xml"/>

 

    </objects>

  </spring>

</configuration>

 

XmlObjectFactory가 생성되면서 //spring/objects 하위의 노드를 읽어들이고 그곳에서 <import/>를 사용해서 Dalong2.Win 어셈블리에 포함되어 있는 Dalbong2WinObjects.xml을 읽어들이는 것이다.

XmlObjectFactory가 생성되면서 xml에 설정된 객체들에 대한 정보를 읽어들인 다음 프로그램적으로 화면 객체들에 대한 정보를 읽어들이고 있다. 앞의 코드에서는 화면 객체들에 대한 정보를 담기위해서 Dalbong2ElementInfo라는 타입을 하나 만들어서 사용하고 있다.

public class Dalbong2ElementInfo

{

    string _ID = "";

    string _QulifiedFullTypeName = "";

    string _AssemblyName = "";

    string _LoadUrl = "";

 

    public Dalbong2ElementInfo() { }

    public Dalbong2ElementInfo(string id, string qualifiedFullTypeName, string fileName, string loadUrl)

    {

        _ID = id;

        _QulifiedFullTypeName = qualifiedFullTypeName;

        _AssemblyName = fileName;

        _LoadUrl = loadUrl;

    }

 

    public string ID

    {

        get { return _ID; }

        set { _ID = value; }

    }

    /// <summary>

    /// 네임스페이스 + 클리스명,

    /// Loose XAML 파일인 경우는 설정하지 않는다.

    /// </summary>

    /// <example>Dalbong2.Win.CO.TestClass</example>

    public string QulifiedFullTypeName

    {

        get { return _QulifiedFullTypeName; }

        set { _QulifiedFullTypeName = value; }

    }

 

    /// <summary>

    /// 어셈블리명 또는 페이지명

    /// </summary>

    public string FileName

    {

        get { return _AssemblyName; }

        set { _AssemblyName = value; }

    }

    /// <summary>

    /// http://서버명/디렉토리경로/

    /// </summary>

    public string LoadUrl

    {

        get { return _LoadUrl; }

        set { _LoadUrl = value; }

    }

}

 

화면 객체에 대한 정보를 List에 담아서 최종적으로  Dalbong2XmlObjectFactory의 RegisterDalong2ElementDefinitions()에 넘겨주면, 화면 객체들에 타입 정보가 등록되게 된다.

이제 화면 객체들에 대한 정보가 Spring.NET 컨테이너에 등록되어 있으니 이 정보에 해당하는 객체들을 생성해서 사용하면 된다.

dalbong2ObjectFactory.GetObject("01");

그러나 개발자들이 이렇게 화면 객체를 직접 호출해서 사용할 일은 자주 없을 듯 하다. 아마도 개발 프레임워크단에 이런 식으로 화면 객체에 접근할 일이 있을 것이다.

 

GetObject() 메소드로 화면 객체를 얻기 위해서는 물론 화면이 포함된 어셈블리가 로딩되어야 한다. 그러나 우리는 그 어셈블리들을 원격의 서버에서 가져올 것이다. XmlObjectFactory는 원격에서 어셈블리를 로딩하는 기능을 기본적으로는 지원하지 않는다. 그래서 달봉이는 이 로직을 추가하는 작업을 해야 했다. XmlObjectFactory가 객체를 생성하는 단계도 기업용으로 변신해야 한다는 것이다. 이것을 위해 어떻게 확장했는지는 다음에 정리하도록 하겠다.

 

다음은 달봉이가 처음에 시도했던 방법이다. 이렇게도 할 수도 있었다는 기록이다. try는 트라이 일뿐! 이대로 하지 말자.

Spring.NET도 XML을 읽어들여서 객체 타입을 로딩하는 부분이 있을 것이다. XML 정보가 Spring.NET에서 관리하는 타입 객체로 변환될때 달봉이가 프로그램적으로 화면 객체들의 정보를 끼워넣으면 되지 않을까 하는 생각을 했다. ?! 

XmlObjectFactory를 사용하면 결국 XML 정보를 읽어들이는 것은 DefaultObjectDefinitionDocumentReader라는 녀석이다. 이 녀석의 RegisterObjectDefinitions()이라는 메소드가 그 일을 하는데 실제로 XML을 파싱하는 작업을 하는 것은 그 내부에서 호출되는 ParseObjectDefinitions()라는 녀석이다. 그 녀석이 호출되기전에 원본 XML을 수정할 수 있는 기회를 준다. 바로 PreProcessXml() 메소드가 호출되는데 이 녀석은 protected virutal로 선언되어 있다. 

DefaultObjectDefinitionDocumentReader를 상속해서 달봉이만의 DocumentReader를 만들면 되겠다 싶었다. 그래서 오버라이딩한 PreProcessXml()에서 원본 XML에 화면 객체에 해당하는 <object/> 요소들을 끼워넣을 계획이었다.

그러나 DefaultObjectDefinitionDocumentReader를 참조하고 있는 녀석이 있었고 다시 그 녀석을 참조하는 녀석이 있었다.

이게 무슨 말인가 하면 DefaultObjectDefinitionDocumentReader의 protected virtual PreProcessXml()을 사용하기위해서는 위 3개의 클래스를 각각 상속하는 작업을 해야 한다는 것이다.

어쨋든 달봉이는 그렇게 했다. 그렇게 해서 PreProcessXml()에서 원본 XML에 화면 객체를 나타내는 <object/> 끼워넣고 Spring.NET이 제공하는 객체 정보 파서를 이용하였다.

무려 토,일요일을 모두 바쳐 구현했건만 XmlObjectFactory에서 객체 정보를 프로그램적으로 등록할 수 있는 API를 제공하고 있었던 것이다. 분명 있을 법도 한데 왜 없을까 하면서 잠시 찾아보기 했는데 XmlObjectFactory의 부모 객체에서 구현하고 있었던 것이다. 쓰으…

이쯤에서 궁금한 점이 하나 있을 수 있다.

이렇게 컨테이너를 구성하는 것이 어려운데 왜  굳이 UI단에서 Spring.NET 컨테이너를 사용하려고 하는가?

Spring.NET이 제공하는 컨테이너를 사용하면 추후 AOP 기능을 사용할 수 있을까 싶어서이다. 예를 들어 웹서비스 또는 WCF 서비스를 호출할때 프레임워크단에서 해 줘야 하는 일들이 몇 가지 있다. 호출 주소를 변경해준다든지 또는 서버측으로 클라이언트측의 몇 가지 정보를 함께 넘겨줘야 하는 작업등이 있을 수 있다. 그리고 서비스 호출 시 로그를 남길 수도 있다. 이런 작업은 개발자가 모르게 프레임워크단에서 해 줘야 하는 것이 이상적이다. 만약 Spring.NET에서 제공하는 컨테이너와 AOP 기능을 사용하면 충분히 그런 작업에 대한 어드바이스들을 서비스 호출 전, 후에 끼워 넣을 수 있을 것이라는 예상이다.

너무 날림으로 정리하는 듯 하다. 현재 진행하는 프로젝트때문에 시간이 없다. 잊기 전에 기록해둘려고 시간을 내서 정리하다 보니 날림 공사가 되는 기분이다.