태터데스크 관리자

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

태터데스크 메시지

저장하였습니다.

'전체'에 해당되는 글 207건

  1. 2010/03/08 Spring.NET 개발 가이드 by dalbong2
  2. 2009/12/31 [메모] WCF 호출시 병목현상 관련 by dalbong2
  3. 2009/12/29 [메모] Cross-domain 접근을 허용하는 방법 by dalbong2
  4. 2009/12/29 [메모] Getting Started with Silverlight development by dalbong2
  5. 2009/12/03 [메모] CLR v4 Security Policy Roundup by dalbong2
  6. 2009/12/03 [메모] Tying your IE Hosted Control to a Manifest by dalbong2
  7. 2009/11/19 MIXSH인증용 새 페이지 by dalbong2
  8. 2009/10/27 [메모] White Box testing tool for .NET by dalbong2
  9. 2009/08/08 개발 프레임워크 만들기 대장정 47 - UI단 프레임워크 추가사항 by dalbong2
  10. 2009/07/20 개발 프레임워크 만들기 대장정 46 - 서비스 호출하기 by dalbong2
  11. 2009/07/20 개발 프레임워크 만들기 대장정 45 - 서비스 구성하기 by dalbong2
  12. 2009/07/20 개발 프레임워크 만들기 대장정 44 - WCF 확장하기 III ( 프로그레스바 자동 보여주기) by dalbong2
  13. 2009/07/12 개발 프레임워크 만들기 대장정 43 - WCF 확장하기 II (사용자정보 전달하기) by dalbong2
  14. 2009/07/08 개발 프레임워크 만들기 대장정 42 - WCF 확장하기 I (사용자 정보 받기 ) by dalbong2
  15. 2009/07/01 개발 프레임워크 만들기 대장정 41 - 화면 객체 로딩 테스트 by dalbong2
  16. 2009/06/19 개발 프레임워크 만들기 대장정 40 - 메뉴정보 로딩/출력하기 by dalbong2
  17. 2009/06/18 개발 프레임워크 만들기 대장정 39 - 화면 객체 생성 by dalbong2
  18. 2009/06/18 개발 프레임워크 만들기 대장정 38 - 화면 정보 로딩 by dalbong2
  19. 2009/06/13 WPF UserControl 베이스 클래스 만들기 by dalbong2
  20. 2009/06/07 개발 프레임워크 만들기 대장정 37 - POC 애플리케이션 - 개발구조 by dalbong2
  21. 2009/06/06 개발 프레임워크 만들기 대장정 36 - 개발 프레임워크 보안 설계 by dalbong2
  22. 2009/05/24 개발 프레임워크 만들기 대장정 35 - Spring.NET 트랜잭션 관리 by dalbong2 (2)
  23. 2009/05/18 개발 프레임워크 만들기 대장정 34 - Spring.NET 트랜잭션 관리(Strategy 패턴) by dalbong2
  24. 2009/05/08 ClickOnce 애플리케이션의 자동 시작과 제거 by dalbong2
  25. 2009/04/25 블로그 이전 완료 by dalbong2 (1)
  26. 2009/04/25 Hundreds Island ! 들어들 봤나 by dalbong2
  27. 2009/04/25 여기는 바기오 ! by dalbong2
  28. 2009/04/25 어학연수 준비중 by dalbong2
  29. 2009/04/24 개발 프레임워크 만들기 대장정 33 - Spring.NET의 Result Mapping by dalbong2
  30. 2009/04/24 개발 프레임워크 만들기 대장정 32 - Spring.NET의 MVC 패턴 지원 by dalbong2

오랜만에 포스팅을 한다.

그동안 새로운 회사에 입사를 했다.

현재 솔루션 개발 프로젝트에 참여하고 있는데, Spring.NET을 기본 프레임워크로 선정했다.

해서 Spring.NET 개발 가이드라는 문서를 하나 작성했다.
Spring.NET이 공개소스(Apache 라이센스)이니 관련 문서도 공개를 한다.


Posted by dalbong2

WCF 서비스를 호출할때, 클라이언트 수에 따라서 병목 현상이 일어날 수 있다. 이에 대한 이해 및 설정 내용을 다루는 포스트에 대한 링크들이다.

 

How to throttle a Wcf service, help prevent DoS attacks, and maintain Wcf scalability

 

Throttling in WCF  InstanceContextMode, ConcurrencyMode, and Server-side Threading

Posted by dalbong2

How to Use Cross Domain Policy Files With Silverlight

실버라이트도 웹 클라이언트 기술이기 때문에 클라이언트측의 웹 브라우저의 보안 샌드 박스에서 실행된다. 또한 웹 사이트 접근 제한 정책에 영향을 받는다. 그중의 하나가 하나가 바로 Cross-domain 접근 제한이다.

이게 뭐냐면, 한 domain에서 호스팅이되고 있는 웹 애플리케이션이 다른 domain에서 호스팅되고 있는 애플리케이션에는 기본적으로 접근할 수 없다는 것이다.

그러나 웹 애플리케이션에서 특정 도메인으로부터의 접근을 허용해주는 방법이 있다. cross-domain 정책 파일로 알려진 xml 파일을 이용하면 이런 접근에 대한 제한을 해제할 수 있다.

clientaccesspolicy.xml

crossdomain.xml

서비스를 제공하는 웹 애플리케이션에서 cross-domain 접근 제한을 해제해 주기 위해서, 이 두 파일을 어떻게 이용하는지 위 링크의 비디오에서 설명해준다.

Posted by dalbong2

Microsoft Silverlight 사이트

Get started building silverlight 3 applications

A blog by Tim Heuer

Getting started with silverlight development

Posted by dalbong2

.NET Security Blog

이 블로그의 주인이 어떤 사람인지는 모르겠지만, 이 포스트들을 읽다 보면 재밌다. 그래서 자주 들러보는 블로그중의 하나다.
이번에도 이 블로그에 올라온 글을 하나 메모해 두려 한다.

CLR v4 Security Policy Roundup
저작자 표시
Posted by dalbong2
.NET Security Blog에서 있는 포스트이다.

Tying your IE Hosted Control to a Manifest

I talked about the Orcas feature which allows you to provide a manifest to elevate your control's permissions declaratively.  We also saw how to generate manifests that would state what permissions your control needs (and the rules associated with those manifests).  Now it's time to tie it all together and create an HTML page that has a control and its associated manifests.
저작자 표시
Posted by dalbong2

KygnjE5KWHVT4TUvNje7zmd0Ck7WR0_1ijq4FxstZDc,

'Others' 카테고리의 다른 글

MIXSH인증용 새 페이지  (0) 2009/11/19
[메모] White Box testing tool for .NET  (0) 2009/10/27
블로그 이전 완료  (1) 2009/04/25
Hundreds Island ! 들어들 봤나  (0) 2009/04/25
여기는 바기오 !  (0) 2009/04/25
어학연수 준비중  (0) 2009/04/25
Posted by dalbong2

웹 서핑을 하다, vs 2008 용 테스트 툴을 보게 되서 메모해둔다.

 

Getting started with Pex in Visual Studio 2008

