태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

지금까지 작성한 코드는 다음 링크를 통해서 다운로드받을 수 있다.

Dalbong2WithSpring.NET.alz
Dalbong2WithSpring.NET.a01
Dalbong2WithSpring.NET.a00


이제 메뉴를 출력해보자. 그러자면 우선 메뉴 소스가 데이터베이스든 XML이든 구성되어 있어야 할 것이다. 그러나 여기서는 우선 아주 쉬운 방법을 택하겠다. 하드코딩 !

메뉴 소스가 구성되어 있다면 그것을 읽어들여 메뉴 계층 구조를 만들겠다. 이 계층구조는 눈으로 볼 수 있는 트리구조가 아니다. 단지 논리적인 메뉴 객체들의 트리구조이다. 따라서 ‘논리적인 메뉴 구조를 구성’한다고 표현할 수 있겠다. 메뉴의 논리적인 트리 구조를 만들기 위해서 필요한 클래스를 만들어보자.

우선 메뉴 아이템 정보를 나타내는 클래스를 만들었다.

메뉴 아이템을 나타내는 클래스들은 Dalbong2.Win 프로젝트에 포함시켰다. 이 녀석들은 현장의 프로젝트와는 상관없기때문이다.

메뉴 트리를 구성하는 아이템들은 윈도우 탐색기의 트리구조의 각 항목들에 해당한다고 볼 수 있다 : 드라이브, 폴더, 최종 파일.

달봉이는 파일 시스템의 각각의 노드에 해당하는 메뉴 클래스를 만들어서 메뉴 아이템의 각 항목을 표현하도록 했다 : DriveMenuItemInfo, FolderMenuItemInfo, FileMenuItemInfo.

그리고도 RootMenuItemInfo, MenuItemInfo, IMenuItemInfo를 추가적으로 만들었다. RootMenuItemInof는 추후 메뉴 구조가 컨트롤로 출력될때 보이지는 않겠지만 논리적인 트리 구조를 만들기 위한 녀석이다. 그리고 MenuItemInfo은 Drive, Folder, File 그리고 Root 메뉴 정보 클래스의 부모 클래스이다. 그리고 IMenuItemInfo는 메뉴 정보 객체라면 노출해야 하는 속성과 메소드를 정의해놓은 인터페이스이다.

메뉴 아이템 클래스들은 다음과 같이 구성된 테이블을 염두에 두고 만들었다.

코드를 보자.

public interface IMenuItemInfo

{

 

    string ID {get;}

    string Name {get;}

    IMenuItemInfo ParentMenuItemInfo

    {

        get;

        set;

    }

    PropertyCollection ExtraPropertyValues

    {

        get;

        set;

    }

    void AddChild(IMenuItemInfo itemInfo);

    List<IMenuItemInfo> GetChildren();

}

각 메뉴 아이템은 고유한 아이디와 그리고 이름이 있다. 그리고 메뉴 아이템 각각은  부모 아이템을 알고 있도록 했고, ParentMenuItemInfo 속성을 통해서 설정할 수 있고 조회할 수 있다. 그리고 자신에 속한 자식 메뉴 아이템을 외부에 제공할 수 있어야 한다. 이것은 GetChildren()이 담당한다. 그리고 자식 메뉴 정보를 추가할 수 있는 API가 있도록 했다 :  AddChild().  ExtraPropertyValues 라는 PropertyCollection 타입의 속성이 있다. 메뉴 관리 프로그램에서 관리하는 정보가 각 현장의 사이트마다 달라질 수 있을 것이다. 이런 정보는 모두 ExtraPropertyValues 속성에 저장될 것이다. 그 값을 참조할때는 다음과 같은 형식으로 할 수 있다.

itemInfo.ExtraPropertyValues["ColumnName"].ToString();

MenuItemInfo 클래스는 기본적인 IMenuItemInfo를 구현해놓은 추상클래스이다.

public abstract class MenuItemInfo : IMenuItemInfo

{

    string _ID = "";

    string _Name = "";

 

    IMenuItemInfo _ParentMenuItemInfo = null;

    List<IMenuItemInfo> _Childern = null;

    PropertyCollection _ExtraPropertyValues = null;

 

    public  MenuItemInfo(string id, string name)

    {

        _ID = id;

        _Name = name;

    }

    public string ID

    {

        get { return _ID; }

    }

    public string Name

    {

        get { return _Name; }

    }

    public IMenuItemInfo ParentMenuItemInfo

    {

        get { return _ParentMenuItemInfo; }

        set { _ParentMenuItemInfo = value; }

    }

    public void AddChild(IMenuItemInfo itemInfo)

    {

        itemInfo.ParentMenuItemInfo = this;

 

        if (_Childern == null)

            _Childern = new List<IMenuItemInfo>();

        _Childern.Add(itemInfo);

    }

    public List<IMenuItemInfo> GetChildren()

    {

        return _Childern;

    }

 

    public PropertyCollection ExtraPropertyValues

    {

        get { return _ExtraPropertyValues; }

        set { _ExtraPropertyValues = value; }

    }

}

특별한 것은 없다. 다만 눈여겨 볼 것은 AddChild()를 이용해서 자식 메뉴 아이템을 추가할 때 부모 아이템으로 자신을 지정해주는 것 정도가 있다.

다음은 RootMenuItemInfo 다

public class RootMenuItemInfo : MenuItemInfo

{

    public RootMenuItemInfo(string id, string name) : base( id, name)

    {

        base.ParentMenuItemInfo = null;

    }

}

 

다음은 DriveMenuItemInfo, FolderMenuItemInfo

 

public class DriveMenuItemInfo : MenuItemInfo

{

    public DriveMenuItemInfo(string id, string name ) : base(id, name)

    {

    }

}

public class FolderMenuItemInfo : MenuItemInfo

{

    public FolderMenuItemInfo(string id, string name) : base(id, name)

    {

    }

}

 

다음은 FileMenuItemInfo. 이 녀석은 메뉴 구조의 마지막 레벨의 메뉴 아이템을 나타내는 녀석으로서 이 녀석이 나타내는 컨트롤을 사용자가 클릭하게 되면 화면이 뜨게 되는 것이다. 따라서 이 녀석은 화면 객체 정보 Dalbong2ElementInfo를 가지고 있어야 한다.

public class FileMenuItemInfo : MenuItemInfo

{

 

    private Dalbong2ElementInfo  _Dalbong2ElementInfo = null;

 

    public FileMenuItemInfo(string id, string name)

        : base(id, name)

    {

    }

    public FileMenuItemInfo(string id, string name, Dalbong2ElementInfo dalbong2ElementInfo)

        : base(id, name)

    {

        _Dalbong2ElementInfo = dalbong2ElementInfo;

    }

    public Dalbong2ElementInfo Dalbong2ElementInfo

    {

        get { return _Dalbong2ElementInfo; }

        set { _Dalbong2ElementInfo = value; }

    }

}

이제 이 녀석들을 이용해서 논리적인 메뉴 구조를 구성할 녀석을 만들어보자. 이 녀석은 MenuHelper라는 이름으로 만들었다. MenuHelper는 현장의 프로젝트 프레임워크에 추가했다. 다음 그림은 가상의 BONG이라는 가상 회사의 프레임워크를 담당하는 부분이다.

 

이 녀석부터는 현장의 요구에 따라서 구현이 달라질 수 있을 것이라는 생각때문이다. 예를 들어 XML로 메뉴를 구성할 수도 있겠고, 데이터베이스로 메뉴를 구성할 수도 있을 것이다( 물론 대부분의 기업형 애플리케이션은 데이터베이스로 관리한다).

public static class MenuHelper

{

 

    //기본적인 컬럼네임

    public  const string MenuIDColumnName = "ID";

    public  const string MenuNameColumnName = "Name";

    public  const string MenuTypeColumnName = "MenuType";

 

    public  const string ElementIDColumnName = "ClassName";

    public  const string FullyQualifiedTypeNameColumnName = "ClassName";

    public  const string FileNameColumnName = "FileName";

    public  const string LoadUrlColumnName = "LoadUrl";

    public  const string ParentMenuIDColumnName = "parentid";

 

    public  const string RootMenuID = "ROOT";

    public  const string RootMenuTypeCode = "ROOT";

    public  const string DriveMenuTypeCode = "DRIVE";

    public  const string FolderMenuTypeCode = "FOLDER";

    public  const string FileMenuTypeCode = "FILE";

 

    private static List<Dalbong2ElementInfo> _Dalbong2ElementInfos = null;

 

    public static RootMenuItemInfo ConstructLogicalMenuItemInfoTree(DataSet dsMenuInfo)

    {

        RootMenuItemInfo rootMenuInfo = new RootMenuItemInfo(RootMenuID, RootMenuID);

        //DataSet 파싱

        ProcessMenuInfo(rootMenuInfo,dsMenuInfo.Tables[0]);

        return rootMenuInfo;

    }

 

    private static void ProcessMenuInfo( IMenuItemInfo parentItemInfo, DataTable dtMenuInfo)

    {

        IMenuItemInfo menuItemInfo = null;

        Dalbong2ElementInfo dalbong2ElementInfo = null;

        PropertyCollection propertis = null;

 

        DataRow[] drs = GetChildernOf(parentItemInfo.ID, dtMenuInfo);

        if (drs == null) return;

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

        {

            //MenuItemInfo 객체 생성

            switch( drs[i][MenuTypeColumnName].ToString() )

            {

                case DriveMenuTypeCode :

                    menuItemInfo = new DriveMenuItemInfo(drs[i][MenuIDColumnName].ToString(), drs[i][MenuNameColumnName].ToString());

                    break;

                case FolderMenuTypeCode:

                    menuItemInfo = new FolderMenuItemInfo(drs[i][MenuIDColumnName].ToString(), drs[i][MenuNameColumnName].ToString());

                    break;

                case FileMenuTypeCode:

                    menuItemInfo = new FileMenuItemInfo(drs[i][MenuIDColumnName].ToString(), drs[i][MenuNameColumnName].ToString());

                    dalbong2ElementInfo = new Dalbong2ElementInfo(

                        drs[i][ElementIDColumnName].ToString(),

                        drs[i][FullyQualifiedTypeNameColumnName].ToString(),

                        drs[i][FileNameColumnName].ToString(),

                        drs[i][LoadUrlColumnName].ToString());

 

                    ((FileMenuItemInfo)menuItemInfo).Dalbong2ElementInfo = dalbong2ElementInfo;

                    if (_Dalbong2ElementInfos == null)

                        _Dalbong2ElementInfos = new List<Dalbong2ElementInfo>();

 

                    //Dabong2ElementInfo 객체는 다음에 Dalbong2XmlObjectFactory에 넘겨줄것을 대비해서

                    //미리 별도로 저장해둔다.

                    _Dalbong2ElementInfos.Add(dalbong2ElementInfo);

                    break;

                default :

                    throw new Exception(string.Format("This menu type is not defined.type:{0}, menuid:{1}",

                        drs[i][MenuTypeColumnName].ToString(), drs[i][MenuIDColumnName].ToString()));

            }

 

            if( menuItemInfo != null)

            parentItemInfo.AddChild( menuItemInfo );

 

            //기타 추가된 메뉴 정보는 ExtraPropertyValues에 (컬럼명, 값)쌍으로 추가한다.

            foreach( DataColumn ds in dtMenuInfo.Columns )

            {

                if( propertis == null )

                    propertis = new PropertyCollection();

 

                switch( ds.ColumnName )

                {

                    case MenuIDColumnName :

                        break;

                    case MenuNameColumnName:

                        break;

                    case MenuTypeColumnName:

                        break;

                    case FullyQualifiedTypeNameColumnName:

                        break;

                    case FileNameColumnName:

                        break;

                    case LoadUrlColumnName:

                        break;

                    case ParentMenuIDColumnName:

                        break;

                    default:

                        propertis.Add( ds.ColumnName, drs[i][ds.ColumnName] );

                        break;

                }

            }

 

            if( propertis != null)

                menuItemInfo.ExtraPropertyValues = propertis;

            //재귀호출

            ProcessMenuInfo(menuItemInfo, dtMenuInfo);

        }

    }

 

    private static DataRow[] GetChildernOf(string parentID, DataTable dt)

    {

        DataRow[] drs = dt.Select(string.Format(ParentMenuIDColumnName + "='{0}'", parentID));

        return drs;

    }

    public static List<Dalbong2ElementInfo> GetDalbong2ElementInfos()

    {

        return _Dalbong2ElementInfos;

    }

}

MenuHelper 클래스는 static으로 정의했고 모든 멤버들은 static이다.  외부로 노출된 것은 두개가 있다.

 public static RootMenuItemInfo ConstructLogicalMenuItemInfoTree(DataSet dsMenuInfo)

 public static List<Dalbong2ElementInfo> GetDalbong2ElementInfos()

첫번째 녀석은 메뉴 정보 테이블을 가지고 있는 DataSet 객체를 받아서 논리적으로 트리 구조를 구성한 후 그 트리구조의 루트를 반환한다. 호출하는 코드에서는 루트에 대한 참조만 알고 있으면 모든 메뉴 구조를 찾아갈 수가 있다 : 각 메뉴 아이템은 GetChildren() 메소드를 통해서 자신의 자식들을 반환해줄 수 있기때문이다.

그리고 MenuHelper에서는 메뉴 구조를 구성하면서 화면 정보 객체 Dalbong2ElementInfo 리스트도 구성한다. 이렇게 구성된 화면 정보 리스트 객체 List<Dalbong2ElementInfo>는 이전 포스트에서 본 것처럼 Dalbong2XmlObjectFactory의 RegisterDalbong2ElementDefinitions()에 건네져서 화면 정보를 로딩하는데 사용된다.