( http://channel9.msdn.com/posts/Peli/Getting-started-with-Pex-in-Visual-Studio-2008/ )

 

“Pex”로 검색을 해 보니, MS 에반젤리스트들의 IT블로그에 툴을 사용하는 법도 정리되어 있다.

 

.NET White Box Testing 도구, PEX (Program EXploration)

( http://blogs.msdn.com/eva/archive/2009/01/28/net-white-box-testing-pex-program-exploration.aspx )

'Others' 카테고리의 다른 글

MIXSH인증용 새 페이지  (0) 2009/11/19
[메모] White Box testing tool for .NET  (0) 2009/10/27
블로그 이전 완료  (1) 2009/04/25
Hundreds Island ! 들어들 봤나  (0) 2009/04/25
여기는 바기오 !  (0) 2009/04/25
어학연수 준비중  (0) 2009/04/25
Posted by dalbong2

UI단 프레임워크가 거의 완성되었다. 그러나 고려해봐야 할 녀석들이 몇 가지 있다. 달봉이가 참여한 프로젝트가 빡빡한 일정때문에 힘들어지고 있다. 아마 당분간은 정리가 힘들것 같다.  생각나는 대로 메모를 해 둬야겠다. 요즘은 메모를 해 두지 않으면 금방 잊어버린다. 이러다가 영화 “메멘토” 수준으로 될 것 같은 기분이 요즘 든다.

 

-업무 화면의 베이스 클래스 타입

-업무 화면 객체의 출력 컨트롤

-화면 객체의 라이프사이클

-서버측 서비스 환경 설정

-사용자 정보 객체의 사이트별 확장

 

 

■업무 화면의 베이스 클래스 타입

WPF의 루트 요소를 고려해서 UserControl, Page  두 종류의 베이스 클래스가 있어야 할 것 같다.

 

■업무 화면 객체의 출력 컨트롤

두 종류의 업무 화면 베이스 클래스를 출력할 수 있으려면? Frame 객체 사용 고려

 

■화면 객체의 라이프사이클

화면 종료시, 객체 저장소에서의 화면 객체 제거 여부 결정하기

메모리 사용량과 관련

 

■서버측 서비스 환경 설정

계층 구조로 구성된 WCF 서비스 애플리케이션에서 공통 확장 모듈에 대한 configuration을 어떻게 할 것인가.

 

■사용자 정보 객체의 사이트별 확장

달봉이 프레임워크의 코드를 수정하지 않고, 사이트별로 확장된 사용자 정보 및 개인 권한 정보 객체를 애플리케이션에서 사용할 수 있도록 등록할 것인가?

 

이상

Posted by dalbong2

이제 클라이언트에서 달봉이가 만들어놓은 프락시 팩토리를 이용해서 서비스를 호출해보자.

 

■ 서비스 참조 추가하기

 

우선 서비스에 대한 참조를 클라이언트 프로젝트에서 추가하자. BONG.WIN.CO.UserMgmt 프로젝트의 References 노드를 오른쪽 클릭해서 “Add Service Reference…”를 선택한다.

그럼 다음과 같은 서비스 참조 추가 창이 뜬다.

Address 박스에 이전 포스트에서 봤던 주소를 복사해 넣는다.

그런 다음 “Go”버튼을 클릭한다. 그럼 앞의 그림처럼 SampleService가 보이게 된다. 이제 Namespace 텍스트박스에 “SampleAsyncService”라 입력한다.

그리고 마지막으로 “Advanced…”버튼을 클릭한다.

그래서 Generate aysnchronous operations 체크박스를 선택한다.

이렇게 해서 참조 추가를 마친다. 이렇게 하면 Visual Studio는 클라이언트에서 사용할 수 있는 SampleService를 동기와 비동기적으로 호출할 수 있는 메소드를 갖는 프락시 클래스를 자동으로 생성해준다. 달봉이 프락시 생성 팩토리는 이 자동 생성 프락시를 사용하게 된다.

 

■ 업무 화면 준비

 

이제 업무 화면에 버튼을 하나 올려놓고 버튼을 클릭했을때 실행될 이벤트 핸들러를 작성해보자. UserMgmt.xaml 마크업 코드는 다음과 같이 되어 있다.

<Bong:BongControlBase x:Class="BONG.WIN.CO.UserMgmt.UserMgmt"

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

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

    xmlns:Bong="clr-namespace:Bong.Win;assembly=Bong.Win"

    Height="Auto" Width="Auto">

    <Border BorderBrush="Black" BorderThickness="2">

        <StackPanel>

            <Button Height="23" Name="btnHello" Width="75" Click="btnHello_Click">Hello</Button>

            <TextBlock Name="tbReply"></TextBlock>

        </StackPanel>

    </Border>

</Bong:BongControlBase>

 

■동기호출

 

이제 달봉이가 만들어놓은 프락시 팩토리 객체를 이용해서 서비스를 호출할 차례다.

먼저 동기로 호출하는 코드이다. 이것은 심플하다.

 BongServiceProxyFactory<SampleAsyncService.ISampleService> sampleProxyfactory = null;

SampleAsyncService.ISampleService asyncServiceProxy = null;

 string strHello = "";

 private void btnHello_Click(object sender, RoutedEventArgs e)

{

     //동기호출

     this.tbReply.Text = "";

     try

     {

         sampleProxyfactory = new BongServiceProxyFactory<SampleAsyncService.ISampleService>();

         asyncServiceProxy = sampleProxyfactory.GetServiceProxy("BaseUrlOfCOService",

             "BONG.CO.UserMgmt.Service/SampleService.svc"); //, this);

         strHello = asyncServiceProxy.Hello();

     }

     catch

     {

         //예외 처리

     }

     finally

     {

         sampleProxyfactory.Close();

     }

     //결과값 이용

     this.tbReply.Text = strHello;

 

BongServiceProxyFactory<T>를 생성한다. 개발자가 사용할 서비스에 대한 프락시 객체를 생성해줄 공장(?)이다. 그 다음 그 팩토리 객체를 이용해서 SampleService에 대한 주소를 건네주고 서비스에 대한 프락시 객체를 건네받는다.

이때 마지막 인자로 this를 넘겨줘도 상관없지만 내부적으로는 서비스 시작과 끝을 알리는 이벤트가 발생하지만 겉으로 보기에는 아무일도 일어나지 않는다. 프로그레스바도 나타나지 않는다. 왜? 동기호출이니까. (물론 동기 호출이라도 프로그레스바를 보여주는 방법은 있다. 프로그레스바를 보여주는 또다른 쓰레드를 만들어서 사용할 수도 있지만, 이렇게 프로그레스바를 보여주는 방법은 그닥 사용자가 보기에는 좋지 않다. 어플리케이션과 프로그레스바가 따로 따로 논다. 좋지 않다.)

서비스 프락시 객체 asyncserviceProxy에서 점을 찍으면 인텔리센스 기능에 의해 다음과 같은 호출 가능한 메소드 후보들이 나타난다.

이 중에서 Begin, End로 시작하는 Hello() 버전은 동기를 위한 것이고, Hello()가 동기 호출을 위한 것이다.  Hello() 를 선택한다. 이제 서비스를 호출하고 결과값을 반환받아서 필요한대로 사용하면 된다. 코딩은 앞에서처럼 try~catch~finally식으로 하면 된다.

 

■ 비동기 호출

 

다음은 비동기 호출을 위한 코딩 패턴이다. 개발자가 비동기 호출을 할때는 서비스를 호출하는 부분과 결과값을 처리하는 부분을 분리해서 작성해야 한다.

복잡한 듯 보이지만 다음 패턴대로만 한다면 그렇게 복잡한 것도 아니다.

BongServiceProxyFactory<SampleAsyncService.ISampleService> sampleProxyfactory = null;

SampleAsyncService.ISampleService asyncServiceProxy = null;

string strHello = "";

private void btnHello_Click(object sender, RoutedEventArgs e)

{

    //비동기호출

    //서비스 호출하는 부분

    this.tbReply.Text = "";

    sampleProxyfactory = new BongServiceProxyFactory<SampleAsyncService.ISampleService>();

    asyncServiceProxy = sampleProxyfactory.GetServiceProxy("BaseUrlOfCOService",

        "BONG.CO.UserMgmt.Service/SampleService.svc", this);

    AsyncCallback callback = new AsyncCallback(HelloCallback);

    //서비스 프락시 객체를 서버측으로 보낸다.

    asyncServiceProxy.BeginHello(callback, asyncServiceProxy);

}

이 부분이 서비스를 비동기로 호출하는 부분이다. 서비스 프락시 객체 asyncServiceProxy의 BeginHello() 메소드를 호출하고 있다. 이 메소드가 호출되고 나서 UI 쓰레드( 현재 메소드를 호출하는 쓰레드)는 이곳에서 서비스 답변을 기다리지 않는다. 그냥 호출만 하고 자신은 계속 실행을 진행한다. 앞의 코드에서는 서비스를 호출하고 나서 아무 일도 하지 않고 그냥 btnHello_Click() 메소드가 종료될뿐이다. 이때 내부적으로는 쓰레드가 하나 새롭게 하나 생성되어서 서비스 호출을 담당하게 된다.

BeginHello() 메소드에 두 개의 인자를 넘겨주고 있다. 서비스를 호출하는 부분에서는 서비스 답변을 기다리지 않고 그냥 종료되었으므로 그 결과를 처리할 부분을 프레임워크에 알려줘야 한다. 첫번째 인자가 그 답변을 처리할 곳에 대한 정보를 제공하는 역할을 한다. HelloCallback이라는 메소드에서 비동기적으로 호출한 서비스의 답변을 기다리겠다는 의미로 HelloCallback 메소드를 프레임워크에게 알려줘야 하는데 그냥 건네주면 프레임워크는 인식할 수 없다. AsyncCallback이라는 것으로 한번 감싸서 보내줘야 한다. 첫번째 인자 callback의 의미는 그렇다.

두번째 인자로 asyncServiceProxy 객체를 넘겨주고 있는데 서비스 메소드를 호출하는 프락시 객체 자신을 넘겨주고 있다. 두번째 인자는 원래 결과값을 처리하는 곳으로 어떤 특별한 값을 보내고 싶을때 사용한다. 이곳에 값을 넘겨주면 서비스 작업 호출 후 프레임워크에서는 결과값을 받는 메소드 HelloCallback을 호출할 때 그 값을 그대로 건네준다. 이곳에서는 서비스 프락시 객체 asyncServiceProxy 자신을 그대로 콜백 함수에 넘겨주겠다는 것이다. 그럼 콜백 함수에서 하는 일을 보도록 하자.

/// <summary>

/// 결과값 처리하는 부분

/// </summary>

/// <param name="result"></param>

private void HelloCallback(IAsyncResult result)

{

    if (result.IsCompleted)

    {

        if (!this.Dispatcher.CheckAccess())

        {

            this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,

            new AsyncCallback(HelloCallback), result);

            return;

        }

        try

        {

            //서버측으로 보낸 서비스 프락시 객체를 복원한다.

            asyncServiceProxy = (SampleAsyncService.ISampleService)result.AsyncState;

            strHello = asyncServiceProxy.EndHello(result);

            this.tbReply.Text = strHello;

        }

        catch (Exception e)

        {

            //예외처리

            MessageBox.Show(e.Message);

        }

        finally

        {

            sampleProxyfactory.Close();

        }

    }

}

콜백 함수 HelloCallback 메소드는 프레임워크로부터 인자를 하나 받는다 : IAsyncResult 타입의 result객체. 콜백함수에서는 서비스 호출이 종료되었는지를 확인해서 다음 작업을 해 줘야 하는데, 서비스 호출 종료는 코드에서처럼 result객체의 IsCompleted 속성을 통해서 확인할 수 있다.

그 다음 볼드체 부분에서 하는 일은 이렇다. 앞에서 서비스를 비동기로 호출하면 내부적으로 쓰레드가 하나 생성되고 그곳에서 서비스 호출이 실행된다고 했다. 그 새로운 쓰레드에서 콜백함수 HelloCallback 메소드를 호출하고 있다. 그러나 UI 컨트롤들에 접근하기 위해서는 원래의 쓰레드로 돌아와야 한다. this 즉 UserControl의 Dispatcher를 통해서 현재 호출하는 쓰레드가 UserControl이 속한 쓰레드와 일치하는지를 확인하고 있다. 만약 쓰레드가 달라서 UI 컨트롤에 접근할 수 없다고 판단되면 다시 Dispatcher의 BeginInvoke()를 호출해서 다른 쓰레드를 통해서 HelloCallback 함수를 다시 호출한다. BeginInvoke()를 호출할때 콜백함수를 인자로 넘겨주는 이유이다. 이런 작업을 UI 쓰레드에 도착할때까지 반복하는 것이다. 최종적으로 UI 쓰레드에 도착해서 this.Dispatcher.CheckAccess() 확인 작업을 통과하고 나면 이후의 코드가 실행될 수 있다.

첫번째 작업은 result 객체의 AsyncState 속성을 호출하고 있는데, 이 속성을 통해서 이전에 서비스를 호출할때 넘겨준 서비스 프락시 객체 asyncServiceProxy를 복원할 수 있다. 이 복원된 객체를 통해서 EndHello() 메소드를 호출하는데 이로써 서비스 호출 결과를 받을 수 있다. 결과를 받아서 이제 필요한대로 사용하면 된다.

처음에는 asyncServiceProxy 객체를 넘겨 받지 않고 원래의 객체에 직접 접근했었다. finally 블록에 보면 sampleProxyfactory 객체는 서비스를 호출할때 넘겨서 받지 않고 원래의 객체에 직접 접근하고 것처럼. 그랬더니 가끔가다 에러가 발생했는데, 이 에러가 항상 발생하는 것은 아니었다. 어쩌다 에러가 발생하는데 에러 내용이 뭐였는지 기억이 나지 않아서 지금 재현해볼려니까 또 발생하지 않는다. 써글… 다음에 발생하면 이 부분에 대해서 보완하도록 하겠다. 이 포스트는 여기서 끝내야 겠다.

어휴…힘들다.

다음에는 Spring.NET을 이용해서 비즈니스 레이어에서 트랜잭션을 처리하는 방법과 데이터베이스에 접근하는 방법을 해 볼까 한다.

Posted by dalbong2

이번 포스트부터는 달봉이가 제작한 ServiceProxyFactory 객체를 이용해서 동기 호출과 비동기 호출에 대한 코딩 예를 보여준다.

 

■ IIS 서비스 환경 구성하기

 

우선 서버측 서비스를 구성해 보자. 달봉이는 서비스 구현 프로젝트와 서비스 노출 프로젝트를 분리했다. 서비스 구현은 BONG.SVC.CO.UserMgmt에 있고, 노출 프로젝트는 웹 애플리케이션을 이용한다.

SampleService.cs에는 다음처럼 간단한 서비스가 구현되어 있다. 다음 코드는 사용자 정보를 서버측으로 전달하는 과정을 설명하는 포스트에서도 봤다.

namespace BONG.SVC.CO.UserMgmt

{

    [ServiceContract]

    public interface ISampleService

    {

        [OperationContract]

        string Hello();

    }

    public class SampleService : Dalbong2ServiceBase, ISampleService

    {

        public string Hello()

        {

            //실행 좀 멈춘다.

            System.Threading.Thread.Sleep(10000);

            //현재 사용자의 ID를 사용한다

            return String.Format("Hello, you're {0}", base.UserInfo.ID);

        }

    }

}

이제 이 서비스를 외부로 노출시키자. 달봉이는 이 WCF 서비스 노출을 위해서 IIS를 이용하고 있다.

탐색기를 열어 달봉이의 폴더 구조를 보면 다음과 같이 되어 있다.

03 SVC 폴더를 기본 웹 사이트의 가상 디렉토리로 만들었다.

달봉이는 가상 디렉토리명을 “BongSvc”로 했다.

다음 CO 폴더를 보면 WCF 구현을 포함하고 있는 BONG.SVC.CO.UserMgmt 폴더가 있다. 이제 이것을 노출할 서비스를 만들기 위해서 BONG.CO.UserMgmt.Service폴더를 하나 더 만들자.

이제 IIS 관리 콘솔에서 BONG.CO.UserMgmt.Service에 대한 웹 애플리케이션을 하나 만들자. IIS 관리 콘솔에서 BONG.CO.UserMgmt.Service를 오른쪽클릭하면 다음과 같은 메뉴가 보인다.

이 중에서 “Convert to Application” 메뉴를 선택한다. 다음과 같은 애플리케이션 추가 창이 뜬다.

다른 값은 기본값을 사용하고, Application pool을 다른 값으로 선택할 수도 있다.

달봉이는 DalbongAppPoos( 미스 스펠링 –_-;;)을 미리 만들어 두었다. 하지만 지금은 DefaultAppPool을 사용해도 상관없다.

OK버튼을 클릭하면 다음처럼 애플리케이션이 생성된다.

이제 Visual Studio로 가자. UserMgmt 폴더를 오른쪽 클릭해서 “New Web Site…”를 선택한다.

 

템플릿에서 “WCF Service”를 선택한다.

“Browse…”버튼을 클릭해서 앞에서 만들어 놓은 웹 애플리케이션을 선택한다.

작업을 마치고 나면 Visual Studio는 다음처럼 된다.

샘플로 WCF 구현을 만들어놓은 IService.cs와 Service.cs, Service.svc가 있다.  달봉이는 이미 BONG.SVC.CO.UserMgmt에 서비스를 구현해 놨다. 해서 앞의 녀석들을 삭제한다. WCF 서비스 프로젝트를 오른쪽 클릭해서 새 항목을 추가하도록 하자.

WCF Service 템플릿을 선택하고 페이지 이름을 “SampleService.cs”로 한다. 자동 생성되는 ISampleService.cs, SampleService.cs 파일을 삭제한다.

이제 서비스를 구현해 놓은 BONG.SVC.CO.UserMgmt에 대한 참조를 추가하자. WCF 프로젝트를 선택해서 오른쪽 클릭을 한 다음 “Property pages”를 선택한다.

“Add…”버튼을 클릭한다.

Projects 탭에서 BONG.SVC.CO.UserMgmt를 선택한다.

이제 SampleService.svc 페이지를 더블 클릭하면 다음과 같은 서비스 선언문이 나타난다.

<%@ ServiceHost="" Language="C#" Debug="true" Service="SampleService" CodeBehind="~/App_Code/SampleService.cs" %>

Codebehind 부분을 제거하고, 다음처럼 수정한다.

<%@ ServiceHost="" Language="C#" Debug="true" Service="BONG.SVC.CO.UserMgmt.SampleService"  %>

이제 Web.config 파일을 수정해야 한다.

<system.serviceModel>

    <behaviors>

        <serviceBehaviors>

            <behavior name="SampleServiceBehavior">

                <serviceMetadata httpGetEnabled="true" />

                <serviceDebug includeExceptionDetailInFaults="false" />

            </behavior>

        </serviceBehaviors>

    </behaviors>

    <services>

        <service behaviorConfiguration="SampleServiceBehavior" name="BONG.SVC.CO.UserMgmt.SampleService">

            <endpoint address="" binding="wsHttpBinding" contract="BONG.SVC.CO.UserMgmt.ISampleService">

                <identity>

                    <dns value="localhost" />

                </identity>

            </endpoint>

            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />

        </service>

    </services>

</system.serviceModel>

볼드체로 되어 있는 부분이 SampleService, ISampleService로 되어 있을 것이다. 이 녀석들을 위 코드처럼 수정한다. 서비스 선언문의 Service 어트리뷰트값과 web.config의 <service>의 name 어트리뷰트값이 일치해야 한다.

이제 Visual Studio에서 SampleService.svc 항목을 오른쪽 클릭해서 View in browser를 선택한다. 제대로 되었다면 다음과 같은 페이지가 출력된다.

이제 서버측 서비스 구성은 다 끝났다. 브라우저에 보이는 주소를 복사해뒀다가 클라이언트에서 서비스를 참조할때 사용하면 된다.

 

■ WCF 서비스 확장 설정하기

 

앞의 web.config에 이전 포스트에서 보았던 사용자 정보를 받기 위한 configuration을 추가한다. 그럼 완전한 <system.serviceModel/>모습은 다음과 같이 된다.

<system.serviceModel>

    <services>

        <service behaviorConfiguration="SampleServiceBehavior"

                 name="BONG.SVC.CO.UserMgmt.SampleService">

            <endpoint address=""

                      binding="wsHttpBinding"

                      behaviorConfiguration="MyEndPointInspectors"

                      contract="BONG.SVC.CO.UserMgmt.ISampleService">

                <identity>

                    <dns value="localhost" />

                </identity>

            </endpoint>

            <endpoint address="mex"

                      binding="mexHttpBinding"

                      contract="IMetadataExchange" />

        </service>

    </services>

    <behaviors>

        <serviceBehaviors>

            <behavior name="SampleServiceBehavior">

                <serviceMetadata httpGetEnabled="true" />

                <serviceDebug includeExceptionDetailInFaults="false" />

            </behavior>

        </serviceBehaviors>

        <endpointBehaviors>

            <behavior name="MyEndPointInspectors">

                <UserInfoEndpointExtention/>

            </behavior>

        </endpointBehaviors>

 

    </behaviors>

 

   <extensions>

        <behaviorExtensions>

            <add name="UserInfoEndpointExtention"

                 type="Dalbong2.Service.Interceptors.UserInfoBehaviorExtensionElement, Dalbong2.Service, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

        </behaviorExtensions>

    </extensions>

</system.serviceModel>

한가지 주의할 것이 있다.

마지막 부분에서 type 어트리뷰트값이 길지만, 타입명과 어셈블리명 사이를 반드시 같은 줄에 적어줘야 한다. 뿐만 아니라 타입명과 어셈블리명 사이에 반드시 하나의 공백을 둬야 한다. 안그러면 고생 좀 하게 될 것이다. 달봉이가 보기에는 뭐 특별한 이유가 있는 것이 아니라, configuration 컴파일러 개발자가 빨리 만들고 어디 놀러갈 일이 있었나 보다. 덕분에 달봉이 많이 고생했다.

이것으로 서버측 서비스 설정은 끝났다. 다음 포스트에서는 클라이언트측에서의 서비스 호출을 알아보도록 한다.

Posted by dalbong2

■ 프로그레스바 출력 시나리오

 

프로그레스바 자동 출력 기능을 위해서 달봉이는 다음과 같은 시나리오를 정의했다.

프로그레스바를 보여주는 경우를 생각해보자. 이 녀석은 서비스를 호출할때마다 보여줘야 할까? 달봉이는 아니라고 생각하고 달봉이 개발 프레임워크를 구현했다. 예를 들어 업무성 코드 목록을 가져와서 드롭다운 리스트를 채우거나 사용자에게 보여줄 메세지를 가져오는 경우라면 프로그레스바 없이 내부적으로 조용히 처리하면 될 것이다. 달봉이는, 서비스 호출 결과가 늦어질 가능성이 있는 경우에만 프로그레스바를 보여주자고 결정했다.

프로그레스바 보여주는 것과 관련해서 또 한가지 달봉이가 결정한 것은 프로그레스바를 보여줄 정도로 시간이 걸리는 작업은 비동기로 구현하겠다는 것이다. 서비스 호출하고 나서 사용자를 아무 반응도 않는 애플리케이션을 바라보고만 있도록 하는 것은 바람직하지 않는 듯하다. 탭을 선택해서 다른 화면을 볼 수도 없다. 너무 답답한 일이다.

그리고 달봉이는 프로그레스바를 보여주지 않아도 되는 서비스 호출의 경우는 동기를 쓰도록 하겠다. 그러나 강제적으로 개발 프레임워크단에서 제한할 수 있는 방법은 없다. 어떤 방식의 호출을 사용할지는 개발자의 선택에 달려있다. 다만 달봉이가 제공하는 프락시 팩토리 객체는 동기와 비동기 호출을 할 수 있는 방법을 모두 제공해줄 뿐이다. 업무적인 시나리오에 맞게 개발자가 적절한 호출을 선택해야 할 것이다.

그리고 또 한가지 프로그레스바는 업무 화면 객체별로 그 상태를 가지게 될 것이다. 여러 개의 웹 페이지가 각각의 탭으로 열려있는 웹 브라우저를 생각해보자. 하나의 페이지에서 서버에 요청을 보내면 브라우저의 상태바에 프로그레스바가 출력된다. 그러나 다른 페이지의 탭을 선택하면 프로그레스바가 사라지고 현재 선택된 페이지의 진행상태에 따라서 프로그레스바의 출력 여부가 결정된다. 달봉이는 이 시나리오를 염두에 두고 구현을 했다.

 

■ 관련 클래스들

 

프로그레스바를 개발자의 코딩없이 보여줄 수 있도록 구현된 달봉이의 코드를 보도록 하자. 몇 개의 타입이 다시 정의되거나 새롭게 정의된다. 복잡하다.

 ServiceCallNotigyBehavior

Dalbong2.ServiceClient 프로젝트의 ServiceCallNotifyClientEndpointBehavior.cs에 정의되어 있다.

이 녀석이 WCF 런타임에 등록될 behavior이다. 달봉이는 “ServiceCallNotify” behavior라 부르기로 했다. 서비스 호출이 시작되거나 종료될때 위에 정의에 두 이벤트를 발생시켜준다.

 Dalbong2ProxyFactory1

Dalbong2.ServiceClient 프로젝트의 Dalbong2ProxyFactory.cs에 정의되어 있다.

이 녀석은 ServiceCallNotify behavior를 WCF 런타임이 인식할 수 있도록 behavior 목록에 등록시켜준다. 또한 ServiceCallNotify에서 발생한 이벤트를 클라이언트 코드에 전달해주는 역할을 한다. 이를 위해서 Start, End에 해당하는 자신만의 이벤트 멤버를 가지고 있다.

 BongServiceFactory

이 녀석부터 차즘 UI 컨트롤과 접촉을 시도하는 부분이다. 이 녀석이 Bong.Win 프로젝트에 정의되어 있는 이유이다. 우선 이 녀석은 화면 객체에 대한 참조를 받는다. 뒤에서 보겠지만 화면 객체들은 IProgressBarPerceptible 인터페이스를 구현하고 있다. 개발자는 서비스 호출시 프로그레스바를 보여주고 싶다면 서비스 프락시 객체를 생성할때 IProgressBarPerceptible 객체를 받는 프락시 객체 생성 메소드를 이용해야 한다.

public TService GetServiceProxy(string baseAddressKey,

string relativeAddress,

IProgresssBarPerceptible progressBarPerceptibleElement )

세번째 인자로 IProgressBarPerceptible 객체를 넘겨준다. 보통 업무 화면에서 이 메소드를 사용해서 서비스를 호출할때는 this를 넘겨주면 된다. 달봉이의 모든 업무 화면 객체는 IProgressBarPerceptible 인터페이스를 구현하고 있기때문이다.

 IProgressBarPerceptible

BongServiceProxyFactory는 Dalbong2ProxyFactory 객체로부터 서비스 호출 시작/종료에 대한 알림을 받으면 GetServiceProxy() 메소드를 통해서 받은 IProgressBarPerceptible 객체의 속성을 변경시켜준다. 우선 서비스 호출이 시작되었음을 WhileCallingService 속성과 ProgressBarVisibility값을 true로 변경시켜준다.

만약 프로그레스바를 보여주고 싶지 않다면 다음 버전의 프락시 객체 생성 메소드를 사용해야 한다.

public TService GetServiceProxy(string baseAddressKey, string relativeAddress)

업무성 코드나 메세지값을 받아 올때는 두번째 버전을 사용하면 되겠다.

다음은 업무 화면의 베이스 클래스의 정의이다.

 Dalbong2ControlBase

Dalbong2ControlBase 클래스는 그림처럼 IProgressBarPerceptible을 구현하고 있다. 또한 BongServiceProxyFactory가 설정하는 ProgressBarVisibility 속성이 변경되면 ProgressBarVisibleChanged 이벤트가 발생한다.

 

■ 프로그레스바 컨트롤 접근

 

프로그레스바 컨트롤은 업무 화면에서 직접 접근하는 것이 아니다. 단지 업무화면 객체에서는 WhileCallingService 속성과 ProgressBarVisibleChanged 이벤트만을 노출시켜준다. 화면이 전환되거나 또는 서비스 호출이 종료되어 업무 객체의 ProgressBarVisibility 속성이 변경되어 ProgressBarVisibleChanged 이벤트가 발생했을때 그 변화를 감지해서 프로그레스바 컨트롤에 직접 접근해서 그 visible 상태를 변경시켜주는 것은 UI 컨트롤에서 담당한다.

 ProgressBarVisibility

이런 구조로 가면, 사용자 정의 프로그레스바 컨트롤을 사용하더라도 그것을 직접 참조하고 있는 UI 컨테이너만 수정되면 된다. UI 컨테이너가 프로그레스바의 Visible 상태를 변경하기 위해서 그 컨트롤에 직접 직접하는 경우는 두 가지이다.

 

■ 프로그레스바 컨트롤 visible 상태 변경 경우

 

우선 사용자가 화면 탭을 전환해서 현재 보여주는 화면이 변경되는 경우이다. 사용자가 화면 A에서 화면 B로 전환할때 현재는 탭 컨트롤의 SelectionChanged 이벤트 핸들러를 이용하고 있다.

/// <summary>

/// 탭 컨트롤, 탭변경시 작업

/// 1. 프로그레스바 Visibility 변경

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

private void tabControl1_SelectionChanged(object sender, SelectionChangedEventArgs e)

{

    // 프로그레스바 보여주기/숨기기

    TabItem tabItem = (sender as TabControl).SelectedItem as TabItem;

    if (tabItem == null) return;

    //tabItem의 Tag 객체에 저장해둔 업무 화면 객체를 IProgressBarPerceptioble로 변환한다.

    IProgresssBarPerceptible element = tabItem.Tag as IProgresssBarPerceptible;

    if (element != null)

    {

        //현재 업무 화면이 서비스 호출중인지를 확인해서,

        //프로그레스바 컨트롤에 접근해서 visible 상태를 변경한다.

        if( element.WhileCallingService  )

            this.ProgressBar.Visibility = Visibility.Visible;

        else

            this.ProgressBar.Visibility = Visibility.Hidden;

    }

    else

    {

        this.ProgressBar.Visibility = Visibility.Hidden;

    }

}

UI 컨테이너가 프로그레스바 컨트롤의 visible 상태를 변경하기 위해서 직접 접근하는 경우로는 업무 화면 객체가 ProgressBarVisibleChanged 이벤트를 발생시켰을 경우이다.

UI 컨테이너는 이 이벤트를 받기 위해서 업무 화면 객체가 생성해서 탭 컨트롤에 출력할때 그 핸들러를 등록하고 있다. 업무 화면 객체를 Spring.NET 컨테이너에서 가져와서 탭 컨트롤에 추가하는 코드는 이전에 보았다. 이 작업은 메뉴 트리 컨트롤의 MouseUp 이벤트에서 하고 있다. 이 코드는 Shell.cs에 포함되어 있다.

void treeItem_MouseUp(object sender, MouseButtonEventArgs e)

{

    TreeViewItem item = sender as TreeViewItem;

    //Tag 속성에 메뉴 정보 객체 복원

    FileMenuItemInfo menuInfo = item.Tag as FileMenuItemInfo;

    if (menuInfo != null)

    {

        string elemetName = menuInfo.ElementInfo.ID;

 

        IDalbong2Element existingElementInfo = null;

        //이미 같은 화면이 로딩되어 있는지 확인

        //이미 로딩되어 있다면 탭을 생성하지 않고 리턴

        <중략…>

        // Spring.NET 객체 생성기를 통해서 화면 객체를 얻는다.

        Dalbong2ControlBase  uiElement =

            ( Dalbong2ControlBaseBongWinAppContext.XmlObjectFactory.GetObject(elemetName);

       uiElement.ProgressBarVisibleChanged +=

            new ProgressBarVisibleChangedEventHandler(uiElement_ProgressBarVisibleChangedEvent);

 

        <중략…>

 

 

        //탭을 탭 컨트롤에 추가한다.

        this.tabControl1.Items.Add(tabItem);

 

        tabItem.IsSelected = true;

    }

}

UI컨테이너의  uiElement_ProgressBarVisibleChangedEvent 핸들러는 다음과 같이 구현되어 있다.

/// <summary>

/// 화면객체 로딩시, 프로그레스바 Visibility 변경

/// This method checks to see if the current thread needs to be marshalled

/// to the correct (UI owner) thread. If it does a new delegate is created

/// which recalls this method on the correct thread

/// </summary>

/// <param name="sender"></param>

/// <param name="arg"></param>

void uiElement_ProgressBarVisibleChangedEvent(object sender, ProgressBarVisibleChangedEventArgs arg)

{

    if (!this.Dispatcher.CheckAccess())

    {

        this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,

        new ProgressBarVisibleChangedEventHandler(uiElement_ProgressBarVisibleChangedEvent),

        sender, arg);

        return;

    }

 

    // 어떤 화면 객체에서 이벤트를 발생했고,

    // 어떤 이벤트 인자를 넘겼는지를 구한다.

    IDalbong2Element element = sender as IDalbong2Element;

    Visibility visibility = arg.Visibility;

    if (this.CurrentElement != null)

    {

        if (this.CurrentElement.ElementInfo.ID == element.ElementInfo.ID)

        {

            this.ProgressBar.Visibility = visibility;

        }

    }

}

첫번째 이탤릭체로 되어 있는 부분은 다음 포스트에서 비동기 호출을 보면서 다시 보게 될 것이다. 그때 다시 설명하겠다. 지금은 이 이벤트 핸들러가 UI 쓰레드와 다른 쓰레드에서 호출되기때문에 이 이탤릭체 부분의 코드가 필요하다는 것만 언급해두고 넘어가겠다.

볼드체로 되어 있는 부분에서는 먼저, ProgressBarVisibleChangedEvent 이벤트를 발생시킨 업무 화면 객체와 이벤트 인자를 통해서 넘겨준 인자를 구하고 있다.

ProgressBarVisibleChangedEvent 이벤트는 업무 화면 객체 Dalbong2ControlBase( 그리고 Dalbong2PageBase)에 구현되어 있다.

public class Dalbong2ControlBase : UserControl, IDalbong2Element, IProgresssBarPerceptible

{

    #region IDalbong2Element Members

    <중략>…

    #endregion

 

    #region IProgresssBarPerceptible Members

    public event ProgressBarVisibleChangedEventHandler ProgressBarVisibleChanged;

 

    private bool _CallingService = false;

    public bool WhileCallingService

    {

        get

        {

            return _CallingService;

        }

        set

        {

            _CallingService = value;

        }

    }

 

    private Visibility _ProgressBarVisibility = Visibility.Collapsed;

    public Visibility ProgressBarVisibility

    {

        get

        {

            return _ProgressBarVisibility;

        }

        set

        {

            if (_ProgressBarVisibility != value)

            {

                _ProgressBarVisibility = value;

                ProgressBarVisibleChangedEventArgs arg =

                    new ProgressBarVisibleChangedEventArgs(_ProgressBarVisibility);

                OnProgressBarVisibleChanged(arg);

            }

        }

    }

    #endregion

 

    protected virtual void OnProgressBarVisibleChanged(ProgressBarVisibleChangedEventArgs arg)

    {

        if (ProgressBarVisibleChanged != null)

        {

            ProgressBarVisibleChanged(this, arg);

        }

    }

}

ProgressVarVisibility 속성의 값을 설정할때 현재값과 다른 값이 들어오면 ProgressBarVisibleChanged 이벤트를 발생시킨다. 앞의 그림에서 본 것처럼 ProgressVarVisibility 속성은 BongServiceProxyFactory에서 설정한다.

ProgressBarVisibleChangedEvent 이벤트 발생시 추가적인 정보를 전달하기 위해서 다음과 같은 이벤트 인자를 정의하고 있다.

public delegate void ProgressBarVisibleChangedEventHandler( object sender,

ProgressBarVisibleChangedEventArgs arg );

public class ProgressBarVisibleChangedEventArgs

{

 

    private Visibility _Visibility = Visibility.Collapsed;

 

    public ProgressBarVisibleChangedEventArgs(Visibility visibility)

    {

        _Visibility = visibility;

    }

 

    public Visibility Visibility

    {

        get

        {

            return _Visibility;

        }

    }

}

이 이벤트 인자는 ProgressBarVisibleChangedEvent 발생시 현재 visible 상태를 전달하고 있다.

UI 컨테이너에서는 ProgressBarVisibleChangedEvent 이벤트를 발생시킨 업무 화면 객체가 현재 활성화되어 있는 업무 객체와 같은지를 우선 체크하고 그리고 이벤트 인자를 통해서 넘겨준 visible 상태를 통해서 프로그레스바 활성화를 결정하게 되는 것이다.

이것으로 달봉이가 구현해놓은 프로그레스바 자동 출력하기 구조 설명은 끝났다. 좀 복잡한 듯하다.

 

■ Dalbong2ProxyFactory vs. BongServiceProxyFactory

 

마지막으로 하나 더 언급할 것은 Dalbong2ProxyFactory와 BongServiceProxyFactory의 차이점이다.

첫번째로 Dalbong2ProxyFactory는 단지 서비스 시작과 종료를 알리는 이벤트만을 발생시킬 뿐이다. Dalbong2ProxyFactory에게는 UI단에서 어떤 컨트롤을 사용하고 어떤 애플리케이션에서 이 이벤트를 사용하는지는 중요하지 않다. 그래서 Dalbong2ProxyFactory를 다음처럼 Dalobg2.Win 프로젝트에 포함된 것이 아니라 Dalbong2.ServiceClient 프로젝트에 포함되어 있다.

 Dalbong2ProxyFactory

그러나 BongServiceProxyFactory는 서서히 UI단과 관계를 갖기 시작한다. 서비스 프락시 객체를 생성할때  IProgresssBarPerceptible 인자를 받는데 이 녀석은 업무 화면 객체가 구현하고 있는 인터페이스이다. 따라서 BongServiceProxyFactory는 Bong.Win 프로젝트에 구현되어 있다.

  BongServiceProxyFactory

프로그레스바의 자동 출력을 원하는 개발자는 BongServiceProxyFactory 객체를 사용해서 프락시 객체를 이용해야 하는 것이다.

 

■ 실행결과

 

이제 이 기능을 이용해서 구현된 프로그레스바 출력 기능이 어떻게 나타나는지를 보자.

 화면1

 화면2

업무 화면 1과 업무 화면 2는 같은 서버측 메소드를 호출하고 있다. 각각의 버튼을 클릭해서 서비스를 호출해서 서버측에서 서비스를 처리중이더라도 탭 전환이 가능하다. 또한 탭 전환이 이뤄질때 각 화면의 상태에 따라서 상태바에 있는 프로그레스바의 출력 여부가 결정된다.

이것으로 끝이다.

그럼 다음 포스트에서는 BongServiceProxyFactory 객체를 이용해서 서비스를 호출하는 코드를 작성해본다.

Posted by dalbong2

이제 클라이언트측 behavior를 끼워넣는 작업을 해본다.

근데 behavior라는 단어를 들으면 느낌이 파악 오는지 모르겠다. 달봉이는 이 단어를 학교다닐때 참 많이 들었다. 달봉이는 토목의 구조를 전공했다. 예를 들어 교량같은 대형 건물을 설계할 때 이 단어가 많이 나온다. 우리는 교량이 어떻게 “거동(behavior)”하는가라는 식으로 표현했었다. 근데 밖에 나와서 거동이라는 표현을 썼더니 잘 모르는 것 같았다. 발음 그대로 “비헤이비어” 라고 표현하는 사람들도 많았다. 그러나 달봉이는 여전히 거동이라는 표현이 마음에 든다.

behavior는 AOP의 advice같은 개념이다. 그 개념을 구현해 놓은 코드 조각을 인터셉터(interceptor)라고 한다. 이 behavior라는 것을 머리에 그릴때는, 서비스의 엔드 포인트( 엔드 포인트뿐만 아니라 WCF에는 behavior 코드 조각을 끼워넣을 수 있는 포인트는 여러 곳이 있다)에 여러개의 behavior 코드 조각을 끼워 넣어서 서비스의 전체적인 거동을 확장할 수 있다는 개념을 떠올리면 될 것 같다.

클라이언트측에서도 달봉이는 이 behavior를 몇 군데 사용해서 확장하고 있다. 이전 포스트에서 설명했지만, 서버측의 프레임워크단에서 사용자 정보를 받을 수 있도록 클라이언트단에서 사용자 정보를 서비스 호출시 하부구조에서 보내주는데, 사용자 정보를 하부 구조에 끼워넣는데 이 behavior를 사용하고 있다.

이번 포스트에서는 클라이언트단에서 사용자 정보를 보내주는 behavior를 어떻게 구현하고 있는지 알아본다.  지난 포스트에서는 파란 박스의 파일에 구현된 것을 설명했고, 이번에는 붉은 박스의 파일에 구현된 내용을 설명한다.

간단히 먼저 설명하면 UserInfoClientEndpointBehavior.cs에 구현된 것은 무슨 일을 끼워 넣을 것인가(what)를 구현한 것이고, 붉은 점선 박스에 있는 UserInfoClientBehaviorExtensionElement.cs는 config 파일에 <UserInfoClientExtention/>같은 식으로 표시를 해서 WCF 런타임이 UserInfo를 끼워넣는 behavior를 인식할 수 있도록 해주는 코드가 구현되어 있다.

달봉이는 모든 서비스 호출시, 클라이언트의 프레임워크단에서 사용자 정보를 넘겨줄 것이다. config에 설정하든 안하든 무조건 넘긴다. 따라서 붉은 박스에 있는 파일은 사용되지 않고 있다. 다른 붉은 박스에 있는 Dalbong2ProxyFactory.cs에서 프로그램적으로 behavior를 추가하고 있다.

이제 코드를 보도록 하자. 먼저 사용자 정보를 서비스 호출시에 끼워넣는 behavior를 구현한

/// <summary>

/// Implements methods that can be used to extend run-time behavior

/// 메세지 inspector를DispatchRuntime..::.MessageInspectors속성에 추가한다.

/// </summary>

public class UserInfoClientEndpointBehaviorIEndpointBehavior, IClientMessageInspector

{

 

    #region "IMessageInspector구현"

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, IClientChannel channel)

    {

        IUserInfo userInfo = null;

        object obj = AppDomain.CurrentDomain.GetData("__UserInfo__");

        if (obj != null)

        {

            userInfo = (IUserInfo)obj;

            string strUserInfo = SerializationHelper.ToBase64String(userInfo);

            MessageHeader mh = MessageHeader.CreateHeader("__UserInfo__", "http://Dalbong2/", strUserInfo);

            request.Headers.Add(mh);

        }

        else

            throw new Exception("Calling service is not possible without the information about a current user");

        return null;

    }

    public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)

    {

        return;

    }

 

    #endregion

클라이언트측  WCF 런타임이 bahavior 코드를 인식할 수 있기 위해서는 IClientMessageInspector라는 인터페이스를 구현해야 한다. 이 인터페이스는 두개의 메소드를 정의하고 있다 : BeforeSendRequest, AfterReceiveReply.  그 메소드명을 보면 대충 이 메소드들이 언제 호출되는지 알 수 있을 것이다. 클라이언트측 WCF 런타임은 자신에게 등록된 behavior들을 등록된 순서대로 이 메소드들을 호출해준다. 서비스를 호출하기전에는 BeforeSendRequest를 호출해주고, 서비스로부터 답변을 받은 후에는 AfterReceiveReply를 호출해준다. 개발자는 서비스 호출하기 전, 후에 할 일을 이 두 메소드에서 구현하면 된다.

우리는 서비스를 호출하기전에 사용자 정보를 서비스 호출정보에 추가하는 것이다. 먼저 AppDomain에 저장소에서 사용자 정보를 구한다. 달봉이 애플리케이션에서는 애플리케이션이 시작되면 현재 로그인한 사용자 정보 IUserInfo 객체를 이곳에 저장해 둘 것이다. 이제 사용자 정보 객체를 직렬화, 인코딩하여 문자열로 만든다. 그런 다음 그 문자열을 서비스 호출의 메세지 헤더 MessageHeader 컬렉션에 추가한다. 구현은 심플하다. 서비스 답변을 받은 후는 현재 아무것도 하지 않고 있다.

이제 이 구현된 behavior, UserInfoClientendpointBehavior를 WCF 런타임에 등록하는 절차가 남아 있다. 앞에서 말한대로 WCF 런타임이 인식할 수 있도록 특정 behavior 저장소에 저장해둬야 한다. 그러나 개발자가 직접 그 저장소에 추가하지 않는다. IEndpointBehavior라는 인터페이스를 구현하면 된다. 이 인터페이스에는 저장소에 간접적으로 접근할 수 있는 방법을 제공하고 있다.

#region IEndpointBehavior Members

public void AddBindingParameters(ServiceEndpoint serviceEndpoint,

    System.ServiceModel.Channels.BindingParameterCollection bindingParameters)

{

    return;

}

 

public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime behavior)