_Dalbong2XmlObjectFactory.RegisterDalbong2ElementDefinitions(_Dalbong2ElementInfos);

이제 간단히 UI 컨테이너를 구성해보자. UI 컨테이너란 앞에서 말한 것처럼 사용자에게 보여질 모습을 구성하는 곳이다. 이곳에 메뉴가 출력되고, 화면이 달린 메뉴를 클릭하면 해당 화면이 출력되는 비주얼한 컨테이너이다.

이 프로그램은 당근 WPF 애플리케이션으로 구성된다. Shell.xaml을 하나 추가했다. Shell.xaml의 디자인은 다음처럼 되어 있다.

 

좌측 컬럼에 트리 컨트롤을 사용해서 메뉴 트리가 출력될 것이다.

그리고 xaml 코드는 다음과 같다.

<Window x:Class="BongApp.Shell"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    Title="Shell" Height="200" Width="250">

    <Grid ShowGridLines="True" SnapsToDevicePixels="True ">

        <Grid.RowDefinitions>

            <RowDefinition Height="25*" />

            <RowDefinition Height="137*" />

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="48*" />

            <ColumnDefinition Width="180*" />

        </Grid.ColumnDefinitions>

        <TabControl Grid.Column="1" Grid.Row="1"  Name="tabControl1">

            <TabItem Header="tabItem1" Name="tabItem1" Loaded="tabItem1_Loaded">

 

                <Grid />

            </TabItem>

        </TabControl>

        <TreeView  Grid.Row="1" Name="tvMenuTree"  />

        <DockPanel Grid.Row="0" Grid.Column="1"  Name="dpHeader">

            <StackPanel  Name="spHeaderContent" />

        </DockPanel>

    </Grid>

</Window>

다음은 코드 비하인드 파일의 내용이다.

public partial class Shell : Window

{

    public Shell()

    {

        InitializeComponent();

    }

 

    Dalbong2XmlObjectFactory _Dalbong2XmlObjectFactory = null;

 

    private void tabItem1_Loaded(object sender, RoutedEventArgs e)

    {

        //메뉴 정보를 조회한다.

        DataSet dsMenuItemInfos = GetMenuItemInfos();

        //메뉴의 로지컬 트리를 구성한다.

        RootMenuItemInfo rootMenuInfo = MenuHelper.ConstructLogicalMenuItemInfoTree(dsMenuItemInfos);

        //Dalbong2Element 정보를 구한다.

        List<Dalbong2ElementInfo> _Dalbong2ElementInfos = MenuHelper.GetDalbong2ElementInfos();

        //메뉴를 비쥬얼하게 디스플레이한다.

        DisplayVisualTree(tvMenuTree, rootMenuInfo);

 

        //Dalbong2Element 객체 정보 등록

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

        _Dalbong2XmlObjectFactory.RegisterDalbong2ElementDefinitions(_Dalbong2ElementInfos);

 

    }

Shell.xaml이 로딩되면서 실행되는 코드이다.

먼저 GetMenuItemInfos()를 호출해서 메뉴 정보를 메뉴 소스에서 가져온다. 현재는 하드 코딩했다.

    /// <summary>

    /// 내부에서는 실제로 데이터베이스를 호출해서 메뉴 정보를 가져온다.

    /// </summary>

    /// <returns></returns>

    private DataSet GetMenuItemInfos()

    {

        DataSet ds = null;

 

        ds = GetMenuItemInfos_IMSI();

 

        return ds;

    }

    private DataSet GetMenuItemInfos_IMSI()

    {

        DataSet ds = new DataSet();

        DataTable dt = new DataTable();

        ds.Tables.Add(dt);

 

        dt.Columns.Add(new DataColumn(MenuHelper.MenuIDColumnName));

        dt.Columns.Add(new DataColumn(MenuHelper.MenuNameColumnName ));

        dt.Columns.Add(new DataColumn( MenuHelper.MenuTypeColumnName));

        dt.Columns.Add(new DataColumn(MenuHelper.FullyQualifiedTypeNameColumnName));

        dt.Columns.Add(new DataColumn(MenuHelper.FileNameColumnName));

        dt.Columns.Add(new DataColumn(MenuHelper.LoadUrlColumnName));

        dt.Columns.Add(new DataColumn(MenuHelper.ParentMenuIDColumnName));

 

 

        DataRow dr = null;

 

        //루트 메뉴 추가

        dr = dt.NewRow();

        dr[MenuHelper.MenuIDColumnName] = MenuHelper.RootMenuID;

        dr[MenuHelper.MenuNameColumnName] = "ROOT";

        dr[MenuHelper.MenuTypeColumnName] = MenuHelper.RootMenuTypeCode;

        dt.Rows.Add(dr);

 

        //레벨 1 메뉴 추가

        dr = dt.NewRow();

        dr[MenuHelper.MenuIDColumnName] =  "01";

        dr[MenuHelper.MenuNameColumnName] = "01 드라이브";

        dr[MenuHelper.MenuTypeColumnName] = MenuHelper.DriveMenuTypeCode ;

        dr[MenuHelper.ParentMenuIDColumnName] = MenuHelper.RootMenuID;

        dt.Rows.Add(dr);

 

 

        dr = dt.NewRow();

        dr[MenuHelper.MenuIDColumnName] = "02";

        dr[MenuHelper.MenuNameColumnName] = "02 드라이브";

        dr[MenuHelper.MenuTypeColumnName] = MenuHelper.DriveMenuTypeCode;

        dr[MenuHelper.ParentMenuIDColumnName] = MenuHelper.RootMenuID;

        dt.Rows.Add(dr);

 

 

        // 01드라이브의 폴더

        dr = dt.NewRow();

        dr[MenuHelper.MenuIDColumnName] = "0101";

        dr[MenuHelper.MenuNameColumnName] = "01 폴더";

        dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FolderMenuTypeCode;

        dr[MenuHelper.ParentMenuIDColumnName] = "01";

        dt.Rows.Add(dr);

 

        dr = dt.NewRow();

        dr[MenuHelper.MenuIDColumnName] = "0102";

        dr[MenuHelper.MenuNameColumnName] = "02 폴더";

        dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FolderMenuTypeCode;

        dr[MenuHelper.ParentMenuIDColumnName] = "01";

        dt.Rows.Add(dr);

 

        //01드라이브/01폴더의 파일아이템

 

        string loadUrl = "";

        string deployServer = System.Configuration.ConfigurationManager.AppSettings["DeployServer"];

        string smartControlsDirectory = System.Configuration.ConfigurationManager.AppSettings["SmartControlsDirectory"];

        loadUrl = System.IO.Path.Combine(String.Format("http://{0}", deployServer), smartControlsDirectory);

 

        dr = dt.NewRow();

        dr[MenuHelper.MenuIDColumnName] = "010101";

        dr[MenuHelper.MenuNameColumnName] = "01 화면";

        dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FileMenuTypeCode ;

        dr[MenuHelper.FullyQualifiedTypeNameColumnName] = "BONG.WIN.CO.UserMgmt";

        dr[MenuHelper.FileNameColumnName] = "BONG.WIN.CO";

        dr[MenuHelper.LoadUrlColumnName] = loadUrl;

        dr[MenuHelper.ParentMenuIDColumnName] = "0101";

        dt.Rows.Add(dr);

 

        dr = dt.NewRow();

        dr[MenuHelper.MenuIDColumnName] = "010102";

        dr[MenuHelper.MenuNameColumnName] = "02 화면";

        dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FileMenuTypeCode;

        dr[MenuHelper.FullyQualifiedTypeNameColumnName] = "BONG.WIN.CO.UserMgmt02";

        dr[MenuHelper.FileNameColumnName] = "BONG.WIN.CO";

        dr[MenuHelper.LoadUrlColumnName] = loadUrl;

        dr[MenuHelper.ParentMenuIDColumnName] = "0101";

        dt.Rows.Add(dr);

 

        return ds;

    }

 

FileMenuItemInfo를 만들 때, 어셈블리 파일을 다운로드할 경로를 config에서 설정하도록 했다.

    <appSettings>

        <!-- 배포서버-->

        <add key="DeployServer" value="dalbong2-pc" />

        <!--스마트컨트롤을 다운로드할 디렉토리

         최종 다운로드 경로 : "DeployServer"값 + "SmartControls Directory"값 -->

        <add key="SmartControlsDirectory" value="SmartControls"/>

    </appSettings>

</configuration>

이렇게 메뉴 소스를 구성한 다음 결과물인 DataSet 객체를 MenuHelper에 건네줘서 논리적인 메뉴 트리를 구성한다.

RootMenuItemInfo rootMenuInfo = MenuHelper.ConstructLogicalMenuItemInfoTree(dsMenuItemInfos);

그런 다음 넘겨 받은 RootmenuItemInfo 객체와 트리 컨트롤을 이용해서 실제로 트리 구조를 만들어낸다.

DisplayVisualTree(tvMenuTree, rootMenuInfo);

 

    private void DisplayVisualTree(TreeView tv, RootMenuItemInfo rootMenuItemInfo)

    {

        // Clear the tree.

        tv.Items.Clear();

        //루트의 직속 자식들에 대한 treeitem을 출력한다.

        TreeViewItem treeItem = null;

        foreach (IMenuItemInfo itemInfo in rootMenuItemInfo.GetChildren())

        {

            treeItem = new TreeViewItem();

            treeItem.Header = itemInfo.Name;

            treeItem.Tag = itemInfo;

            tv.Items.Add(treeItem);

 

            //재귀호출을 시작한다.

            ProcessItem(treeItem, itemInfo);

        }

    }

 

    private void ProcessItem(TreeViewItem parentItem, IMenuItemInfo parentItemInfo)

    {

        TreeViewItem treeItem = null;

        List<IMenuItemInfo> lstMenuItemInfos = parentItemInfo.GetChildren();

        if( lstMenuItemInfos == null )

            return;

        foreach (IMenuItemInfo itemInfo in lstMenuItemInfos)

        {

            treeItem = new TreeViewItem();

            treeItem.Header = itemInfo.Name;

            treeItem.Tag = itemInfo;

            parentItem.Items.Add(treeItem);

 

            ProcessItem(treeItem, itemInfo);

        }

    }

한편 MenuHelper로부터 화면 정보 객체 목록을 건네 받아서 Dalbon2XmlObjectFactory를 이용해서 화면 정보를 등록하고 있다.

        List<Dalbong2ElementInfo> _Dalbong2ElementInfos = MenuHelper.GetDalbong2ElementInfos();

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

        _Dalbong2XmlObjectFactory.RegisterDalbong2ElementDefinitions(_Dalbong2ElementInfos);

 

이제 이 프로그램을 실행시키면 다음과 같은 썰렁한 결과가 출력된다.

이제 좌측 최종 메뉴를 클릭하면 우측의 탭 컨트롤에 화면이 출력되는 로직을 추가할것이다. 그러나 그 전에 화면 객체를 나타내는 Dalbong2Element를 구성할 것이다. 이것도 생각이 많은 부분이다.

 벌써 일요일도 다 갔다. 급한 마음으로 저녁을 먹다 뭔가 걸렸다. 체를 한 듯하다. 쓰으…소화제가 없을텐데.

Posted by dalbong2

이전 포스트에서는 Spring.NET에서 제공하는 XmlObjectFactory를 이용해서 화면 객체에 대한 정보를 로딩하는 작업을 했다. 이번 포스트에서는 XmlObjectFactory( Spring.NET 컨테이너 )에 화면 객체를 요구하는 작업을 하겠다. 아래 그림의 붉은 색 부분이 오늘 포스트의 주제이다.

객체생성기

컨테이너에 객체를 요구할때는 GetObject(“객체ID”)를 호출해서 그 참조를 얻을 수 있다.

 

dalbong2ObjectFactory.GetObject("01");

 

이 메소드 내부 소스를 분석해보면 이전 포스트에서 로딩한 객체 정보를 이용해서 해당 어셈블리를 로딩한 후 그 어셈블리를 통해서 동적으로 객체를 생성해낸다. 문제는 그 어셈블리를 어디에서 로딩하느냐이다. 기본적으로 Spring.NET은 현재 애플리케이션 도메인으로부터 어셈블리를 로딩한다.

그러나 기업형 애플리케이션에서는 그렇게 달갑지 않은 방식이다. 이렇게 로컬 머신에서만 어셈블리를 찾는 다는 것은 애플리케이션을 설치할때 모든 화면단 어셈블리들을 사용자 로컬 머신으로 다운로드받고 시작해야 한다는 의미이다. 그러나 기업 애플리케이션에서는 사용자가 자신의 작업을 하는데 필요한 화면을 포함하는 어셈블리만 사용자 머신으로 다운로드하면 된다. 다른 사람들 작업에 필요한 어셈블리까지 다운로드받는 것은 비효율적이다.

더욱 더 큰 문제는 운영시에 있다. 운영시 사용자의 요구로 인한 화면단의 변경은 수시로 있게 된다. 변경된 화면이 포함된 어셈블리만 copy&paste로 서버에 올려 놓는 구조로 가면 편리할 것이다. 그러나 화면단의 일부 변경이 있을때마다 전체 애플리케이션의 게시 버전을 올려 다시 서버로 게시해야 한다는 것은 운영하는 입장에서는 무리가 아닐 수 없다. 변수 하나 바뀌고 데이터베이스 테이블 컬럼하나 변경되어도 다시 전체 애플리케이션을 게시해야 하다니… 현재 모 기업의 운영 시스템이 이렇게 운영되고 있다.

따라서 달봉이는 화면이 포함된 어셈블리를 원격 서버로부터 다운받아서 로딩할 수 있는 구조로 XmlObjectFactory의 기본 구조를 확장 하고 싶다. 물론 환경 설정에 따라서 로컬에서 로딩할 수도 있도록 할 것이다.

XmlObjectFactory의 기본적인 객체 생성 전략은 소스를 추적해 들어가다 보니, 다음 클래스에서 지정한 SimpleInstantiationStrategy라는 녀석이 담당하고 있었다.

simple instantiation strategy

XmlObjectFactory는 MethodInjectionInstantiationStrategy라는 녀석을 사용하고 있지만, 실질적으로 객체를 생성하는 로직은 SimpleInstantiationStrategy에 있었다. 이 녀석은 XmlObjectFactory의 조상 클래스인 AbstractAutowireCapableObjectFactory에서 InstanticationStrategy라는 속성으로 노출되어 있었다.

instantiationstrategy

 

이 속성은 protected로 지정되어 있고 set, get이 가능하도록 되어 있다. 이것이 의미하는 바는? 바로 AbatractAutowireCapableObjectFactory를 상속하는 자식 클래스에서 새로운 객체 생성 전략 클래스를 만들어서 기본 객체 생성 전략을 대체할 수 있다는 의미이다. 새로운 객체 생성 전략 클래스를 하나 만들어서 XmlObjectFactory의 InstantiationStrategy 속성에 지정하면 된다는 것이다.

이제 달봉이는 Dalbong2InstantiationStrategy라는 클래스를 만들었다.

Dalbong2InstatiationStrategy

 

 

/// <summary>

/// 원격의 서버로부터 어셈블리를 다운받아,

/// 타입의 인스턴스를 생성할 수 있는 인스턴싱 전략클래스

/// </summary>

public class Dalbong2InstantiationStrategy : MethodInjectingInstantiationStrategy

{