{

   behavior.MessageInspectors.Add(this );

}

 

public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint,

    EndpointDispatcher endpointDispatcher)

{

    return;

}

 

public void Validate(ServiceEndpoint serviceEndpoint)

{

    return;

}

#endregion

이 인터페이스에는 ApplyClientBehavior라는 메소드가 있는데, 우리가 구현해 놓은 behavior를 등록할 수 있는 적절한 곳이다. 클라이언트측 WCF 런타임은 서비스 호출 정보를 서버로 보내기 전에 ApplyClientBehavior를 호출해서 서비스 클라이언트측에서 요청한 behavior를 등록한다.  필요하다면 Add() 메소드를 통해서 여러개의 behavior를 등록할 수 있다. 현재 달봉이는 사용자 정보를 메세지 헤더에 추가하는 behavior만을 끼워넣고 있다. 이곳에서 추가되려면 반드시 IClientMessageInspector를 구현하고 있어야 한다. 앞의 코드에서는 Add() 메소드에 this를 넣고 있는데, UserInfoClientEndpointBehavior 는 IEndpointBehavior뿐만 아니라 IClientMessageInspector도 구현하고 있기 때문이다. IEndpointBehavior의 ApplyDispatchBehavior는 서버측에서 behavior를 추가할때 사용할 수 있다. 앞의 포스트에서 이것을 사용하는 예를 봤었다. 다른 메소드들에 대해서 관심이 있다면 MSDN을 참고하기 바란다.

지금까지는 behavior의 구현 코드, 그 behavior를 behavior 목록에 추가하는 단계였다. 이제 엔드 포인트의 behavior 목록을 클라이언트측의 WCF 런타임이 인식할 수 있도록 해야 한다. 앞의 포스트에서는 BehaviorExtensionElement 를 상속해서 config 파일을 이용하는 방법을 사용했다. 이번에는 직접 프로그램적으로 등록하는 방법을 사용하겠다. 클라이언트측에서는 ChannelFactory<T> 객체( 또는 ClientBase 객체)를 이용하면 런타임에 behavior 목록을 등록할 수 있다. 

다음 코드를 보자. 이 코드는 Dalbong2ProxyFactory.cs에 있다.

 /// <summary>

 /// "http://donbox-pc/BongSvc/CO/BONG.CO.UserMgmt.Service/SampleService.svc"

 /// </summary>

 /// <param name="baseAddress">"http://donbox-pc/BongSvc/CO/"</param>

 /// <param name="relativeAddress">"BONG.CO.UserMgmt.Service/SampleService.svc"</param>

 /// <returns></returns>

 public TService CreateProxy(string baseAddress, string relativeAddress )

{

     string completeAddress = System.IO.Path.Combine(baseAddress, relativeAddress);

     WSHttpBinding wsHttpBinding = new WSHttpBinding();

     EndpointAddress endpointAddress = new EndpointAddress(completeAddress);

     _channel = new ChannelFactory<TService>(wsHttpBinding, endpointAddress);

 

     //서비스 호출 알리미 interceptor 끼워넣기

     <중략>…

 

     //사용자 정의 interceptor 끼워넣기

    _channel.Endpoint.Behaviors.Add(new UserInfoClientEndpointBehavior());

 

     return _channel.CreateChannel();

}

WCF 서비스를 호출할 수 있기 위해서는 채널 객체가 필요한데, ChannelFactory<TService>으로 구현되어 있다. 이 채널 객체가 서비스를 호출할 수 있기위해서는 다시 WCF의 ABC가 필요하다 : Address, Binding, Contract.

Address란 WCF 서비스 구현이 노출된 네트워크상의 URI이다. 위의 주석에 Address의 모습의 예가 있다. Binding은 메세지를 전송하는데, 어떤 transport 프로토콜( HTTP, TCP, MSMQ 등 )을 사용하고 어떤 XML 인코딩( 텍스트, 바이너리 또는 MTOM)을 사용하고 그리고 트랜잭션, 보안, 신뢰할 수 있는 메세징을 사용할 지에 대한 정보를 기술하고 있다. 이 바인딩에서 기술된 대로 서비스 호출시 채널 스택이 다르게 설정된다( 어렵다 –_-;;). 여튼 Binding이란것은 서버측에서는 서비스 구현과 네트워크를 연결해주고 클라이언트에서는 클라이언트 호출 코드와 네트워크를 연결해주는 방법을 설명하는 녀석이라고 보면 되겠다. Contract란 서비스가 구현하고 있는 인터페이스를 말한다. 위 코드에서는 “TService”를 통해서 채널 객체에게 알려 줄 수 있다. Binding을 설명하자면 좀 길어지겠다.

달봉이가 구현된 채널 객체 생성 모듈은 우선 외부에서 address를 받는다. 그리고 바인딩은 이미 WCF에서 제공하고 있는 built-in 바인딩 객체중에서 WSHttpBinding이란 것을 사용한다. WSHttpBinding은 tranport 프로토콜로 HTTP, 메세지 인코딩 방식으로는 텍스트 방식을 사용한다는 등 Binding에서 필요한 설정이 미리 구현되어 WCF와 함께 제공되고 있다. 이렇게 미리 구현되어 제공되고 있는 built-in 바인딩은 이외에도 여러가지가 있는데, 다음 링크를 보면 자세히 설명하고 있다. 그리고 Contract에 대한 정보는 Dalbong2ProxyFactory를 생성하는 외부 코드에서 TService를 통해서 전달한다.

이렇게 ChannelFactory 객체를 생성하고 나서 이 객체의 Endpoint 속성의 Behaviors 속성을 통해서 사용자 정의 behavior를 끼워넣을 수 있다. 앞의 코드에 달봉이가 만든 UserInfoClientEndpointBehavior를 끼워넣는 코드가 마지막 부분에 있다. 이런 식으로 behavior 목록을 채널 객체에 알려주면 서비스를 호출할때 WCF 런타임은 이 behavior를 차례로 실행시켜주게 된다.

이 Dalbong2ProxyFactory를 사용하는 코드는 다음과 유사한 코드가 될 것이다.

Dalbong2ProxyFactory<SampleAsyncService.ISampleService> service =

    new Dalbong2ProxyFactory<BONG.WIN.CO.UserMgmt.SampleAsyncService.ISampleService>();

SampleAsyncService.ISampleService svcProxy = service.CreateProxy(

    "http://donbox-pc/BongSvc/CO"

    ,"BONG,UserMgmt.Service/SampleService.svc");

svcProxy.Hello();

그러나 달봉이는 개발자가 직접 Dalbong2ProxyFactory를 호출하도록 하지 않으려고 한다. 대신에 Dalbong2ProxyFactory를 한 더 감싸는 클래스를 하나 더 만들 것이다. 이유는 서비스를 호출할때 프로그레스바를 자동으로 보여주는 기능을 구현하는 것과 관련되어 있다.