    public Dalbong2InstantiationStrategy()

        : base()

    {

    }

 

    #region "IInstantiationStrategy"

 

    /// <summary>

    /// IInstatiationStrategy 인터페이스 구현

    /// </summary>

    /// <param name="definition">The definition of the object that is to be instantiated. </param>

    /// <param name="name">

    /// The name associated with the object definition.

    /// The name can be the null or zero length string

    /// if we're autowiring an object that doesn't belong to the supplied factory.

    /// </param>

    /// <param name="factory">The owning IObjectFactory</param>

    /// <returns></returns>

    public override object Instantiate(RootObjectDefinition definition, string name, IObjectFactory factory)

    {

        PropertyValue pv = definition.PropertyValues.GetPropertyValue("dalbong2ElementInfo");

        if (pv != null)

        {

            return InstantiateFrom(definition);

        }

        else

        {

            return base.Instantiate(definition, name, factory);

        }

    }

    <중략>…

 

    #endregion

 

    /// <summary>

    /// Dalbong2ControlBase 객체를 실제로 생성하는 메소드

    /// </summary>

    /// <param name="definition"></param>

    /// <returns></returns>

   private Dalbong2ControlBase InstantiateFrom(RootObjectDefinition definition)

    {

        object instance = null;

        if (!definition.ObjectType.IsAssignableFrom(Type.GetType("Dalbong2.Win.Dalbong2ControlBase"))

            && !definition.ObjectType.IsAssignableFrom(Type.GetType("Dalbong2.Win.Dalbong2PageBase")))

            throw new Exception(String.Format("생성하려는 객체가 적절한 타입이 아닙니다. 타입:{0}", definition.ObjectTypeName));

 

 

        bool devMode = true;

        string fullyQualifiedTypeName= "";

        string fileName = "";

        string assemblyPath = "";

 

        //element 정보를 조회한다.

        Dalbong2ElementInfo elInfo = null;

        elInfo = (Dalbong2ElementInfo)(definition.PropertyValues.GetPropertyValue("dalbong2ElementInfo").Value);

 

        fileName = (String.IsNullOrEmpty(elInfo.FileName) ? "" : elInfo.FileName);

        assemblyPath = (String.IsNullOrEmpty(elInfo.LoadUrl) ? "" : elInfo.LoadUrl);

        fullyQualifiedTypeName= (String.IsNullOrEmpty(elInfo.FullyQualifiedTypeName) ? "" : elInfo.FullyQualifiedTypeName);

 

        // 원격 서버에서 어셈블리를 가져오는 경우는 devMode를 false로 한다.

        if (assemblyPath.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) || assemblyPath.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))

        {

            devMode = false;

        }

 

        //확장자 ".dll"이 있는지를 확인한다.

        string assemblyFile = fileName.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase) ? fileName : fileName + ".DLL";

 

        // devMode가 true인 경우 어셈블리를 로컬머신( 애플리케이션 도메인)에서 로딩하도록 경로를 지정한다.

        if (!devMode)

        {

           assemblyPath = System.IO.Path.Combine(assemblyPath, fileName);

        }

        else

        {

            assemblyPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);

        }

 

        try

        {

            Assembly assembly = Assembly.LoadFrom(assemblyPath);

            instance = assembly.CreateInstance(fullyQualifiedTypeName, true);

        }

        catch (System.IO.FileNotFoundException ex0)

        {

            throw new System.IO.FileNotFoundException(String.Format("파일({0})을 찾을 수 없습니다.", assemblyPath), ex0);

        }

 

        <중략>…

 

 

        //Dalbong2ControlBase의 ElementInfo 속성을 이용해서 화면객체에 대한 정보를 저장해둔다.

        Dalbong2ControlBase dalbong2Control = null;

        dalbong2Control = (Dalbong2ControlBase)instance;

        dalbong2Control.ElementInfo = elInfo;

 

        return dalbong2Control;

    }

}

 

Dalbong2InstantiationStrategy는 MethodInjectingInstantiationStrategy를 상속받아서 기존의 객체 생성 전략도 그대로 유지할 수 있도록 하고 있다. XmlObjectFactory는 객체를 생성하기 위해서 인터페이스 IInstantiationStrategy의 멤버 Instantiate() 메소드를 호출할 것이다.

그 메소드를 호출할때 넘어오는 인자중에서 RootObjectDefinition 타입의 definition객체가 넘오는데, 이곳에 객체 타입에 대한 정보가 포함되어 있다. 이 정보에서 우리가 이전 포스트에서 넘겨 주었던 “dalbong2ElementInfo”라는 이름의 속성값이 존재한다면 이 녀석은 우리가 직접 생성해야 하는 화면 객체라고 생각할 수 있을 것이다.

만약 “dalbong2ElementInfo”라는 이름의 속성값이 존재한다면 InstantiateFrom() 메소드를 호출하고 있다.

이 메소드에서는 객체 정보 등록시 건네주었던, 클래스명( FullyQualifiedTypeName ), 어셈블리명( FileName), 어셈블리를 가져올 원격 주소( LoadUrl )값을 이용해서 원격서버에서의 어셈블리 파일의 위치를 결정한다. 그런 다음 Assembly.LoadFrom() 메소드를 통해서 해당 어셈블리를 가져와서 메모리로 로딩한다. 그런 다음 assembly.CreateInstance()를 통해서 동적으로 해당 화면 객체를 생성해서 반환한다.