프로그레스바를 자동으로 출력하는 기능도 behvior를 사용하고 있지만 이 녀석은 UI의 컨트롤과 관련되어 있다. 이것에 대해서는 어떻게 설명해야 할지 정리를 좀 해야겠다.

Posted by dalbong2

앞의 포스트까지는 메뉴를 클릭했을 때 해당 업무 화면을 로딩하는 것까지 진행했다. 이제 업무 화면에서 서비스를 호출하는 기능을 구현하도록 하자. 이때 개발 프레임워크단에서는 흔히 서비스를 호출할 수 있는 프락시 객체를 제공한다. Visual Studio를 사용하면 쉽게 프락시 클래스를 만들어 주지만 개발자가 그것을 그대로 사용하기에는 너무 기능적으로 부족한 감이 있다. 개발시 사용했던 서비스에 대한 URI도 설정에 따라서 자동으로 변경해 줄 수 있어야 하고 사용자 정보도 서버측으로 건네줘야 한다. 그리고 필요하다면 프로그레스바도 출력해줘야 한다. Visual Studio가 만들어주는 프락시를 한번 더 감싸서 이런 기능을 할 수 있는 프락시 클래스를 만들어 볼까 한다.

이런 기능을 갖는 프락시 클래스를 만들기 위해서는 사용하는 커뮤니케이션 방법을 확장할 수 있는 방법을 알아야 한다. 달봉이는 커뮤니케이션 방법으로 WCF를 사용하겠다. 즉 개발자가 직접 사용할 프락시 클래스를 만들기 위해서는 WCF 확장을 알아야 한다는 것이다. 

기본 기능을 확장하기 위해서는 그 프레임워크에서 제공하는 확장 포인트들을 먼저 확인할 필요가 있다. Spring.NET의 설명서에서도 그런 확장 포인트들을 설명하는 부분을 별도로 할애하고 있다. 그래서 어떤 부분에서 어떤 기능을 확장할 수 있는지를 알아야 할 것이다. “WCF 확장하기”도 여기서부터 출발한다. 

예전 포스트에서 Soap 확장하는 방법에 대해서 포스팅을 한 적이 기억난다. 지금 기억하는 것은 개념은 좋은데 구현하기 어려웠다는 것이다. 근데 WCF 확장 기술은 다르다. 개념도 좋고 구현도 쉽게 되어 있다.

그러나 WCF 확장에 대한 이론적인 개념은 나중에 기회되면 정리하도록 하겠다( 정말? ) 이번 포스트에서는 현재 달봉이가 제작하고 있는 개발 프레임워크에 구현된 예를 설명하도록 하겠다.

근데 그 동안 네임스페이스 변경도 있었고, 프로젝트의 분리도 많았다. 막상 변경된 부분을 정리하려고 하니 조금 막막하다.

현재 WCF 확장을 이용해서 구현해 놓은 기능은 2가지이다.

1. 서비스 호출시 서버로 사용자 정보 보내기

2. 비동기 호출시 프로그레스바 보여주기

프로그레스바 보여주는 확장은 할 말이 좀 많다. 우선 서비스 호출시 서버로 사용자 정보를 보내는 예제를 통해서 확장에 대한 개념을 알아보도록 하자.

먼저 샘플 서비스 내용을 보자. BONG.SVC.CO.UserMgmt 프로젝트의 SampleService.cs 파일의 내용이다.

public class SampleService : Dalbong2ServiceBase, ISampleService

{

    public string Hello()

    {

        //실행 좀 멈춘다.

        System.Threading.Thread.Sleep(10000);

        //현재 사용자의 ID를 사용한다

        return String.Format("Hello, you're {0}", base.UserInfo.ID);

    }

}

코드를 보면 SampleService를 구현하는데 base.UserInfo.ID 를 사용하고 있다. 베이스 클래스 Dalbong2ServiceBase를 보면 다음과 같다.

public class Dalbong2ServiceBase

{

    IUserInfo _UserInfo = null;

    public Dalbong2ServiceBase()

    {

        _UserInfo = (UserInfoBase)CallContext.GetData("__UserInfo__");

    }

    protected IUserInfo UserInfo

    {

        get

        {

            return _UserInfo;

        }

    }

}

생성자에서 CallContext에서 “__UserInfo__”라는 키의 값을 읽어와서 로컬 참조 변수에 캐시해두고 있다. 그것을 UserInfo 속성을 통해서 자식 클래스에 공개하고 있다.

CallContext라고 지금까지 들어보지 못한 것이 나왔다고 걱정하지 말라. 동일한 쓰레드 내에서 이후 실행되는 메소드 호출에서 접근할 수 있는 공용 저장소라고 생각하면 된다. 말이 어렵나? 그럼 웹 프로그램에서 Session 저장소에다 값을 저장해뒀다 이후의 페이지에서 그 값에 접근하는 코드는 많이 작성해봤을 것이다. 물론 이것과는 다른 개념이긴 하지만…

그럼 CallContext에 사용자 정보를 저장하는 곳이 있을 거다. Dalbong2.Service 프로젝트의 UserInfoServiceEndpointBehavior.cs 파일의 일부이다.

/// <summary>

///  클라이언트에서 올라온 사용자정보 객체를 복원하는 MessageInspector.

///  그 MessageInspector를 등록하는 Behavior.

/// </summary>

public class UserInfoServiceEndpointBehavior : IEndpointBehavior, IDispatchMessageInspector

{

    #region "IDispatchMessageInspector 구현"

    IUserInfo _UserInfo = null;

    public void BeforeSendReply(ref Message reply, object correlationState)

    {

        return;

    }

    public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request,

        System.ServiceModel.IClientChannel channel,

        System.ServiceModel.InstanceContext instanceContext)

    {

        string strUserInfo = request.Headers.GetHeader<String>("__UserInfo__", "http://Dalbong2/");

        object o = SerializationHelper.FromBase64String(strUserInfo);

        if ( o != null )

        {

            _UserInfo = (UserInfoBase)o;

            CallContext.SetData("__UserInfo__", _UserInfo);

        }

        else

        {

            throw new AccessViolationException("You can't access to this service");

        }

        return o;

    }

    #endregion

UserInfoServiceEndpointBehavior라는 길고도 긴 클래스의 일부이다. 이 클래스는 IDispatchMessageInspector라는 인터페이스를 구현하고 있다. 이 인터페이스의 메소드중에는 AfterReceiveRequest()라는 메소드가 있다. 이름이 암시하듯 클라이언트에서 요청을 받은 후에 할 일이 있다면 이곳에 구현해 놓으라는 것이다.

달봉이는 서버측에 전달된 메세지 객체 request의 헤더에서 “__UserInfo__”라는 키의 값을 구하고 있다. 그 값은 Base64로 인코딩된 string타입의 값을 반환한다. 그 값은 사용자 정보 객체를 직렬화해서 인코딩된 문자열이다. 이것을 다시 FromBase64String() 메소드를 통해서 객체를 복원하고 있다.

그런 다음 UserInfoBase로 타입 변환을 한 뒤 드디어 CallContext에 저장해 두는 것이다.

그럼 이 사용자 정보를 메세지 객체의 헤더에 넣어 주는 클라이언트측 코드가 있을 것이다. 이 일을 클라이언트측 프락시가 해 주는 것이다. 이것은 뒤에서 보도록 하자.

여튼 클라이언트에서 요청을 받은 후에 할 일을 구현해놨다. 이것만 하면 될까? 이 클래스를 서버측 WCF 엔진( 런타임 )이 인식할 수 있도록 런타임에 등록을 해 줘야 한다.

런타임이 인식할 수 있기 위해서는 IEndpointBehavior 인터페이스를 구현해야 한다. 달봉이가 작성한 클래스 UserInfoServiceEndpointBehavior는 이 인터페이스도 구현하고 있다. 그 구현 부분은 다음과 같다. 앞의 파일과 동일한 파일에 구현되어 있다.

#region "IEndpointBehavior 구현"

 

public void AddBindingParameters(

ServiceEndpoint endpoint,

BindingParameterCollection bindingParameters)

{

    //Not implemented

}

 

public void ApplyClientBehavior(

    ServiceEndpoint endpoint,

    ClientRuntime clientRuntime)

{

    //Not implemented

}

 

public void ApplyDispatchBehavior(ServiceEndpoint endpoint,

    EndpointDispatcher endpointDispatcher)

{

   endpointDispatcher.DispatchRuntime.MessageInspectors.Add( this );

}

 

public void Validate(ServiceEndpoint endpoint)

{

    //Not implemented

}

 

#endregion

다른 것은 구현되어 있지 않고, ApplyDispatchBehavior() 메소드내에서 런타임의 MessageInspectors 속성에 this를 추가하고 있다. 여기서 this는  IDispatchMessageInspector인터페이스를 구현한 객체이다. 현재 달봉이의 this, UserInfoServiceEndpointBehavior

객체는 이 인터페이스를 구현하고 있기때문에 this를 추가하는 것이 가능하다.

MessageInspectors.Add()를 이용하면 여러개의 IDispatchMessageInspector 객체를 추가할 수 있다. 즉 IEndpointBehavior 메소드들은 런타임에 등록되기를 희망하는 모든 IDispatchMessageInspector 객체를 가지고 있다.

이제 이 IEndpointBehavior 객체를 런타임에 등록하면 IEndpointBehavior에 등록된 IDispatchMessageInspector 객체들이 서비스 호출시 또는 반환시 작동되게 된다.

IEndpointBehavior를 런타임에 등록하는 방법은 어트리뷰트를 사용하는 방법, config 설정을 하는 방법, 프로그램적으로 하는 방법이 있다. 달봉이는 config를 통해서 하고 있다. 아래 보이는 것처럼 config를 통해서 behavior를 등록하려면 한가지 할 일이 더 남아 있다. 이 코드는 Dalbong2.Service 프로젝트에 포함되어 있다.

public class UserInfoBehaviorExtensionElement : BehaviorExtensionElement

{

    public override Type BehaviorType

    {

        get

        {

            return typeof(UserInfoServiceEndpointBehavior);

        }

    }

    protected override object CreateBehavior()

    {

        return new UserInfoServiceEndpointBehavior();

    }

}

BehaviorExtensionElement를 구현하면 아래와 같은 같은 config 설정이 가능하다.

달봉이는 SampleService를 호스팅하고 있는 애플리케이션으로 웹 애플리케이션을 사용하고 있다.

이곳에 포함된 web.config의 일부를 보면 다음과 같다.

    <system.serviceModel>

        <services>

            <service name="BONG.SVC.CO.UserMgmt.SampleService"

                     behaviorConfiguration="MyServiceBehavior" >

                <endpoint address=""

                          binding="wsHttpBinding"

                          contract="BONG.SVC.CO.UserMgmt.ISampleService"

                          behaviorConfiguration ="MyEndPointInspectors"> –> ③

                    <identity>

                        <dns value="localhost" />

                    </identity>

                </endpoint>

                <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />

            </service>

        </services>

        <behaviors>

            <serviceBehaviors>

                <behavior name="MyServiceBehavior">

                    <serviceMetadata httpGetEnabled="true" />

                    <serviceDebug includeExceptionDetailInFaults="false" />

                </behavior>

            </serviceBehaviors>

            <endpointBehaviors>

                <behavior name="MyEndPointInspectors">  –> ②²

                    <UserInfoEndpointExtention/>  –> ②¹

                </behavior>

            </endpointBehaviors>

        </behaviors>

        <extensions>

            <behaviorExtensions>

                <add name="UserInfoEndpointExtention" –> ②¹에서 사용

                     type="Dalbong2.Service.Interceptors.UserInfoBehaviorExtensionElement, Dalbong2.Service, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> –> ①

            </behaviorExtensions>

        </extensions>

    </system.serviceModel>

</configuration>

<system.serviceModel>…</system.serviceModel>이 WCF 서비스와 관련된 부분이다. 이 중에서도 달봉이의 확장과 관련된 부분이 볼드체로 된 부분이다.

① 부분이 앞에서 마지막으로 제작한 BehaviorExtensionElement를 이용해서 EndpointBehavior를 등록하는 부분이다. 여러개가 있을 수 있다. 이 중에서 사용하고 싶은 것이 있다면 ②에서처럼 <behavior></behavior>에 다시 등록한다. 이때 사용하는 <UserInfoEndpointExtension/>은 <behaviorExtensions>에 등록된 name값이다.

그런 다음 최종적으로 이 EndpointBehavior를 서비스에 적용하면 된다. 이것이 ③ 단계이다.

이렇게 하면 현재 SampleService이 호출될때마다 서버측에서는 사용자 정보를 메세지 객체 헤더에서 찾는 앞의 로직은 자동으로 활성화된다.

 

에이~무리다. 간단히 설명하기는 어렵다. 이런 확장에 대한 컨셉을 처음 접하는 독자라면 한번에 이해됐을 거라고는 생각지 않는다.

WCF를 확장할 수 있는 포인트는 다양하다. WCF 확장에 대해서 좀더 알고 싶다면 다음 링크를 살펴보자. 달봉이가 검색한 아티클중에서 가장 괜찮았다고 생각되는 것이다.

 

이렇게 서버측에서 사용자 정보를 받을 수 있으려면 클라이언트측에서 사용자 정보를 메세지 헤더에 넣는 부분이 있을 것이다. 다음에는 클라이언트측에서 사용자 정의 Behavior를 어떻게 클라이언트 런타임에 끼워(?)넣는지를 알아본다. 클라이언트에서는 이런 behavior를 끼워넣는 작업을 프락시 클래스에서 처리한다.

다음 링크에서 지금까지 작성한 코드를 다운로드할 수 있다. 코드에 있는 웹 애플리케이션이 IIS에 생성되어 있어야 샘플코드가 실행된다.

Posted by dalbong2

지난 포스트까지는 메뉴 정보를 로딩해서 트리로 출력하는 과정을 봤다. 오늘은 트리의 최종 노드를 클릭했을 때 화면을 로딩시켜보자.

샘플 화면을 하나 추가하자.

항목 추가 템플릿에서 User Control(WPF) 템플릿을 선택하고 이름을 UserMgmt.xaml이라고 넣는다.

이렇게 해서 추가된 사용자 컨트롤 코드를 좀 수정해야 한다.

우선 베이스 클래스를 우리가 이전에 만든 BongControlBase로 수정한다.

public partial class UserMgmt : BongControlBase

그런 다음 xaml 마크업 코드도 다음처럼 수정한다.

<Bong:BongControlBase x:Class="BONG.WIN.CO.UserMgmt.UserMgmt"

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

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

    xmlns:Bong="clr-namespace:Bong.Win;assembly=Bong.Win"

    Height="Auto" Width="Auto">

    <Border BorderBrush="Black" BorderThickness="2">

        <DockPanel Name="dockPanel1" LastChildFill="True" >

            <Button Height="23" Name="btnHello" Width="75" Click="btnHello_Click">Hello</Button>

        </DockPanel>

    </Border>

</Bong:BongControlBase>

이게 무슨 말인가하면…모르겠다면, 이전 포스트를 보자.

간단히 말하면 UserControl의 베이스 클래스를 사용자 정의 클래스( 여기서는 BongControlBase)로  지정하다보니 이런 모양이 되었다는 것이다.

( 물론 실전 프로젝트에서 개발자들에게 이렇게 수작업을 시킬 수는 없는 일일 것이다. Visual Studio 메뉴에 File->Export Templates… 가 있다. 이것을 사용하면 미리 수정된 사용자 컨트롤을 추가할 수 있는 템플릿이 앞의 그림의 항목 추가 템플릿 창에 나오게 할 수 있다. 이 방법에 대해서는 구글링해보면 나올 것으로 본다. Export Templates, Visual Studio를 적절히 조합해서 검색해 보자.)

달봉이는 이 사용자 컨트롤에 버튼을 하나 올렸놨다.

이 사용자 컨트롤을 메뉴 정보에 등록하는 과정은 이전 포스트에서 한 대로이다. 그러나 그 정보가 잘못 등록되어서 다시 다음처럼 수정했다.

dr = dt.NewRow();

dr[MenuHelper.MenuIDColumnName] = "010101";

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

dr[MenuHelper.MenuTypeColumnName] = MenuHelper.FileMenuTypeCode;

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

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

dr[MenuHelper.LoadUrlColumnName] = loadUrl;

dr[MenuHelper.ParentMenuIDColumnName] = "0101";

dt.Rows.Add(dr);

하단의 링크를 통해서 수정된 소스를 받을 수 있다.

이제 트리를 클릭했을때 화면을 탭으로 출력하는 코드를 보자. 우선 트리 최종 노드를 클릭했을때 반응할 수 있는 이벤트 핸들러를 등록하는 코드를 보자.

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;

        FileMenuItemInfo item = itemInfo as FileMenuItemInfo ;

        if( item != null )

        {

            treeItem.MouseUp += new MouseButtonEventHandler(treeItem_MouseUp);

        }

        parentItem.Items.Add(treeItem);

 

        ProcessItem(treeItem, itemInfo);

    }

}

메뉴 트리를 구성할 때, 메뉴가 최종 노드( FileMenuItemInfo)인 경우만 트리 노드의 MouseUp이벤트에 핸들러 treeItem_MouseUp를 등록하고 있다. 그리고 이전에 트리 노드treeItem의 Tag 속성에 메뉴 정보 객체 itemInfo를 저장해 두고 있다. 나중에 트리 노드가 클릭되면 Tag 속성에 저장해뒀던 메뉴 정보 객체를 복원해서 로딩할 화면에 대한 정보를 얻게 될 것이다.

treeItem_MouseUp의 구현 내용은 다음과 같다.

void treeItem_MouseUp(object sender, MouseButtonEventArgs e)