반환되는 화면 객체는 XmlObjectFactory가 받아서 singleton객체로 컨테이너에 캐싱하게 될 것이다.

이상!

아니다. 이 Dalbong2InstantiationStrategy를 기본 생성 전략 객체로 대체하는 곳을 보여주지 않았다. 이전 포스트에서 보여주었던 Dalbong2XmlObjectFactory 정의 코드를 다시 일부 보도록 하자.

public class Dalbong2XmlObjectFactory : XmlObjectFactory

{

    public Dalbong2XmlObjectFactory(IResource resource ) : base(resource)

    {

        base.InstantiationStrategy = new Dalbong2InstantiationStrategy();

    }

<이하 생락>…

 

이렇게 XmlObjectFactory를 상속, 확장한 클래스의 생성자에서 새로운 InstantiationStrategy 인스턴스를 넘겨주면 된다.

이제 xmlObjectFactory.GetObject(“객체ID”)를 통해서 호출되는 객체가 화면 객체인 경우는 달봉이의 새로운 생성 전략을 따라서 생성되게 될 것이다.

이상.

Posted by dalbong2

달봉이는 프리젠테이션 레이어를 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 기능을 사용하면 충분히 그런 작업에 대한 어드바이스들을 서비스 호출 전, 후에 끼워 넣을 수 있을 것이라는 예상이다.

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

Posted by dalbong2

Spring.NET의 컨테이너를 UI단에 적용하는 작업은 끝났다. 근데 정리할 시간이 없다. 7월부터 시작하는 UI단 프레임워크 개발 작업이 있다. 이것을 준비해야 하는 관계로 WPF를 좀 더 공부해야 한다. 그래서 정리 순서를 바꾸도록 했다. Spring.NET 컨테이너의 UI단 적용은 다음에 정리해야 할 것 같다.

 

흔히 기업용 애플리케이션을 제작할때는 윈폼 클래스 또는 사용자 정의 클래스들의 베이스 클래스를 만든다. 그래서 그곳에서 사용자 정보, 권한 정보들을 캐싱해 둔다. 현장의 개발자들은 그것을 상속해서 화면을 만든다.

달봉이도 이런 시나리오를 염두에 두고 WPF에서 제공하는 UserControl의 베이스를 하나 만들려고 했었다. 윈폼 시절의 UserControl을 생각해서 쉽게 될 줄 알았다. 근데, 이것 때문에 반나절을 고생했다.

다음은 WPF에서 UserControl의 상속이 지원이 안 되는  경우이다.

우선 Visual Studio의 솔루션 탐색기의 새 항목 추가 창을 통해서 UserControl 템플릿을 “Dalbong2ControlBase.xaml”이라는 이름으로 하나 추가한다.

그런 다음 동일한 방법으로 UserControl을 DerivedControl.xaml이라는 이름으로 추가한다.

그런 다음 DerivedControl.xaml의 코드 비하인드에서 DerivedControl 클래스의 베이스 클래스를 Dalbong2ControlBase로 수정한다.

빌드하면 에러가 발생한다.  구글링을 해 보고 수정을 반복하다 보면 만나게 되는 에러가 주로 다음 두 가지일 것이다.

이 에러 발생 원인을 설명하자면 XAML의 페이지 구조부터 컴파일 절차까지 설명해야 할 것 같다. 그러나 달봉이 시간이 없다. 지금 진행하고 있는 프로젝트가 WPF로 진행될 거라 빨리 프레임워크를 준비해야 한다.  이 에러의 원인만 정리한다.

WPF의 UserControl의 경우, UI가 있는 컨트롤 즉 XAML이 있는 UserControl은 자식 UserControl이 상속을 할 수 없다.

그러나 우리는 사용자 컨트롤의 베이스 클래스가 필요하다. 다음과 같은 방법을 사용할 수 있다.

사용자 컨트롤의 베이스 클래스로는 XAML이 없는 클래스를 사용하도록 한다.

다음 그림을 보자.

선택된 두 파일중에서 아래 것은 Visual Studio의 템플릿을 통해서 추가한 사용자 컨트롤이다. 이 녀석은 앞에서 얘기한 것처럼 베이스 클래스로 사용할 수 없다. UserControl의 베이스 클래스를 만들고 싶다면 순수한 C# 클래스 파일을 추가해서 다음처럼 UserControl을 상속받도록 해야 한다.

다음 코드를 보자.

    public class Dalbong2ControlBase : UserControl, IDalbong2Element

    {

    }

그런 다음 이 베이스 클래스 Dalbong2ControlBase는 다음처럼 사용할 수 있다.

먼저 코드 비하인드에서 베이스 클래스를 Dalbong2ControlBase로 수정한다.

 

    public partial class UserControl1 : Dalbong2.Win.Dalbong2ControlBase

    {

        public UserControl1()