{

    TreeViewItem item = sender as TreeViewItem;

    //Tag 속성에 메뉴 정보 객체 복원

    FileMenuItemInfo menuInfo = item.Tag as FileMenuItemInfo;

    if (menuInfo != null)

    {

        string elemetName = menuInfo.ElementInfo.FullyQualifiedTypeName;

        FileMenuItemInfo existigItemInfo = null;

        //이미 같은 화면이 로딩되어 있는지 확인

        //이미 로딩되어 있다면 탭을 생성하지 않고 리턴

        foreach (TabItem existingItem in tabControl1.Items)

        {

            existigItemInfo = existingItem.Tag as FileMenuItemInfo;

            if (existigItemInfo != null &&

                existigItemInfo.ElementInfo.FullyQualifiedTypeName == elemetName)

            {

                existingItem.IsSelected = true;

                return;

            }

        }

        // Spring.NET 객체 생성기를 통해서 화면 객체를 얻는다.

        Dalbong2ControlBase  uiElement =

        ( Dalbong2ControlBaseDalbong2WinAppContext.XmlObjectFactory.GetObject(elemetName);

        //탭 컨트롤에 추가할 탭을 준비한다.

        TabItem tabItem = new TabItem();

        tabItem.Header = menuInfo.Name;

        tabItem.Tag = menuInfo;

        ScrollViewer sv = new ScrollViewer();

        sv.Content = uiElement;

        Frame f = new Frame();

        f.MinHeight = 300;

        f.MinWidth=300;

        f.BorderBrush = Brushes.Blue;

        f.BorderThickness = new Thickness(2);

        f.Content = sv;

 

        tabItem.Content = f;

        //탭을 탭 컨트롤에 추가한다.

        this.tabControl1.Items.Add(tabItem);

        tabItem.IsSelected = true;

    }

}

이게 끝이다. 트리 노드의 Tag 속성에서 메뉴 정보 객체를 복원한다.

그 다음 탭 컨트롤의 탭들중에 이미 해당 화면 객체가 로딩되어 있는지를 확인한다. 이미 로딩되었는지 확인하는 로직에서 탭의 Tag 속성에 저장된 FileMenuItemInfo 객체를 이용하고 있다. 이미 로딩되어 있다면 해당 탭을 선택해주고 리턴한다.

아직 로딩되어 있지 않다면 Spring.NET 컨테이너로부터 객체를 해당 화면 객체를 얻는다( 이 부분의 코드도 약간 수정되었다. 뭔지는 기억이 안난다 –_-;;)

 

그 다음 탭을 하나 동적으로 생성한다. 코드에서는 탭에 바로 화면 객체를 할당하지 않고 Frame 객체를 하나 생성해서 그곳에 화면을 넣고 그 Frame 객체를 탭의 컨텐트로 지정하고 있다.

탭을 탭 컨트롤에 추가하고 선택해주면 된다.

다음은 실행시킨 모습이다.

다운로드 받은 코드에는 달봉이가 테스트해보느라 이것 저것 들어 있을 것이다. 최종 코드가 될때까지 참고 테스트해보길 바란다.
UserMgmt 화면에서 Hello 버튼을 클릭하면 WCF 서비스를 호출하는 부분이 있다. 이 부분은 우선 주석처리하고 실행시키길 바란다.

Posted by dalbong2

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

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

 

정리가 힘들었던 포스트였다. AOP에 대한 부분을 연수 전에 작성한 것이라 리마인드를 위해서 그것까지 다시 읽어야했고 읽다 보니 그곳에서도 수정해야 할 부분도 있었다. 어떻게 정리해야 하나 고민이 많았던 부분인데 쌈박하지는 못한 것 같다. 토요일부터 시작해서 오늘에야 끝난다.

 

이 포스트에서는 Spring이 어떻게 트랜잭션 기능을 제공하는지를 이해할 것이다. 그리고 트랜잭션 기능을 이용하기 위해서 어떻게 설정하는지도 알아본다. “트랜잭션 관리자”, “트랜잭션용 AOP 프락시”, “트랜잭션 어드바이저”, “어드바이스 즉 인터셉터”, “포인트컷”같은 용어를 이해할 필요가 있을 것이다. “AOP 프락시”, “어드바이저”, “어드바이스”, “포인트컷”등은 Spring의 트랜잭션용 용어가 아니라 AOP 용어이다. 지난 포스트를 참조할 수 있다 ( AOP 프로그래밍 개념 I, AOP 프로그램 개념 II )

 

  • 트랜잭션 관리자

 

앞 포스트에서 말한 대로 Spring에서는 트랜잭션을 구현하는 코드를 제공하지는 않는다. 다만 기존의 트랜잭션 관리자들에서 시스템 환경에 적절한 것을 사용할 수 있도록 strategy 패턴을 이용해서 추상화했을 분이다. Spring에서는 다음과 같은 전략 인터페이스를 정의하고 있다.

public interface IPlatformTransactionManager

{

    ITransactionStatus GetTransaction(ITransactionDefinition definition);

    void Commit(ITransactionStatus transactionStatus);

    void Rollback(ITransactionStatus transactionStatus);

}

이 전략 인터페이스를 구체적으로 트랜잭션 전략의 구현체로는 다음과 같은 것을 제공하고 있다.

AdoPlatformTransactionManager

ADO.NET기반의 로컬 트랜잭션 제공

ServiceDomainPlatformTransactionManager

Enterprise Services기반의 분산 트랜잭션 제공

TxScopePlatformTransactionManager

System.Transactions 사용한 로컬 / 분산 트랜잭션 관리자

HibernatePlatformTransactionManager

NHiberate기반 또는 ADO.NET&NHibernate를 함께 사용해서 로컬 트랜잭션 제공

 

이 트랜잭션 처리 방법들은 각각의 특성이 있다. Spring.NET 문서 17.2절을 보면 잘 요약되어 있다. : 각 트랜잭션 관리자와 관련해서 로컬 트랜잭션, 글로벌 트랜잭션의 지원 여부 및 관리자의 특성등을 살펴볼 필요가 있겠다.  TxScopePlatformTransactionManager가 가장 유연할 것으로 보인다.

여튼 이런 트랜잭션 관리자는 외부에서 제공( configuration 또는 프로그램적으로 제공)되는 정보를 이용해서 적절한 트랜잭션을 생성, 관리하는 역할을 할 것이다.

트랜잭션을 개념적으로 설명할때는 어떤 “영역(바운더리) 또는 공간”으로 표현한다.  “하나의 트랜잭션 공간안에서 실행되는 코드로부터 영향을 받는 리소스( 데이터베이스 데이터)는 모두가 커밋되거나 또는 모두가 롤백된다”는 식으로 표현한다. 상당히 개념적인 표현이다. 트랜잭션에 참여하는 객체들에는 트랜잭션 성격을 정의하는 하나의 객체들이 달라붙어 있다. 객체와 트랜잭션 객체가 쌍으로 존재한다는 의미이다. 이 트랜잭션 객체의 속성이 같은 객체들은 동일한 트랜잭션 공간에 존재한다고 볼 수 있다. 만약 그 공간이 제공하는 트랜잭션 옵션들이 마음에 안들면 객체는 다른 트랜잭션 옵션을 갖는 객체를 만들어서 새로운 공간을 만들 수도 있다.

여튼 트랜잭션 공간을 생성하기위해서는 트랜잭션을 성격을 결정할 수 있는 몇 가지 옵션들을 정의해줘야 한다는 것인데, 다음과 같은 옵션들이 있다.

Isolation값

Propagation값

Timeout 값

Read-only 여부

트랜잭션은 데이터의 ACID (Atomicity, Consistency, Isolation, Durability) 개념을 보장할 수 있어야 한다. 이런 개념들을 보장하기 위해서 이와 같은 값들의 설정이 필요하다. 이런 값을 어떻게 설정하느냐에 따라서 그 트랜잭션 공간이 보장할 수 있는 성격이 조금씩 변경될 수 있다는 것이다.

이것들을 설명할 기력이 없다. 구글링해보자. “acid, transaction”로 검색하면 안 나올라나? “acid”만으로 검색하면 “산성비”에 대한 결과가 나올 것도 같은데…^^.

여튼 트랜잭션 공간을 생성하기 위해서는 이런 값을 지정해줄 필요가 있는데, 앞의 인터페이스의 메소드 GetTransaction()의 인자로 넘겨주는 객체 ITransactionDefinition을 통해서 그것이 가능하다. 이런 트랜잭션 옵션을 지정하는 것을 “트랜잭션을 정의”하는 것으로 표현하고 있다.

ITransactionDefinition 객체를 GetTransaction()을 호출하면 하나의 트랜잭션 공간이 생긴다고 보면 된다. 이 메소드로부터 반환받는 객체 ITransactionStatus를 통해서 트랜잭션 작업을 수행할 수 있다. 달봉이가 얼른 생각할때는 이 반환되는 객체의 이름을 왜 ITransaction이라고 짓지 왜 끝에 Status를 붙여놨을까 했다. 달봉이도 모른다. 이름만으로 판단해보면 트랜잭션의 상태에 대한 정보도 가지고 있는듯한데, 매뉴얼을 보면 이 녀석이 하는 일은 실제로 트랜잭션에 대한 상태를 제공한다. 그리고 이 녀석을 통해서 트랜잭션을 실행시킬 수도 있단다. 생각해보면 이해도 갈 수 있는 부분이다. Spring 입장에서는 트랜잭션을 처리하는 로직을 구현하고 있는 것이 아니다. 그냥 필요한 옵션들을 받아서 트랜잭션을 직접 생성하고 트랜잭션 처리를 로우 레벨에서 구현한 것은 각각의 트랜잭션 전략 객체들이다. Spring은 녀석들에게 줄 것 주고 “트랜잭션 시작하세요, 트랜잭션 현재 상태는 어때요? 좋아요? 그럼 커밋하세요” 라는 명령을 내리거나 상태를 조회하는 일만 하면 될 것 같다. Spring을 설계한 사람들이 잘 알아서 했겠는가? 우선은 그들의 설계를 이해하는 차원에서 공부하자.

그러나 사실 Spring 프레임워크를 사용하면 앞의 인터페이스의 메소드를 개발자가 직접 호출할 일은 거의 없을 것이다. 이런 API보다는 트랜잭션 관리자의 개념적인 의미에 집중하도록 하자.

기업에서 업무를 구현하는 개발자들을 위한 최종 개발 프레임워크라면 이런 트랜잭션 처리 API에 접근할 필요가 없도록 지원해줘야 한다. 물론 최종 트랜잭션에서 정의(ITransactionDefinition)한 것이 적합하지 않은 특별한 업무가 발생하는 경우 프로그램적으로 새로운 트랜잭션을 생성해야 한다면 직접 개발자가 이런 API를 호출할 수도 있겠다.

트랜잭션 관리자는 내부적으로 트랜잭션을 생성, 관리한다고 했다. 그러나 구체적으로 어떤 관리자를 사용해서 트랜잭션을 관리할 지는 설정을 해 줘야 한다.

 

  • 사용할 트랜잭션 관리자 설정

 

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

xmlns:db="http://www.springframework.net/database">

  <db:provider id="DbProvider"

               provider="SqlServer-1.1"

               connectionString="Data Source=(local);Database=Spring;...">

  </db:provider>

  <object id="TransactionManager"

          type="Spring.Data.AdoPlatformTransactionManager, Spring.Data">

    <property name="DbProvider" ref="DbProvider"/>

  </object>

  . . . other object definitions . . .

</objects>

 

<object/>를 이용해서 일반 객체를 설정하듯 하면 된다. 이 설정은 AdoPlatformTransactionManager를 사용하겠다는 것이고 DbProvider 속성을 이용해서 데이터베이스 연결 설정을 함께 하고 있다.

참고로 앞의 설정에서 트랜잭션 관리자(AdoPlatformTransactionManager)를 정의할때 DbProvider 속성이 있는 것에 주목해보자.
데이터베이스 연결을 트랜잭션 관리자가 관리하고 있다는 이 사실은 중요한 부분이다.
트랜잭션 관리 기술중에서 TransactionScope을 사용하는 가장 큰 이유를 알고 있을 것이다. 그런가?
하나의 트랜잭션 내부에서 접근하는 데이터베이스 서버의 수가 한대인 경우 로컬 트랜잭션으로 작업을 하다가 만약 2대 이상의 데이터베이스 작업을 하게 되면 자동으로 전역 트랜잭션( 분산 트랜잭션)으로 자동 전환( promotion)된다는 이점이 있다.
로컬 트랜잭션으로 작업을 하는 것이 당연 성능 측면에서 유리하다.
Enterprise Services의 MS-DTC를 이용하게 되면 모든 트랜잭션을 전역으로 수행한다. 그리고 ADO.NET은 로컬 트랜잭션만한 지원한다.
따라서 트랜잭션 관리에는 TransactionScope을 주로 이용하게 되는데 이 녀석의 단점이 있다. MS-SQL 서버로만 구성된 분산 환경에서라면 이 기능을 사용하는데 문제가 없다. 그러나 오라클 데이터베이스가 포함된 분산 환경에서는 전역 트랜잭션으로의 자동 전환( promotion)이 일어나지 않는다.
이곳에서 말하고 싶은 이것이 아니다. 더 중요한 단점이 있다. 데이터베이스 서버가 한대인 경우에도 전역 트랜잭션으로 자동전환되는 경우가 있다. 하나의 트랜잭션에서 첫번째 연결이 열리는 순간까지는 로컬 트랜잭션으로 작업을 하다가, 동일한 데이터베이스로의 연결이 두번째 열리는 경우 전역 트랜잭션으로 자동전환되어 버린다. 설령 데이터베이스 연결 문자열이 동일하다 해도, 두번째 데이터베이스 연결이 열리는 순간 트랜잭션 관리자는 분산 환경으로 판단한다는 것이다.
그렇다면 하나의 트랜잭션 안에서 하나의 데이터베이스에 작업을 할때는 동일한 데이터베이스 연결을 사용하면 되는 것이다. 흔히들 DAO단에서 연결 정보를 관리하는 구조에서는 이 단점을 피해갈 수 없다. 해서 Spring.NET에서는 트랜잭션 관리자에게 연결 정보를 주고 DAO( DAC, DSL이라고도 흔히 부른다)단에서 데이터베이스에 연결할때는 이 녀석을 사용하도록 하고 있다.
위의 트랜잭션 관리자(AdoPlatformTransactionManager) 설정에 DbProvider 속성이 있다는 것이 이런 이유에서 합리적이라고 할 수 있겠다.
 

  • 트랜잭션용 AOP 프락시

 

AOP 프락시에 대해서는 이전 포스트에서 설명했다. Spring의 트랜잭션용 프락시가 어떤 위치에 있는지 개념적으로 다시 한번 더 보자. Spring.NET 레퍼런스 문서 17.5.1에 나와 있는 그림이다.

그림을 보면 클라이언트 코드는 직접 타겟 객체를 참조하지 않고 트랜잭션용 AOP 프락시를 참조한다. 트랜잭션용 AOP 프락시는 클라이언트의 호출을 받으면 바로 타겟 객체의 메소드를 호출하지 않는다. 트랜잭션용 어드바이저를 호출한다. 이 녀석은 트랜잭션 작업을 수행하는데, 트랜잭션이 생성되어 있지 않다면 새로운 트랜잭션을 생성하거나 또는 기존의 트랜잭션 공간에 참여하는 작업을 하게 되는 것이다. 커스텀 어드바이저가 또 있다면 그 녀석들의 작업을 차례로 수행하고 나서 마지막으로 타겟 객체의 메소드를 호출하게 된다.

타겟 메소드가 리턴을 하게 되면 그 결과는 다시 호출순과 반대순으로 차례로 전달된다. 이때 트랜잭션용 어드바이저는 해당 호출에 대해 예외가 발생하지 않고 호출이 성공을 하게 되면 해당 트랜잭션을 커밋하게 된다.

이런 트랜잭션용 AOP 프락시를 생성하는 방법을 Spring.NET에서는 여러가지를 제공한다.

 

  • Spring의 트랜잭션용 프락시 객체 생성 방법

 

Spring.NET에서 제공하는 트랜잭션용 프락시 객체를 생성하는 방법에는 몇 가지가 있지만 이곳에서는 기업용 애플리케이션에 적용하기 적합한 2가지만 언급하겠다.  다른 방법에 대해서는 Spring.NET 레퍼런스 문서를 참고하길 바란다.

.ProxyFactoryObject  :

이 녀석을 사용해서 타겟 객체에 대한 프락시 객체를 얻는 방법은 이전의 포스트에서 설명한 적 있다. 이 녀석을 이용하기 위해서는 타겟 객체에 대한 참조 그리고 트랜잭션용 어드바이스( 인터셉터 )에 대한 참조를  넘겨줘야 한다.

그러나 이 녀석보다는 기업용 애플리케이션에 적합한 것은 다음 녀석이다.

.DefaultAdvisorAutoProxyCreator :

이 녀석은 TransactionAttribute와 함께 사용해서 트랜잭션용 프락시를 생성하고 싶은 메소드를 손쉽게 결정할 수 있다. 트랜잭션 커밋을 하기 위해서 [AutoComplete]같은 어트리뷰를 사용해 본적이 있을 것이다. 비슷하게 [Transaction]어트리뷰트를 사용해서 해당 메소드를 호출할때는 트랜잭션용 프락시가 자동으로 생성될 수 있도록 하는 방법이다.

public class UserMgmtDSL : ...

{

    [Transaction()]

    public void Save( ... )

    {

        //필요한 작업을 한다.

    }

}                              

이 녀석에게는 타겟 객체에 대한 참조나 트랜잭션용 어드바이스에 대한 참조를 명시적으로 건네주지 않아도 된다.  이게 무슨 말인지 ProxyFactoryObject를 사용하는 설정과 DefaultAdvisorAutoProxyCreator를 사용하기 위한 설정을 비교해 보도록 하자.

 

  • ProxyFactoryObject를 사용하기 위한 설정

 

<!--프락시 생성자-->

<object id="proxyCreator" type="Spring.Aop.Framework.ProxyFactoryObject, Spring.Aop">

  <property name="Target" ref="bslObject"/>

  <property name="ProxyInterfaces">

    <value>Spring.Data.IBSLObject</value>

  </property>

  <property name="InterceptorNames">

    <value>transactionInterceptor</value>

  </property>

</object>

 

<!-- 트랜잭션용 어드바이스-->

<object id="transactionInterceptor" type="Spring.Transaction.Interceptor.TransactionInterceptor, Spring.Data">

  <property name="TransactionManager" ref="transactionManager"/>

  <!-- note do not have converter from string to this property type registered -->

  <property name="TransactionAttributeSource" ref="methodMapTransactionAttributeSource"/>

</object>

<!-- 포인트컷 및 트랜잭션 옵션-->

<object name="methodMapTransactionAttributeSource"

type="Spring.Transaction.Interceptor.MethodMapTransactionAttributeSource, Spring.Data">

  <property name="MethodMap">

    <dictionary>

      <entry key="Spring.Data.BSLObject.SaveTwoTestObjects, Spring.Data.Integration.Tests"

      value="PROPAGATION_REQUIRED"/>

      <entry key="Spring.Data.BSLObject.DeleteTwoTestObjects, Spring.Data.Integration.Tests"

      value="PROPAGATION_REQUIRED"/>

    </dictionary>

  </property>

</object>

 

<!--   BSL, DAO 객체 정의   -->

<object id="bslObject" type="Spring.Data.BSLObject, Spring.Data.Integration.Tests">

  <property name="DaoObject" ref="daoObject"/>

</object>

 

프락시 생성자 ProxyFactoryObject 를 설정하는 부분에서는 어떤 타겟 객체에 대해서 프락시를 생성해야 할지를 “Target” 속성을 통해서 설정해줘야 한다. 그리고 “InterceptorNames” 속성을 통해서 사용할 트랜잭션용 어드바이스도 설정해줘야 한다.

위 설정에서는 “bslObject”라는 이름으로 정의된 BSL단 객체에 대해서 프락시를 생성하겠다는 것을 보여주고 있다. 기업용 애플리케이션에서는 수많은 BSL단 객체들이 나올텐데 이것들을 프락시 생성자들에 모두 등록하는 것은 아니올시다이다. 그리안해도 BSL단의 객체들은 모두 위 설정의 아래처럼 configuration에 정의해야 한다. 맨 아래처럼 해야 하는 것은 Spring.NET의 IoC 기능이나 AOP 기능을 위해서는 어쩔 수 없는 것이라 해도 트랜잭션을 위해서 또 한번 객체마다 트랜잭션용 프락시에 등록작업을 해야 한다는 것은 중복작업이라는 기분을 피할 수 없다.

 

  • DefaultAdvisorAutoProxyCreator용 설정

 

Spring.NET은 TransactionAttribute를 인식할 수 있는 기능을 지원한다고 했으니 configuration에 정의된 객체중에서 [Transaction()] 어트리뷰트가 표시된 메소드들이 호출될때는 스스로 인식해서 트랜잭션용 AOP 프락시를 생성해서 반환해주는 기능이 있으면 좋을 것이다.

Spring.NET은 DefaultAdvisorAutoProxyCreator와 TransactionAttribute를 사용해서 그런 시나리오를 지원해준다는 것이다. 즉 DefaultAdvisorAutoProxyCreator를 사용하면 BSL단의 객체를 이 녀석에게 등록할 필요가 없다는 것이다. configuration에 정의된 모든 객체들중에서 TransactionAttribute가 표시된 객체들에 대해서 자동으로 트랜잭션용 AOP 프락시를 생성한다는 것이다. 또한 “InterceptorNames” 속성도 “transactionInterceptor”라고 설정하면 Spring.NET에서 기본적으로 제공되는 트랜잭션용 어드바이스를 사용하게 된다.

따라서 굳이 트랜잭션용 프락시나 어드바이스 설정을 해 주지 않아도 된다는 것이다. 그런 기본적인 구현을 사용하겠다는 표시를 해주면 된다. 다음 설정을 보자.

<!--기본 트랜잭션용 AOP 프락시 생성자 및 어드바이스 사용 -->

<tx:attribute-driven transaction-manager="transactionManager"/>

 

<!--   BSL, DAO 객체 정의   -->

<object id="bslObject"

        type="Spring.Data.BSLObject, Spring.Data.Integration.Tests">

  <property name="DaoObject" ref="daoObject"/>

</object>

<object id="daoObject" type="Spring.Data.DaoObject, Spring.Data.Integration.Tests">

  <property name="AdoTemplate" ref="adoTemplate"/>

</object>

트랜잭션용 AOP 프락시를 생성하는데 DefaultAdvisorAutorProxyCreator를 사용하고 기본적인 트랜잭션 어드바이스를 사용하겠다는 표시로 <tx:attribute-driven>을 추가하고 있다. that’s all !

이런 방식으로 트랜잭션용 프락시와 어드바이스 설정을 하기 위해서는 한 가지가 더 필요하다. 트랜잭션용 네임스페이스를 파싱할 수 있는 파서가 등록되어야 한다.

 

  • 트랜잭션용 네임스페이스 파서 등록

 

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

<configuration>

  <configSections>

    <sectionGroup name="spring">

      <section name="parsers" type="Spring.Context.Support.NamespaceParsersSectionHandler, Spring.Core" />

      <!-- other spring config sections like context, typeAliases, etc not shown for brevity -->

    </sectionGroup>

  </configSections>

  <spring>

    <parsers>

      <!-- -->

      <parser type="Spring.Data.Config.DatabaseNamespaceParser, Spring.Data" />

      <parser type="Spring.Transaction.Config.TxNamespaceParser, Spring.Data" />

      <parser type="Spring.Aop.Config.AopNamespaceParser, Spring.Aop" />

    </parsers>

  </spring>

  </configSections>

 

  • 트랜잭션용 최종 configuration 모습

 

달봉이는 기업용 애플리케이션에서 Spring.NET을 이용해서 비즈니스 서비스 레이어의 트랜잭션 처리를 해야 한다면 DefaultAdvisorAutoProxyCreator를 사용할 것이다. 트랜잭션 처리를 해야 하는 객체가 대규모로 있는 상황에서 반복되는 설정을 가장 피할 수 있는 방법으로 가장 적합한 녀석으로 판단된다.

이제 트랜잭션이 필요한 데이터베이스 프로그래밍을 할때 필요한 전체 설정들을 통합해보도록 하자. 기업 애플리케이션의 설정은 다음과 유사한 모습이 될 것이다.

 

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

<configuration>

  <configSections>

    <sectionGroup name="spring">

      <section name="parsers" type="Spring.Context.Support.NamespaceParsersSectionHandler, Spring.Core" />

      <!-- other spring config sections like context, typeAliases, etc not shown for brevity -->

    </sectionGroup>

  </configSections>

  <spring>

    <parsers>

      <!-- -->

      <parser type="Spring.Data.Config.DatabaseNamespaceParser, Spring.Data" />

      <parser type="Spring.Transaction.Config.TxNamespaceParser, Spring.Data" />

      <parser type="Spring.Aop.Config.AopNamespaceParser, Spring.Aop" />

    </parsers>

  </spring>

  </configSections>

 

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

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

           xmlns:tx="http://www.springframework.net/tx"

           xmlns:db="http://www.springframework.net/database" >

    <!-- 데이터베이스 연결 정보-->

    <db:provider id="DbProvider"

                  provider="SqlServer-1.1"

                  connectionString="Data Source=(local);Database=Spring;User ID=springqa;Password=springqa;Trusted_Connection=False"/>

    <!-- 트랜잭션 관리자-->

    <object id="transactionManager"

            type="Spring.Data.AdoPlatformTransactionManager, Spring.Data">

      <property name="DbProvider" ref="DbProvider"/>

    </object>

 

    <!--데이터베이스 접근을 위한 AdoTemplate 정의  -->

    <object id="adoTemplate" type="Spring.Data.AdoTemplate, Spring.Data">

      <property name="DbProvider" ref="DbProvider"/>

    </object>

 

    <!--기본 트랜잭션용 AOP 프락시 생성자 및 어드바이스 사용 -->

    <tx:attribute-driven transaction-manager="transactionManager"/>

 

    <!--   BSL, DAO 객체 정의   -->

    <object id="bslObject"

            type="Spring.Data.BSLObject, Spring.Data.Integration.Tests">

      <property name="DaoObject" ref="daoObject"/>

    </object>

    <object id="daoObject" type="Spring.Data.DaoObject, Spring.Data.Integration.Tests">

      <property name="AdoTemplate" ref="adoTemplate"/>

    </object>

  </objects>

</configuration>

 

이 설정에는 어떤 메소드에 트랜잭션이 적용될 것인가가 나와 있지 않다. BSL단 객체의 메소드에 어트리뷰트( TransactionAttribute)를 표시하고 있다. 앞에서 말한대로 Spring은 configuration에 정의된 모든 객체들중에서 어트리뷰트( Transaction() )이 표시된 객체들을 찾는다. 그런 다음 TrasanctionAttribute에 설정된 옵션값들( IsolationLevel, Propagation값 등)로 세팅된 AOP 트랜잭션 인터셉터를 생성한다.

이제 앞에서 보여준 그림을 다시 한번 보도록 하자. “타갯 객체의 메소드 호출자가 트랜잭션용 프락시를 통해서 메소드를 호출하게 되면 이제 트랜잭션 관리자는 트랜잭션 인터셉터에 설정된 옵션들을 이용해서 트랜잭션 객체를 생성하고 트랜잭션 관리를 시작하게 된다.” 이제 이 말이 이해가 되어야 하는데….

만약 개발자가 TransactionAttribute를 사용할때 아무 트랜잭션 옵션도 제공하지 않는다면 즉 [Transaction()]만 사용한다면 기본 옵션값으로 트랜잭션이 설정된다. 다음은 Spring.NET에서 제공하는 트랜잭션 옵션들의 기본값들이다.

 

propagation : TransactionPropagation.Required

isolation : IsolationLevel.ReadCommitted

read-only : false 즉 read/write

timeout : 이 값은 사용되는 트랜잭션 관리자에 따라서 기본값이 달라진다.

 

  • 트랜잭션 기본 옵션 변경

 

만약 이 기본값을 시스템 전체적으로 바꾸고 싶다면 어떻게 해야 하나. <tx:advice> 요소를 사용해서 변경할 수 있다.

<!-- the transactional advice (i.e. what 'happens'; see the <aop:advisor/> object below) -->

<tx:advice id="txAdvice" transaction-manager="transactionManager">

  <!-- the transactional semantics... -->

  <tx:attributes>

    <!-- all methods starting with 'get' are read-only -->

    <tx:method name="Get*" read-only="true"/>

    <!-- other methods use the default transaction settings (see below) -->

    <tx:method name="*" propagation="Required" isolation="ReadUncommitted" timeout="60" read-only="false"/>

  </tx:attributes>

</tx:advice>

 

이 설정은 메소드명이 “Get”으로 시작하는 메소드는 읽기 전용의 트랜잭션을 사용하고 그 외는 기본 트랜잭션 옵션을 설정된 트랜잭션을 이용하라는 내용이다. 이렇게 설정하면 읽기 전용의 트랜잭션을 사용하기 위해서 개발자는 메소드명을 지을때 “Get”으로 시작하는 이름을 사용하면 된다. 그 외의 모든 메소드( name=”*” )는 설정에서 앞의 설정에서 주어진 트랜잭션 컨텍스에서 실행된다.

이 설정은 전체 시스템의 기본값을 변경한다. 만약 필요하다면 메소드별로 개발자는 TransactionAttribute의 속성을 변경해서 이 값을 변경할 수 있다.

 

트랜잭션을 위한 Spring.NET 설정에 대한 이야기를 이것으로 마무리해야 겠다. Spring.NET 메뉴얼을 보면 트랜잭션과 관련해서 다른 얘기도 많이 있지만 달봉이는 기업용 애플리케이션에서 사용하기에 적절하다고 판단되는 설정에 대한 이야기를 위주로 했다.

 

  • 다음 작업

 

이제 남은 것은 트랜잭션을 사용할 BSL 및 DAO 객체를 코딩하는 것이다.

 

휴~~

Posted by dalbong2

연수 떠나기 전에 다 하지 못했던, Spring.NET 프레임워크 연재를 계속하기 위해서 다시 공부를 시작했다.

이번에는 Spring.NET이 트랜잭션을 지원하는 방식을 정리하려고 하고 있다. 그러나 이번 포스트의 토픽은 이것이 아니다. Spring.NET이 미들 티어의 트랜잭션 관리를 어떻게 하고 있는지를 설명하기 전에 패턴을 하나 미리 정리하는 것이 나을 것 같았다.

 

Strategy 패턴이 뭐여

 

달봉이도 이렇게 사용하는 것이 Strategy 패턴이구나 하고 이제서야 알게 되었다. 사실은 이런 패턴을 사용했던 것은 달봉이가 학교 다닐 때부터였다. 다음은 달봉이의 논문에 포함되어 있는 그림이다.

다른 것은 볼 필요없고, 붉은 박스 안을 보자. 교량을 건설하다 보면 어떤 특정 부분의 재료가 ‘빔(beam)’이라는 것이 사용될 수도 있고, ‘트러스바(trussbar)’라는 것이 사용될 수도 있다. 이런 부품으로는 어떤 것도 올 수 있다는 것이다. 그림에서 부품의 종류를 나타내는 마지막 박스는 점점(…)으로 채워져있는 것은 아직 개발되지 않은 부품이 나중에 사용될 수도 있다는 것이다.

이런 부품에 따라서 교량을 설계 또는 안정성 해석의 프로그램에 입력되는 값들이 달라지고 계산의 구체적인 로직이 조금씩 달라진다. 이 설계의 목적은 해석 모델의 부품은 언제든지 교체될 수 있어야 한다는 것이다.

요는 이렇다. 이런 부품들은 언제든지 변할 수 있으니 부품이 바뀌더라도 다른 부분의 코드는 수정이 없어야 한다는 것이다. 이런 시나리오가 바로 Strategy 패턴이 적용될 수 있는 부분이다.

근데 왜 Strategy라는 이름이 붙었을까? 검색을 해 보면 어딘가에 그 사연이 있겠지만 지금으로서는 달봉이도 모른다. 그러나 Strategy란 것이 교체 가능한 부품 하나 하나를 말한다고 한다. Strategy 패턴을 설명하는 wikipeadia의 설명에 따르면 Strategy를 policy라고도 한단다. 전체적인 컨셉(인터페이스)은 고정되어 있더라도 구체적인 전략이나 정책은 언제든지 바뀔 수 있는 것이다.

Spring.NET에서 다시 설명할 기회가 있겠지만, Spring.NET의 트랜잭션 관리를 예로 들어서 알아보자. Spring.NET의 트랜잭션 관리란 말은 새로운 트랜잭션 기술을 구현해서 제공하고 있다는 것은 아니다. 이미 구현되어 있는 기술들을 Strategy 패턴을 이용해서 언제든지 필요한대로 각 기술들을 사용할 수 있는 환경( 컨텍스트 )를 제공한다는 것이다. 트랜잭션을 처리할 수 있는 이미 구현된 기술들은 어떤 것들이 있나. 우리가 흔히 사용해온 COM+( Enterprise Services), ADO.NET, System.TransactionScope를 이용하는 방법들이 있겠고, NHibernate라는 것을 이용하는 방법도 있다. 그외 다른 방법도 있을 것이다. 각 트랜잭션 처리 구현 방법들은 각각의 장단점이 있을 수 있겠고 그 장단점에 맞게 사용해야 하는 환경이 있을 수 있다.

즉 트랜잭션을 처리해야 한다는 컨셉은 동일하나 그것을 구현하기 위해서 구현한 기술들은 여러 가지가 있을 수 있다. 그리고 프레임워크가 실제 기업에 적용되었을때는 어떤 기술을 사용될지는 알 수 없다. 이런 경우 프레임워크를 준비하는 입장에서 생각해 볼 수 있는 패턴이 바로 Strategy 패턴이라 하겠다. 익숙한 플러그인 개념과도 상통한다고 볼 수 있겠다.

만약 프레임워크단에서 if문이나 switch문을 이용한다면 문제가 아닐 수 없다. 다음과 같은 프레임워크 코드가 있다고 해보자.

string 트랜잭션 = 설정된트랜잭션기술


switch( 트랜잭션 )


    case : EnterpriseService
        EnterpriseService용 API를 이용한 트랜잭션 처리


    case( ADO.NET )
        ADO.NET API를 이용한 트랜잭션 처리


    case( TransactionScope )
        TransactionScope API를 이용한 트랜잭션 처리
    ...

 

만약 이 프레임워크가 case문에 없는 다른 트랜잭션 관리 기술을 지원해야 한다는 필요성이 제기된다면 프레임워크 코드를 수정해야 하나? 이미 이 프레임워크가 기업들에 배포된 경우라면?

 

UML 다이어그램

 

이제 현실적인 문제점과 이 패턴의 필요성에 대한 동기 부여는 되었을 것으로 보인다. 그럼 이 패턴을 좀더 이론적으로 정리해보자. 다음 그림은 wikipeadia에 있는 이 패턴의 UML 다이어그램이다.

오른쪽 상단에 Strategy라는 인터페이스가 있다. 이 인터페이스에는 필요한 컨셉 예를 들어서 트랜잭션을 처리할 수 있는 기술들이라면 최소한 이런 것들은 구현될 필요가 있다는 전체적인 컨셉을 미리 정의해 놓고 있다. 트랜잭션 시작, 트랜잭션 커밋, 트랜잭션 롤백 등. 구체적인

그리고 하단에 그 인터페이스를 구현하고 있는 실제적인 전략 구현 클래스들( ConcreteStrategyA, ConcreteStrategyB )이 있다. 이 클래스들은 인터페이스에서 정의한 기본 약속들을 자신들에 맞게 실제로 구현한다. 이 전략 구현 클래스들은 언제든지 필요하다면 확장될 수 있다.

좌측 상단 부분의 Context라는 것은 전략들의 기능을 사용하는 클라이언트 코드이다. 그러나 이 클라이언트 코드가 바라보는 것은 실제적인 구현 전략들이 아니라 인터페이스만을 참조하고 있다. 즉 클라이언트 코드 입장에서는 실제 구현 전략들에는 관심도 없고 그것들의 각각에 의해 영향을 받을 일도 없다는 것이다. 걔들이 하기로 한 일들( 인터페이스에서 정의한 약속)만 잘 해주면 되는 것이다. 참고로 만약 그림의 Context도 여러 종류가 있을 수 있다면 이것도 실제로 구현된 클래스가 되어서는 안될 것이다. 이것 또한 일부 구체적으로 구현될 부분은 빈 껍데기(?)로 되어 있는 타입이 되어야 할 것이다. 이때 인터페이스를 사용할것인가? 추상 클래스를 사용할 것인가? 추상 클래스 !
이것에 인터페이스와 클래스, 어떤 타입을 사용할 것인가에 대해서 이전 포스트에서 달봉이 나름대로 정리했었다. 참조할 수 있을 것이다.

앞의 UML 다이어그램에 맞는 예제 코드도 역시 wikipeadia에서 볼 수 있다.

 

Spring.NET의 적용

 

Spring.NET은 다음과 같은 방식으로 이 패턴을 적용하고 있을 것이다.

 

인터페이스 IPlatformTransactionManager가 전략 클래스들이 구현해야 하는 약속을 정의하고 있다. 

그림에서 보이는 실제 전략 클래스들의 이름은 Spring.NET의 실제 클래스명과는 다르다. 실제는 이 이름들보다 훨씬 길어서 간략하게 줄였다.

Spring.NET 프레임워크에서는 어떤 전략 트랜잭션 관리자를 사용할 것인가를 설정할 수 방법( configuration 설정 방법 또는 프로그램적인 방법 제공)을 제공하고 있다. 그럼 Spring.NET 프레임워크는 그 설정에 맞는 적절한 관리자를 로딩해서 트랜잭션 처리에 사용하게 된다. Spring.NET 프레임워크의 사용자는 어떤 트랜잭션 관리자를 사용할 것인가에 대한 설정만 하면 된다.

몇 시간의 여유가 있어서 근무 시간에 약간의 눈치를 보며 지금까지 머리속으로만 정리했던 것을 후다닥 작성한 포스트다. 잘못된 곳이 있을 수 있다. 이해하고 있는 부분이라면 그냥 넘어가도 좋고, 영 걸린다 싶으면 댓글 남겨주면 다시 한번 더 보도록 하겠다.

Posted by dalbong2

방명록에 어떤 분이 질문을 올렸는데, 답변이 약간 길어져서 포스트로 올린다.
정신적 여유가 없어서 글이 성의없게 작성되었다는 것을 미리 밝힌다.


음...기본적으로 ClickOnce에서는 어플리케이션을 시작 프로그램(Startup)으로의 등록과 제거를 지원하지 않고 있습니다.

startup등록은 약간의 꽁수면 됩니다. 사실 clickonce와 startup으로 구글링해보면 만족할 만한 결과가 나옵니다.
제 책에 이 내용을 썼던 것 같은데, 기억이 가물합니다( 지금 원고가 어디로 가 버렸는지 찾을 수가 없습니다.-_-;; ).
요는 ClickOnce 어플리케이션을 설치하면 생성되는 shortcut 파일을 Startup 폴더에 복사하는 방법입니다.
다음 링크를 참조하면 코드가 나와 있습니다. 이 문서에는 xp, vista에서의 이슈도 기술되어 있습니다.
http://keithelder.net/blog/archive/2009/04/18/how-to-run-a-clickonce-application-on-startup.aspx
http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/75d2112c-707c-4051-a5fc-eb51802558bb/

근데, 제거에 대해서는 사실 이전에 생각해 본 봐가 없습니다.
오늘 처음으로 님의 글을 보고 심각하게 생각해봤습니다.
startup 폴더로 복사한 shortcut 파일을 삭제해야 하는데 이것을 어떻게 할 수 있느냐가 문제인 것이죠.
프로그램 추가/제거를 통해서 삭제를 하면 일단 ClickOnce가 머신에 복사하고 변경한 파일은 모두 삭제해 주는데 코딩을 통해서 복사한 이 shortcut 파일은 ClickOnce가 모른다는 거죠.

구글링을 해봐도 그에 대한 이슈는 여러명이 제기한 것 같은데 답은 없군요.

근데 그 기능이 꼬옥 필요하다면 궁여지책의 방법이 하나 생각나긴 하는데, 실험 정신(?)이 필요할 것 같습니다.
저도 실은 한번도 테스트해보지 않은 방법입니다.

이런 식의 어플리케이션을 일반 사용자를 대상으로 한다면 다음 방법 또한 쓸모가 없게 됩니다.
인트라넷 애플리케이션인 경우에만 유효하다는 것을 미리 말씀드립니다.

저의 포스트중에서 .NET 카테고리에 가보면 "기본 AppDomain 생성자 변경하기"라는 것이 있는데, 이것을 이용하면 어떨까 하는 생각을 해 봤습니다.
간단히 말하면( 간단히 말할 수 있으려나...) 사용자가 .NET 어플리케이션을 실행하면 그 어플리케이션이 실행되기 전에 .NET의 CRL은 그 어플리케이션이 실행될 공간 즉 AppDomain을 먼저 생성되는데,
이 공간을 생성하는 녀석을 우리가 커스터마이징할 수 있다는 얘기입니다.
이 커스터마이징 코드에서 만약 님이 원하시는 애플리케이션이 삭제되었다는 것을 확인(?)만 할 수 있다면,
예의 shortcut 파일을 삭제하고 AppDomain을 생성하는 작업을 포기하고 그냥 리턴하면 되지 않을까 하는 생각을 해 봤습니다.

AppDomainManager 클래스를 상속해서 InitializeNewDomain()을 상속해서 님의 어플리케이션이 제거되었는지 여부를 확인해서 제거 되었다면 shortcut 파일을 제거하고 그렇지 않다면
AppDomain을 생성하는 base.InitializeNewDomain() 메소드를 호출하면 되지 않을까 되지 않을까하는 편한 생각을 해 봅니다.
AppDomainManager에 대한 설명은 앞에서 말한 필자의 포스트나 MSDN( http://msdn.microsoft.com/en-us/library/system.appdomainmanager_methods.aspx )을 참조하시길 바랍니다.

님께서 방향을 원하셔서 답글은 남깁니다만 개인적인 지적 호기심이라면 모르겠지만, 고객의 요청이라면 먼저 고객과의 합의를 시도해보는 것은 어떨런지.
참내...기술 블로그에서 이런 해결책을 제시하다니...저도 이제 게을러 진건지...죄송합니데.

Posted by dalbong2

블로그 이전 완료

Others 2009/04/25 19:30
블로그를 티스토리로 이전했다. 그러나 아직 잘못된 링크를 수정하는 작업등 몇 가지가 남아있다.
포스트는 겨우 수작업으로 이전을 했지만, 방명록, 댓글까지는 복구하지 못했다 -_-;;
흔적을 남겨주신 분들께 죄송한 마음이다.

5개월 정도 프로젝트에서 손을 놨더니 적응할 시간이 좀 필요한듯 하다.
몇개월 지나지도 않았는데, 왜 이렇게 새로운 기술이나 개념들이 많이 나왔는지...
IT 개발자로 살아남는 것이 어렵다는 것을 새삼 느끼게 된다.

이제 내 생에 가장 여유로왔던 시간으로 기억될 것 같은 순간이 지났다.
이제부터 새로운 마음으로 글쓰기를 다시 시작해볼까 한다.
마음 추스리고 시동을 거는 것이 좀 늦는 타입이라 언제 어떤 주제로 글을 다시 쓰게 될지는 모르겠다.



'Others' 카테고리의 다른 글

MIXSH인증용 새 페이지  (0) 2009/11/19
[메모] White Box testing tool for .NET  (0) 2009/10/27
블로그 이전 완료  (1) 2009/04/25
Hundreds Island ! 들어들 봤나  (0) 2009/04/25
여기는 바기오 !  (0) 2009/04/25
어학연수 준비중  (0) 2009/04/25
Posted by dalbong2

매일 작은 골방에 쳐박혀 영어만 쳐다보고 있으니 답답함이 한 가득이 되었다. 그렇다고 영어가 되는 것도 아니다. 아무 생각없이 머리를 비울때가 된 것 같아 검색하다 발견한 곳이 Hundreds Island 라는 섬이다. 섬이라기 보다는 100개의 섬으로 이뤄진 섬군(群)이다.

 

나는 정말 사람이 사는 섬들이 백개 모여있는 줄 알았다. 그러나 섬이라기 보다는 차라리 큰 바위였다.

 

이런 배를 타고 몇몇 섬들을 일주 시켜준다.

위에서 본 섬 모습이다. 무슨 거북떼들이 쉬고 있는 것 같기도 하고...

물은 정말 깨끗하다. 사진상으는 흐릿하게 나왔지만...

얼마나 깨끗한지 물만 한번 찍어봤다.

내가 타고 갔던 Helen이라는 배이다.

섬에는 특별히 먹을 것이 없다길래, 먹을 것을 사들고 들어갔는데 도저히 먹을 수 없는 것들이라 버렸다. 배가 고파서 땀을 삐질 흘리고 있는 모습이다.

 

여기가 어디냐면...Jolibee라는 필리핀 fast food점이다. 왜 찍었냐고? 잊을 수 없을 것 같아서...

그날 특별히 먹은 것도 없고 해서 뭔가로 허기를 채우려고 들어갔다. 위에 게시되어 있는 주문판을 보고 french fries하고 mango pie를 주문했다.  내 영어가 해독불가능하면 손가락으로 가리켜주면 될 것이라는 예상을 하고 말이다. "one large french fries, one mango pie" 하고 외쳤다. 근데 문제가 one mango pie에서 생겼다.

주문을 받는 알바남이 뭐라 다시 되뭇는다. 내가 손가락으로 가리키면서 저것을 달라고 해도 계속 뭔가를 되묻는다. "excuse me?"를 했다. 뭐라한다. 그러고 나서도 몇번을 excuse me?를 외쳤다. 나중에는 "pardon me?"로 바꿔서 외쳤다. 옆 사람들이 모인다. 결국에 내가 하는 말...

"Please, Don't ask me any more. give me anything! "

뒤에 짐작해 보니, mango pie도 여러 종류의 과일이 섞이게 있는데, 어떤것을 먹을 지 과일들을 열거해 줬던 것 같다.

어쨌든 맛은 좋았다.


내가 이 섬에 대해서 오해한 것이 2가지다. 수상 가옥이 있다는 정보가 있었는데, 잘못된 정보였다는 것이고, 다른 하나는 이 섬에 가면 아무 생각없이 머리를 비우기에 적당하는 것이었다.  맞긴 하다. 내가 있는 곳에서 이곳에 도착하려면 3번을 갈아타서 4~5 시간을 가야했다. 올때도 그랬다. 하루 여행의 9~10시간을 차만 탔다. 자다가 깨면 경치 보고 자다가 깨면 경치보고 잠결에 다른 생각을 할 수 가 없었다. Hundreds Island ! 아무 생각없이 즐기기엔 딱 좋은 섬이다.

Posted by dalbong2

여기는 바기오 !

Others 2009/04/25 19:28

필리핀에 도착한지 한 달정도다. 그동안 절간같은 건물에서 살았다. 시내로 나오려면 큰맘 먹지 않고는 쉽지 않은 곳이었다. 인터넷도 되지 않았다.

새로운 건물로 옮겼다. 이제 무선 인터넷도 된다.

아래 사진은 기존 절간같은 곳에서 내려다본 풍경이다.

매일 점심때즘이면 안개가 적군처럼 쳐들어온다.

 

산 중턱에 있는 집에서 정상까지 쉽게 올라갈 수 있는 길이 나 있다.


몇달해서 말하고 듣기가 될 지는 모르겠다. 지금은 가능성을 타진해 보고 있다.


여전히 관리자 모드로는 들어가지 못하고 있다. 그래도 Windows Live Writer를 이용하면 포스팅은 가능하다.

Posted by dalbong2

어학연수 준비중

Others 2009/04/25 19:25

몇 달간 해외 어학 연수를 하기로 했다.

선수금을 지불하고 등록 절차중이다.

필리핀의 바기오라는 지방인데 좀 외진 곳인듯하다. 이곳의 "더유닛"이라는 학원에 등록했다. 

여기서 혼자서도 할 수 있는 자생력 정도까지만 키워올 생각이다.


또 연재를 "막무가내"로 종료해야 할 것 같다.

아직 Spring.NET에 대해서 할 것이 남았는데..쓰으.

흑흑 또 뒷심 부족이다.


▶ IoC 컨테이너에 대해 좀 더

지금까지의 Configuration 설정에 의해서 컨테이너에 생성된 객체들은 Singleton 객체들이었다. 즉 애플리케이션 실행동안 하나의 각 타입별로 하나의 객체만 생성된다. 그러나 때로는 singleton 타입이 아닌 객체(prototype 객체)로 설정할 수도 있다.

ProxyFactoryObject 속성

- IsSingleton

- TargetName. p.139

▶ Attribute를 이용한 AOP 프로그래밍

▶ Expression evaluaion 프레임워크

- Spring.Expressions

▶ Validation 프레임워크 - Spring.Validation

▶ NHibernate.net

▶ iBatis.NET

▶ Ajax 지원

▶ Spring.NET MVC 패턴 애플리케이션

▶ 예외 처리

▶ Localization & Message Source

▶ 실제 적용 전략

- 스마트클라이언트, 웹애플리케이션

- 실제로 기업형 애플리케이션에 적용하기 위한 전략 및 예제를 만들어본다. Good!!


돌아와서 할 수 있게 되길 바란다.

Posted by dalbong2

▶ Result Mapping 이란.


Result Mapping ! 이것이 뭐냐면 "애플리케이션의 흐름"을 제어하는 방법중의 하나다. 애플리케이션 흐름? Response.Redirect, Server.Transfer 등이 바로 애플리케이션의 흐름을 제어하는 메소드들이다.  여기서 말하는 애플리케이션의 흐름은 페이지의 수행 결과에 따라서 이 페이지 저 페이지로 리다이렉트되는 것을 말하고 있다. Spring.NET에서는 이렇게 결과에 따라서 적절한 페이지로 리다이렉트될 수 있도록 사전에 매핑을 설정할 수 있는 방법이 있다.

페이지 수행 결과 리다이렉트될 대상 페이지
"SUCCESS" OK.aspx
"FAIL" Sorry.aspx

Spring.NET에서는 이런 설정을 configuration에 포함시킬 수 있다는 것이다. 그리고 실제의 aspx.cs에서는 Response.Redirect, Server.Transfer 메소드를 없앤다. Spring.NET이 지원하는 Result Mapping 방법을 이용하면 Controller가 직접 다른 View를 참조할 필요가 없다. 예를 들어 만약 수행 결과가 "SUCCESS" 인 경우 "success.aspx" 페이지로 리다이렉트되어야 한다면 OK.apsx로 설정되어 있는 내용을 success.aspx로 변경만 하면 된다. success.aspx로 넘겨야 하는 파라미터가 있다면 그 값도 configuration 할 수 있다.


▶ 기존 방법의 MVC 패턴 위배


aspx.cs에서 이런 메소드를 사용해서 직접 개발자가 페이지를 대상 페이지로 리다이렉트시키는 방법은 앞에서 말한 MVC 패턴에도 맞지 않는 부분이 있다. 앞에서 말한 MVC 패턴에서는 개발 작업에서 UI를 비즈니스 로직에서 분리시키는 것이 중요한 목표였다. 그러나  Controller 즉 Page 객체에서 직접 Redirect, Transfer 메소드를 사용하면 다음과 같은 상태가 되고 만다.

이런 상태가 되면 애플리케이션의 흐름이 변경된 경우 재 컴파일이 필요하고 필요하면 테스트, 배포 작업도 다시 해야 할 수 있다. 


▶ 수행 결과 추상화 -  Result  클래스


Spring.NET에서는 페이지의 수행 결과를 Spring.Web.Support 네임스페이스의 Result 타입으로 추상화하고 있다. 예를 들어 페이지 수행 결과 "SUCCESS", "FAIL"을 표현할 수 있는 객체이다.

다음과 같은 시나리오를 생각해보자. 사용자 등록 페이지 UserRegistration.aspx가 있다고 하자. 이 페이지에서 저장 버튼을 클릭하면 사용자 정보를 저장하고 나서 홈 페이지 Default.aspx로 리다이렉트되고 그리고 사용자 등록 페이지에서 취소 버튼을 클릭하면 로그인 페이지 login.aspx로 이동한다고 하자.  여기서 "홈 페이지로 이동"하고 "로그인 페이지로 이동"하는 것을 Spring.Web.Result로 표현하면 아래에서 첫번째, 두번째 <object> 설정과 같다. 

<object id="homePageResult" type="Spring.Web.Result, Spring.Web">

  <property name="TargetPage" value="~/Default.aspx"/>

  <property name="Mode" value="Transfer" />

  <property name="Parameters">

    <dictionary>

      <entry key="literal" value="My Text"/>

      <entry key="name" value="%{UserInfo.FullName}" />

      <entry key="host" value="%{Request.UserHostName}"/>

    </dictionary>

  </property>

</object>


<object id="loginPageResult" type="Spring.Web.Result, Spring.Web">

  <property name="TargetPage" value="Login.aspx"/>

  <property name="Mode" value="Transfer" />

</object>


<object type="UserRegistration.aspx" parent="basePage">

  <property name="UserManager" ref="userManager"/>

  <property name="Results">

    <dictionary>

      <entry key="SUCCESS" value-ref="homePageResult"/>

      <entry key="CANCEL" value-ref="loginPageResult"/>

    </dictionary>

  </property>

</object>

각 Result 타입에는 해당 결과로 인해 리다이렉트될 대상 페이지를 나타내는 TargetPage 속성이 있다.  위 설정에서 "homePageResult"라는 결과가 사용될때는 "~/Default.aspx"로 리다이렉트되고 "loginPageResult" 결과가 사용될때는 "Login.aspx"로 이동한다. 그리고 Result 타입에는 Mode 속성이 있다. 이 속성은 리다이렉트 방법을 나타낸다. 이 속성의 값으로는 "Transfer", "TransferNoPreserve", "Redirect"가 있는데 설정하지 않으면 "Transfer"가 사용된다. "TransferNoPreserve"는 Tranfer 메소드에서 "preserveForm=false"와 같다.

public void Transfer(
   string path,
   bool preserveForm
);

preserveForm 인자가 false이면 QueryStringForm 컬렉션 데이터는 지워진다.

세번째 <object/> 요소에서는 "UserRegistration.aspx" 페이지에서 이 Result 객체를 참조해서 사용하고 있다는 것을 설정하고 있다. 즉 UserRegistration.aspx의 실행 결과가 "SUCCESS"로 설정되는 경우는 "homePageResult"에서 정의한대로 리다이렉트로되고 결과가 "CANCEL"로 설정되는 경우는 "loginPageResult"에서 설정한대로 리다이렉트된다.

만약 리다이렉트될 대상 페이지에 파라미터를 넘기고 싶다면, Result 타입의 딕션너리 속성 Parameters을 사용한다. 이 속성에 <entry/> 요소를 파라미터가 필요한대로 포함시킨다. homePageResult 결과 설정예를 보면, "literal", "name", "host" 파라미터가 추가되어 있다. "literal" 속성처럼 리터럴 문자열이 파라미터 값으로 사용될 수도 있지만, 호출하는 페이지의 속성 값을 동적으로 설정할 수도 있다. "name", "host" 파라미터에는 현재 호출하는 페이지 즉 UserRegisteration 페이지의 UserInfo 속성의 FullName 속성값이 설정된다. 현재 호출하는 페이지를 나타내는 Page 객체에 FullName 속성을 갖는 UserInfo 속성이 public 속성으로 노출되어야 한다는 것을 말한다. 참고로 이때  "%{....}" 내부의 문자열을 해석하는 expression evaluation 프레임워크가 사용된다.

이렇게 추가된 파라미터들은 Result의 속성 Mode에 따라서 대상 페이지로 다르게 전달된다. Mode를 redirect로 설정하면 모든 파라미터들은 문자열로 변환되어 쿼리 문자열에 추가된다. 반면 transfer로 설정되면 HttpContext.items에 추가되어 대상 페이지로 전달된다.

UserRegistration.aspx 페이지에서 결과를 설정하는 코드 예를 보면 다음과 유사하게 된다.

protected override void OnInit(EventArgs e)

{

    //...

    this.saveButton.Click += new EventHandler(this.SaveUser);

    this.cancelButton.Click += new EventHandler( this.Cancel );

    //...

}


private void SaveUser(Object sender, EventArgs e)

{

    UserManager.SaveUser(UserInfo);

    SetResult("SUCCESS");

}


private void Cancel(Object sender, EventArgs e)

{

    SetResult("CANCEL");

}

Spring.Web.UI.Page에서 제공하는 SetResult 메소드를 사용하면 현재 페이지의 결과를 지정할 수 있다. 이제 이렇게 지정된 결과와 설정에 따라서 적절한 페이지로 리다이렉트된다.


정리를 하자면, Response.Redirect("~/Default.aspx") 대신에 SetResult("SUCCESS")를 사용한다는 것이다. 그래서 음....UI와 비즈니스 로직 분리,  MVC 패턴 준수 뭐 그렇다는 것이다.

이상!

Posted by dalbong2

Spring.NET의 트랜잭션을 알아볼까 NHibernate를 설명해볼까 했었다. 일단 둘 다 개념은 어느 정도 알겠는데 막상 글로 옮기려다 보니 쉽게 정리가 안된다. 해서 또 뒤로 미루겠다. 대신 이번에는 UI 프레임워크에 대해서 알아보도록 하겠다.

여기서 말하는 UI 프레임워크란 Spring.NET이 지원하고 있는 MVC 패턴을 말한다. 패턴을 공부하다보면 주로 제일 먼저 나오는 패턴중의 하나이다. 달봉이도 자바쪽 프로그래밍에 대해서는 잘 모르지만 이야기를 들어보면 자바쪽 웹 프로그램쪽에서는 MVC 패턴에 기반한 프로그래밍이 예전부터 이뤄지고 있다고 한다. 그래서 많은 개발자가 처음 프로그래밍을 배우면서부터 자연스럽게 이 패턴에 익숙해진다는 것이다.

우선 많은 사람들이 MVC 패턴에 대해서 들어봤겠지만 한번 더 간단히 정리해보고 가자. 상세히는 하지 않겠다. 왜? 말빨을 지원해줄만한 지식이 딸린다.  일단 많이 본 그림을 다시 보자.

패턴 공부를 시작한 사람들이라면 많이 봤을 그림이다. 그렇지만 좀 시간만 지나면 다시 까먹는다. 그림이 뭘 말하는지 까먹고 또 까먹고. 다시 볼때마다 네모와 화살표만 보인다-_-;; 달봉이도 그랬다. 현실적으로 이 패턴을 활용할 기회가 없었기 때문이다. 

이 패턴에 대한 좀 더 아카데믹한 설명은 다른 전문 패턴 설명 문서를 참조하기를 바란다. 달봉이의 추측성 설명보다는 그쪽이 더 바람직할 것이다. 다만 여기서는 이 패턴이 ASP.NET으로 어떻게 구현될 수 있는지 HOW-TO 위주로 알아본다. 다음 그림은 이 패턴의 각 요소에 대응되는 ASP.NET 웹 애플리케이션의 요소이다.

아직 다는 이해가 되지 않지만 익숙한 aspx, aspx.cs를 보니 쪼옴 숨이 트일 것이다. "애플리케이션 도메인 객체"라는 새로운 요소가 나타나 있다. MVC 패턴의 Model에 해당하는 것이 "애플리케이션 도메인 객체"로 되어 있다. 기존의 .NET 애플리케이션에서는 데이터 액세스 레이어, 비즈니스 레이어 그리고 UI 레이어간에 데이터를 전달할때 흔히 Dataset 객체를 사용하는 경우가 흔했다. 이런 구조에 익숙한 사람이라면 "애플리케이션 도메인 객체"라는 용어를 들어볼 기회가 별로 없었을 듯 싶다. 

사용자 정보를 관리하는 페이지를 예로 해서 MVC 패턴을 좀 더 이해해보도록 하자. MVC 패턴에 맞게 구성하면 다음과 유사하게 될 것이다. "사용자 정보"라는 사용자 정의 객체가 필요한데, 이것이 Model 객체가 된다.  이 Model 객체는 사용자에 대한 정보를 가지고 있게 된다. 이 "사용자 정보" 객체는 사용자에 대한 정보( 사원번호, 이름, 현재 부서 등 )를 간직한다.  이 객체는 그림에서처럼 Controller인 Page 객체로부터 상태 변경 요청을 받기도 하고 View로 부터 상태 조회 요청을 받기도 한다.

aspx는 특정 시점의 사용자 정보 즉 실제 Model 객체의 상태를 보여주는 View 를 제공한다.

Controller인 Page 객체는 사용자가 View를 통해 입력한 Model 객체에 대한 정보를 HTTP로 받아서 해석한다. 그런 다음 Model 객체로 전달해서 상태 변경을 요청한다. 또는 Model 객체에서 변경된 상태를 View에 보내서 반영을 요청하기도 한다.

Controller는 도메인 객체의 현재 상태를 조회해서 UI의 컨트롤에 출력을 요청하기도 한다. 그러나 상태를 UI에 출력하기 위해서는 그림에서처럼 때로는 View에서 직접 도메인 객체에 접근해서 그 상태롤 요청하는 경우도 있을 수 있다. 뒤의 샘플 코드에서 보겠지만 미리 살짝 언급하면 사용자가 입력한 정보를 Model 객체에 반영하고 Model 객체의 상태를 UI에 출력하는 방법으로 데이터 바인딩 기술이 사용될 수 있다.  

참조를 나타내는 화살표 방향을 이해해 두는 것도 중요할 듯 싶다.  달봉이가 이해한 대로 그렸지만 용어나 그림이 정확한지는 달봉이도 잘 모르겠다.

아직 이 구조가 몸에 착 달라붙지는 않을 것이다. 샘플 코드를 보자. 다음에 보여주는 코드를 통해서는 MVC  패턴이 실제로 어떻게 구현되는지 그 구조 이해에 중점을 두도록 한다. 화살표가 제대로 구현되고 있는지 확인해 보도록 한다. 이 샘플 코드는 Spring.NET의 소스 코드와 함께 제공되는 샘플 프로젝트중에서 SpringAir.Web.2005를 참조하고 있다.

다음 샘플 코드에서 보여줄 페이지에서는 사용자가 비행기 예약을 하고 취소할 수 있는 페이지이다. 이 페이지를 위한 Model 객체가 어떻게 정의되어 있는지 먼저 보자. 


▶ Model - Trip 객체


namespace SpringAir.Domain

{

    [Serializable]

    public class Trip

    {

        #region Fields


        private TripMode mode = TripMode.RoundTrip;

        private TripPoint startingFrom = new TripPoint();

        private TripPoint returningFrom = new TripPoint();


        #endregion


        #region Constructor (s) / Destructor


        public Trip()

        {

        }


        public Trip(TripMode mode, TripPoint startingFrom, TripPoint returningFrom)

        {

            this.mode = mode;

            this.startingFrom = startingFrom;

            this.returningFrom = returningFrom;

        }


        #endregion


        #region Properties


        public TripMode Mode

        {

            get { return this.mode; }

            set { this.mode = value; }

        }


        public TripPoint StartingFrom

        {

            get { return this.startingFrom; }

            set { this.startingFrom = value; }

        }


        public TripPoint ReturningFrom

        {

            get { return this.returningFrom; }

            set { this.returningFrom = value; }

        }


        #endregion


        /// <summary>

        /// Returns a <see cref="System.String"/> representation of this

        /// <see cref="SpringAir.Domain.Trip"/>.

        /// </summary>

        /// <returns>

        /// A <see cref="System.String"/> representation of this

        /// <see cref="SpringAir.Domain.Trip"/>.

        /// </returns>

        public override string ToString()

        {

            StringBuilder buffer = new StringBuilder();

            buffer

                .Append(Mode).Append(", from ")

                .Append(StartingFrom).Append(" to ")

                .Append(ReturningFrom);

            return buffer.ToString();

        }

    }

}

코드를 보면 알겠지만, Trip 클래스는 출발지와 반환지를 표현하기 위해서 TripPoint 타입을 사용해서 StartingFrom, ReturningFrom 속성으로 노출하고 있다. 그리고 그 여행이 편도인지 왕복인지를 나타내기 위해서 TripMode 타입의 Mode 속성을 노출하고 있다.

aspx.cs에서 DataSet 객체를 구성해서 바로 비즈니스 레이어 객체를 호출해서 넘기는 방식의 코딩에 익숙한 대부분의 ASP.NET 웹 애플리케이션 개발자들에게는 이런 Model 객체가 익숙하지 않을 것이다. 그러나 MVC 패턴에서는 조회나 수정에서 이런 Model 객체를 사용하게 된다. 따라서 비즈니스 설계 또한 필요하다면 이 Model 객체를 도출할 수 있도록 수정될 필요도 있을 것이다.

이제 특정 시점에서의 이 Model 객체의 정보(상태)를 출력하는 UI를 보도록 하자.


▶ View - TripForm.aspx


<asp:Content ID="body" ContentPlaceHolderID="body" runat="server">

  <div style="text-align: center">

    <h4>

      <asp:Label ID="caption" runat="server"></asp:Label>

    </h4>

    <spring:ValidationSummary ID="validationSummary" runat="server" />

    <table>

      <tr class="formLabel">

        <td>&nbsp;</td>

        <td colspan="3">

          <spring:RadioButtonGroup ID="tripMode" runat="server">

            <asp:RadioButton ID="OneWay" onclick="showReturnCalendar(false);" runat="server" />

            <asp:RadioButton ID="RoundTrip" onclick="showReturnCalendar(true);" runat="server" />

          </spring:RadioButtonGroup>

        </td>

      </tr>

      <tr>

        <td class="formLabel" align="right">

          <asp:Label ID="leavingFrom" runat="server" />

        </td>

        <td nowrap="nowrap">

          <asp:DropDownList ID="leavingFromAirportCode" runat="server" />

          <spring:ValidationError id="departureAirportErrors" runat="server" />

        </td>

        <td class="formLabel" align="right">

          <asp:Label ID="goingTo" runat="server" />

        </td>

        <td nowrap="nowrap">

          <asp:DropDownList ID="goingToAirportCode" runat="server" />

          <spring:ValidationError id="destinationAirportErrors" runat="server" />

        </td>

      </tr>

      <tr>

        <td class="formLabel" align="right">

          <asp:Label ID="leavingOn" runat="server" />

        </td>

        <td nowrap="nowrap">

          <spring:Calendar ID="leavingFromDate" runat="server" Width="75px" AllowEditing="true" Skin="system" />

          <spring:ValidationError id="departureDateErrors" runat="server" />

        </td>

        <td class="formLabel" align="right">

          <asp:Label ID="returningOn" runat="server" />

        </td>

        <td nowrap="nowrap">

          <div id="returningOnCalendar">

            <spring:Calendar ID="returningOnDate" runat="server" Width="75px" AllowEditing="true" Skin="system" />

            <spring:ValidationError id="returnDateErrors" runat="server" />

          </div>

        </td>

      </tr>

      <tr>

        <td class="buttonBar" colspan="4">

          <br/>

          <asp:Button ID="findFlights" OnClick="SearchForFlights" runat="server"/>

        </td>

      </tr>

    </table>

텍스트가 없는 몇개의 레이블 컨트롤이 있다. 텍스트값은 런타임시에 할당된다. 이것은 애플리케이션의 지역화와 관계된 것으로서 지금의 MVC 패턴 설명에서는 별로 중요하지 않은 부분이다. 중요한 것은 UI에 입력 컨트롤들이 있다는 것이다 .  라디오 버튼 그룹 컨트롤인 tripMode, 드롭 다운 리스트 컨트롤인 leavingFromAirportCode, goingToAirportCode 그리고 달력 컨트롤인 departureDate, returnDate 등이 배치되어 있다. 어떤 것은 ASP.NET에서 제공하는 표준 컨트롤이고 어떤 것은 Spring에서 제공하는 커스터마이징 컨트롤이다. 이 컨트롤들은 Model 객체의 상태값을 출력하게 될 것이다. 그 출력은 MVC의 Controller가 담당한다고 했다. ASP.NET에서는 코드 비하인드 페이지의 Page 객체가 담당하게 된다.

이제 이 Model 객체의 상태값을 UI 컨트롤에 출력하는 Page 객체를 보도록 하자.


▶ Controller 객체 - TripForm 페이지 객체


public partial class TripForm : Page

{

    #region Fields


    private const string DisplaySuggestedFlights = "displaySuggestedFlights";


    private IBookingAgent bookingAgent;

    private IAirportDao airportDao;

    private Trip trip;

    #endregion


    #region Properties


    /// Biz 레이어의 객체로서 Spring IoC 컨테이너에 의해서 페이지 객체에 injected된다.

    public IBookingAgent BookingAgent

    {

        set { bookingAgent = value; }

    }


    /// Dao 레이어의 객체로서 Spring IoC 컨테이너에 의해서 페이지 객체에 injected된다.

    public IAirportDao AirportDao

    {

        set { airportDao = value; }

    }

    /// 이 도메인 객체의 상태는 정의한 바인딩 규칙에 따라 UI의 컨트롤이 제공하는 값으로 채워진다. 

    public Trip Trip

    {

        get { return trip; }

        set { trip = value; }

    }



    #endregion


    #region Model Management and Data Binding Methods


    //--> 베이스 페이지의 Init 이벤트에서 포스트백이 아닌 경우 호출된다.

    protected override void InitializeModel()  

    {

        trip = new Trip();

        trip.Mode = TripMode.RoundTrip;

        trip.StartingFrom.Date = DateTime.Today;

        trip.ReturningFrom.Date = DateTime.Today.AddDays(1);

    }


    //--> 베이스 페이지의 Init 이벤트에서 포스트백인 경우 호출된다.

    protected override void LoadModel(object savedModel)

    {

        trip = (Trip)savedModel;

    }


    // --> 베이스 페이지의 PreRender 이벤트에서 호출된다.

    protected override object SaveModel()

    {

        return trip;

    }



    //--> 베이스 페이지의 Init 이벤트에서 포스트백인 경우 호출된다.

    //--> InitializeModel()보다 먼저 호출된다.

    protected override void InitializeDataBindings()

    {

        BindingManager.AddBinding("tripMode.Value", "Trip.Mode");

        BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode");

        BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode");

        BindingManager.AddBinding("leavingFromDate.SelectedDate", "Trip.StartingFrom.Date");

        BindingManager.AddBinding("returningOnDate.SelectedDate", "Trip.ReturningFrom.Date");

    }


    #endregion


    #region Page Lifecycle Methods


    protected override void OnInitializeControls(EventArgs e)

    {

        if (!IsPostBack)

        {

            BindAirportDropdowns();

        }

    }



    /// 페이지가 로딩되면서, 출발지, 도착지를 나타내는 드롭다운 컨트롤이 채워진다.

    private void BindAirportDropdowns()

    {

        ArrayList airportList = new ArrayList();

        airportList.Add(new Airport(0, string.Empty, string.Empty, "-- " + GetMessage("selectAirport") + " --"));

        airportList.AddRange(airportDao.GetAllAirports());


        leavingFromAirportCode.DataSource = airportList;

        leavingFromAirportCode.DataTextField = "Description";

        leavingFromAirportCode.DataValueField = "Code";

        leavingFromAirportCode.DataBind();


        goingToAirportCode.DataSource = airportList;

        goingToAirportCode.DataTextField = "Description";

        goingToAirportCode.DataValueField = "Code";

        goingToAirportCode.DataBind();

    }

    #endregion


    #region Controller Methods


    protected void SearchForFlights(object sender, EventArgs e)

    {

        if (Validate(trip, tripValidator))

        {

            FlightSuggestions suggestions = this.bookingAgent.SuggestFlights(Trip);

            if (suggestions.HasOutboundFlights)

            {

                Session[Constants.SuggestedFlightsKey] = suggestions;

                SetResult(DisplaySuggestedFlights);

            }

        }

    }


    #endregion

}

이 코드에서 어떤 일이 일어나는지 차례대로 정리해보자.

1. 페이지가 처음 로딩될때( IsPostback == false )부터 보자. InitializeModel 메소드는  페이지가 처음 로딩될때만 호출된다. 그 메소드에서는 Trip객체가 생성되고 기본 속성값으로 상태가 세팅된다.  페이지가 렌더링되기직전 즉 베이스 페이지의 PreRender 이벤트에서 SaveModel 메소드가 호출되는데 이때 Trip 객체가 베이스 클래스로 반환되어 HTTP 세션에 캐싱된다.

2. 그런 다음 페이지가 포스트백될때 LoadModel 메소드가 호출되는데 이때 베이스 클래스에서는 앞에서 HTTP 세션에 저장한 Trip 객체을 복원해서 LoadModel의 인자로 넘겨준다.

샘플 페이지에서는 Model이 Trip 객체하나로 구성되어 있지만 실제로는 여러개의 Model 객체로 구성된 딕션너리가 SaveModel 메소드에서 저장되고 LoadModel 메소드에서 복원될 것이다.

3. InitailizeDatabindings 메소드에서는 View의 컨트롤과 Model 객체의 속성들간의 바인딩 규칙을 지정하고 있다. 이 메소드는 페이지가 처음 호출될때 호출되어 바인딩 규칙을 구성하여 캐싱하고 이후부터는 캐싱된 결과를 이용한다. 바인딩 규칙을 추가하기 위해서 BindingManager 속성의 AddBinding 메소드를 사용하고 있는데, 넘겨지는 인자들이 모두 문자열로 되어 있다. 이 문자열들은 컨트롤의 속성, 적절한 Model 객체의 속성으로 파싱된다. 이때 Spring.NET의 Expression Language를 사용하게 된다. 그 파싱 규칙도 이해해 둘 필요가 있을 것이다. 이 규칙을 이해하면 폼위의 컨트롤과 Model 객체외에도 바인딩의 대상을 넓혀 좀 더 유용하게 응용할 수 있을 것이다.

4. 폼 위의 버튼에 대한 핸들러로 SearchForFlights 메소드가 정의되어 있다. 이 메소드를 보면 View의 컨트롤에 대한 참조가 전혀 없다. 이 메소드에서는 injected된 서비스 레이어의 BookingAgent 객체와 Model 객체 trip만을 사용하고 있다. 이곳에서 만약 서비스 레이어의 객체를 호출한 결과를 이용해서 Model 객체 trip의 상태를 변경하면 자동으로 View의 컨트롤의 상태 출력도 변경되어 있을 것이다.


샘플 페이지에서 MVC 패턴을 구현해서 controller 객체 즉 TripForm에서 View쪽의 컨트롤에 대한 참조를 없애는 것이었다. 즉 View와 Controller를 디커플링시키는 것이다. 비즈니스 로직이 포함될 수 있는 Controller쪽에서는 View측의 어떠한 컨트롤에 대한 참조도 없기때문에 좀 더 자유롭게 Controller쪽에 있을 지도 모르는 비즈니스 관련 코드를 좀 더 자유롭게 수정할 수 있게 된다. 이런 MVC 패턴 구현이 가능하게 된 것은 Spring.NET의 웹 프레임워크에서 제공하는 바인딩 기술때문이라는 것을 마지막으로 지적하고 싶다.

Controller 역할을 다시 생각해보자. 사용자로부터 입력된 정보를 Model 객체에 반영하고 Model 객체의 변경된 상태를 View에 출력하도록 요청한다고 했다.  TriplForm 객체는 이런 일을 앞에서 말한 바인딩 기술로 구현하고 있다.  즉 View와 Model의 상태 동기화를 바인딩 기술을 이용하고 있다.

앞의 TripForm 객체가 상속받고 있는 Page는 ASP.NET에서 제공하는 표준 객체가 아니다. Spring.Web.UI에 포함된 객체로서 Spring에서 표준 페이지 객체를 상속해서 확장한 객체이다. 이 객체에서는 BindingManager라는 속성을 노출시키고 있는데 이 속성이 반환하는 객체가 "바인딩 객체"로서 View와 Model 객체간의 바인딩을 관리한다. 이 바인딩 관리자는 컨트롤과 Model 객체의 속성간의 바인딩 규칙을 개발자로부터 입력받아야 한다. 앞의 코드중에서 바인딩 규칙을 제공하는 부분을 다시 보면 다음과 같다. 페이지가 로딩되면서 다음과 같은 코드가 실행되어(처음 로딩될때. 포스트백시에는 실행되지 않는다) 바인딩 규칙을 바인드 관리자에게 알려준다.

   protected override void InitializeDataBindings()

    {

        BindingManager.AddBinding("tripMode.Value", "Trip.Mode");

        BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode");

        BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode");

        BindingManager.AddBinding("leavingFromDate.SelectedDate", "Trip.StartingFrom.Date");

        BindingManager.AddBinding("returningOnDate.SelectedDate", "Trip.ReturningFrom.Date");

    }

바인딩 관리자 및 바인딩에 대한 자세한 내용은 다음에 기회대는 대로 알아보도록 하겠다.

MVC 패턴에서 Controller가 하는 역할 즉 사용자가 입력한 정보를 Model객체에 반영하고 Model객체의 변경된 상태를 View에 반영하는 역할을 Spring.NET이 제공하는 페이지의 바인딩 관리자가 담당하고 있는 것이다. 바인딩 관리자는 View와 Model에 있는 객체를 직접 참조하는 대신에 Expression Evaluation 프레임워크( 레퍼런스 문서 11장)를 통해서 양쪽의 속성을 연결하는 것이다. Spring.NET의 Expression Evaluation 프레임워크가 MVC 패턴 구현에 핵심 역할을 하고 있다는 것을 마지막으로 지적하고 싶다. 앞에서도 말했듯이 이 Expression Evaluation 프레임워크를 좀 더 이해하는 것이 필요할 것으로 보인다. 또한 바인딩의 유효성 검사를 위해서 Validation 프레임워크( 레퍼런스 문서 12장)를 사용하고 있는데 이 또한 공부거리로 보인다.


▶  Spring.NET의 MVC 패턴 지원


View와 Controller는 모두 Model에 의존하고 있지만, Model은 View와 Controller 어떤 것도 참조하지 않고 있다는 것을 알아차리는 것이 중요하다고 보여진다. View, Controller와 Model의 분리는 UI 상관없이 Model의 테스트가 자유롭게 이뤄질 수 있다는 것이다. 또한 View와 Controller의 분리 또한 중요한 이점중의 하나이다.

이 두 이점은 모두, MVC 패턴을 이용하면 비즈니스 로직에서 UI를 분리할 수 있다는 장점을 제공할 수 있다는 것이다.

[ 추가 내용 ]

Spring.NET이 MVC 패턴을 지원하는 것은 결국 이렇게 UI와 비즈니스 로직을 분리할 수 있는 기반을 제공하고 있다는 것을 말한다.


ASP.NET 팀에서는 현재 ASP.NET MVC 프레임워크를 제작했다. 그리고 그 프레임워크에 대한 소스 코드도 제공되고 있다.  코드플렉스 사이트에서 ASP.NET MVC Preview 2 소스를 받아볼 수 있다. 코드는 Visual Studio 2008용 솔루션 파일로 묶여져 있다. 앞으로는 ASP.NET에서도 MVC 개발 패턴에 대한 적극적 지원이 있지 않겠냐는 생각이다. 해서, 이즈음해서는 애플리케이션 개발자라면 MVC 패턴의 개념 정도는 이해하고 있을 필요가 있겠다 하겠다.

Posted by dalbong2