        {

            InitializeComponent();

        }

    }

다음은 XAML 페이지로 간다.

붉은 색 부분을 추가해야 한다. Dalbon2.Win 네임스페이스에 있는 Dalbong2ControlBase를 사용자 컨트롤의 루트( 비쥬얼 컨테이너 )로 사용하겠다는 의미이다. 그리고 파란색 부분은 코드 비하인드의 현재 클래스를 나타낸다.

달봉이 고민

공통 정보는 베이스 클래스를 이렇게 작성해서 그곳에 넣어두면 된다지만,  UI 상속은 어떻게 해야 하는지 알 수 없다. 굳이 필요하다면 커스텀 컨트롤을 만들어서 사용할 수도 있겠다는 생각이지만,….그렇게까지 해야 하는 마음이 드는 것은 이전의 Windows Form 시절의 UserControl이 아쉽다는 생각이 들기 때문이다.

Posted by dalbong2

이미 Spring.NET이 기업형 애플리케이션에 적용되고 있다는 얘기도 가끔 듣고 있다. 그러나 달봉이가 직접 POC( Proof Of Concept) 애플리케이션을 하나 만들어서 Spring.NET이 기업에 어떻게 적용될 수 있는지 나름대로 검토를 해 보고자 한다.

달봉이가 만들 애플리케이션은 흔히 현장에서 사용하고 있는 3 티어 구조를 고려한다. 그리고 사용될 주요 기술은 다음과 같다.

 

UI 레이어 WPF 기반, Spring.NET 컨테이너
통신 방법 Spring.NET 지원의 WCF
비즈니스 레이어 트랜잭션 관리 Spring.NET의 TxScopePlatformTransactionManager
Spring.NET 컨테이너
데이터접근 Spring.NET의 DAO
( 그때의 기분이 동하면 NHibernate를 사용하는 예도 볼 수 있겠다. )
Spring.NET 컨테이너

 

최종 개발자가 개발을 할 때 구성하게 될 Visual Studio의 개발 구조는 이렇게 될 것이다. 이 개발 구조의 네임스페이스는 Bong이라는 가상 기업을 상상해서 구성한 것이다.

_Framework 폴더 부분을 보면 두 부분으로 나뉘었다 : 01 Spring, 02 BONGCo

01 SPRING 부분은 달봉이가 Spring.NET을 확장하거나 수정하는 부분이다. 이 부분은 최대한 현장에서 확장/변경될 수 있는 부분을 고려해서 만들어 질 부분이다. 즉 이상적인 모습은 현장에 상관없는 부분이 되어서, 현장에서 소스의 수정이 없는 부분이 되도록 할 것이다. 따라서 이 부분이 현장으로 배포될때는 DLL 형태로 배포될 것이다. 

02 BONGCo라는 폴더에는 앞에서 말한대로 01 SPRING에서 제공하는 프레임워크를 현장에 맞게 확장하는 부분이다. 이 부분은 현장의 공통팀이 맞게 될 것이다.

만약 현장에서 변경요청이 들어왔고, 그 부분을 현장 프레임워크이 커버하지 못한다면, 결국  01 SPRING 부분이 그 변경 요청을 수용할 수 밖에 없다. 이런 경우라면 결국 01 SPRING 부분의 소스도 현장의 요구 사항을 고려해서 수정되어야 한다. 결국 01 SPRING 프레임워크이 현장에서 변경될 수 있는 부분을 최대한 고려해서 만들어져야 한다. 현장마다 변경될 수 있는 부분은 최대한 현장 공통팀이 담당하는 프레임워크에서 처리할 수 있도록 하는 구조로 가야 한다.

예를 들어서 프레임워크에서 사용자 정보를 캐싱하고 있다가 개발자 코드에서 요구하면 프레임워크단에서 제공하는 시나리오는 흔한 요구사항이다. 그러나 그 사용자 정보를 구성하는 항목은 기업마다 다를 수 있다. 이런 경우 01 SPRING에서는 기본적인 사용자 정보(ID )만을 갖는 베이스 클래스 정도만을 가지고 있으면 된다. 그리고 현장의 공통팀에서는 그 베이스 클래스를 상속해서 그 현장에서 요구하는 추가 정보를 가지는 사용자 정보 클래스를 만들면 된다. ( 참고로 현장에서 확장한 사용자 정보클래스를 생성하는 녀석은 01 SPRING에서이다. 01 SPRING에서 어떻게 현장에서 정의한 클래스에 대한 정보를 알 수 있는지는 나중에 언급할 기회가 있을 것이다.)

개발 구조를 다시 보자. 현장 업무 개발팀이 담당해야 하는 부분이 있고, 그리고 Shell이라는 폴더에 BongApp라는 WPF 애플리케이션이 있다. 이 BongApp 애플리케이션은 업무 시스템의 이 시작 프로젝트가 된다. 이 녀석이 시작되면서 메뉴가 로딩되고 화면이 출력되기 시작하는 것이다. 업무 시스템의 엔트리 포인트라 할 수 있다. 

 

다음 포스트에서는 이 BongApp 프로젝트를 구성하는 작업부터 시작할 것이다. Spring.NET 컨테이너가 기업형 애플리케이션에 적합한지, 어떤 확장이 필요한지를 다룰 것이고, 실제로 애플리케이션의 메뉴 항목들을 컨테이너로 로딩하는 작업등이 있을 것이다.

Posted by dalbong2

아직 Spring.NET의 구체적인 API 및 설정 방법 등에 대해서는 모두는 알아보지 않았다. 그러나 사용 컨셉은 알게 되었다. 따라서 최소한 달봉이 머릿속에는 기업용 개발 프레임워크를 만들기 위해서 어떻게 사용해야 하는지는 레이어별로 가닥이 잡힌 듯 하다.

 

혹시 Spring.NET을 어떻게 사용할까 또는 이것을 이용해서 기업용 개발 프레임워크를 직접 만들 수 있지 않을까 하는 기대를 가지고 이 글을 읽고 있는 독자들이 있다면 달봉이와 같은 마음이길 바란다. 그러나 아직 그렇지 않더라도 실망할 필요는 없을 듯 하다. 사실 달봉이는 몇개의 프레임워크 다뤄본 경험이 있다. 그래서 그것을 염두에 두고 글을 쓰고 있기에 기존의 기업용 개발 프레임워크에서 필요로 하는 요구사항들을 어느 정도는 알고 있다. 그래서 그 기능을 구현하기 위해서 어떻게 Spring.NET을 사용할 수 있을까를 생각하면서 글을 쓰고 있는 것이다. 그런 요구사항들에 아직 경험이 없는 독자들이라면 아직 Spring.NET을 어떻게 사용해야 할지를 난감해할 지도 모르겠다. 그러나 언젠가는 빛이 나타날 수 있을 것이라 본다. 달봉이도 최선을 다해서 이 연재를 마치려고 한다. 언젠가는 Spring.NET을 이용해서 꼭 개발 프레임워크의 구현을 이루고 싶다. 독자들에게는 그때가 바로 Spring.NET이 “하늘에 떠 있는  뜬 구름”이 아닌 “땅위에 완성된 건물”로 다가올 것이라 본다.

 

이번 포스트 토픽으로 무엇을 할 것인가 고민을 했다. 지난 포스트에서 Spring.NET의 트랜잭션 얘기를 했으니 이번에는 실제로 트랜잭션을 이용하는 샘플을 만들어 볼까도 했다. 그러나 어차피 기업용 개발 프레임워크를 본격적으로 구현하는 단계에 들어가면 샘플은 있어야 한다. 구현된 코드는 그때 가서 보기로 결정했다.

아니면 Spring.NET의 WCF에 대한 지원 이야기를 해 볼까도 생각했다. 그러나 Spring.NET이 지원하는 내용은 WebServices에 대한 대한 내용과 크게 다를 것은 없을 것 같다는 생각을 했다. 물론 WebServices와 WCF 자체의 기술적인 차이는 크다. 그러나 Spring.NET 입장 그리고 개발 프레임워크를 만드는 우리 입장에서는 그 기술들의 세세한 내용은 지금 단계에서는 중요하지 않다고 판단했다. 이 또한 구현의 단계에서 고려해야 할 토픽이라고 여겼다.

이렇게 생각하고 보니 다음 토픽으로 무엇을 해야 할까가 쉽게 떠오르지 않았다. 만약 토픽이 없다면 이제 개발로 들어가야 하나 하는 생각을 하다 하나를 발견했다.

바로 권한 설계 !

 

프레임워크 보안 설계(권한 설계)

 

흔히 개발 현장에서는 “권한”이라는 말을 자연스럽게 쓰고 있지만 상당히 뭉뚱그린 표현이다. 대화의 문맥에 따라 “권한”이라는 표현이 “사용자별 접근 가능한 메뉴”에 대한 권한일 수도 있고 또는 “한 화면에서의 CRUD에 대한 권한”이라는 의미를 가질 수도 있고 그리고 어떤 상황에서는 “사용자별 접근 가능한 데이터”라는 의미로도 사용된다.

그러나 구분없이 사용하는 이 “권한”이라는 표현을 영문으로 하면 문맥에 따라서 전혀 다른 표현으로 사용된다.

개발 프레임워크의 보안(권한) 설계에 대한 이해를 좀 더 명확히 하는데 도움이 되기 위해서, 이 포스트에서는 먼저 Windows 시스템의 보안에 대한 내용을 개념적으로 잠시 짚어보고 갈 것이다. 달봉이는 이 개념들을 개발 프레임워크의 보안 설계에 그대로 적용하려고 한다.  Windows의 보안에 대한 기본 개념은 지난 포스트에서 설명한 적이 있다. 참고하면 많은 도움이 될 것으로 보인다.

Windows 보안에 대한 자세한 내용을 보고 싶다면 여기 링크를 클릭해 보길 바란다. 책 한 권을 온라인에 올라와 있다. .NET 개발자로서 알아야 하는 Windows 보안에 대해 설명해 놓고 있다. 

Windows 보안과 관련된 기본적인 녀석들이 박스로 표현되어 있다. 먼저 Privilege라는 것이 것이 있다. 이 녀석은 사용자가 시스템( Operation Systme )에 대해서 어떤 작업( operation)을 할 수 있는지를 결정한다. 시스템 관리자는 “Local Security Policy”라는 관리툴을 이용해서 사용자(그룹)별로 어떤 작업을 할 수 있나를 설정할 수 있다. 커맨드 창에서 “secpol.msc”를 실행하면 수행된다.

작업(operation)들을 보면 시스템의 time zone을 변경하거나 시스템을 리부팅시킬 수 있는 권한을 부여하거나 하는 작업을 할 수 있다. Privilege라는 것은 사용자(그룹)가 시스템을 상대로 하는 작업에 대한 권한이라고 볼 수 있겠다.

사용자 Identity라는 것은 로그인 인증을 통해서 갖게 되는 정보이다. 사용자가 로그인을 하게 되면 사용자에 대한 토큰이 생성된다. 이 토큰에는 사용자 Identity, 사용자의 Privilege에 대한 정보 외에도 더 많은 정보가 포함되어 있지만 기본적으로 이 녀석은 “Who are you?”, “What are you allowed to do?”에 대해 답할 수 있는 정보가 포함되어 있다. 사용자가 프로그램을 실행시키면 사용자 토큰은 그대로 프로그램의 프로세스로 전달된다. 그런 식으로 사용자의 권한이 그 프로세스가 할 수 있는 권한을 결정하게 되는 것이다.

그림에는 Permission이라는 녀석이 있다. 이것도 우리말로는 흔히 “권한”이라는 것으로 표현되는데 이 녀석은 Privilege와는 다르다. 리소스에 대한 접근 권한이다. Windows 시스템에는 Permission에 의해 접근이 결정될 수 있는 여러 자원이 있다 : Files, registry keys, services, 프로세스 같은 커널 객체들 등. 이런 녀석들은 사용자( 프로세스 )가 자신들에 어떤 작업을 할 수 있는지를 결정해 놓은 목록이 있다. 이름하여 ACL( Access Control List)라는 것이다.

예를 들어 파일 시스템에서는 어떤 파일에 대해서 어떤 사용자가 어떤 작업( CRUD )을 할 수 있는지를 설정해 놓은 목록이 있다. 파일 및 폴더 자원에 대한 ACL 관리는 우리가 자주 사용하는 파일이나 폴더의 속성창을 통해서 할 수 있다.

사용자가 자원을 사용할 수 있느냐 마느냐는 이 사용자에 대해서 설정된 Permission에 의해서 결정되는 것이다.

분명 Privilege와 Permission은 개념적으로 다른 녀석들이다. “권한”이라는 하나의 표현으로 뭉뚱그리기에는 억울한 녀석들인 것이다.

 

Windows의 기본 보안 개념의 개발 프레임워크로의 적용

 

Privilege는 시스템을 상대로 해서 어떤 작업(operation)을 할 수 있는가를 결정한다고 했다.업무용 시스템에서라면 그 operation이라는 것은  “업무”를 말할 것이다. 예를 들어서 사용자 정보를 관리하는 업무, 인사 정보를 관리하는 업무 등등. 그런 업무들은 계층 구조를 가질 수 있어서 구체적인 업무로 다시 분류될 수도 있을 것이다. 이런 업무는 시스템에서는 “메뉴”로 구분될 수 있다. 즉 메뉴 체계는 보통 그 기업에서의 업무 구조를 근거로 해서 만들어지게 된다. 메뉴를 통해서 어떤 업무 작업을 할 수 있다는 것은 바로 Windows의 Privilege의 구현이라고 볼 수 있겠다. 즉 메뉴에 대한 사용자별( 사용자 그룹별) 권한을 부여한다는 것은 사용자에 대한 Privilege를 부여하는 작업에 해당된다고 할 수 있다. 혹시 달봉이가 만들 샘플 시스템에 예를 들어서 Privilege Mangement 라는 메뉴가 있다면 사용자(그룹)에 메뉴에 대한 권한(privilege)를  관리하는 프로그램이라고 보면 되겠다.

사용자 Identity는 사용자를 구분( authentication )하는 최소한의 정보이다. 달봉이가 구상하는 프레임워크에서는 이것을 표현할 UserIdentity 같은 클래스가 만들어질 것이다. 여기에는 사용자의 Id가 전부이다. 이 Id는 그 기업에서의 사번이 될 수도 있겠고 아니면 주민번호도 될 수 있을 것이고 아니면 고유한 일련의 문자열이 될 수도 있을 것이다. 사용자 Identity는 사용자를 인증하는 최소한의 정보를 가지고 있다.

그러나 시스템에 로그인하면 기본적으로 프레임워크단에서 가지고 있어주면 좋겠다는 사용자 정보에 대한 요구 항목이 기업마다 달라질 수 있다. 보통 사용자가 소속된 부서 코드만 포함시켜주길 바라는 곳도 있지만 어떤 기업 시스템에서는 그 기업만이 갖는 특수한 구조때문에 사용자 정보에 그런 항목도 포함되길 바라는 곳도 있다.

이런 상황을 고려해서 달봉이는 4개 이상의 타입을 도입할 것이다 : IIdentity, UserIdentity, IUserInfo, UserInfo, SiteUserInfo. 앞에서 4개 정도는 달봉이의 개발 프레임워크에서 제공할 것이고, 마지막 녀석은 현장에서 기업별로 제공되는 사용자 정보 클래스이다. 이 타입들의 구조에 대한 이야기는 다음 구현 단계에서 한번 더 있을 것이다.

Permission은 자원에 대한 접근 권한이라고 했다. 이때의 자원이라면 데이터에 대한 접근 권한이라고 할 수 있겠다. 즉 데이터에 대한 CRUD 권한은 달봉이 프레임워크에서는 Permission이라는 용어로 표현될 것이다.

 

이제 다음 포스트는 뭐로 할까요. 이제 개발로 들어가야 하나. 배고파! 돈이 없으니 더 배고프다. 쓰으..

Posted by dalbong2