'개발/개발프레임워크'에 해당되는 글 48건

  1. 2010/03/08 Spring.NET 개발 가이드 by dalbong2 (2)
  2. 2009/08/08 개발 프레임워크 만들기 대장정 47 - UI단 프레임워크 추가사항 by dalbong2
  3. 2009/07/20 개발 프레임워크 만들기 대장정 46 - 서비스 호출하기 by dalbong2
  4. 2009/07/20 개발 프레임워크 만들기 대장정 45 - 서비스 구성하기 by dalbong2
  5. 2009/07/20 개발 프레임워크 만들기 대장정 44 - WCF 확장하기 III ( 프로그레스바 자동 보여주기) by dalbong2
  6. 2009/07/12 개발 프레임워크 만들기 대장정 43 - WCF 확장하기 II (사용자정보 전달하기) by dalbong2
  7. 2009/07/08 개발 프레임워크 만들기 대장정 42 - WCF 확장하기 I (사용자 정보 받기 ) by dalbong2
  8. 2009/07/01 개발 프레임워크 만들기 대장정 41 - 화면 객체 로딩 테스트 by dalbong2
  9. 2009/06/19 개발 프레임워크 만들기 대장정 40 - 메뉴정보 로딩/출력하기 by dalbong2
  10. 2009/06/18 개발 프레임워크 만들기 대장정 39 - 화면 객체 생성 by dalbong2
  11. 2009/06/18 개발 프레임워크 만들기 대장정 38 - 화면 정보 로딩 by dalbong2
  12. 2009/06/07 개발 프레임워크 만들기 대장정 37 - POC 애플리케이션 - 개발구조 by dalbong2
  13. 2009/06/06 개발 프레임워크 만들기 대장정 36 - 개발 프레임워크 보안 설계 by dalbong2
  14. 2009/05/24 개발 프레임워크 만들기 대장정 35 - Spring.NET 트랜잭션 관리 by dalbong2 (2)
  15. 2009/05/18 개발 프레임워크 만들기 대장정 34 - Spring.NET 트랜잭션 관리(Strategy 패턴) by dalbong2
  16. 2009/04/24 개발 프레임워크 만들기 대장정 33 - Spring.NET의 Result Mapping by dalbong2
  17. 2009/04/24 개발 프레임워크 만들기 대장정 32 - Spring.NET의 MVC 패턴 지원 by dalbong2
  18. 2009/04/24 개발 프레임워크 만들기 대장정 31 - Spring.NET의 데이터 액세스 III by dalbong2
  19. 2009/04/24 개발 프레임워크 만들기 대장정 30 - Spring.NET의 데이터 액세스 II by dalbong2
  20. 2009/04/24 개발 프레임워크 만들기 대장정 29 - Spring.NET의 데이터 액세스 I by dalbong2
  21. 2009/04/24 개발 프레임워크 만들기 대장정 28 - Spring.NET의 Web Services 지원 by dalbong2
  22. 2009/04/24 개발 프레임워크 만들기 대장정 27 - Spring.NET의 advice 종류와 적용 by dalbong2
  23. 2009/04/24 개발 프레임워크 만들기 대장정 26 - AOP 적용 예제 II by dalbong2
  24. 2009/04/24 개발 프레임워크 만들기 대장정 25 - AOP 적용 예제 I by dalbong2
  25. 2009/04/24 개발 프레임워크 만들기 대장정 24 - Aspect Oriented Programming 개념 II by dalbong2
  26. 2009/04/24 개발 프레임워크 만들기 대장정 23 - DI( Dependencies Injection ) 설정 by dalbong2
  27. 2009/04/24 개발 프레임워크 만들기 대장정 22 - 샘플 프로젝트 및 Spring 컨테이너 by dalbong2
  28. 2009/04/24 개발 프레임워크 만들기 대장정 21 - Aspect Oriented Programming 개념 I by dalbong2
  29. 2009/04/24 개발 프레임워크 만들기 대장정 20 - Spring.NET::IoC by dalbong2
  30. 2009/04/24 개발 프레임워크 만들기 대장정 19 - Spring.NET by dalbong2

오랜만에 포스팅을 한다.

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

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

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


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

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



이제 메뉴를 출력해보자. 그러자면 우선 메뉴 소스가 데이터베이스든 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이 기업형 애플리케이션에 적용되고 있다는 얘기도 가끔 듣고 있다. 그러나 달봉이가 직접 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

▶ 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

지난 포스트에서 말한대로 이번에는 Spring.NET에서 지원하는 OR매핑( Object Relational Mapping) 기능에대해서 알아본다. AdoTemplate의 Execute류의  메소드를 이용하면 CRUD 모두가 가능하다. 그러나 조회의 경우 Spring.NET의 데이터 접근 모듈에서는 좀 더 특별한 API를 제공한다. 지금까지의 개발 방식에서는 보통 조회를 하면 DataSet으로 넘어오고 이것을 그대로 Biz 레이어, UI 레이어로 넘겨서 레코드별로 루프를 돌면서 필요한 데이터를 꺼내서 작업을 했었다. 그러나 Spring.NET에서는 조회된 각 레코드를 사용자 정의 객체와 매핑시킬 수 있는 기회를 제공하고 있다. 예를 들어 여러 건의 사용자 정보 레코드가 조회되었을 경우 하나의 레코드는 하나의 UserInfo 객체로 변환된다. 그래서 Biz 레이어나 UI 레이어로 반환될때는 전체 레코드는 UserInfo 컬렉션으로 변환되어 반환된다.

개발자는 도메인 객체 UserInfo를 정의해야 한다. 도메인 객체는 애플리케이션의 서버측과 클라이언트측 모두에서 참조되어 사용될 수 있다. 따라서 별도의 어셈블리로 분리하여 개발하는 것이 보통이다. 개발자는 또한 레코드의 컬럼과 UserInfo의 속성을 연결시켜주는 매핑 정보를 제공해야 한다.  이런 작업을 OR 매핑( Object Relational Mapping )작업이라고 한다. OR매핑 작업을 좀 더 편하게 할 수 있는 전문적인 OR매핑툴도 있다. 대표적인 것으로 NHibernate라는 것이 있는데 이것은 뒤에 별도로 살펴볼 것이다.

Spring.NET에서는 QueryWith로 시작하는 데이터 접근용 메소드가 많은데 이런 메소드를 이용하면 개발자가 OR 매핑 작업을 할 수 있는 기회를 제공받을 수 있게 된다. OR매핑 작업이 수행되는 구조는 앞에서의 AdoTemplate의 Execute류 메소드를 이용하는 콜백 구조와 유사한 구조를 갖는다.


▶  OR매핑 작업 구조



AdoTemplate객체의 Execute를 사용할때는 DB에 접근하는 작업을 콜백객체의 콜백함수에서 개발자가 직업 수행했었다. 게릿? QueryWith류의 메소드를 사용하면 DB 접근해서 조회하는 작업은 AdoTemplate에서 수행하게 된다. 그리고 Execute 메소드를 사용할때도 직접 콜백함수에서 조회된 테이블의 각 레코드를 사용자 정의 객체로 변환해서 반환하면 된다. 그러나 QueryWith류의 메소드를 이용하면 그런 작업을 좀 더 편하게 할 수 있다. 콜백 함수에서 파라미터로 받는 것은 IDataReader 객체이다. 어떤 타입의 콜백 객체를 사용하느냐에 따라서 IDataReader 객체는 전체 조회결과를 받는냐 아니면 한 레코드씩을 받느냐가 결정된다. 콜백 객체의 타입은 다음 3 종류가 있다.

콜백객체 타입 설명
IResultSetExtrator/ResultSetExtractorDelegate - AdoTemplate으로 부터 조회 결과를 전부 넘겨 받는다. 즉 커서가 처음 위치에 있는 IDataReader 객체를 넘겨받는다.
- 클라이언트 코드로 넘겨줄 사용자 정의 객체를 모두 구성해서 반환해준다. 
- 클라이언트 코드에서는  QueryWith 메소드이 반환값으로 사용자 정의 객체 컬렉션을 받을 수 있다.
IRowCallback / RowCallbackDelegate - AdoTemplate으로 부터 레코드 하나씩을 건네받는다. 즉  커서가 현재 위치로 이동한 상태의 IDataReader 객체를 건네받는다.
- 레코드별 사용자 정의 객체를 콜백 객체에 쌓아둔다.
- 클라이언트 코드에서 나중에 콜백 객체에서 구성해둔 사용자 정의 컬렉션을 가져간다.
IRowMapper / RowMapperDelegate - AdoTemplate으로 부터 레코드 하나씩을 건네받는 것은 이전 로우 콜백 타입과 같다.
- 레코드별 사용자 정의 객체 컬렉션을 AdoTemplate쪽에서 담당한다.
- 클라이언트 코드에서는 QueryWith 메소드이 반환값으로 사용자 정의 객체 컬렉션을 받을 수 있다.

어떤 타입의 콜백 객체를 사용하느냐에 따라서 QueryWith 메소드, 그리고 QueryQith 메소드내에서 호출되는 콜백 함수가 달라진다. 그러나 콜백함수의 인자로 넘어가는 것은 언제나 IDataReader 객체이다. 이 객체도 다시 어떤 타입의 콜백객체를 사용하느냐에 따라서 콜백 객체에서 받는 IDataReader객체의 현재  상태가 달라질 수 있다.

IDataReader 객체는 forward-only 속성이 있다. 즉 테이블의 레코드를 가리키는 현재 커서는 항상 앞으로만 움직일 수 있다. 콜백 객체의 타입은 선정은 이 커서의 위치에 영향을 줄 수 있다. 만약 IResultSetExtractor / ResultSetExtractorDelegate를 선택했다면 현재 커서가 움직이지 않은 상태의 원래의 IDataReader 객체를 넘겨받는다. 콜백 함수에서는 하나씩 앞으로 커서를 움직이면서 OR매핑 작업을 구현해야 한다.  그러나 Row로 시작하는 나머지 두 타입은 콜백 객체에서는 커서가 진행된 IDataReader 객체를 받는다. 레코드 루핑은 AdoTemplate쪽에서 일어난다. 이 경우 콜백 함수에서는 사용자 정의 객체를 하나씩만 생성하면 된다.

그러나 이 경우에도 두 타입에는 차이가 있다. 사용자 정의 객체의 집합을 어디에서 관리하느냐 하는 문제를 다르게 두 타입별로 해결하고 있다. IRowCallback / RowCallbackDelegate 타입은 사용자 정의 객체의 집합을 콜백 객체에서 간직하고 있다. 그래서 클라이언트 코드는 그 집합을 나중에 직접 콜백 객체에서 가져가야 한다. 그러나 IRowMapper / RowMapperDelegate를 사용하면 사용자 정의 객체의 결과 집합을 AdoTemplate에서 간진한다. 콜백 함수에서는 레코드를 받아서 하나씩 사용자 정보 객체를 생성해서 AdoTemplate으로 반환해준다. AdoTemplate에서는 콜백 함수에서 받을 객체를 차곡차곡 쌓아두었다가 QueryWith 메소드가 종료될때 클라이언트 코드로 객체 집합을 반환해준다.

이제 몇가지 콜백 객체 유형별로 개발 샘플 코드를 보도록 하자.


▶ ResultSetExtractor 타입의 콜백 객체 사용


다음은 ResultSetExtrator 타입의 콜백 객체를 사용하는 구조에서의 DAO 객체의 정의 일부이다. ResultSetExtractorDao.cs 페이지에 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    /// <summary>

    /// A simple DAO that uses Generic.AdoTemplate ResultSetExtractor functionality

    /// </summary>

    public class ResultSetExtractorDao : AdoDaoSupport

    {

        ...

        private string customerByCountryAndCityCommandText =

                @"select ContactName from Customers where City = @City and Country = @Country";

        public virtual IList<string> GetCustomerNameByCountryAndCity(string country, string city)

        {

            // note no need to use parameter prefix.


            // This allows the SQL to be changed via external configuration but the parameter setting code

            // can remain the same if no provider specific DbType enumerations are used.


            IDbParameters parameters = CreateDbParameters();

            parameters.AddWithValue("Country", country).DbType = DbType.String;

            parameters.Add("City", DbType.String).Value = city;


            return AdoTemplate.QueryWithResultSetExtractor(CommandType.Text,

                                                           customerByCountryAndCityCommandText,

                                                           new CustomerNameResultSetExtractor<List<string>>(),

                                                           parameters);

        }

        ...

GetCustomerNameByCountryAndCity 메소드는 country와 city 파라미터값을 받아서 해당하는 고객 집합을 IList<string> 타입으로 반환하는 DAO 객체의 메소드이다. 이 메소드 내부에서는 넘겨받은 country, city값을 DB 파라미터로 변환해서 AdoTemplate에 넘겨줄 준비를 하고 있다. DbParameter를 구성하는 코드는 어렵지 않으니 눈치껏 이해하기 바란다. 이제 적절한 QueryWith 메소드를 선택해야 한다.  코드에서는 ResultSetExtractor 타입의 객체를 콜백객체로 받을 수 있는 메소드로서 QueryWithResultSetExtrator 메소드를 사용하고 있다. 넘겨주는 구체적인 콜백 객체는 CustomerNameResultSetExtrator<List<string>> 타입의 객체이다.

콜백 객체는 개발자가 정의해야 하는 타입으로서 샘플에서는 그 구현을 다음처럼 하고 있다. 이 콜백 객체도 같은 페이지에 internal로 정의되어 있다.

internal class CustomerNameResultSetExtractor<T> : IResultSetExtractor<T> where T : IList<string>, new()

{

    /// <summary>

    /// Implementations must implement this method to process all

    /// result set and rows in the IDataReader.

    /// </summary>

    /// <param name="reader">The IDataReader to extract data from.

    /// Implementations should not close this: it will be closed

    /// by the AdoTemplate.</param>

    /// <returns>An arbitrary result object or null if none.  The

    /// extractor will typically be stateful in the latter case.</returns>

    public T ExtractData(IDataReader reader)

    {

        T customerList = new T();

        while (reader.Read())

        {

            string contactName = reader.GetString(0);

            customerList.Add(contactName);

        }

        return customerList;

    }

}

실제 사용될 구체적인 콜백객체는 IResultSetExtractor<T>를 구현하고 있다. 그리고 그 인페이스에서 정의하고 있는 콜백 함수 ExtractData()를 구현하고 있다. 이 메소드는 AdoTemplate에서 파라미터로 IDataReader 타입의 객체 reader를 받고 있는데 이 객체는 커서가 움직이지 않은 초기상태로 넘어온다. 클라이언트 코드 즉 DAO 객체로 넘겨줄 최종 값은 reader를 이용해서 개발자가 이곳에서 모두 구성해야 한다. 코드에서도 while 문을 돌면서 필요한 반환값을 구성한 다음 반환하고 있다.

이 코드에서는 반활될 값으로 특별히 사용자 정의의 객체를 사용하고 있지는 않다. 그래서 OR 매핑 작업은 구현하고 있지 않다. 그러나 만약 반환값이 IList<string>이 아니라 IList<사용자정의타입>으로 되었다고 하면 while 문을 돌면서 사용자 정의 객체를 생성해서 리스트에 추가하면 된다.

while문에서 통해서 구성된 최종 반환값이 반환되면 이 값이 결국 DAO객체에서 호출을 시작한 메소드의 반환값으로 된다는 것을 알 수 있다. 그래서 Biz 레이어 객체에게로 넘어갈 것이다.


▶ RowCallback 타입의 콜백 객체 사용


이제 RowCallback 타입의 콜백 객체를 사용해서 OR 매핑을 구현하는 구조를 알아보자. 이 타입의 콜백 객체를 사용하면 DAO 객체로 반환될 최종 객체 집합이 사용자 정의의 콜백 객체에 있는 구조가 된다고 했다.  코드를 보자. 우선 DAO객체의 호출 메소드이다. RowCallbackDao.cs 페이지에 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    public class RowCallbackDao : AdoDaoSupport

    {

        private string cmdText = "select ContactName, PostalCode from Customers";


        public virtual IDictionary<string, IList<string>> GetPostalCodeCustomerMapping()

        {

            PostalCodeRowCallback statefullCallback = new PostalCodeRowCallback();

            AdoTemplate.QueryWithRowCallback(CommandType.Text, cmdText,

                                            statefullCallback);


            // Do something with results in stateful callback...

            return statefullCallback.PostalCodeMultimap;

        }

    }

    ...

RowCallbackDao라는 타입의 DAO객체를 정의하고 있다. 이 객체에 GetPostalCodeCustomerMapping() 메소드에서 DB 데이터에 액세스하고 그 결과를 조작하려는 작업을 하고 있다. 우선 RowCallback을 사용하는 OR 매핑 구조에서는 AdoTemplate의 QueryWithRowCallback 메소드를 호출하고 있다. 앞의 코드에서는 DB 접근에 필요한 값이 없어서 DB 파라미터를 구성하는 코드는 없다. QueryWithRowCallback에는 콜백 객체로서 PostalCodeRowCallback 타입의 객체 statefulCallback이 넘겨지고 있다.

이 객체의 구현은 다음과 같다. 같은 페이지에 정의되어 있다.

internal class PostalCodeRowCallback : IRowCallback

{

    private IDictionary<string, IList<string>> postalCodeMultimap =

        new Dictionary<string, IList<string>>();


    public IDictionary<string, IList<string>> PostalCodeMultimap

    {

        get { return postalCodeMultimap; }

    }


    /// <summary>

    /// Implementations must implement this method to process each row of data

    /// in the data reader.

    /// </summary>

    /// <remarks>

    /// This method should not advance the cursor by calling Read()

    /// on IDataReader but only extract the current values.  The

    /// caller does not need to care about closing the reader, command, connection, or

    /// about handling transactions:  this will all be handled by

    /// Spring's AdoTemplate

    /// </remarks>

    /// <param name="reader">An active IDataReader instance</param>

    /// <returns>The result object</returns>

    public void ProcessRow(IDataReader reader)

    {

        string contactName = reader.GetString(0);

        string postalCode = reader.GetString(1);

        IList<string> contactNameList;

        if (postalCodeMultimap.ContainsKey(postalCode))

        {

            contactNameList = postalCodeMultimap[postalCode];

        }

        else

        {

            postalCodeMultimap.Add(postalCode, contactNameList = new List<string>());

        }

        contactNameList.Add(contactName);

    }

}

IRowCallback 인터페이스는 ProcessRow라는 메소드 하나만을 정의하고 있다. 이 메소드가 AdoTemplate의 QueryWithCallback에서 호출하는 콜백함수이다.

이 QueryWithCallback 메소드에서는 DB 조회 결과를 가지고 있으면서 IDataReader 객체의 루프를 돈다. 그래서 커서가 하나씩 앞으로 움직인 상태의 reader 객체를 콜백 함수로 넘겨주는 것이다. 콜백 함수가 AdoTemplate 객체로부터 콜백될때 넘겨받는 IDataReader 타입의 객체 reader의 커서는 이미 필요한 만큼 움직인 상태이다. 따라서 콜백 함수에서는 현재 readerd 객체에서 필요한 필드의 값을 뽑아 사용하면 된다. 이 콜백 메소드는 반환값이 없다. 구성된 값을 반환하는 대신에 로컬 변수인 postalCodeMultimap에 Add 시키고 있다.

즉 결과값을 콜백 객체에서 자체적으로 관리하고 있다. 그런 다음 최종 구성값을 외부에서 접근할 수 있도록 public 속성 PostalCodeMultimap을 통해서 노출시키고 있다. DAO 객체의 GetPostalCodeCustomerMapping() 메소드의 return문을 보면 이곳에서 콜백 객체의 공개 속성에 접근하고 있는 것을 볼 수 있다.

이 구조에서 AdoTemplate는 조회 결과의 루프만 돌면서 콜백 함수에 각 레코드를 건네주기만 하면 된다. 각 레코드의 정보로 반환될 값을 구성하고 관리하는 작업은 모두 콜백 객체에서 담당해야 한다. 이런 구조는 DB에서 조회된 결과에 레코드별로 추가할 가공 작업이 많은 경우 편리할 것이다.


▶ RowMapper 타입의 콜백 객체 사용


다음은 AdoTemplate에서 DB조회 결과에 대한 루핑 작업뿐만 아니라 콜백 객체에서 생성된 결과 집합도 관리하는 구조이다.  콜백 객체에서는 레코드의 값을 이용해서 필요한 작업을 한 후 그 결과값을 AdoTemplate로 반환만 해 주면 된다. RowMapperDao.cs 페이지에 샘플 코드가 있다.

namespace Spring.DataQuickStart.Dao.GenericTemplate

{

    public class RowMapperDao : AdoDaoSupport

    {

        private string cmdText = "select Address, City, CompanyName, ContactName, " +

                            "ContactTitle, Country, Fax, CustomerID, Phone, PostalCode, " +

                            "Region from Customers";



        public virtual IList<Customer> GetCustomersWithDelegate()

        {

            return AdoTemplate.QueryWithRowMapperDelegate<Customer>(CommandType.Text, cmdText,

                        delegate(IDataReader dataReader, int rowNum)

                            {

                                Customer customer = new Customer();

                                customer.Address = dataReader.GetString(0);

                                customer.City = dataReader.GetString(1);

                                customer.CompanyName = dataReader.GetString(2);

                                customer.ContactName = dataReader.GetString(3);

                                customer.ContactTitle = dataReader.GetString(4);   

                                customer.Country = dataReader.GetString(5);

                                customer.Fax = dataReader.GetString(6);

                                customer.Id = dataReader.GetString(7);

                                customer.Phone = dataReader.GetString(8);

                                customer.PostalCode = dataReader.GetString(9);

                                customer.Region = dataReader.GetString(10);

                                return customer;

                            });

        }

    }

}

RowMapperDao라는 DAO 객체를 정의하고 있고 GetCustomersWithDelegate()라는 업무 메소드를 정의하고 있다. 이 메소드에서 DB 작업을 위해서 AdoTemplate을 사용하고 있는데, 조회된 결과를 조작하기 위해서 QueryWithRowMapperDelegate() 메소드를 호출하고 있다. 이 메소드의 인자로 앞 포스트에서 본 것과 유사한 익명 델리게이트 객체를 넘겨주고 있다. 콜백 객체와 콜백 함수 등 구조를 좀 더 명확히 하고 싶다면 익명 델리게이트 대신에 표준 구조로 변환해보길 권한다. 이 작업은 앞 포스트를 참조한다.

RowMapper를 사용하는 콜백 구조에서의 콜백 함수에서는 한 레코드에 대한 정보를 인자로 넘겨진 IDataReader 객체로부터 얻어서 반환에 필요한 값을 구성하고 구성된 값을 자신에게 남겨둘 필요없이 바로 반환해주면 된다. AdoTemplate에서는 반환된 값을 모두 차곡차곡 모아두었다가 클라이언트 코드( DAO 객체)넘겨준다. 


이제 알겠지만, Spring.NET에서 제공하는 방법은 전문적인 OR 매핑 방법은 아니다. 단지 Spring.NET에서 제공하는 방법을 사용하면 개발자가 수동으로 OR 매핑을 할 수 있는 기회를 제공받을 수 있다는 것이다. 개발자가 수동으로 해야 한다는 것은 불편한 일이다. 이렇든 저렇든 Spring.NET이 제공하는 이 3가지 OR 매핑 방법을 사용했을 경우, 혹시라도 DB 테이블의 컬럼이 변경되거나 사용자 정의 객체의 구조가 변경되면 소스 코드를 다시 빌드해야 하는 것은 피할 수 없다. 그러나 이런 불편은 또 프레임워크에서 질색을 하는 단점중의 하나이다.  좀 더 전문적인 OR매핑툴 NHibernate을 사용하면 좀 더 발전된 매핑 작업을 할 수 있지 않을까 기대해본다.  그럼 다음 포스트에서. 아니 모르겠다. 트랜잭션을 먼저 해야 할지. 이것을 먼저 해야 할지. 먼저 준비되는 것부터 하기로 한다.

Posted by dalbong2

앞에서 AdoTemplate을 이용하는 코딩 구조를 알아봤다. 이 포스트에서는 AdoTemplate를 이용해서 DB 데이터에 액세스하는 코드를  살펴본다. Dao 객체, AdoTemplate의 Execute를 호출하기, 이 호출시 콜백 객체( ICommandCallback 객체 또는 CommandDelegate 객체)를 넘겨주기, 콜백객체에서 AdoTemplate에서 넘겨준 command 객체를 이용해서 DB에 접근하기 등의 과정을 염두에 두면서 코드를 따라가 보자. 레이어관점에서 봤을때 DAO객체나 콜백객체 그리고 Spring.NET의 AdoTemplate는 모두 데이터 액세스 레이어에 속하는 객체들이다.


▶  AdoTemplate를 이용하는 샘플 코드


AdoTemplate를 이용할때 개발자가 개발해야 하는 부분은 무엇인가.  비즈니스 설계에 맞게 DAO 객체를 만들어야 하고 그리고 콜백 객체를 만들어야 한다.  Spring.DataQuickStart.2005 샘플 프로젝트에는 샘플 DAO 객체와 콜백 객체가 구현되어 있다.

많은 페이지가 있지만, 그 중에서 CommandCallbackDao.cs 페이지를 보자.  이 페이지에는 CommandCallbackDao 타입의 DAO를 객체를 정의하고 있고 그리고 ICommandCallback, CommandDelegate 타입의 콜백 객체의 사용을 모두 보여주고 있다. 


▶ 콜백객체로 ICommandCallback 객체 사용하기


먼저 콜백객체로 인터페이스 ICommandCallback를 사용하는 경우를 보자.

private class PostalCodeCommandCallback<T> : ICommandCallback<T> where T : ResultObject, new()

{

    private string postalCode;

    public PostalCodeCommandCallback(string postalCode)

    {

        this.postalCode = postalCode;

    }


    public T DoInCommand(DbCommand command)

    {

        T resultObject = new T();


        // 예제에서는 명령문을 지정하는 부분이 빠져 있다. 에러다.

        command.CommandText = cmdText;
        command.CommandType = CommandType.Text; //CommandType.StoredProcedure;

        DbParameter p = command.CreateParameter();

        p.ParameterName = "@PostalCode";

        p.Value = postalCode;

        command.Parameters.Add(p);


        resultObject.count = (int)command.ExecuteScalar();

        return resultObject;

    }

}

제너릭 타입의 ICommandCallback 인터페이스를 상속해서 PostalCodeCommandCallback 타입이 구현되고 있다.  이 구현체의 DoInCommand() 메소드를 AdoTemplate객체의 내부에서 콜백하게 된다.  DoInCommand 메소드 내부네서는 command 객체를 이용해서 실제로 DB 작업을 하게 된다. DbCommand 객체 command에는 이미 DB작업에 필요한 커넥션 정보와 명령( sql, 저장 프로시져 등)이 있다. 마지막으로 DAO 객체에서 넘겨준 값 postalCode를 이용해서 파라미터를 구성해서 command 객체에 넘겨주면 된다. 그 다음 command 객체의 메소드 ExecuteScalar()를 호출해서 실제 DB 작업을 하고 반환값을 받아온다.  참고로 현재 샘플 소스에서는 붉은 색 부분의 코드가 빠져 있다. command 객체에 전달할 명령문cmdText와 명령문의 타입을 지정해줘야 한다. 이 부분을 콜백 객체 외부로부터 전달받던 내부에 하드 코딩하던 이 정보가 있어야 한다.

제너릭 타입 T는 DoInCommand 메소드의 반환값 타입으로 사용되는데,  where 절 부분을 보면 제너릭 타입 T의 구체적인 타입으로 ResultObject 타입을 사용하고 있다.  그리고 그 정의는 다음과 같이 되어 있다. int는 제너릭 타입으로 사용될 수 없기 때문에 이런 래핑 타입이 필요하다.

public class ResultObject

{

    public int count;

}

단순히 int 값을 가지고 있는 타입이긴 하지만 현실의 실제 프로젝트에서의 반환값은 이 보다 더 복잡한 정의가 될 것이다.

이제 정의된 콜백 객체를 이용해서 DAO 객체에서 AdoTemplate 객체를 호출하는 코드를 보자.

public class CommandCallbackDao : AdoDaoSupport

{


    private string cmdText = "select count(*) from Customers where PostalCode = @PostalCode";


    ...


    public virtual int FindCountWithPostalCode(string postalCode)

    {

        // Type inference allows you not to explicitly write .Execute<ResultObject>


        return AdoTemplate.Execute(new PostalCodeCommandCallback<ResultObject>(postalCode)).count;

    }

    ...

DAO 객체 CommandCallbackDao의 FindCountWithPostalCode() 메소드를 보자. AdoTemplate 객체의 Execute() 메소드를 호출하고 있는데 이 AdoTemplate은 CommandCallbakDao 타입이 상속받고 있는 부모 타입 AdoDaoSupport 타입의 속성으로 정의되어 있다.

AdoDaoSupport 타입의 AdoTemplate 속성을 호출하면 AdoTemplate 타입의 객체가 반환된다. AdoDaoSupport 타입의 AdoTemplate 속성에 대한 정의를 보면 다음과 같다.

public class AdoDaoSupport : DaoSupport

{

    private AdoTemplate adoTemplate;

    ...

    public AdoTemplate AdoTemplate

    {

        set

        {

            adoTemplate = value;

        }

        get

        {

            return adoTemplate;

        }


    }

    ...

샘플 프로젝트에서는 AdoTemplate 속성에 인스턴스를 할당하는 작업에 Spring.NET 컨테이너의 Inversion of Control 기능을 사용하고 있다. DAO 객체가 정의되어 있는 Spring.DataQuickStart.2005 프로젝트를 사용하는 클라이언트 프로젝트 Spring.DataQuickStart.Test.2005 프로젝트를 보면 환경 설정 파일이 있다. DataQuickStart/GenericTemplate 폴더에 있는 ExampleTests.xml 파일을 보자. 다음은 그 일부이다.

<object id="commandCallbackDao" type="Spring.DataQuickStart.Dao.GenericTemplate.CommandCallbackDao, Spring.DataQuickStart">

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

</object>

String.DataQuickStart.Dao.GenericTemplate 네임스페이스하의 CommandCallbackDao 객체를 사용할때는 그 속성 AdoTemplate에 "adoTemplate"라는 id로 참조하고 있는 객체를 자동으로 할당하라는 표시이다. 참조하고 있는 객체를 따라가 보면 다음과 같은 정의가 있다.

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

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

  <property name="DataReaderWrapperType" value="Spring.Data.Support.NullMappingDataReader, Spring.Data"/>

</object>

id가 "adoTemplate"로 설정되어 있는 객체에 대한 설정을 보면 Spring.Data.Generic 네임스페이스하의 AdoTemplate에 대한 정의를 나타내고 있다. 이 정의의 DbProvider라는 속성은 다시 "dbProvider"로 정의되어 있는 객체를 참조하고 있다. 역시 AdoTemplate객체가 인스턴스화될때에는 이 id로 정의되어 있는 객체가 자동으로 인스턴스화되어 속성에 할당될 것이다. 참조하고 있는 dbProvider라는 id의 객체 정의를 보면 다음과 같다.

<db:provider id="dbProvider"

              provider="SqlServer-2.0"

              connectionString="Data Source=.\SQL2005;Initial Catalog=Northwind;Persist Security Info=True;User ID=springqa;Password=springqa"/>

provider값으로 "SqlServer-2.0"을 사용하고 있는데 이 값은 .NET V2.0에 있는 MS SQL 서버 , provider v2.0.0.0을 프로바이더로 사용하고 있다는 의미이다. 그리고 이 요소에는 DB 연결정보도 있다. 

Spring.NET 컨테이너가 DAO 객체 CommandCallbackDao 객체의 인스턴스를 생성하면 결국 DB에 연결하기 위해서 필요한 정보를 갖는 AdoTemplate객체의 인스턴스도 자동 생성되어 CommandCallbackDao 객체의 AdoTemplate 속성에 할당되게 된다.

이제 다시 CommandCallbackDao 객체의 FindCountWithPostalCode() 메소드를 호출하는 부분으로 가 보자. 이 메소드에서는 AdoTemplate 객체의 Execute() 메소드를 호출하면서 인자로 이전에 정의한 콜백객체 PostalCodeCommandCallback<ResultObject>객체를 넘겨주고 있다. 이때 DB 작업에 필요한 변수값 postalCode도 함께 넘겨준다. 그럼 AdoTemplate에서는 DbCommand 객체를 정의해서 ICommandCallback에서 정의한 DoInCommand()를 호출하는데 사용한다. 그 이후는 앞의 코드에서 보는대로이다.


▶ 콜백객체로 CommandDelegate 타입의 객체 사용하기


public class CommandCallbackDao : AdoDaoSupport

{


    private string cmdText = "select count(*) from Customers where PostalCode = @PostalCode";


    /// <summary>

    /// Finds the number of customers with the given postal code.

    /// </summary>

    /// <param name="postalCode">The postal code.</param>

    /// <returns>Number of customers with the given postal code.</returns>

    public virtual int FindCountWithPostalCodeWithDelegate(string postalCode)

    {

        // Using anonymous delegates allows you to easily reference the

        // surrounding parameters for use with the DbCommand processing.


        return AdoTemplate.Execute<int>(delegate(DbCommand command)

               {

                   // Do whatever you like with the DbCommand... downcast to get

                   // provider specific funtionality if necesary.

 

                   command.CommandText = cmdText; 

                   DbParameter p = command.CreateParameter();

                   p.ParameterName = "@PostalCode";

                   p.Value = postalCode;

                   command.Parameters.Add(p);

 

                   return (int)command.ExecuteScalar();

 

               });

    }

    ...

AdoTemplate객체에서 사용하고 있는 Execute() 메소드는 다음과 같은 정의의 버전을 사용하고 있다.

public T Execute<T>(CommandDelegate<T> del)

CommandDelegate<T> 타입의 델리게이트 객체를 제공하기 위해서 익명 델리게이트 객체를 사용하고 있다. 익명 델리게이트 객체? 지금 Execute 메소드의 파라미터를 넘겨받는 괄호( )안에서 인라인 형식으로 델리게이트가 가리킬 코드를 정의하고 있다. 이 코드는 DbCommand 객체를 넘겨받는 메소드이다. 이 코드 자체를 Execute 메소드의 인자로 넘기는 것이 아니라 이 코드가 정의하고 있는 메소드를 가리키고 있는 델리게이트 객체를 넘기고 있는 것이다.

앞의 익명 델리게이트를 사용하는 코드를 완전한 모습으로 재정의해서 구성하면 다음과 같은 유사한 모양이 될 것이다. 

public virtual int FindCountWithPostalCodeWithDelegate(string postalCode)

{


    CallbackObject<ResultObject> callbackObject = new CallbackObject<ResultObject>(cmdText, postalCode);

    return AdoTemplate.Execute<ResultObject>(

        new CommandDelegate<ResultObject>(callbackObject.CallbakcMethod ) ).count;


}

private class CallbackObject<T> where T : ResultObject, new()

{

    string cmdText = string.Empty;

    string postalCode = string.Empty;

    public CallbackObject(string cmdText, string postalCode)

    {

        this.cmdText = cmdText;

        this.postalCode = postalCode;

    }


    public T CallbakcMethod(DbCommand command)

    {

        T resultObject = new T();

        command.CommandText = cmdText;

        DbParameter p = command.CreateParameter();

        p.ParameterName = "@PostalCode";

        p.Value = postalCode;

        command.Parameters.Add(p);

        resultObject.count = (int)command.ExecuteScalar();

        return resultObject;


    }

}

콜백 메소드에서 정의한 코드가 간단하다면 익명 델리게이트 표현을 사용한 방법을 사용해서 콜백 객체의 타입과 메소드를 만들지 않아도 될 것이다.  그러나 실제 현실 프로젝트에서는 코드의 통일성이 중요하기 때문에 다소 복잡하더라도 이런 정식 표현이 더 바람직할 것으로 보인다. 이렇게 원래의 모습으로 변형시켜 놓으면 이제 ICommandCallback 타입의 콜백객체를 사용할때와 구조는 유사하게 된다.

지금까지는 AdoTemplate 객체와 콜백 객체를 사용해서 DB 데이터에 접근하고 조작하는 구조에 대한 이야기였다.

AdoTemplate의 Execute형의 메소드를 사용하면  DB의 데이터를 조작하고 쿼리하는 모든 작업을 할 수 있다. 그러나 AdoTemplate에는 데이터 쿼리를 위한 좀 더 특별한 메소드 타입이 더 있다.  QueryWith를 접두어로 하고 있는 메소드류가 그것인데, 이 타입의 메소드를 사용하면 OR 매핑( Object Relational Mapping )이라는 작업을 할 수 있다. 이 OR 매핑 작업을 해주면 조회되는 레코드를 애플리케이션단에서 정의하고 있는 객체로 변환시킬 수 있게 된다. 이 방법을 사용하면 데이터 액세스 레이어의 DAO 객체로 DataSet 대신에 사용자 정의 객체 집합을 반환해 줄 수 있게 된다.  이 방법에 대해서는 다음 포스트에. 휴~ !

Posted by dalbong2

일반적인 기업형 애플리케이션은 대부분 N티어 구조를 갖는다.   다음 그림은 간단한 N티어 애플리케이션을 표현하고 있다. 참고로  티어는 물리적인 의미이고 레이어는 논리적인 의미라고 한다.

물리적으로 UI 레이어는 웹 애플리케이션의 경우는 웹 서버 그리고 윈폼 애플리케이션은 클라이언트 PC가 될 것이다. 그리고 서비스 레이어와 데이터 액세스 레이어는 보통 하나의 미들티어 서버에 존재한다. 물론 더 복잡한 물리적 구조로도 존재할 수 있다. 

Spring.NET의 IoC 컨테이너는 UI 애플리케이션 서버(PC)에서도 적용가능하고 미들티어 서버에서도 적용가능하다. 앞 포스트까지는 UI를 제공하는 웹 서버/사용자 PC 또는 미들티어 서버에서 실행될 수 있는 IoC 컨테이너에 대한 얘기를 했다. 그리고 UI 서버와 비즈니스 서버간의 통신 방법중의 하나인 웹 서비스에 대한 Spring.NET의 지원 얘기도 했다.

이제 남아 있는 큰 주제는 Spring.NET의 트랜잭션과 데이터 액세스 지원 얘기이다. 트랜잭션 이야기를 먼저 할지 데이터 액세스 이야기를 먼저 할지 고민하다 데이터 액세스부터 하기로 결정했다. 왜냐면 참고 문서를 읽다 보니까 그쪽이 먼저 이해가 되었다. 


■ Spring.NET의 Data액세스 지원


Spring.NET에서는 DB에 접근할 때 ADO.NET 기술을 이용할 수도 있고, NHibernate기술을 이용할 수도 있다.  우선 ADO.NET을 이용하는 기술을 알아본다. ADO.NET이 제공하는 .NET 프레임워크의 표준 API를 이용해서 DB에 접근할 수도 있다. 그렇지만 Spring.NET에서는 ADO.NET 기술을 한번 래핑한 API를 제공하는데 이 API를 사용하면 편리하다.

Spring.NET에서 제공하는 래핑 API도 두 종류로 구분할 수 있다. 하나는 "template"기반의 방식이고 하나는 객체 지향 기반의 방식이다(  이런 이름이 붙여진 것이 이해는 개인적으로 이해는 된다. 그렇지만 불행히도 지금 이것을 말로 표현할 정도는 아니다). 여튼 하나는 템플릿 방식이고 하나는 객체 지향 방식인가보다 -_-;; 이 방식에 따라서 사용자의 DB 접근 프로그래밍 스타일이 달라진다.

"template"기반의 방식을 이용하면 DB에 접근해서 작업을 하는데 AdoTemplate라는 클래스를 사용하게 된다. 즉 데이터 액세스 레이어의 객체( Data Access Object, 이하 DAO로 표현한다 )는 AdoTemplate객체를 사용해서 모든 DB 작업을 하게 된다.

객체 지향 기반의 DB 접근 방식에서는 DB에 대한 작업을 구분해서 각 작업을 클래스화했다. DB에 대한 작업은 그 성격에 따라 C( Create), R( Read), U( Update), D( Delete ) 나뉠 수 있다. 이 중에서 CUD작업은 DB에 영향을 미치기는 하나 조회하는 값이 없다. 이런 작업을 위해서 AdoNonQuery 타입을 제공하고 있고 그리고 읽기 전용의 작업을 위해서 AdoQuery 타입을 제공한다. 그리고 저장 프로시져를 이용하는 작업을 위해서 StoredProcedure라는 타입을 제공하고 있다. 그러나 이 방식의 DB 접근은 아직 널리 사용되지는 않은 모양이다.

이 포스트에서는 AdoTemplate객체를 이용하는 "template"기반의 방식를 설명할 것이다. 만약 객체 지향 기반의 프로그래밍 스타일을 알고 싶다면 Spring.NET에서 제공하는 레퍼런스 문서의 20.15절을 참고하기 바란다( 링크하나 걸어주고 싶은데 맘뿐이다. 쓰으...).


▶ AdoTemplate이용 구조


AdoTemplate를 이용해서 DB 데이터에 액세스하는 작업을 초 간단히 개념적으로 그렸다.

클라이언트 코드라 함은 여기서는 DAO( 데이터 액세스 객체)가 된다. 클라이언트 코드는 Spring.NET 프레임워크의 AdoTemplate에서 콜백될 객체를 넘겨준다. "콜백된다"는 것은 "다시 호출된다"는 의미로 쉽게 생각하자. 그러니까 AdoTemplate객체는 클라이언트 코드에서 넘겨받은 "콜백 객체를 다시 호출"하게 된다. 물론 AdoTemplate도 콜백객체를 어떻게 호출할지를 사전에 알 수 있다. 어떻게 아는지는 뒤에 보자. 야튼 이때 콜백 객체를 호출할때 AdoTemplate은 command 객체라는 것을 생성해서 호출 인자로 넘겨준다. command 객체에는  DB 액세스에 필요한 커넥션 정보가 포함되어 있다. 그리고 호출하는 클라이언트 코드의 트랜잭션 컨텍스트를 바탕으로 한 트랜잭션 정보도 설정되어 있다. AdoTemplate 객체를 작업하면 개발자가 작성한 소스에 DB 커넥션 정보나 트랜잭션 정보를 설정하는 코드는 없어도 된다는 이야기다.  콜백 객체에서는 넘겨받은 command 객체를 이용해서 이제 DB를 대상으로 실행 명령( sql문 또는 저장 프로시져 등 )을 수행하면 된다. 

방금 AdoTemplate 객체가 클라이언트 코드에서 넘겨받은 콜백 객체를 어떻게 호출할 수 있는지 알 수 있다고 했다. 이것은 AdoTemplate에서도 알고 있는 인터페이스와 델리게이트를 사용하기때문이다. 다시 말하면 클라이언트 코드에서 넘어오는 사용자 정의 콜백 객체는 AdoTemplate도 인식할 있는 사전에 정의한 인터페이스 또는 델리케이트를 구현한 객체여야 한다는 조건이 있다. 그 인터페이스와 델리게이트의 타입으로 ICommandCallback, CommandDelegate 정의가 되어 있다. 아래 그림에서는 이런 인터페이스 또는 델리게이트 기반의 콜백 구조를 보여주고 있다.


▶ ICommandCallback, CommandDelegate 기반의 콜백 구조


클라이언트 코드에서는 AdoTemplate객체의 Execute() 메소드를 호출할때 콜백 객체를 넘겨준다. 다음 그림을 보자. 

 

AdoTemplate 클래스에는 Execute() 메소드가 파라미터 타입에 따라서 여러 버전이 존재한다. 즉 ICommandCallback 또는 CommandDelegate이외의 다른 인터페이스, 델리게이트 객체를 받을 수도 있다는 것이다. 그림에서는 그 중에서 대표적인 두 개의 Execute() 버전을 보여주고 있다. 하나는 인터페이스 ICommandCallbakc 타입의 객체 c 를 받고 다른 하나는 델리게이트 CommandDelegate 객체 d를 받고 있다.

ICommandCallback 파라미터를 받는 버전을 먼저 보자. 우선 호출하는 코드의 컨텍스트에서 정보( DB 커넥션 정보, 트랜잭션 정보)를 구해서 command 객체를 생성한다. 그런 다음 파라미터로 받은 ICommandCallback 객체 c의 DoInCommand() 메소드를 호출하면서 콜백 객체로 방금 생성한 command객체를 넘겨준다. 이제 사용자 정의 콜백객체의 DoInCommand()에서는 넘겨받은 command객체를 이용해서 DB에대해서 명령을 수행한다.  이때 command객체에는 DB 액세스에 필요한 커넥션 정보, 실행 명령(sql, 저장 프로시져등)가 있다.

CommandDelegate 타입의 객체 d를 받는 Execute() 버전도 유사하게 작동한다. 역시 호출하는 클라이언트 코드의 컨텍스트에서 필요한 정보를 얻어서 command 객체를 생성한다. 그런 다음 넘겨받은 델리게이트 객체 d를 호출해서 command 객체를 콜백 객체로 넘겨준다. command 객체를 넘겨받은 콜백 객체의 사용자 정의 콜백함수에서는 command 객체를 이용해서 DB 액세스를 하게 된다. 이제 이 콜백 구조를 이용하는 샘플 코드를 보자.


▶  AdoTemplate를 이용하는 샘플 코드


이건 다음에 하자.

Posted by dalbong2

Spring.NET IoC 컨테이너나 Spring.NET이 지원하고 있는 AOP 프로그래밍에 대해서 아직 해야할 얘기는 남아있다. 객체 타입 즉 singleton, prototype으로 설정하는 방법 및 객체의 생명주기에 대한 얘기, Attribute를 이용해서 AOP를 구현하는 방식등등. 필요한 얘기이기는 하지만 나중에 하기로 하자. 우선 전체적인 애플리케이션을 구성하는 구성 기술들을 Spring.NET이 어떻게 지원하는지를 알아본다.


■ Spring.NET 웹 서비스 구조


먼저 Spring.NET이 .NET의 Web Services를 어떻게 보완, 지원해주는지 알아보자. Visual Studio.NET 또는 WSDL 커맨드 툴을 이용해서 클라이언트측 프락시를 만들어서 웹 서비스를 사용했던 기존의 구조는 다음처럼 표현할 수 있겠다.

Spring.NET 팀에서는 이 구조가 문제가 있었다고 봤다는 거다. 문제라기 보다는 좀 더 효율적인 구조로 가고 싶었다는 것이다.

기존의 구조에서는 클라이언트측에서 생성되는 프락시가 곧바로 클래스로 구현되었다는 것이다. WSDL기반의 클래스이다. Spring.NET 팀에서는 프락시가 곧바로 클래스로 구현되는 대신에 서비스 인터페이스를 구현하는 구조로 가기를 원했었다. 그렇게 가면 추후에 웹 서비스 구현체의 수정을 좀 더 유연하게 할 수 있다는 것이다.

그림에서 "웹 서비스용 클래스"란 WebService, WebMethod같은 어트리뷰트를 가지고 있는 클래스를 말한다. 즉 일반 클래스에 웹 서비스 목적의 어트리뷰트가 섞인 상태이다.

Spring.NET 팀에서는 다음과 같은 구조의 웹 서비스 구조를 구현했다.

우선 서버측을 보면 "PONO"라는 것이 있다. 이것은 "plain old .NET object" 약자인데, 일반 클래스를 말한다. 자바 버전의 Spring에서는 POJO라는 용어를 사용하고 있다. 앞의 그림의 "웹 서비스용 클래스"라는 용어와 대비된다. 웹 서비스용 클래스에는 웹 서비스 노출에 필요한 어트리뷰트가 섞인 웹 서비스 전용 클래스이다. 그러나 PONO는 사용자 정의의 일반 클래스 객체이다. 즉 Spring.NET 웹 서비스에서는 웹 서비스로 노출시키기 위한 특별한 클래스가 존재하는 것이 아니라 일반 타입의 객체를 사용하고 있다는 것이다. 이 PONO는 웹 서비스로 노출될 수 있지만, 코드를 수정하지 않고 그대로 .NET Remoting, Enterprise Service(COM+)에 사용될 수 있다는 것을 Spring.NET 팀에서는 목표로 했다는 것이다.

그러나 웹 서비스로 노출될 PONO는 서비스 인터페이스를 정의하고 그것을 구현하고 있어야 한다. 만약 웹 서비스로 노출시키고 싶은 메소드와 .NET Remoting으로 노출시키고 싶은 메소드가 다르다면 다른 인터페이스를 사용하면 된다. 여러 개의 인터페이스를 구현하는 PONO에서 어떤 메소드를 노출시킬지를 인터페이스를 선택함으로써 변경할 수 있다.

그러나 Spring.NET이 웹 서비스 인프라를 완전히 새롭게 구현한 것은 아니다. 이 말은 웹 서비스 타겟용 객체로 PONO를 사용해도 결국은 WebService 또는 WebMethod 같은 어트리뷰트로 꾸며진 웹 서비스용 클래스를 만들어내야 한다는 것이다. 그림에 표현된 서버측 프락시는 이렇게 만들어진 최종 웹 서비스용 클래스이다. 클라이언트측에서는 이 서버측 프락시를 바라보게 된다.

다음은 클라이언트측 프락시 구조에 대해서 알아보자. 클라이언트측에서는 여전히 WSDL 기반의 프락시를 사용한다. 이것은 여전히 클래스로 구현된다. 그러나 서비스 인터페이스를 상속해서 또다른 프락시를 만들어 내고 있다. 해서 Spring.NET이 만들어내는 프락시는 프락시의 프락시( proxy for proxy )인 셈이다. 클라이언측 코드에서는 이 두번째 프락시 객체를 참조하게 된다. 서버측 및 클라이언트측의 프락시는 실행시 동적으로 만들어진다.

최종적으로 이런 구조를 만들어내기위해서 Spring.NET에서는 서버측과 클라이언트측에서 프락시 객체를 만들어내는 객체를 제공한다. 이 객체들은 팩토리 패턴을 이용해서 프락시 객체를 만들어낸다.

서버측 프락시 생성 객체 : WebServiceExporter, 클라이언트측 프락시 생성 객체 : WebServiceProxyFactory


■ Spring.NET의 프락시용 팩토리 객체


▷ WebServiceExporter

이 타입은 Spring.Web.Services 네임스페이하에 정의되어 있다. 서버측 프락시 생성 객체의 타입 이름을 보면 "웹 서비스를 노출"시키는 기능을 하는 객체라는 것을  표현하고자 한 것 같다. 이것은 PONO를 래핑하고 있는 서버측 프락시 객체를 웹 서비스로 노출시키는 객체로 해석될 수 있겠다. PONO 객체에 대한 정보를 받아들여서 클라이언트에 노출되는 웹 서비스용 클래스를 만들어내는데, 이 결과물이 서버측 프락시 객체이다.

▷ WebServiceProxyFactory

이 객체는 WSDL 기반의 프락시 클래스를 만들고 다시 서비스 인터페이스 정보를 이용해 클라이언트에서 사용할 웹 서비스 프락시 객체를 만들어낸다. (WebServiceProxyFactory를 사용하면 웹 서비스 메소드에서 반환하는 사용자정의 타입도 클라이언트측에 자동 생성해줄 수 있는 건가? 이것은 나중에 해봐야 겠다. )

런타임시 Spring.NET은 이 두 객체를 이용해서 앞의 그림 같은 최종 구조를 만들어내는 것이다. 런타임시의 웹 서비스 최종 구조가 생성되는 과정과 실행 구조를 보면 다음과 같다.

클라이언트 코드에서는 WSDL 정보와 서비스 인터페이스에 대한 정보를 WebSeriviceProxyFactory에 전네주고 프락시 객체를 요청한다(1번 절차). 팩토리 객체는 전달받은 정보를 이용해서 프락시 객체를 동적으로 생성해서 건네준다(2번절차). 이때 WSDL 정보는 노출된 웹 서비스에 접근할 수 있는 URL 형태로 주면 된다. 이 과정이 프로그램적으로 될 수도 있겠지만 설정을 이용할 수도 있다. 그래서 WebServiceProxyFactory를 <object/>로 설정하고 이 객체를 IoC 컨테이너에 요청하게 되면 이 팩토리 객체 자체에 대한 참조가 반환되는 것이 아니라 이 팩토리 객체가 생성한 객체 즉 클라이언트측 프락시에 대한 참조가 반환된다.

클라이언트 코드에서는 IoC 컨테이너에서 절달받은 프락시 객체를 통해서 서버측으로 HTTP 요청을 보낼 수 있다(3번 절차). 이 요청은 일반 ASP.NET 웹 서비스 처리 핸들러가 받는 것이 아니라 Spring.NET에서 제공하는 핸들러가 받도록 설정을 변경해야 한다. Spring.NET의 웹 서비스 핸들러는 IoC 컨테이너에게 PONO 정보를 건네주면서 WebSerivceExporter 객체를 요청한다(4번절차). IoC 컨테이너는 이 요청에 대해서 WebServiceExporter객체를 직접 보내주는 것이 아니라 PONO를 이용해서 웹 서비스용 클래스를 동적으로 만들어내서 핸들러에게 반환해준다(5번 절차). 핸들러는 클라이언트에서 요청한 서비스를 프락시에 요청하고 나서(6번 절차) 그 반환값을 이용해서 HTTP 반응을 만들어서 클라이언트 프락시로 보낸다(7번 절차). 클라이언트측 프락시는 결과를 클라이언트 코드로 넘겨주게 되고 이로써 한 사이클의 웹 서비스 요청/반응이 끝나게 된다. 휴 ~~.

따라서 객체들의 참조 구조를 간단하게 정리하면 다음과 유사한 구조로 되겠다.


■ 서버측에서 PONO를 웹 서비스로 노출하기


앞에서 팩토리 객체들을 프로그램적으로 호출해서 프락시 객체에 대한 참조를 얻을 수도 있지만, 설정을 통해서도 가능하다고 했다. 이 포스트에서는 설정을 통해서 얻는 방법을 알아본다. 왜냐고? 첫번째는 내맘이고, 둘째는 설정을 통한 방법에 익숙해지는 것이 IoC 컨테이너에 대한 개념이 좀 더 빨리 그려질 수 있을 것 같아서이다. 세번째는 설정을 통한 방법을 이해하면 API를 이용하는 방법은 혼자서도 할 수 있다(아닌가).

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

  <object id="helloWorld" type="HelloWorldApp.HelloWorldService, HelloWorldApp">

    <property name="Message" value="안녕하세요!"/>

  </object>

  <object id="HelloWorldService" type="Spring.Web.Services.WebServiceExporter, Spring.Web">

    <property name="TargetName" value="helloWorld"/>

    <property name="Namespace" value="http://MySpringSample_WS/HelloWorldService"/>

  </object>

</objects>


<system.web>

  <httpHandlers>

    <add verb="*" path="*.asmx"

        type="Spring.Web.Services.WebServiceHandlerFactory, Spring.Web"/>

  </httpHandlers>

  <httpModules>

    <add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/>

  </httpModules>

</system.web>

첫번째 <object/>는 웹 서비스로 노출될 PONO 객체에 대한 정보이다. HelloWorldApp 네임스페이 아래에 HelloWorldService라는 이름의 클래스를 웹 서비스의 타겟 객체로 지정하고 있다. 이 객체에 대한 정의는 다음과 같다. 

namespace HelloWorldApp

{

    public interface IHelloWorld

    {

        string HelloWorld();

    }


    public class HelloWorldService : IHelloWorld

    {

        private string message;

        public string Message

        {

            set { message = value; }

        }


        public string HelloWorld()

        {

            return this.message;

        }


        public string SayNo()

        {

            return "No";

        }

    }

}

웹 서비스로 노출될 PONO는 서비스 인터페이스를 정의해야 한다고 했다. 코드에서는 IHelloWrold라는 인터페이스를 정의해서 구현하고 있다. 이 인터페이스는 PONO 객체의 메소드중에서 웹 서비스로 노출시킬 메소드를 지정하는 역할을 하기도 한다. 이 HelloWorldService에는 두 개의 메소드가 있지만, 이 코드에서는 메소드 HelloWorld()만을 노출시키고 있는 것이다. 설정에서는 PONO 객체 HelloWorldService를 id를 "helloWorld"로 지정하고 있다.

다음 <object/>에는 이 PONO에 대한 웹 서비스용 프락시 클래스를 만들어 낼 WebServiceExporter에 대한 정의가 있다. 어떤 PONO에 대한 프락시를 생성할지를 첫번째 <property/>의 TargetName으로 표현하고 있다. 이 설정에서는 앞에서 설정한 HelloWorldService에 대한 id를 지정해주고 있다. 설정에서는 노출될 웹 서비스의 네임스페이스도 지정해주는 설정이 있다.


■ 웹 서비스 요청에 대한 Spring.NET Http Handler 지정하기


Spring.NET은 웹 서비스에 대한 요청을 핸들링하는 사용자 정의 핸들러를 제공하고 있다: Spring.Web.Services아래에 있는 WebServiceHandlerFactory 이다. 앞의 설정에서는 클라이언트로부터의 asmx 파일에 대한 요청이 오면, ASP.NET가 제공하는 웹 서비스 핸들러 대신에 Spring.NET이 제공하는 WebServiceHandlerFactory가 작동하도록 하고 있다.

클라이언트에서 올라오는 asmx에 대한 HTTP요청에는 어떤 WebServiceExporter에 대한 요청인지를 알 수 있는 정보가 있다.

클라이언트에서 오는 HTTP 요청은 다음과 같은 형식의 URL을 갖는다. 

http://서버/~/서비스명.asmx

서비스명 : 서버측에 지정된 WebServiceExporter 객체의 id

앞의 요청 URL의 형식에서 "서비스명"에 해당하는 값이 web.config에서 지정된 WebServiceExporter의 id에 해당한다. asmx에 대한 요청을 받은 핸들러 WebServiceHandlerFactory는 요청 URL로부터 WebServiceExporter에 대한 정보를 얻고 IoC 컨테이너에 이 객체를 요청한다. IoC는 WebServiceExporter에 설정된 PONO 객체에 대한 정보를 통해서 프락시 객체를 생성해서 핸들러에게 건네줄 것이다. 이제 SOAP 프락시로부터 호출할 메소드, 메소드에 전달할 인자에 대한 정보를 구해서 프락시는 요청한 서비스 메소드를 호출하고 결과를 클라이언트에 보내주면 된다.


■ 클라이언트측의 WebServiceProxyFactory 객체 설정하기

이제 클라이언트에서의 WebServiceProxyFactory에 대한 설정을 보자. 서비스 인터페이스에 대한 정보와 노출된 웹 서비스에 대한 URL을 팩토리 객체에 설정하면 된다.

<object id="helloWorldService"

    type="Spring.Web.Services.WebServiceProxyFactory, Spring.Services">

  <property name="ServiceUri" value="http://localhost/HelloWorldWeb/helloworldservice.asmx"/>

  <property name="ServiceInterface" value="HelloWorldApp.IHelloWorld, HelloWorldApp"/>

</object>

이 설정에서는 helloworldservice.asmx를 ServiceUri 속성에 지정하고 있다. 이 값은 앞에서도 말한것처럼 서버측에 가서는 helloworldserivce라고 지정된 WebServiceExporter 객체를 찾는데 사용된다. 그리고 사용할 서비스 인터페이스로는 IHelloWorld로 지정하고 있다.

이제 이 팩토리 객체를 이용하는 코드이다.

IApplicationContext ctx = ContextRegistry.GetContext();


IHelloWorld helloWorld = (IHelloWorld )ctx.GetObject("helloWorldSerivce");

helloWorld.HelloWorld();


■ Spring.Calculator.Web.2005  예제 보기


앞 포스트에서도 보았지만 Spring.Calculator.Web.2005 프로젝트를 실행하면 default.aspx 페이지가 실행된다. 

default.aspx 페이지를 보면 다음과 같다.

<td align="center">

  <h2>

    <a href="calculatorService.asmx">CalculatorService</a>

  </h2>

  <br />

  <h2>

    <a href="calculatorServiceWeaved.asmx">CalculatorServiceWeaved</a>

  </h2>

</td>

물론 Spring.Calculator.Web.2005 프로젝트를 보면 asmx 웹 서비스 페이지는 없다.

이 예제에서는 웹 서비스의 클라이언트측 프락시는 사용하지 않고 있다. ASP.NET에서 제공하는 테스트 페이지가 클라이언트 코드 역할을 한다.

이 예제는 서버측의 WebServiceExporter만을 설정해서 사용하고 있는데, web.config를 보면 다음과 같은 설정이 있다. 

<context>

  <resource uri="config://spring/objects"/>

  <resource uri="~/Config/webServices.xml"/>

  <resource uri="~/Config/webServices-aop.xml"/>

</context>

설정이 webServices.xml과 webServices-aop.xml에 분리되어 있다. 설정을 이런식으로 분리할 수 있다는 것도 유용한 정보일 것이다. 이 두 파일을 보면 그곳에 서버측 프락시 팩토리에 대한 설정이 있다. 이 중에서 webServices-aop.xml의 내용이다.

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


  <description>webServices-aop</description>


  <object id="calculatorServiceWeaved" type="Spring.Web.Services.WebServiceExporter, Spring.Web">

    <property name="TargetName" value="calculatorWeaved" />

    <property name="Namespace" value="http://SpringCalculator/WebServices" />

    <property name="Description" value="Spring Calculator Web Services" />

  </object>


</objects>

id가 calculatorServiceWeaved  로 되어 있고, TargetName 속성은  calculatorWeaved 으로 되어 있다.  이 속성이 참조하고 있는 calculatorWeaved 는 web.config에 정의되어 있는 객체이다.  이제 클라이언트 코드에서 calculatorWeaved가 참조하고 있는 객체에 대한 메소드를 호출하면 서버측에서 처리할 수 있다.

Posted by dalbong2

■ advice 종류

▶ around advice

앞에서 알아본 CommonLoggingAroundAdvice 타입은 around advce중의 하나였다. 즉 인터페이스 IMethodInterceptor를 상속해서 Invoke() 메소드를 구현하고 있다. 아래는 IMethodInterceptor의 정의이다.

namespace AopAlliance.Intercept

{

    public interface IMethodInterceptor : ...

    {

           object Invoke(IMethodInvocation invocation);

    }

}

이 메소드의 인자로 넘어오는 invocation은 인터셉트된 타겟 객체에 대한 호출을 나타낸다. 이 인자의 Proceed() 메소드를 호출하면 인터셉트되어서 중단된 타겟 메소드 호출이 계속 진행된다.. Invoke()의 간단한 구현 코드이다.

...

public object Invoke(IMethodInvocation invocation)

{

    Console.WriteLine("Before invocation");

    object returnValue = invocation.Proceed();

    Console.WriteLine("After invocation and before return");

    return returnValue;

}

...

Proceed()가 호출되기 전에 원하는 작업을 할 수 있고, 호출된 후 그리고 반환값이 클라이언트 코드로 넘어가기 전에 또한 원하는 작업을 할 수 있다. 이곳에서 리턴되는 반환값을 조작해서 추가 정보를 넣거나 또는 제거할 수도 있다. 이처럼 타겟 메소드 호출 전, 후에 원하는 작업을 할 수 있는 advice를 around advice라고 한다.

참고로 이 advice가 설정되는 xml을 다시 보면 아래와 같다.

<!-- Aspect -->


<object id="commonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!--타겟객체-->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

<!-- Applies AOP on the contact service. -->

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

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>commonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

이렇게 설정해 놓으면 타겟 객체( calculator)의 메소드가 호출이 될때 commonLoggingAroundAdvice의 Invoke()에 의해서 인터셉트된다는 것이다. 가릿?

▶ before advice

before, after advice는 이름에서 예상되는 것처럼 호출전에 또는 호출된 후( 반환값을 반환하기전에)에만 끼어들기가 가능한 좀 더 간단한 advice이다.  before advice에서는 타겟 객체를 호출하기 위해서 Proceed()를 호출할 필요가 없다.  before advice를 구현하려면 인터페이스 IMethodBeforeAdvice를 상속해서 메소드 Before()를 구현해야 한다. 

public interface IMethodBeforeAdvice : ...

{

    void Before(MethodInfo method, object[] args, object target);

}

Before() 메소드는 타겟 메소드가 호출되기 전에 Spring.NET 프레워크에 의해서 호출된다. 이곳에서 필요한 사용자 정의 작업을 할 수 있다. method는 현재 인터셉트된 메소드에 대한 정보이고 args는 그 메소드를 호출할때 넘겨준 인자들에 대한 정보를 가지고 있다. 그리고 target은 타겟 객체에 대한 참조를 가지고 있다.

CommonLoggingAroundAdvice와 유사하게 CommonLoggingBeforeAdvice같은 advice를 구현해 놓고 앞에서처럼 설정을 하면 타겟 객체의 메소드들이 호출되기 전에 Before()가 호출된다는 것이다. 가릿? 가릿!

Before()를 수행하다 예외가 발생하면 이 예외는 이 메소드를 호출한 호출자로 전달된다.

▶ after advice

after advice 객체를 구현해서 설정해놓으면 타겟 메소드가 호출된 후에 클라이언트 코드로 반환되기 전에 호출될 수 있는데, 호출되는 메소드는 IAfterReturningAdvice 인터페이스를 구현한 객체의 AfterReturning()이다.

public interface IAfterReturningAdvice : ...

{

    void AfterReturning(object returnValue, MethodInfo method, object[] args, object target);

}

AfterReturning() 메소드에 전달되는 인자를 통해서 반환값, 타겟 메소드등에 대한 정보에 접근할 수 있다.

before advice에서는 예외가 발생하면 실행 경로를 역으로 진행해서 호출한 호출자에게 예외를 전달했다. 그러나 after advice는 실행 경로를 계속 유지한다. 그러나 리턴값을 반환하지 않고 예외를 반환한다.  실행 경로(excution path)란 advice 체인이 실행되는 순서를 말하는데 아래의 advice 체인에서 보여주는 그림을 참조하라.

▶ throws advice

throws advice 객체는 예상대로 타겟 객체에서 예외가 발생할때 호출되는 객체이다. 정확히는 throws advice가 적용된 후의 실행 경로상에서 예외가 발생할때 호출된다. 자세한 내용은 뒤의 "advice 체인"을 설명하는 곳을 참조한다.  이 녀석도 다른 녀석들처럼 throws advice가 되기 위해서는 상속해야 하는 인터페이스가 있다.

public interface IThrowsAdvice : IAdvice

{

}

근데 이 녀석은 다른 녀석들과는 다르게 구현해야 하는 메소드는 없다. 단지 구현되는 타입이 throws advice임을 나타내기만 하는 마커 인터페이스(marker interface 또는 태그 인터페이스 tag interface라고도 한다)이다. 그럼 예외가 발생했을때 Spring 프레임워크는 구현체의 어떤 메소드를 호출하게 될까. 구현체의 AfterThrowing() 메소드를 호출한다. Spring 프레임워크에 하드 코딩되어 있다고 볼 수 있겠다. AfterThrowing() 메소드에 전달되는 인자도 다음 두 유형중의 하나여야 한다.

void AfterThrowing(Exception ex)

void AfterThrowing( MethodInfo method, Object[] args, Object target, Exception ex)

다음은 실제 구현체에 대한 간단한 예제이다.

public class ConsoleLoggingThrowsAdvice : IThrowsAdvice

{

    public void AfterThrowing(Exception ex) // 실제로 이렇게 두 메소드이 예외 타입이 동일하게 구현하면 에러난다. 이유는 조금 아래에 있다.

    {

        // 예외정보로 필요한 예외 처리를 한다.

    }


    public void AfterThrowing(MethodInfo method, Object[] args, Object target, Exception ex)

    {

        // 메소드, 호출 인자, 타겟 객체에 대한 정보, 예외 정보로 필요한 예외 처리를 한다.

    }

}

그럼 왜 다른 advice처럼 아래와 유사한 형식으로 인터페이스를 정의하지 않았을까.

public interface IThrowsAdvice : IAdvice

{

    void AfterThrowing(Exception ex); // 왜 이와 유사한 메소드를 정의하지 않았을까?

    void AfterThrowing(MethodInfo method, Object[] args, Object target, Exception ex)

}

이렇게 하지 못하는(아니 하지 않는) 이유는 Spring 프레임워크에서 AfterThrowing() 메소드로 전달되는 예외 타입별로 핸들링을 할 수 있는 구조를 제공하기 위한 것이다. 다음과 같은 예외 처리 구조에 대해서는 익히 알고 있을 것이다.

//사용자 정의 예외 객체

public class MyException : Exception

{

}

...

public void method()

{

    try

    {

     // 작업...

    }

    catch (SqlException ex1)

    {

        // SqlException  예외를 처리한다.

    }

    catch (MyException ex2)

    {

        // MyException 예외를 처리한다.

    }

    catch (Exception ex3)

    {

        //Exception 예외를 처리한다.

    }

}

예외별로  다른 처리를 하고 싶다면 이런 구조적인 예외 메커니즘을 이용한다.

Spring.NET에서도 이런 유사한 구조를 제공하고자 한다. 해서 예외가 발생하면 Spring 프레임워크는 그 예외의 타입을 인식해서 적용된 throws advice에 구현되어 있는 AfterThrowing() 메소드들의 마지막 인자 즉 예외 객체의 타입과 비교를 한다. 그래서 만약 일치하는 예외 타입의 인자를 갖거나 또는 일치하는 예외 타입이 없다면 호환될 수 있는 예외 타입을 가지고 있는 AfterThrowing()을 호출한다. 만약 정의된 메소드중에서 발생한 예외의 타입과 호환되는 예외 타입의 인자를 갖는 예외 핸들링 메소드가 없다면 발생한 예외는 상위의 호출자로 버블링된다.

만약 AfterThrows() 메소드들중에서 발생한 예외 객체의 타입과 동일한 타입의 예외 인자를 갖는 메소드가 두개이상이라면? 런타임시 예외가 발생한다.

첫줄을 보면 하나의 메소드( AfterThrowing())안에 동일한 예외 타입의 인자를 갖는 메소드가 동시에 정의될 수 없다는 내용이다. 앞에서 예로 든 ConsoleLoggingThrowsAdvice 코드는 따라서 잘못된 것이다. 앞의 코드는 다음처럼 수정해서, AfterThrowing()의 마지막 인자인 예외 타입은 서로 달라야 한다.

public class ConsoleLoggingThrowsAdvice : IThrowsAdvice

{

    public void AfterThrowing(Exception ex)

    {

        Console.Out.WriteLine("Exception handler applied");

    }

    public void AfterThrowing(MyException ex)

    {

        Console.Out.WriteLine("MyException handler applied");

    }

    public void AfterThrowing(MethodInfo method, Object[] args, Object target, SqlException ex)

    {

        Console.Out.WriteLine("SqlException handler applied");

    }

}

얘기가 길어졌다. 이제 앞에서 던진 질문, throws advice에서 IThrowsAdvice에 AfterThrowing() 메소드를 포함하고 있지 않은 이유를 생각해보자. 간단하다. AfterThrowing() 메소드의 마지막 인자 즉 예외 객체의 타입을 미리 알 수 없다는 것이다. 사용자 정의 예외 타입을 사용한다면 어떻게 미리 알 수 있어서 인터페이스 메소드로 포함시키겠는가.

public interface IThrowsAdvice : IAdvice

{

    void AfterThrowing(MyException ex);//??

}

IThrowsAdvice를 상속하는 모든 구현체는 이 예외 타입의 메소드를 구현해야 한다는 얘기다. 해서 Spring에서는 이 방법을 버리고 런타임시, 실제 발생한 예외의 타입과 구현되어 있는 예외 타입을 비교해서 어떤 AfterThrowing()을 호출할지를 결정하는 방법을 선택했을 것이라는 순전히 개인적인 추측이다. 다른 이유가 있는지는 모르겠다.


■  advice 체인과 advice 실행 순서


앞에서 계속 미뤘던 advice 체인 개념을 알아보자. 타겟 객체의 하나의 pointcut에 대해서 하나만 advice를 적용할 수 있는 것은 아니다. 하나의 pointcut에 대해 앞에서 설명한 여러 종류의 advice 객체들을 여러개 적용할 수 있다. 타겟 객체 앞에 여러개의 advice가 체인처럼 연결되어 놓여져 있다. 타겟 메소드를 호출하면 체인처럼 설정되어 있는 모든 advice들을 호출에 적용하고 나서 최종적으로 타겟 메소드가 호출되는 것이다.  다음은 여러개의 advice들을 적용한 설정 예제이다.

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


  <object id="beforeAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingBeforeAdvice1, Spring.AopQuickStart.Common" />


  <object id="beforeAdvice2"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingBeforeAdvice2, Spring.AopQuickStart.Common" />


  <object id="afterAdvice1"

    type="Spring.AopQuickStart.Aspects.ConsoleLoggingAfterAdvice, Spring.AopQuickStart.Common" />


  <object id="aroundAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingAroundAdvice, Spring.AopQuickStart.Common" />


  <object id="throwsAdvice1"

          type="Spring.AopQuickStart.Aspects.ConsoleLoggingThrowsAdvice, Spring.AopQuickStart.Common" />


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

    <property name="Target">

      <object type="Spring.AopQuickStart.Commands.ServiceCommand, Spring.AopQuickStart.Common" />

    </property>

    <property name="InterceptorNames">

      <list>

        <value>throwsAdvice1</value>       

        <value>beforeAdvice1</value>

        <value>aroundAdvice1</value>

        <value>afterAdvice1</value>

        <value>beforeAdvice2</value>

      </list>

    </property>

  </object>


</objects>

before, around, after, throws advice들이 모두 적용되었고 그리고 before advice는 ConsoleLoggingBeforeAdvice1, ConsoleLoggingBeforeAdvice2 두 개가 적용되었다. 다음과 같은 advice 체인을 상상할 수 있다.

여기서 생각해 볼 문제가 하나 있다. advice가 실행되는 순서이다. 예제처럼 beforeAdvice2가 afterAdvice1보다 뒤에 설정되어 있다고 해서 그 적용 순서도 뒤일까. 이렇게 되면 말이 되지 않는다. after advice는 타겟 객체를 호출하고 나서 적용되는  advice이고 before advice는 타겟 객체가 호출되기 전의 advice이다. 설정은 뒤에 오더라도 적용되는 순서는 beforeAdvice2가 먼저여야 한다.

정리하면 advice 체인의 순서가 실제 적용되는 순서는 아니라는 것이다. advice 타입에 따라서 그 적용 순서는 바뀔 수 있다. 다음 그림은 예제에서 설정된 advice들이 실행되는 순서를 그림으로 보여주고 있다.

다음은 앞에서의 예제대로 설정해서 샘플 프로그램을 작성해서 실행한 결과이다.

여기서 한가지 주의할 것은 throws advice는 제일 먼저 설정되어야 한다는 것이다. 그래야 이 후의 advice 적용시 예외가 발생하더라도 그 예외도 설정한 throws advice에서 핸들링할 수 있다. throws advice가 적용되기 전에 중간의 advice에서 예외가 발생하면 그 예외에 대해서는 throws advice가 적용되지 않는다.


지금까지 AOP에 대한 이야기였다. Spring.NET의 중요한 구성 요소중의 하나가 AOP 프레임워크이지만, Spring.NET의 IoC 컨테이너가 AOP에 종속되는 것은 아니다. 이것은 원하지 않는다면 AOP를 사용하지 않고도 Spring.NET 프레임워크를 사용할 수 있다는 것이다.

실전 프로젝트에서도 Spring.NET 원형 그대로를 사용할 수도 있겠지만, Spring.NET 프레임워크를 기반으로 해서 그 프로젝트 상황에 맞도록 한단계 더 추상화된 개발 프레임워크를 제작할 수도 있을 것이다. 이때 다시 프레임워크를 만드는 입장이 되어서 AOP를 사용하면 프레임워크다운 프레임워크가 될 수 있을 것이다. 개발자에게 다양한 Cross cutting concerns에 걸쳐서 동일한 코드를 Copy&Paste하도록 하는 것보다는 하나의 advice 모듈로 구현한 것을 공통팀에서 관리하는 것이 훨씬 더 효율적일 것이라는 것이다. 남은 것은 이제 advice로 구현할 수 있는 cross cutting concerns를 추상화하고 설계하는 것이다.

다음 포스트에서는 별다른 주제가 없으면 Spring.NET이 지원하는 ASP.NET Web Services에 대한 이야기가 될 것이다.

Posted by dalbong2

다음은 AOP를 적용하기 위한 설정으로서 앞 포스트에서 보여준 Spring.Calculator.Web.2005 의 web.config의 일부분이다.

<!-- Aspect -->


<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!--타겟객체-->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

<!-- Applies AOP on the contact service. -->

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

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>CommonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

이 설정은 타겟 객체 AdvancedCalculator의 모든 메소드에 대해서 설정된 advice 로직이 적용된다고 했다. 오늘은 타겟 객체의 특정 메소드만 호출할때에만 CommonLoggingAroundAdvice가 적용되도록 하는 설정법을 알아본다. 즉 특정 메소드를 호출하는 경우에만 로그가 남게 될 것이다. 그러기 위해서는 advice가 적용될 부분 즉 pointcut을 지정해줘야 한다. 그러나 불행히도 현재 사용하고 있는 샘플 프로젝트중에는 이 설정에 대한 예가 없다.  따라서 기존의 설정을 조금 수정해야 한다.  


■ pointcut 필터링하기


<!-- Aspect -->


<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!-- Advisor (advice + pointcut). -->


<object id="regularExpressionMethodCutAdvisor" type="Spring.Aop.Support.RegularExpressionMethodPointcutAdvisor, Spring.Aop">

  <property name="pattern" value="Add*"/><!-- pointcut -->

  <property name="advice" ref="CommonLoggingAroundAdvice"/><!-- advice -->

</object>


<!--타겟객체-->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>


<!-- Applies AOP on the contact service. -->


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

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>regularExpressionMethodCutAdvisor</value>

    </list>

  </property>

</object>

조금이 아닌가?

변한 부분은 두 군데 있다. Advisor라고 주석이 달린 부분의 코드와 ProxyFactoryObject를 설정하는 부분에서 InterceptorNames 속성에 CommonLoggingAroundAdvce에 대한 id값을 추가하는 것이 아니라 추가된 advisor 객체의 id값을 추가하고 있다.

지난 포스트에서 advice와 pointcut을 합친 것을 advisor라고 했다. 앞의 설정에서는 pointcut 단독의 설정 대신에 advisor를 사용해서 advice와 advice가 적용될 pointcut을 함께 표현하고 있다. advice와 pointcut를 설정하기 위해서 RegularExpressionMethodPointcutAdvisor라는 타입을 사용하고 있는데 이 타입의 이름에서 의미있는 두가지 표현이 포함되어 있다:  RegularExpression, MethodPointcut.

AOP 기본 개념중에서 joinpint, pointcut이 있다. joinpoint란 프로그램이 실행되는 동안의 특정 순간들을 말한다. 예를 들어 메소드가 호출되는 순간, 특정 예외가 발생하는 순간등. pointcut이란 실제로 advice가 적용되는 joinpoint들의 집합을 말한다( Spring.NET 레퍼런스 문서. 13.1.1. AOP concepts 절 참조 ). joinpoint가 개념적인 용어라면 pointcut은 실제로 Spring같은 프레임워크에서 구현되어서 advice가 삽입될 곳을 지정하는 지정하는 객체라 할 수 있겠다.

다시 앞의 advisor 타입의 이름에 대한 이야기로 가서, MethodPointcut은 이런 pointcut중에서도 메소드 pointcut이라는 것을 나타내고 있다. 즉 특정 메소드의 호출을 pointcut으로 지정하는 advisor라 하겠다. 그 특정 메소드를 지정하기 위해서 정규식을 사용하겠다는 것이다. 앞의 설정은 타겟 객체의 메소드중에서 메소드명이 "Add"로 시작하는 것에만 advice CommonLoggingAroundAdvice를 적용하겠다는 것을 표현하고 있다. 이런 설정을 포함하고 있는 RegularExpressionMethodPointcutAdvisor 객체의 id를regularExpressionMethodCutAdvisor으로 설정하고 있다.

ProxyFactoryObject는 InterceptorNames 속성에 그 id를 추가시키고 있다. 타겟 객체의 어떤 메소드를 호출할때 advice를 적용할지에 대한 정보가 모두 regularExrpessionMethodCutAdvisor에 포함되어 있다. 따라서 런타임시에 ProxyFactoryObject가 타겟 객체에 대한 프락시 객체를 동적으로 생성해 낼 때에 그 정보를 이용해서 적절한 메소드 호출의 전, 후에 로깅 advice를 적용하는 AOP 프락시를 만들어 낸다.

web.config의 설정을 이렇게 변경하고 Spring.Calculator.Web.2005  웹 애플리케이션을 다시 실행시켜 본다.

AOP가 적용되는 예를 보기 위해서는 두번재 링크를 클릭해야 한다. 이 링크를 클릭하면 웹 서비스를 테스트 할 수 있는 화면이 출력된다.

참고로 웹 애플리케이션 프로젝트를 봐도 calculatorServiceWeaved.asmx 파일은 없다. Spring.NET의 프레임워크 중에서 특별할 HttpHandler를 제공하고 있는데 이것을 사용하면 .asmx 파일에 대한 Http 요청이 오면 이것을 알아서 처리해 준다. 사용자 입장에서는 마치 asmx 파일이 서버에 존재하는 것처럼 보여진다. 이것에 대해서는 뒤에서 Spring.NET이 Web Services를 지원하는 부분을 알아보면서 살펴볼 것이다.

여튼 이 테스트 페이지에서 마음대로 메소드를 호출해서 테스트를  해 보고 나서 남은 로그를 살펴보자.

2008-08-20 20:50:31,984 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Add'
2008-08-20 20:50:32,015 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned '3'

...

Add 메소드에 대한 로그만 남아 있는 것을 알 수 있다.  regularExpressionMethodCutAdvisor advisor의 설정으로 advice가 적용될 메소드가 필터링된것이다.

RegularExpressionMethodPointcutAdvisor를 사용하면 앞에서처럼 정규식을 이용해서 advice가 적용될 메소드(!)를 지정해줄 수 있었다. 이 외에도 NameMatchMethodPointcutAdvisor를 이용하면 지정한 메소드명과 일치하는 타겟 객체의 메소드에만 advice가 적용될 수 있도록 하는 advisor 타입도 있다.

pointcut 필터링에 대한 이야기는 이것으로 마무리하고 한가지만 더 알아보겠다.


■ AOP용 인터페이스 지정하기


만약 ProxyFactoryObject 객체가 넘겨받은 타겟 객체가 하나의 인터페이스만을 구현하것이 아니라 여러개의 인터페이스를 구현한 경우를 생각해보자.  앞의 예제에서의 AdvancedCalculator는 인터페이스 IAdvancedCalculator만을 구현하고 있다. 따라서 ProxyFactoryObject가 AOP 인터페이스를 생성할때 선택이 필요없었다. 그러나 타겟 객체가 두개 이상의 인터페이스를 구현하고 있다면 어떤 인터페이스에 대한 AOP가 적용되어야 할지를 지정해줘야 한다. ProxyFactoryObject의 속성 ProxyInterfaces가 이런 목적으로 사용된다.

<!-- Applies AOP on the contact service. -->


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

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>expressionMethodCutAdvisor</value>

    </list>

  </property>

  <property name="ProxyInterfaces">

    <list>

      <value>Spring.Calculator.Interfaces.IAdvancedCalculator, Spring.Calculator.Contract</value>

    </list>

  </property>

</object>

이 속성은 타입 형식을 표현하는 "네임스페이스.타입, 어셈블리명" 형식의 문자열을 받는다. 타겟 객체가 하나의 인터페이스만을 구현하고 있다면 이 속성은 생략해도 된다.


오늘은 여기까지.  다음 포스트에서는 advice 종류 즉 around, before, after, throws advice에 대한 얘기를 하고 이어서 Spring.NET의 Web Services 지원 얘기를 해 보겠다.

Posted by dalbong2

■ 예제 설명

앞에서 본 샘플 프로젝트 솔루션의 구조이다.

Spring.Calculator.Web 프로젝트를 실행시켜보면 다음과 같은 결과 페이지가 보인다.

첫번째 링크는 단순한 웹 서비스 메소드를 호출하고 있다. AOP가 적용된 메소드를 호출하기 위해서는 두번째 링크를 클릭해야 한다. 이번 포스트에서는 두번째 링크에 대한 웹 서비스를 AOP 예제로 삼겠다.  두번째 링크를 클릭하면 다음과 같은 웹 서비스 테스트 화면이 나온다.

노출된 메소드중에서 Add 메소드를 클릭해서 적절히 값을 넣고 호출한다.

이 메소드를 호출하고 나서 남는 로그는 다음과 같다. 

2008-08-18 23:09:34,406 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Add'

2008-08-18 23:09:34,421 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned '2'

그러나 웹 서비스 메소드에는 로그를 남기는 코드는 없다. 별도의 advice를 사용해서 로그를 남기고 있다. 이 샘플에서는 AOP를 구현하기 위해서 개발자가 해야 할 일을 앞에서 설명한 대로 차례로 진행해보자. 


■ 타겟 객체 정의


먼저 타겟 객체에 대한 소스를 보자. 먼저 두번째 링크가 호출하는 클래스는 Spring.Calculator.Services.2005 프로젝트의 AdvancedCalculator 를 보면 다음과 같다.

public class AdvancedCalculator : Calculator, IAdvancedCalculator

{

    #region Fields

    private int memoryStore = 0;

    #endregion


    #region Constructor(s) / Destructor

    public AdvancedCalculator()

    {}


    public AdvancedCalculator(int initialMemory)

    {

        memoryStore = initialMemory;

    }

    #endregion


    #region IAdvancedCalculator Members

    public int GetMemory()

    {

        return memoryStore;

    }

    public void SetMemory(int memoryValue)

    {

        memoryStore = memoryValue;   

    }

    public void MemoryClear()

    {

        memoryStore = 0;

    }

    public void MemoryAdd(int num)

    {

        memoryStore += num;

    }


    #endregion

}

이 클래스에는 Add 메소드가 없다. 상속을 하고 있는 부모 클래스 Calculator에서 구현하고 있다. 그리고 앞 포스트에서 말한대로 타겟 객체가 되기 위해서는 현재 버전의 Spring.NET( v 1.1.2)에서는 반드시 인터페이스를 구현해야 한다고 했다. 코드를 보면 IAdvcancedCalculator를 상속해서 구현하고 있다.

public interface IAdvancedCalculator : ICalculator

{

    int GetMemory();

    void SetMemory(int memoryValue);

    void MemoryClear();

    void MemoryAdd(int num);

}

IAdvancedCalculator 인터페이스는 ICalculator를 상속해서 인터페이스 정의를 물려받고 있다. ICalculator 인터페이스와 그것을 구현하고 있는 Calculator 클래스 코드는 다음과 같다.

public interface ICalculator

{

    int Add(int n1, int n2);

    int Substract(int n1, int n2);

    DivisionResult Divide(int n1, int n2);

    int Multiply(int n1, int n2);

}

public class Calculator : ICalculator

{

    #region ICalculator Members


    public int Add(int n1, int n2)

    {

        return n1 + n2;

    }


    public int Substract(int n1, int n2)

    {

        return n1 - n2;

    }


    public DivisionResult Divide(int n1, int n2)

    {

        DivisionResult result = new DivisionResult();

        result.Quotient = n1 / n2;

        result.Rest = n1 % n2;

        return result;

    }


    public int Multiply(int n1, int n2)

    {

        return n1 * n2;

    }


    #endregion

}

인터페이스들은 구현 클래스들과는 다른 프로젝트 Spring.Calculator.Contract.2005에 구현되어 있다. 만약 클라이언트 애플리케이션과 서버 애플리케이션이 분리되어 있다면 클라이언트에서는 인터페이스 어셈블리만 참조하면 된다. 물론 서버측에서는 인터페이스 어셈블리와 구현 어셈블리가 같이 참조되어야 한다.


■ advice 코딩하기


이제 타겟 객체를 호출할때 weaving될 advice 코드를 살펴본다. 샘플에서는 프로젝트 Spring.Aspects.2005에 구현되어 있다.

현재는 두개의 로깅 advice가 구현되어 있다. 이 중에서 웹 애플리케이션에서는 CommonLoggingAroundAdvice를 사용해서 로그를 남기고 있다. 이 advice 코드를 보면 다음과 같다.

public class CommonLoggingAroundAdvice : IMethodInterceptor

{

    #region Logging

    private static readonly ILog LOG = LogManager.GetLogger(typeof(CommonLoggingAroundAdvice));

    #endregion


    #region Fields

    private LogLevel _level = LogLevel.All;

    #endregion


    #region Properties

    public LogLevel Level

    {

        get { return _level; }   

        set { _level = value; }

    }

    #endregion


    #region IMethodInterceptor Members


    public object Invoke(IMethodInvocation invocation)

    {

        Log("Intercepted call : about to invoke method '{0}'", invocation.Method.Name);

        object returnValue = invocation.Proceed();

        Log("Intercepted call : returned '{0}'", returnValue);

        return returnValue;

    }


    #endregion


    #region Private Methods

    private void Log(string text, params object[] args)

    {

        switch(Level)

        {

            case LogLevel.All :

            case LogLevel.Debug :

                if (LOG.IsDebugEnabled) LOG.Debug(String.Format(text, args));

                break;

            case LogLevel.Error :

                if (LOG.IsErrorEnabled) LOG.Error(String.Format(text, args));

                break;

            case LogLevel.Fatal :

                if (LOG.IsFatalEnabled) LOG.Fatal(String.Format(text, args));

                break;

            case LogLevel.Info :

                if (LOG.IsInfoEnabled) LOG.Info(String.Format(text, args));

                break;

            case LogLevel.Warn :

                if (LOG.IsWarnEnabled) LOG.Warn(String.Format(text, args));

                break;

            case LogLevel.Off:

            default :

                break;

        }

    }

    #endregion

}

이 advice는 타겟 객체의 메소드를 호출할때 적용된다. 타겟 객체의 어디서, 어떻게 적용될지를 선택할 수 있는 방법이 바로 IMethodInterceptor 인터페이스이다. 이 인터페이스에서는 단지 object Invoke()만을 정의하고 있다.

IMethodInterceptor를 구현하고 있는 advice가 타겟 객체에 적용될때는 타겟 객체의 메소드를 호출하면 항상 IMethodInterceptor인터페이스의 Invoke()가 호출된다. 코드에서 Invocation.Proceed(); 부분이 advice가 캡쳐한 원래의 호출을 다시 타겟 객체로 전달하는 부분이다. 타겟 객체로 호출을 전달하기 전에 예제의 advice에서는 로그를 남기는 작업을 하고 있다. 로그를 남기는 Log()에 대해서는 지금 이곳에서는 중요한 부분이 아니므로 넘어가도록 한다. Proceed()를 호출하고 나서 반환값을 받고서도 클라이언트로 바로 넘기지 않는다. 반환되기 전의 순간도 캡쳐할 수 있다. 예제 advice에서는 타겟 객체에서 반환하는 값에 접근해서 그 값을 로그로 남기고 있다. 그런 다음 최종적으로 클라이언트 코드로 반환값을 넘겨주고 있다. 만약 타겟 객체가 개발자가 개발g한 비즈니스 객체이고 개발 프레임워크에서 이와 같은 advice를 개발해서 적용한다면 얼마나 유용할지 짐작이 갈 것이다.

이렇게 타겟 메소드의 호출 전 후를 캡쳐할 수 있는 기회를 제공하는 advice를 "around advice"라고 한다. IMethodInterceptor는 around advcie의 Spring 프레임워크의 구현이다. 그외에도 before advice, after advice, throws advice등이 있고 이것을 각각 구현한 Spring.NET의 인터페이스들이 있다. 이것에 대해서는 뒤에서 다루기로 하고 지금은 Spring.NET 애플리케이션에서 AOP를 적용하는 전체적인 절차를 계속 알아보도록 하자.

지금까지의 내용을 보면, 인터페이스가 있었고 그리고 인터페이스를 구현한 타겟 객체가 있었다. 그리고 타겟 객체의 메소드를 호출할때 적용될 advice가 있었다. 이제 실제로 advice를 타겟 객체에 적용하기 위한 설정이 필요하다.


■  advice 적용 설정하기( ProxyFactoryObject 설정하기 )


advice를 타겟 객체에 적용하기 위한 설정은 다시 말하면 ProxyFactoryObject 객체 설정과 같은 말이다. 앞의 포스트에서 Spring.NET에서는 프락시 패턴을 이용해서 AOP를 구현하고 있다고 했다. 그리고 advice가 적용된(weaving된) 프락시를 AOP 프락시로 표현했는데, 이 AOP 프락시를 런타임시에 동적으로 생성해내는 객체가 바로 ProxyFactoryObject라고 했다. 클라이언트 코드에서는 타겟 객체에 대한 참조와 advice를 건네주고 ProxyFactoryObject객체로부터 타겟 객체에 대한 AOP 프락시를 받는다고 했다. 이 시나리오를 설정을 통해서 구현하면 다음과 같다. 이 시나리오를 코드상에서 프로그램적으로 구현할 수 있는 API도 제공하고 있다. 이 설정은 웹 프로젝트 Spring.Calculator.Web.2005 의 web.config의 부분이다.

<!-- Aspect -->


<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>


<!-- Service -->


<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

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

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>CommonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

첫번째 <object/>요소에 타겟 객체에 적용될 advice가 정의되어 있다. id는 CommonLoggingAroundAdvice로 하고 있는데, 다른 <object/>에서 이 객체를 참조할때 id 값을 이용할 수 있다. advice는 Spring.Aspects.Logging 네임스페이스에 포함된 CommonLoggingAroundAdvice 클래스에 정의되어 있다는 것을 type 어트리뷰트값을 통해서 나타내고 있다.

두번째 <object/> 요소에서는 타겟 객체에 대한 정의를 표현하고 있다. id는 calculator로 하고 있고 타겟 객체의 타입은 어셈블리 Spring.Calculator.Services의 Spring.Calculator.Services 네임스페이스 아래에 있는 AdvancedCalculator 클래스에서 정의하고 있다.

세번째 <object/>요소는 바로 앞의 advice객체와 타겟 객체를 인자로 받아들이는 AOP 인터페이스 제너레이터 ProxyFactoryObject에 대한 정의이다. ProxyFactoryObject 타입의 속성중에는 Target, InterceptorNames( 대소문자 무관)가 있는데 Target 속성을 통해서 타겟 객체에 대한 참조를 받고, InterceptorNames 속성을 통해서 around advice를 받고 있다. 타겟 객체에 대한 참조를 지정할때 <property/>요소의 ref 어트리뷰트를 사용하는데 그 값으로는 앞에서 AdvancedCalculator 객체를 정의하고 있는 <object/>의 id 어트리뷰트를 지정하고 있다. 그리고 InterceptorNames 속성은 여러개의 advice를 지정할 수 있다. 그래서 <list/>요소 내부에 <value/> 요소를 사용해서 advice를 추가하고 있는데, 다른 advice 객체가 있다면 <value/>를 더 추가할 수 있다. 여기서 <value/>의 값으로 지정된 CommonLoggingAroundAdvice는 advice를 정의하고 있는 첫번째 <object/>요소의 id값이다.

이로써 특정 advice를 특정 타겟 객체에 적용하는 작업은 끝났다.  클라이언트 코드에서는 이제 다음과 같은 방법으로 AOP 프락시에 대한 참조를 얻을 수 있다.

IApplicationContext ctx = ContextRegistry.GetContext();

IAdvancedCalculator firstCalc = (IAdvancedCalculator) ctx.GetObject("calculatorWeaved");

컨텍스트 객체의 GetObject() 메소드에 ProxyFactoryObject 객체에 대한 id값을 넘겨주면 원한는 타겟 객체에 대한 AOP 프락시를 받을 수 있다. 반환되는 객체가 ProxyFactoryObject 객체 자체가 아니라 그 타겟 객체에 대한 프락시임을 다시 한번 더 상기하자. 이제 AOP 프락시를 통해서 클라이언트측 코딩을 해 나가면 된다.

현재 웹 샘플 Spring.Calculator.Web.2005 에서는 클라이언트 코드에서 타겟 객체에 대한 참조를 이용하는 코드가 없다. 현재의 웹 샘플 코드에서는 서버측에서만 AOP를 적용하는 코드가 있다. 타겟 객체의 메소드를 호출하는 클라이언트 코드는 앞의 그림과 같은 ASP.NET에서 제공하는 테스트 페이지를 사용하고 있다. 앞에서와 유사한 코드는 프로젝트 Spring.Calculator.ClientApp.2005의 Program.cs 파일에 있다.

Spring.Calculator.Web.2005 프로젝트에 있는 web.config의 설정은 웹 서비스로 노출된 타겟 객체에 대한 설정이다. 따라서 클라이언트측 로그는 없지만 서버측 객체 호출에 대한 로그는 남는다.


■  웹 애플리케이션 실행하기


Logs폴더 하위의 log.txt 파일을 열어보면 웹 서비스 메소드가 호출될때 남겨진 로그를 볼 수 있다. 참고로 실행중에는 VS.NET의 솔루션에서 오픈하지 말고, 윈도우 탐색기에서 오픈하라. 다음은 메소드 Add()와 Divide()를 호출한 후에 남은 로그 내용이다.

2008-08-20 00:08:52,468 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Add'
2008-08-20 00:08:52,484 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned '3'
2008-08-20 00:09:06,078 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : about to invoke method 'Divide'
2008-08-20 00:09:06,078 [DEBUG] Spring.Aspects.Logging.CommonLoggingAroundAdvice - Intercepted call : returned 'Quotient: '2'; Rest: '1''


지금까지의 설정처럼 하면 타겟 객체의 모든 메소드를 호출할때마다 CommonLoggingAroundAdvice의 내용이 적용될 것이다. 그러나 때로는 타겟 객체의 특정 메소드를 호출하는 경우에만 설정한 advice들이 적용되기를 바랄 수도 있을 것이다. 


■ pointcut 코딩하기


타겟 객체의 특정 메소드만 호출할때 로그를 남기고 싶다면 앞에서와 같은 기본적인 설정만으로는 부족하다. 해서 좀 더 특별한 설정이 필요하다. 특별한 설정이란 바로 타겟 객체의 pointcut을 지정하는 것이다. AOP의 일반적인 이론에서는 여러 joinpoint( advice가 weaving될 수 있는 포인트. 예를 들어 속성 값이 변하기 전, 후)가 있을 수 있겠지만, 현재 AOP 프락시를 이용하여 AOP를 구현하는 방법에서는 메소드만이 pointcut의 대상이 된다. 말이 점점 어려워진다 -_-;;

여튼 현재의 Spring.NET 버전에서는 메소드만이 advice가 적용될 수 있고 메소드중에서 특별한 메소드만 advice가 적용될 수 있도록 하는 방법이 있다. Spring.Aop.Support네임스페이스 아래에 있는 RegularExpressionMethodPointcutAdvisor 타입을 이용해서 그런 설정을 할 수 있는데, 이 타입은 정규식을 이용하고 있다. 즉 특정 정규식에 일치하는 메소드명을 갖는 타겟 메소드에만 advice를 적용시키는 설정을 할 수 있다.

불행히도 현재 사용하고 있는 샘플 프로젝트중에는 이 설정이 없다.  설정을 조금 수정해야 한다.  시간 관게상 이 작업은 다음 포스트에서 하도록 한다. 

Posted by dalbong2

바로 예제 설명으로 들어가려 했으나 아무래도 AOP 개념에 대해 좀 더 설명이 필요할 것 같다. Aspect Oriented Programming하면 떠올라야 하는 개념은 "타겟 객체에 대한 호출을 중간에서 인터셉트할 수 있는 방법"이라는 것이다. 개발 프레임워크 입장에서 생각해본다면 얼마나 근사한 구조인가. 타겟 객체( 개발자가 개발)에 대한 모든 호출( 개발자가 만든 코드에서의 호출)을 개발 프레임워크에서 캐취할 수 있다는 것은 많은 장점을 가지고 있다.

그리고 실제로 AOP를 구현하기 위해서 개발자가 AOP의 컨셉을 모두 개발할 필요는 없다. advice는 개발자가 C#문법을 이용해서 일반 객체를 정의하듯이 구현하면 된다. 그러나 advice나 pointcut 자체는 Spring.NET의 IoC 컨테이너가 관리한다. Spring.NET의 AOP를 구현하기위해서 개발자가 해야 할 일을 정리하면 다음과 같다.

▶ 타겟 객체 개발하기

타겟 객체란 AOP가 적용될 타입의 객체를 말한다. 예를 들어 다음에 설명하는 advice( 예로, 메소드 호출 전 후에 로그를 남기는 코드)를 적용하고 싶은 객체를 말한다. 애플리케이션용 클래스중에서 선택한 클래스가 AOP 프로그래밍의 타겟객체로 설정될 수 있다.  뒤에 설명하겠지만 타겟 객체가 되기 위해서는 인터페이스를 구현해야 하는 제한이 있다.

▶ advice 코딩하기

필요한 aspect를 코딩한다. 그러나 대부분의 구현은 이미 되어 있다:로깅, 트랜잭션, 보안 등.  예를 들어 로깅 모듈로 Log4net이 있다.  그리고 하나의 타겟 객체에 대한 여러개의 advice가 차례로 적용될 수 있다.

▶ pointcut 지정하기

pointcut을 지정할 수 있는 기본적인 방법 또한 Spring.NET에서 제공하고 있다. 예를 들어 정규식을 사용하는 방법으로 타겟 객체에서 "Do"로 시작하는 메소드에 대해서 Advice를 적용하라는 식의 설정을 할 수 있다.

▶ Advice를 타겟 객체에 적용하는 작업하기.

결정된 advice와 pointcut을 configuration하는 작업이다.


■ ProxyFactoryObject를 이용하여 AOP 프락시 생성하기


Spring.NET에서는 프락시 패턴을 이용해서 AOP를 적용할 수 있는 구조를 만든다.

그림을 보면 클라이언트 코드는 직접 타겟 객체를 참조하지 않고 프락시를 통해서 호출하고 있다. 프락시는 클라이언트의 호출을 받으면 바로 타겟 객체의 메소드를 호출하지 않는다. 3번 호출처럼 필요하다면  Aspect 모듈을 호출한다. 이때 Cross Concerns( advice로 구현된다)에서는 타겟 객체의 메소드를 호출하기 전에 advice의 내용(로깅, 트랜잭션 작업 시작 등)을 수행할 수 있다. 그리고 나서 타겟 객체의 메소드를 호출한다. 또한 타겟 객체로부터의 반환을 바로 클라이언트로 넘기지 않는다. 타겟 객체의 반환이 클라이언트로 넘겨주기 전에 또한 필요하다면 Aspect 모듈을 호출해서 마무리 작업( 호출후의 로깅작업, 트랜잭션 작업 등)을 할 수 있다. 그리고 나서 최종적으로 클라이언트에 반환값을 넘겨준다. 이처럼 AOP를 구현하기 위한 프락시를 "AOP 프락시"라고 한다. 이 AOP 프락시에는 타겟 객체의 메소드를 호출하는 코드만 있는 것이 아니라 구현된 advice를 호출하는 메소드 또한 포함되어 있다.

Spring.NET에서는 이런 AOP 프락시를 만드는 녀석이 바로 ProxyFactoryObject이다( 수정일자: 2009.05.24. AOP 프락시를 생성하는 방법은 여러가지가 있다. ObjectNameautoProxyCreator, DefaultAdvisorAutoProxyCreator 등. Spring.NET 레퍼런스 문서 13.5, 13.9절등을 참고한다.) 최종적으로 앞의 그림과 같은 실행 구조가 되기 위해서, 클라이언트는 프락시가 필요한 타겟 객체를 ProxyFactoryObject에게 넘겨주면 ProxyFactoryObject는 타겟 객체에 대해서 advice 호출을 포함하고 있는 AOP 프락시를 생성해서 클라이언트에 넘겨준다. AOP 프락시를 생성하는 작업은 런타임에 일어나는데, 프락시 클래스용 IL코드를 만들어 내기위해서 System.Reflection.Emit 네임스페이스의 클래스들을 사용한다. 

ProxyFactoryObject가 AOP프락시를 런타임시에 동적으로 생성한다는 것은 런타임시에 weaving(advice를 적절한 pointcut에 끼워넣는 작업)이 수행된다는 것이다.  weaving은 AOP를 구현하는 방식에 따라 컴파일시에 또는 클래스 로딩시에 수행될 수 있다. 그러나 Spring.NET에서는 현재 런타임시만을 지원하고 있다. 또한 현재 버전(version 1.1.2)에서는 AOP 프락시를 생성하고자 하는 타겟 객체는 반드시 하나 이상의 인터페이스를 구현해야 한다.  ( 반드시 하나 이상의 인터페이스를 구현해야 하는 제약은 버전 1.1 X부터는 없어졌다고 한다. 아래 최만석님의 댓글 및 다음 링크에서 "13.5.4. Proxying Classes"절을 참조한다.  이 내용에 따르면 일반 클래스도 AOP 프락시의 타겟 객체로 사용될 수 있지만, 이 방법 또한 가상 메소드만 AOP 프락시를 통해서 노출될 수 있다는 제약이 있다. )

public interface ICalculator

{

    int Add(int n1, int n2);


   //....

}


public class Calculator : ICalculator

{

    #region ICalculator Members


    public int Add(int n1, int n2)

    {

        return n1 + n2;

    }


    // ....

    #endregion

}

Calculator를 AOP의 타겟 객체로 만들고 싶다면 반드시 ICalculator같은 인터페이스를 구현 하는 구조로 설계해야 한다는 것이다. 그러나 추후 버전에서는 다른 방식의 AOP 프락시 제너레이터도 제공할 계획이란다.
앞에서 수정한 부분을 참고한다. 반드시 인터페이스를 구현할 필요는 없다.

참고로 .NET 자체에서도 AOP용 프락시를 생성하는 방법이 있다. 그러나 그런 AOP용 프락시를 생성하기 위해서는 타겟 객체가 ContextBountObject 타입을 상속받아야 한다는 단점이 있다. 컨텍스트 기반(Context-bound )의 프락시라고 한다는데, 컨텍스트 스위치와 .NET 리모팅 인프라의 오버헤드때문에 성능적으로도 그렇게 효과적이지는 않다고 한다.


■ ProxyFactoryObject를 configuration하기


Spring.NET에서는 Spring.Aop.Framework.ProxyFactoryObject 클래스를 이용해서 타겟 객체(advised 객체)에 대한 프락시를 얻는다고 했다.  이 ProxyFactoryObject를 이용하면 좋은 점은 advice나 pointcut을 configuration에 포함시킬 수 있다는 것이다. 다음 configuration을 보자.

<object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

  <property name="Level" value="Debug"/>

</object>

<object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

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

  <property name="target" ref="calculator"/>

  <property name="interceptorNames">

    <list>

      <value>CommonLoggingAroundAdvice</value>

    </list>

  </property>

</object>

3개의 객체가 정의되어 있다. 첫번째는 advice 객체에 대한 정의이다. 두번째 객체는  AOP가 적용될 타겟 객체에 대한 정의이다. 세번째 객체에 대한 정의는 첫번째와 두번째 객체에 대한 참조를 받아들여서 타겟 객체에 대한 AOP 프락시를 만들어낼 ProxyFactorObject에 대한 정의이다. 이 설정에 대한 구체적인 설명은 뒤에서 하기로 한다.

이렇게 설정한 내용을 Spring.NET의 컨테이너가 해석할 수 있다는 것이다. ProxyFactoryObject 객체에 대한 이와 같은 설정을 해석할 수 있다는 것이 무슨 의미인지는 이렇다. ProxyFactoryObject에 대한 참조를 얻는 과정은 여느 객체를 얻는 방식과 동일하다.

IApplicationContext ctx = ContextRegistry.GetContext();


IAdvancedCalculator calculator= (IAdvancedCalculator) ctx.GetObject("calculatorWeaved");

이때 ctx.GetObject("calculatorWeaved")가 반환하는 것은 ProxyFactoryObject 객체에 대한 참조가 아니라, Spring.NET 컨테이너는 ProxytFactoryObject에 대한 요청을 인식할 수 있고 이것에 대한 요청은 다르게 처리한다. 즉 이 팩토리가 참조하고 있는 타겟 객체( Target 속성이 가리키는 타입)에 대한 프락시의 참조를 반환한다는 것이다.  이때 참조된 프락시는 설정된 advice, 여기서는 CommonLoggingAroundAdvice의 내용이 weaving된 상태이다.


여기까지 하고 다음 포스트에서 앞에서 알아본 예제를 살펴보도록 하자.

Posted by dalbong2

IoC( Inverse of Control  제어권의 역전, 역제어)와 DI( Dependencies Injection)은 같은 의미로 사용된다. 시간적으로 보면 IoC라는 용어가 먼저 나왔고 뒤에 Martin Fowler라는 사람이 개념상 더 적절하지 않냐면서 내놓은 것이 DI다. 필자의 눈에는 차이점을 잘 모르겠고, 개발자에게는 당장 별로 중요한 차이는 아닐듯하다. 컨테이너가 객체를 생성할때 그 객체가 필요로 하는 의존 객체들을 자동 생성해서 할당해준다는 것이다.

컨테이너가 (대상)객체들을 생성할때 의존객체들을 할당해주기 위해서는 대상객체는 의존 객체들을 외부에서 받아들일 수 있는 public 입구(?)가 있어야 한다. 그래야 컨테이너가 대상 객체를 생성해서 공개된 입구로 객체를 할당해 줄 수 있다. 대상객체 내부에서 private으로 생성하는 객체들에 대해서는 컨테이너가 할당해줄 수 없다. 공개된 입구는 어떤 것이 있는가. 바로 public 생성자,public setter 속성,  public 메소드가 있다. 즉 contructor injection, setter injection, method injection DI가 수행될 수 있기 위해서는 각각에 해당하는 public API가 있어야 한다.

constructor injection : 파라미터가 없는 기본 constructor밖에 없다면 DI고 뭐고 수행될 것이 없겠지만, 만약 파라미터가 있는 공개 constructor라면 constructor injection의 대상이 될 수 있다. 즉 대상 객체를 컨테이너가 생성할때, 파라미터에 해당하는 필요한 인자들이 있다면 컨테이너가 알아서 스스로 인자들을 생성해서 대상 객체를 생성하는데 사용하는 것을 constructor injection이라고 한다.

namespace X.Y

{

    public class Foo

    {

        public Foo(Bar bar, Baz baz)

        {

            //...

        }

    }

    public class Bar

    {

        //...

    }


    public class Baz

    {

        //...

    }

}

코드에서는 Foo의 객체를 생성할때 Bar타입의 bar객체와 Baz 타입의 baz객체를 이용하고 있다. 코드에서 Foo객체를 요구하면 bar, baz를 자동으로 생성해서 이것들을 이용해서 Foo객체를 생성해서 반환해준다.

constructor injection의 대상이 되려면 대상 객체와 constructor의 파라미터들이 모두 컨테이너가 인식할 수 있도록 설정되어 있어야 한다. 설정 방법은 조금 후에 앞에서 봤던 예제의 web.config의 내용으로 알아볼 것이다.

setter injection : 대상객체가 필요한 의존 객체를 대상 객체에 할당해 주기 위해서는 당연히 getter 속성 메소드는 이용할 수 없다. 따라서 property injection이라고 해도 setter injection을 의미하게 된다. 그러나 property injection같은 용어는 아직 보지 못했다. setter 메소드를 이용해서 의존 객체를 할당하는데, 물론 쓰기 가능한 모든 속성을 자동 할당하지는 않는다. 대상 객체를 생성하고 나서 그 객체의 속성중에서 설정을 통해서 요청한 속성에 대해서만 injection이 발생한다.

대상객체를 의존 객체들로 초기화하기 위해서 constructor DI를 사용할지 setter DI를 사용할지에 대한 규칙은 없다. 직접 개발을 하고 있는 상태라면 개발자의 기호에 맞게 사용하면 될 것 같다. 그러나 이미 개발되어 있는 3th파티 클래스의 코드를 구할 수가 없다면 이미 구현되어 있는 상태에 따라 달라질 수 밖에 없다. 예를 들어 공개된 속성이 없고 모든 의존 객체들을 대상 객체 constructor에서 모두 받는다면 어쩔 수 없이 contructor DI를 사용해야 할 것이다.

constructor injection, setter injection에 비해서 method injection은 그렇게 많이 사용되지 않는다. 이것에 대해서는 추후에 알아보겠다.

이제 constructor, setter injection에 대한 설정을 알아보도록 하자.

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


   <!-- Aspect -->

   <object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

    <property name="Level" value="Debug"/>

  </object>

   <!-- Service -->

   <object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>

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

    <property name="target" ref="calculator"/>

    <property name="interceptorNames">

      <list>

        <value>CommonLoggingAroundAdvice</value>

      </list>

    </property>

  </object>

</objects>

<objects/>요소안에 3개의 객체가 등록되어 있다. 첫번째 등록된 객체를 보면, 어셈블리 Spring.Aspects에 포함되어 있는 Spring.Aspects.Logging.CommonLoggingAroundAdvice 타입이 CommonLoggingAroundAdvice라는 id로 등록되어 있다. 그리고 이 객체는 public 속성으로 Level 이 있는데, 이것의 값을 "Debug"로 설정하고 있다. 지금은 Spring.Aspects.Logging.CommonLoggingAroundAdvice 타입이 뭐하는 녀석인지 몰라도 된다.

애플리케이션의 코드상에서 이 객체( id="CommonLoggingAroundAdvice")를 요청하게 되면 컨테이너는 일단 주어진 타입 정보를 통해서 인스턴스를 하나 생성하고 나서 Level 속성에 Debug 값을 설정한다.
애플리케이션이 시작되면 CommonLoggingAroundAdvice 인스턴스가 하나 생성되고( 기본적으로 Spring.NET 컨테이너에 생성되는 객체는 singleton이다), Level 속성에 Debug값을 설정한다.

근데 CommonLoggingAroundAdvice 타입의 Level 속성을 보면 반환값의 타입이 enum LogLevel 라는 열거형을 사용하고 있다. 그러나 여기에 할당될 값은 value어트리뷰트에 문자열 "Debug"이 설정되어 있다. 문자열을 LogLevel이라는 열거형에 할당할 수 있나? 없다. 그럼 어떻게 ? 똑똑한 Spring.NET 컨테이너는 대상 객체의 Level 속성이 LogLevel 열거형이라는 것을 알고 "Debug"를 열거형의 문자열 값중의 하나로 인식해서 LogLevel.Debug를 속성에 할당해준다. 즉 문자열 "Debug"를 열거형 값 LogLevel.Debug로의 변환을 자동으로 해준다. 이 과정에 TypeConvert라는 것이 사용되고 있다는 것을 알아두고 나중에 더 깊이 있는 공부를 할 필요가 있겠다.

두번째 객체는 "calculator"라는 id로 Spring.Calculator.Services.AdvancedCalculator 타입의 객체가 등록되고 있다. 이 객체에서는 DI를 위한 설정은 없다. 단순히 객체 등록만 하고 있다.

세번째로 등록되는 객체에서도 두 개의 setter injection을 위한 설정이 있다.  ProxyFactoryObject 타입을 보면 공개된 두개의 속성 Target, InterceptorNames이 있다. 대소문자는 구분하지 않고 있다.  대상객체 ProxyFactoryObject의 Target 속성은 object 타입을 받는 속성이다. <property/>요소의 ref 어트리뷰트를 사용해서 Target 속성에 할당될 객체를 지정하고 있다. ref 어트리뷰트의 값으로는 XML에 설정된 다른 객체의 id(또는 name)값을 지정해주면 된다. 두번째 속성 InterceptorNames는 문자열 배열 string[]이다. 문자열 배열을 지정할때는 <list/> 하위 요소를 사용해서 필요한대로 <value/>요소를 사용해서 값을 추가하면 된다.

ProxyFactoryObject를 코드에서 요청할때 컨테이너는 인스턴스를 하나 생성하고나서 두개의 공개 속성을 통해서 초기화를 마친 후 준비된 객체를 코드에 반환한다.

예제에서는 나와 있지 않지만, 앞에서 보인 Foo 객체에 대한 constructor injection을 위한 설정을 보이면 다음과 같다.

<object id="fooObject" type="X.Y.Foo, X.Y">

  <constructor-arg name="bar" ref="barObject"/>

  <constructor-arg name="baz" ref="bazObject"/>

  <!--추가된 인자-->

  <constructor-arg name="arg" value="strArg"/>

</object>

<obejct id="barObject" type="X.Y.Bar,X.Y"/>

<object id="bazObject" type="X.Y.Baz,X.Y"/>

컨테이너가 생성되면서 설정된 모든 객체들에 대한 정보(Object definitions)들이 컨테이너에 로딩된다.  그리고 나서 DI가 수행되는 것은 실제로 대상 객체에 대한 인스턴스가 요청될때이다. DI가 수행될때 의존 객체들은 XML 설정 파일이 아니라 컨테이너의 로딩된 object definitions에서 검색된다. 따라서 XML상 의존 객체들이 대상 객체보다 뒤에 설정되어 있어도 상관없다.

앞에서 소개한 문서의 5.3. Dependencies 절을 보면 알겠지만, DI를 위한 설정 표현이 여러 버전이 있다. 예를 들어 다음과 같은 표현도 있다.

<property name="target">

   <ref object="calculator"/>

</property>

각기 표현마다 다른 장단점이 있을 수 있다. 또한 이곳에는 기타 여러가지 설정을 위한 요소 및 어트리뷰트에 대해서 설명한 내용이 있다. 한번 정독해보는 것도 좋을 듯 싶다.

오늘은 여기까지.  자자!

추가 설명

컨테이너에 등록을 했으면 이제 코드에서 객체를 얻는 API도 필요할 것이다. Spring 컨테이너에 등록( 또는 생성)되어 있는 객체에 대한 참조를 얻는 API는 다음과 같다.

IApplicationContext ctx = ContextRegistry.GetContext();

IAdvancedCalculator firstCalc = (IAdvancedCalculator) ctx.GetObject("calculatorService");

IApplicationContext는 System.Core.dll의 Spring.Context 네임스페이스에 포함되어 있다.

Posted by dalbong2

이 예제는 Spring.NET 레퍼런스 문서의 QuickStart에서 설명되고 있는 예제중의 하나이다. 3.1.7절에 나와 있다.

이 예제를 통해서 설명할 주용 내용은 다음과 같다.

▶컨테이너에 객체들을 등록하는 설정

- 이게 무슨 말인지 기억나는가? Unity Application Block의 컨테이너 프레임워크에서와 유사 아니 동일한 개념의 작업이다. 물론 객체를 등록하는 설정 표현(syntax)은 다르다.

▶DI(dependencies Injection) 설정

- Unity Application Block의 setter injection, constructor injection을 기억하는가?

▶AOP 설정

- aspect(Cross concerns )를 메인 로직에 weaving(minxin)하는 작업에 대한 예제를 보게 될 것이다. aspect중에서 예제에서는 로깅 aspect가 예로 보여진다.

▶ 음...그리고 필요한대로.

- 다음 예제에서는 설명할 것이 아주 아주 많다. 간단한 샘플 코드를 보지도 않고 바로 예제로 건너뛰었기때문에 앞에서 말한 항목들외에도 Spring.NET의 기본 설정들에 대해서 먼저 설명이 필요할 것이다. 그리고 Spring.NET에서 제공하는 Web Services관련 기능도 이해해야 한다. ASP.NET Web Services를 만들때 지금까지 .asmx 파일을 추가했어야 했지만 Spring.NET에서는 그럴 필요가 없다. 이런 저런 얘기를 하다보면 다음 예제를 완전히 알아보는데 몇 회의 포스트가 필요할 것이다.

우선 예제 프로젝트를 보면 다음과 같다.

■ 프로젝트 구조

■ 프로젝트 설명

프로젝트 설명
Spring.Calculator.Contract 계산기의 기본적인 오퍼레이션을 정의하고 있는 인터페이스 ICalculator를 포함하고 있다. 그리고 IAdvancedCalculator도 포함하고 있는데 계산 결과가 저장될 메모리 계산에 사용될 인터페이스 메소드들이 정의되어 있단다. 그리고 이 프로젝트에는 도메인 객체 DivisionResult도 포함하고 있다. 도메인 객체라면 여러 레이어(주로 3계층의 애플리케이션이 주로 개발된다)에 걸쳐서 참조되는 객체들이다. 비즈니스 인터페이스와 도메인 객체는 서버측( 리모팅 서버, 웹 서비스 서버)과 클라이언트측( 윈폼 애플리케이션, 웹 애플리케이션)에서 사용될 것이기에 별도의 프로젝트에 분리시켜 놓고 있다.
Spring.Calculator.Services ICalculator와 IAdvancedCalculator에 대한 구현이 포함되어 있다. 이 구현이 서비스로서 노출될 것이다. 각각의 구현 클래스의 이름은 Calculator, AdvancedCalculator로 되어 있다. AdvancedCalculator는 IAdvancedCalculator를 구현했을뿐만 아니라 Calculator를 상속하고 있다. 필자는 솔직히 AdvancedCalculator가 확실히 정의되지 않는다. -_-;; 몰라도 상관없을 것 같기에 그냥 넘어간다.
Spring.Calculator.RemoteApp 리모트 객체 AdvancedCalculator 인스턴스를 호스팅할 서버측 애플리케이션을 포함하고 있다.
Spring.Aspects 리모트 객체에 적용할 로깅 advice들을 가지고 있다. 이 advice들을 통해서 AOP 프로그래밍을 적용하는 예를 보여줄 것이다.
Spring.Calculator.ClientApp 클라이언트측 애플리케이션을 포함하고 있다.  그러나 이번 예제에서는 사용하지 않을 것이다.
Spring.Calculator.Web Spring.Calculator.ClientApp대신에 클라이언트 애플리케이션으로 사용한다.

■ 프로젝트의 아키텍쳐

그림이 좀 이상하긴 하지만... 그림에서 나타내려고 하는 것은, 앞의 예제 프로젝트에서는  하나의 비즈니스 객체( Spring.Calculator.Services 프로젝트의 AdvancedCalculator 객체)를 .NET Remoting 서비스로도 노출시키고 웹 서비스로도 노출시키고 있다는 것을 보여주려고 했다. 그리고 그 객체를 COM+에 등록해서 사용하고 있다.

이 구조는 상황에 따라서 조금씩 변경된다. 웹 서비스를 이용해서 타겟 객체에 접근한다고 했을때 클라이언트는 웹 브라우저가 아니라 웹 애플리케이션을 나타내고 있다.  또 상황에 따라서는 .NET Remoting 애플리케이션, 웹 서비스 애플리케이션, COM+애플리케이션이 참조하는 타겟객체는 실제 객체에 대한 참조가 아니라 타겟 객체의 프락시에 대한 참조일 수도 있다.

클라이언트 애플리케이션은 타겟 객체에 대한 인터페이스를 호출하고 있고 서버측 애플리케이션도 타겟 객체에 대한 인터페이스를 참조하고 있다. 이때 인터페이스는 타겟 객체과 구현하고 있는 순수한 인터페이스는 아니다. 물론 순수한 인터페이스를 참조할 수도 있지만 현재 샘플에서는 AOP를 적용하고 있다. 타겟 객체를 호출할때 로그를 남기는 작업을 수행하고 있다. 이 구조에서 좀 더 정확히 표현하자면 "AOP 인터페이스"가 될 것이다. advice 코드가 적절한 pointcut에 weaving된 형태의 인터페이스이다. AOP 인터페이스의 메소드를 호출하게 되면 호출 전 or/and 후에 로그가 남게 된다. 이게 무슨 말인지는 뒤의 포스트에서 설명되고 있다: 시리즈 25 참조.

여튼 앞에서 구성하고 있는 Visual Studio.NET 솔루션에 포함된 프로젝트들의 전체적인 구조는 이렇다는 것이고 필요하다면 상황별로 자세한 설명을 하도록 하겠다. 그러나 이 중에서 웹 서비스를 호출하는 구조를 중심으로 알아볼 것이다.

■ configuration

Spring.Calculator.Web.2005 프로젝트의 web.config 파일을 보면 다음과 같다. 원래 소스에서는 logging섹션이 spring섹션보다 먼저 나오는데, spring섹션을 먼저 설명해야 할 것 같아서 순서를 조금 변경했다.

<?xml version="1.0"?>

<configuration>


  <configSections>

     <sectionGroup name="spring">

      <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/>

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

    </sectionGroup>

    <sectionGroup name="common">

      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />

    </sectionGroup>

    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>

  </configSections>


  <spring><!-- 이번 포스트에서 설명한다-->

     <context>

      <resource uri="config://spring/objects"/>

      <resource uri="~/Config/webServices.xml"/>

      <resource uri="~/Config/webServices-aop.xml"/>

    </context>


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

      <description>Definitions of objects to be exported.</description>


      <!-- Aspect -->


      <object id="CommonLoggingAroundAdvice" type="Spring.Aspects.Logging.CommonLoggingAroundAdvice, Spring.Aspects">

        <property name="Level" value="Debug"/>

      </object>


      <!-- Service -->


      <object id="calculator" type="Spring.Calculator.Services.AdvancedCalculator, Spring.Calculator.Services"/>


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

        <property name="target" ref="calculator"/>

        <property name="interceptorNames">

          <list>

            <value>CommonLoggingAroundAdvice</value>

          </list>

        </property>

      </object>


    </objects>

   </spring>


  <system.web> <!-- 나중에 설명한다.-->

    <httpHandlers>

      <add verb="*" path="*.asmx" type="Spring.Web.Services.WebServiceHandlerFactory, Spring.Web"/>

    </httpHandlers>

    <httpModules>

      <add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/>

    </httpModules>

    <compilation debug="true"/>

    <customErrors mode="RemoteOnly"/>

    <authentication mode="Windows"/>

    <authorization>

      <allow users="*"/>

    </authorization>

    <trace enabled="false" requestLimit="10" pageOutput="true" traceMode="SortByTime" localOnly="true"/>

    <sessionState mode="InProc" stateConnectionString="tcpip=127.0.0.1:42424" sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout="20"/>

    <globalization requestEncoding="utf-8" responseEncoding="utf-8"/>

  </system.web>


  <common> <!-- 나중에 설명한다.-->

    <logging>

      <factoryAdapter type="Common.Logging.Log4Net.Log4NetLoggerFactoryAdapter, Common.Logging.Log4Net">

        <!-- choices are INLINE, FILE, FILE-WATCH, EXTERNAL -->

        <!-- otherwise BasicConfigurer.Configure is used -->

        <!-- log4net configuration file is specified with key configFile -->

        <arg key="configType" value="INLINE" />

      </factoryAdapter>

    </logging>

  </common>


  <log4net> <!-- 나중에 설명한다-->

    <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">

      <file value="Logs/log.txt"/>

      <appendToFile value="true"/>

      <rollingStyle value="Size"/>

      <maxSizeRollBackups value="10"/>

      <maximumFileSize value="100KB"/>

      <staticLogFileName value="true"/>

      <lockingModel type="log4net.Appender.RollingFileAppender+MinimalLock"/>

      <layout type="log4net.Layout.PatternLayout">

        <conversionPattern value="%date [%-5level] %logger - %message%newline"/>

      </layout>

    </appender>

    <!-- Set default logging level -->

    <root>

      <level value="DEBUG"/>

      <appender-ref ref="RollingFileAppender"/>

    </root>

    <!-- Set logging for Spring.Aspects -->

    <logger name="Spring.Aspects">

      <level value="DEBUG"/>

    </logger>

    <!-- Set logging for Spring.Calculator -->

    <logger name="Spring.Calculator">

      <level value="DEBUG"/>

    </logger>

    <!-- Set logging for Spring -->

    <logger name="Spring">

      <level value="INFO"/>

    </logger>

  </log4net>


</configuration>

■ configuration과 Spring 컨테이너

configuration 구조는 .NET의 표준 구조를 그대로 이용하고 있다. web.config를 이용하고 있으니 당연한 이야기다. 대부분 Spring.NET용 설정만 포함되어 있지만, 기존에 알고 있는 web.config 설정 요소들도 포함될 수 있다.  system.web 섹션을 보면 httpHandler와 httpModule을 등록하는 ASP.NET 설정이 포함되어 있다. spring.net 관련 설정은, spring섹션을 보면 알겠지만 Spring만의 configuration 내용을 별도의 파일에 xml형태로 저장할 수도 있다.

이렇게 설정된 정보들은 Spring 컨테이너가 생성되면서 모두 컨테이너에 읽혀진다. 다음은 레퍼런스 문서에 나와있는 Spring 컨테이너를 설명하는 그림이다.

먼저 spring 섹션 중심으로 설명을 할 것이다. 이 섹션이 Spring 컨테이너를 이해하는 기본 설정을 포함하고 있기 때문이다. system.web, common/logging, log4net 섹션은 나중에 설명한다. 미리 간단하게나만 설명하면 system.web 섹션에 등록되어 있는 httpHandlers때문에 개발자는 asmx 파일을 만들지 않고도 웹 서비스를 노출시킬 수 있게 된다. 그리고 common/logging, log4net은 실제로 로깅을 구현하는 모듈과 로깅 정책을 등록하는 곳이다.

애플리케이션이 시작되면 Spring 컨테이너는 configuration 설정( 문서에서는 "configuration 메타데이터" 라고 하고 있다 )을 읽여들이는데, configuration 설정은 Spring 컨테이너에게 객체를 어떻게 생성하고(예를 들어, singleton으로 생성할지 prototype(일반 객체)으로 생성할지), 객체 생성에 필요한 정보 등을 알려주는 역할을 한다. Spring 컨테이너는 이런 설정 정보를 이용해서 애플리케이션에서 제공하고 있는 클래스들을 이용해서 필요하면 즉시 객체를 생성하는등 시스템이 가동될 준비를 마친다.

■ 컨테이너 생성에 필요한 설정들 - 리소스

실제로 Spring 컨테이너 객체를 생성할때는 첫번째로 필요한 것이 컨테이너를 설정할 리소스들에 대한 정보이다. 이런 리소스들이 대부분 객체들에 대한 정의가 될 것이다. 리소스들이 포함되어 있는 경로에 대한 정보는 <context/resource/>요소로 표현되는데, 이것을 파싱히는 녀석은 <configSections/>에 등록되어 있는 context 섹션 핸들러 WebContextHandler이다.

<configSections>

  <sectionGroup name="spring">

    <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/>

  </sectionGroup>

</configSections>


<spring>

  <context>

    <resource uri="config://spring/objects"/>

    <resource uri="~/Config/webServices.xml"/>

    <resource uri="~/Config/webServices-aop.xml"/>

  </context>

</spring>

WebContextHandler가 Spring 컨테이너를 생성할때 필요한 객체들에 대한 정보는 <context/resource/>요소들을 통해서 얻는다. 이 예제에서는 Spring 컨테이너에 로딩될 객체들에 대한 정보를 <context/resource/>요소로 읽어들이고 있고 이것이 이 요소의 가장 일반적인 용도이다.  그렇지만 다른 리소스도 이 요소를 통해서 로딩할 수 있다.

<context/resource/>요소는 현재 컨테이너 컨텍스트( 애플리케이션 컨텍스트가 아니다. 애플리케이션과 컨테이너는 의미가 다르다. 뒤에 설명할 기회가 있을것이다. 있을가? )을 구성하는 리소스들에 대한 경로를 가지고 있는데, 리소스에 따라서 그것에 접근할 수 있는 다양한 경로 포맷을 갖는다. 리소스는 파일일 수도 있고, 어셈블리내에 포함된 리소스일 수도 있다. 현재 지원되고 있는 리소스의 종류 및 접근 포맷은 다음과 같다.

리소스 접근 포맷 접근 포맷 파싱 모듈
어셈블리에 포함된 데이터 assembly://<AssemblyName>/<NameSpace>/<ResourceName> AssemblyResource
.NET configuration 파일( web.config등)에 포함되어 있는 있는
커스텀 configuration 섹션에 저장된 데이터
config://<path to section> ConfigSectionResource
파일 시스템의 파일 file://<filename> FileSystemResource
Http, Https 프로토콜로 접근할 수 있는 데이터 표준적인 http, https 표현 UriResource

표에서 리소스컬럼은 어떤 리소스인지를 설명하고 있고 접근 포맷은 그 리소스에 접근하기 위해서 사용하고 있는 Uri 포맷을 나타낸다. 그리고 참조고 마지막 컬럼에 접근 포맷을 해석해서 해당 리소스에 접근해서 읽어오는 모듈명을 표시했다. 앞의 예제 web.config에서는 configuration 섹션의 리소스와 http 리소스를 이용하겠다고 설정하고 있다( 근데 "~"로 시작하는 리소스 접근 포맷이 파일 접근 포맷인지, http 접근 포맷인지 확신은 없다. 다만 ASP.NET에서 사용하는 "~"가 나타내는 것이 웹 경로인 것을 보면 http 리소스 일것으로 추측된다 ).

예제에서는 <context/resource/>에서 표시하는 리소스 경로의 내용을 읽어들여보면 모두 <objects/> 요소를 가지고 있다. 리소스 핸들러는 xml 파일이나 <objects> 섹션에 포함된 리리소스들이 모두 객체들에 대한  정의라는 것을 알게 된다. 핸들러는 <spring/objects/> 섹션에 설정되어 있는 <object/>들뿐만 아니라 두개의 xml 파일에 정의되어 있는 <object/>요소들을 읽어들인다. 필자도 이곳에 정의되어 있는 객체들에 대해서 아직은 잘 모른다. 이때 <objects/>를 파싱하기 위해서 호출되는 핸들러가 바로 <configSections/>에 등록된 DefaultSectionHandler이다.

  <configSections>

    <sectionGroup name="spring">

      <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/>

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

    </sectionGroup>

  </configSections>


  <spring>

     <context>

      <resource uri="config://spring/objects"/>

    </context>

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

     ...

    </objects>


  </spring>

두 섹션 <spring/context/>와 <spring/objects/>이 바로 Spring 컨테이너를 정의하는 섹션이다. 두 섹션이 바로 new를 통한 컨테이너 객체 생성 코드에 해당한다는 것이다( 실제로 프로그램적으로도 컨테이너 객체를 생성할 수있는 API가 있다). 예제에서처럼 컨테이너를 설정하는 내용이 별도의 xml 파일로도 존재할 수 있는데, 그런 경우에는 컨테이너를 생성할때 해당 파일의 모든 내용들을 읽어들인다.

■ 객체 정의 <object/>요소

Spring configuration은 최소한 하나의 객체 정의를 가지고 있어야 한다. 그러다 대부분 하나 이상의 객체를 정의하는 요소 <object/>가 <objects/>에 포함된다. 이렇게 <object/>요소에 객체에 대한 정보( 타입과 어셈블리등)를 제공하는 것을 "Spring 컨테이너에 객체를 등록한다"고 표현하고 있다. 이 객체 정의 요소는 애플리케이션이 시작되면 실제 객체에 해당된다. 즉 new를 사용해서 생성한 실제 객체에 해당한다. 그러나 이렇게 configuration에 등록하면 Spring 컨테이너가 대신 자동으로 생성해준다.

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

  <object id="..." type="...">

    <!-- 이 객체가 의존하고 있는 다른 객체 그리고 필요한 설정이 있다면 이곳에 표현한다-->

  </object>

  <object id="..." type="...">

    <!-- 이 객체가 의존하고 있는 다른 객체 그리고 필요한 설정이 있다면 이곳에 표현한다-->

  </object>

  <!-- 더 많은 객체 정의가 가능하다.-->


</objects>

      
애플리케이션별로 예를 들어 웹 애플리케이션에서는 이곳에 어떤 객체들을 등록시키수 있을까 하는 문제는 나중에 좀 고민해서 실전 애플리케이션 개발 전략을 다루는 포스트를 별도로 제작해 볼 계획이다. 미리 예상해보면 비즈니스 레어어 또는 데이터 액세스 레이어의 객체들이 이곳에 등록될 수 있지 않을까 하는 추측을 해 볼 수 있겠다. 지금은 Spring.NET 프레임워크의 개념 파악에 집중하도록 한다.

객체를 생성하는데 필요한 정보들이 있다. 어떤 정보들이 필요할까.

* 타입 이름 : 컨테이너에 생성될 객체가 정의되어 있는 실제 클래스( concrete class)

* 객체가 어떻게 거동할 것인가에 대한 설정 즉 singleton이냐 아니면 prototype(singleton이 아닌 객체를 말한다)이냐. 생명주기 콜백함수( 객체가 초기화되거나 해제될때 호출될 함수)가 있다면 어떤 함수인가.

* 객체가 자신의 작업을 하기 위해서 의존하고 있는 다른 객체들이 있다면 그 의존 객체들( dependencies 또는 collaborators로 표현한다)에 대한 정보 설정

* 또 있는데 무슨 말인지 잘 모르겠다-_-;;

이런 정보들을 표현하기 위해서 <object/>의 어트리뷰트 또는 인라인 <object/> ( <object/>하위에 포함된 다른 <object/>요소)및 여러 요소들을 이용하고 있다.

특징 객체 정보
type 객체를 정의하고 있는 클래스 및 클래스를 포함하고 있는 어셈블리명
id/name 등록되는 객체들은 고유한 아이디를 가지고 있다. 고유한 객체를 나타내기 위해서 <object/>요소의 "id" 또는 "name" 어트리뷰트를 사용한다.
singleton 객체를 singleton으로 생성할지에 대한 여부를 알리는 어트리뷰트. 기본값은 true로서 아무 표시가 없으면 기본적으로 singleton으로 생성된다. 즉 컨테이너별로 객체가 하나 생성된다. singleton으로 표시된 객체는 컨테이너가 생성되면서 객체들도 생성된다. singleton="false"로 되어 prototype으로 설정된 객체들은 실제로 코드상에서 객체가 요청될때 생성된다.
<object/>하위요소  
<constructor-arg/>하위 요소  
<property />하위 요소  

뒤의 3개의 하위 요소들은 DI( dependencies injection)과 관련된 설정들이다. 뒤에서 별도로 공간을 마련해서 설명하도록 하겠다( ^^). 퇴근하자. 아자. 졸려!

다 써 놓고 보니 포스트 구조가 좀 이상하다. 샘플을 제일 먼저 보여주고서는 Spring 컨테이너 얘기만 하고 끝나다니. 야튼 이 샘플은 계속 가지고 가겠다.

Posted by dalbong2

Aspect지향 프로그래밍! 프레임워크 입장에서는 아주 쓸모있고 중요한 개념이다. 개발자들의 코딩을 화~악 줄여줄 수 있고 또한 프로젝트가 진행하고 있는 도중에도 개발자들의 코드 수정없이 프레임워크단에서 갑의 요청 사항을 최대한 흡수해 줄 수 있는 완충 역할을 할 수 있는 방법이다.

그러나 얼른 와 닫지 않는 용어이다. Object Oriented Programming이라는 용어를 처음 들어을때도 이런 떨떠름한 기분이었을까 하는 생각이 든다. Object가 뭔지 정의를 정확히 내리라면 머뭇거리게 되지만, 그래도 우리는 이것에 대해 이해는 하고 있다. 문장의 주어 또는 목적어로 사용될 수 있는 "놈"들이다.  "이 녀석의 어떤 메소드를 호출하면 ..." 또는 "저 녀석의 어떤 메소드를 호출해줘야 ~ 할 수 있다"처럼 마치 이야기의 대상처럼 사용할 수 있는 것이 object이다.

그럼 aspect란? longman 사전을 찾아보면 다음처럼 정의되어 있다 : "one part of a situation, idea, plan etc that has many parts". 그리고는 다음과 같은 예문이 나와 있다 : "Dealing with people is the most important aspect of my work. 사람을 다루는 일이 내 일중의 가장 중요한 일이다". "전체중의 부분 또는 전체중의 단면"을 의미한다고 하겠다.

소프트웨어 개발에서의 aspect도 의미적으로는 이와 비슷한 개념으로 정리될 수 있을 것 같다. 비즈니스 로직을 실제로 구현하다보면 필요한 비즈니스 로직 구현외에도 기능성 코딩을 해야 하는 경우가 많다. 예를 들어 로깅이나 예외처리, 트랜잭션처리등은 비즈니스 요구와는 직접적인 상관은 없지만 계속 반복되는 기능들이다. 이런 기능들을 애플리케이션을 만들때마다 또는 하나의 애플리케이션에서 다른 비즈니스 로직을 구현할때마다 계속 반복해서 코딩하기 보다는 처리 모듈들을 "단면!"별로 분리해서 구현하자는 것인데, 이런 각각의 단면 모듈들(로깅, 예외처리, 트랜잭션처리, 보안처리등)을 aspect라 하고 있다.

비즈니스 계층의 메소드를 개발할때 다음과 같은 형식의 코딩에 대한 경험이 있을 것이다.

public void 메소드()

{

    // 메소드 시작 로깅

    // 메소드 호출

    // 메소드 종료 로깅

}

또는 다음과 같은 형식으로 트랜잭션을 처리해본 경험도 있을 것이다.

public void 메소드()

{

    // 트랜잭션 설정 및 시작

    try

    {

        // 비즈니스 로직 구현


    }

    catch

    {

        // 트랜잭션 롤백


    }

}

순수한 비즈니스 로직 구현 코드와 로깅, 트랜잭션 처리 코드가 섞여 있고 이런 부가적인 코드는 메소드마다 복사되어서 사용되었다. 이런 로깅 그리고 트랜잭션 처리 코드는 특정 비즈니스 로직에서만 사용되는 것이 아니다. 아래 그림에서처럼 계좌이체 모듈, 입출금모듈, 이자계산 모듈 등 여러 관심 모듈에 걸쳐서(cross) 공통적으로 필요한 모듈들이다. 따라서 aspect를 cross concerns이라는 용어로도 표현한다. 다음 그림에서는 로깅, 보안, 트랜잭션과 같은 cross concerns 구현하고자 하는 비즈니스 관심 모듈의 관계를 개념적으로 표현하고 있다.

(객체 지향을 넘어서 관점 지향으로 AOP. http://www.zdnet.co.kr/builder/dev/java/0,39031622,39147106,00.htm )

객체 지향에서는 객체를 분리해내고 그것을 설계하는 것이 중요하듯이 aspect 지향에서는 앞에서와 같은 cross concerns을 정의하고 분리해서 설계하는 것이 중요하다. 객체 지향으로 설계된 객체들을 구현하는 툴로서 C++, C#이 있듯이 aspect 지향으로 설계된 aspect들을 메인 로직과 혼합하는 작업을 가능하도록 하는 툴들이 있다. .NET계열에서는 Spring.NET이 그 대표적인 예라 하겠다. 그러나 aspect 자체를 구현한 코드는 객체 지향 언어를 사용해서 구현한다.

비즈니스 로직과 그것을 구현하기 위한 핵심 클래스 및 메소드는 객체 지향 설계(OOA)로 도출될 수 있다. 그러나 이런 aspect들은 이런 객체 지향 방법론으로 도출할 수 없었다. 그러니 계속해서 같은 목적( 로깅, 예외처리, 트랜잭션 처리)을 갖는 코드가 조금씩 변경되어서 copy&paste 방식으로 이곳 저곳에서 반복되어서 삽입될 수 밖에 없었지만, aspect지향의 컨셉과 그것을 구현할 수 있는 툴들의 제공으로 이제는 코드가 좀 더 깔끔하게, 좀 더 비즈니스 중심으로 될 수 있게 된것이다.

이쯤되면, AOP란 OOP를 대신하는 프로그래밍 기법이 아님을 인식할 수 있었으리라 본다. 오히려 OOP를 기본으로 하되 그것이 처리할 수 없는 부분을 보충해주는 프로그래밍 방법이라 하겠다.

"AOP를 구현한다"는 것은 "분리된 cross concerns을 실제로 코드로 구현하고 그 코드를 필요한 비즈니스 관심 모듈의 적절한 위치에 삽입하는 작업"이라고 할 수 있겠다. 이런 AOP 구현을 이해하기 위해서는 이해해야하는 하위 개념들이 있다. 이런 개념들은 조금은 낯선 용어들로 표현되고 있다.

advice(또는 interceptor)
advice가 바로 앞에서 말한 "단면"을 구현한 코드이다. 즉 로깅, 트랜잭션등을 구현한 코드를 말한다. 이것을 interceptor라고도 한다. 두 표현 모두 옆에서 치고 들어오는 것들 표현하고 있다. advice 즉 충고 또는 훈수라는 것도 옆에서 갑작스레 치고 들어오는 것은 마찬가지다. 메인 비즈니스 로직에 추가되어 부가적인 훈수를 두는 코드를 말한다.

joinpoint

advice가 치고 들어올 수 있는 포인트들이다. cross concerns 모듈의 메인 비즈니스 로직에 삽입이 가능한 후보 위치를 말한다. 비즈니스 로직을 구현한 메소드가 호출되기 전 또는 후, 반환값이 반환되기 전 , 예외가 던져지는 지점, 클래스가 초기화되는 곳, 필드를 액세스하는 부분등이 모두 advice가 삽입될 수 있는 후보 포인트들이다. 그러나 모든 jointpoint가 실제로 advice가 삽입되는 곳은 아니다.

pointcut

joinpoint중에서 실제로 advice가 적용될 위치를 나타낸다. joinpoint가 개념적인 것이라면 툴마다 실제로 구현하고 있는 pointcut은 다를 수 있다. 뒤에서 보게 되겠지만 특정 pointcut를 나타내는 타입들이 Spring.NET에도 이미 구현되어 있다.

advisor

pointcut + advice를 말한다. 즉 어디서(where, pointcut) 무슨 일(what, advice)이 일이 일어날지를 정의한다. advisor가 바로 aspect의 실제 구현된 모습이라고 할 수 있다.

advised object /advised method

문서를 보다 보면 advised된 객체 또는 메소드라는 말을 보게 된다. advice 코드가 삽입된, 적용된 객체 또는 메소드라는 의미이다. advisor에 의해 훈수를 받은 객체 또는 메소드라는 것이다.

AOP는 일반적인 프로그래밍 방법이다. 즉 Spring.NET만의 개념은 아니다.  Spring.NET에서는 이런 AOP 개념들을 모두 구현하기 위한 방법을 제공하고 있지만, Spring.NET의 IoC컨테이너는 이 AOP 기술에 의존하고 있지는 않다. 즉 Spring.NET 사용자는 원한다면 AOP를 사용하지 않아도 된다는 것이다.

그러나 반복되는 코드를 단지 어트리뷰트를 사용해서 선언적(declarative)인 방식으로 해결할 수 있다면 코드가 깔끔해질 수 있을 것이고 유지, 보수에도 효과적인 방법이 될 수 있을 것이다. Spring.NET에서는 AOP를 구현할 수 있는 모든 준비를 갖춰놓고 있다. 사용자는 이제 HOW-TO만 배우면 되는 것이다.

나중에 알게 되겠지만, Spring.NET에서 AOP 개념은 프락시를 이용하고 있다. 그리고 프락시에 대한 소유권은 프레임워크에서 가지게 된다. 개발자가 타겟 객체를 요구할때 프레임워크에서는 그 객체에 대한 프락시를 반환하는 패턴을 이용하게 된다. 이 프락시를 잘 이용하면 개발자의 일명 삽질이 상당히 줄어들 수 있다.

프락시 코드를 프레임워크단이 가진다는 의미는 타겟 객체의 메소드 호출을 프레임워크단에서 모두 catch할 수 있다는 것인다. 즉 실제로 타겟 객체의 메소드 호출을 수행하기 전에 그리고 메소드 호출을 수행하고 나서의 순간들을 모두 프레임워크에서 포착할 수 있게 되어 필요한 작업을 할 수 있다. 필요한 작업이란 예를 들어 타겟 메소드를 호출하기 전에 타이머를 실행시켜 놓은 다음 타겟 메소드의 호출이 종료된 후 타이머의 시간을 재서 메소드의 실행 시간을 체크할 수도 있다는 것이다. 또 다른 예로 프락시를 통해서 타겟 메소드에 대한 정보를 얻어서 적절한 로그를 남기는 작업을 프레임워크단에서 처리할 수도 있다. 이런 작업들이 개발자들의 코드 수정없이 프레임워크단에서 일괄적으로 처리될 수 있다는 것이다. 즉 프락시 패턴을 이용하게 되면 프로젝트 진행 도중에 비즈니스 로직과 상관없는 추가 요구 사항은 최대한 프레임워크단에서 커버할 수 있는 구조가 된다는 것이다.

다음 포스트에서는 AOP 관련 샘플을 통해서 Spring.NET이 지원하는 방법을 알아보겠다. 앞에서 보이지 못한 IoC 예제 코드 즉 객체를 등록하고 설정하는 방법에 대한 것도 이 예제에서 함께 설명하도록 하겠다.

Posted by dalbong2

앞으로 포스트를 진행해가는 방법으로는 개념 설명과 그 개념에 필요한 샘플을 적절히 혼합해가는 방법을 사용하겠다. IoC, AOP처럼 새로 등장하는 개념들은 먼저 설명을 하겠다. 그리고 Spring.NET에서 제공하는 다른 유틸성 기능은 어떻게 사용하는지에 대한 샘플 코드로 바로 들어갈 것이다.  필요한 코드들은 sourceforge.net에서 제공하는 샘플들을 사용하도록 하겠다.

이번 포스트에서는 IoC에 대해서 먼저 알아볼 것이다. IoC? 어디서 많이 들어본 것 같은가? 국제 올림픽위원회 ? No ! Inversion of Control의 약자로서 "역제어" 정도로 해석될 수 있겠는데 영 어색하다. 의미는 이렇다. 기존의 프로그래밍에서는 객체 생성의 제어권을 개발자가 가졌다면 그 제어권이 이제 "반대측"으로 넘어갔다는 것이다. 여기서 "반대측"이란 바로 컨테이너를 말한다.  Spring.NET도 컨테이너 프레임워크중의 하나로서 Spring.NET 컨테이너가 객체를 생성하고 그 생명주기를 제어한다는 것이다.

앞에서 알아본 Unity Application Block 또한 컨테이너 프레임워크중의 하나로서 IoC를 구현하고 있는 프레임워크중의 하나이다. 컨테이너에서  객체들을 생성하고 lifecycle, scope를 관리하고 또는 적절한 곳에서 필요하다면 dependency를 inject해주는 역할을 한다. dependencies inject하면 setter injection, contructor injection 그리고 method injection이 있다는 것을 이전 포스트에서 알아봤다.

개념적으로는 어려울게 없다. IoC 하면 "컨테이너 프레워크", "dependency injection"을 떠올리면 된다. 객체를 컨테이너에 등록하는 절차 및 injection이 일어날 곳 그리고 어떤 dependencies가 어느 위치에서 inject될지에 대한 정보를 configuration을 통해서 할 수 있다. 물론 프로그램적으로도 가능하지만 configuration을 이용하는 방법이 더 실질적인 방법이다. 여기까지만 떠올릴 수 있다면 개념은 잡힌 것이다.

IoC 또는 DI(dependencies injection)이라는 용어를 누가 만들었고 어떤 차이점이 있는지는 지금 단계에서는 그닥 중요하지  않다. 나중에 내공이 생기고 관심이 있다면 좀 더 개인적으로 찾아보면 될 것이다.

다음 포스트에서는 아주 유용한 개념을 소개할 것이다. AOP( Aspect Oriented Programming)!

Posted by dalbong2

이제 다시 개발 프레임워크 얘기로 가 보도록 하겠다. 이제부터는 Spring.NET 프레임워크를 알아볼 것이다. 앞에까지는 Unity Application Block을 알아봤는데 사실 필자는 이것보다 Spring.NET 프레임워크에 대해서 먼저 들었다. 그러나 마음먹고 공부해본적은 없다. 알고 있는 것은 단지 오픈 소스 프로젝트라는 것 그리고 Spring이라는 이름으로 자바쪽에서 먼저 나왔고 Spring.NET 프레임워크는 자바 버전이 .NET쪽으로 포팅된 것이라는 것 정도이다.

며칠동안 틈나는 대로 Spring.NET 레퍼런스를 읽어보고 있다.  www.springframework.net에 가보면 문서 및 관련 소스를 받아 볼 수 있다. 원서라서 속도가 나질 않아서 아직 다 읽어보지는 못했다. Unity 블럭에서처럼  포스팅을 하면서 공부를 해야 할 것같다. Spring과 관련된 자바진영의 문서는 꽤 있는 듯하다. 그래서 개념은 자바진영의 문서를 통해서 잡고 구현만 .NET진영의 문서를 참조해야 겠다고 생각했다. 그래서 한글로 된 자바진영의 책을 먼저 읽었고 다음으로 앞의 사이트에서 다운받은 레퍼런스 문서로 공부하고 있는 중이다. 이 문서의 메인 주제들을 정리해보면 다음과 같다.

▶ IoC 개념

▶ Configuring

  - configuring object with xml

  - creating objects automatically

  - using parent and child object definition

▶ 객체 Scope

▶ 객체 생명 주기 관리 - 생명주기 관련 interfaces

- IInitializingObject/ init-method

- IDisposable / destory-method

- IObjectPostProcessor

▶ Spring.NET 커스터마이징

- 객체 생성 -> IObjectPostProcessor.PostProcessBeforeInitailization -> IInitializeingObject/init-method의 콜백메소드 AfterPropertiesSet -> IObjectPostProcessor.PostProcessAfterInitailization

▶ 메세지 리소스 관리/사용하기

▶ Validation framework

▶ Aspect Oriented Programming with Spring.NET

▶ 트랜잭션 관리

▶ 예외처리

▶ Object Relational Mapping( ORMapping )

▶ Spring.NET Web Framework

▶ ASP.NET Ajax

▶ Enterprise Application에 Spring.NET 적용 전략

▶ Testing

이 주제들 하나 하나가 모두 굵직 굵직하다. 이것들을 하나씩 붙잡고 개념과 구현을 설명해나가야 할까하는 문제는 아직 결정을 내리지 못하고 있다. 이것들을 모두 설명하기에는 너무 많은 시간이 소모될 것 같고 그다지 투자 대비 효과도 좋지 않을 것 같다는 생각이다.

그렇다면 개념별 Quick start 샘플 중심으로 갈 것이냐 아니면 하나의 샘플로 시작해서 개념들을 완성시켜나가는 방식으로 갈 것이냐. 아니면 Spring.NET 사이트에서 제공하고 있는 샘플 중심으로 공부를 해 나가야 할 것이냐. 아직 결정하지 못했다.

다음 포스트가 언제 올려질지는 모르겠지만, 그 포스트가 올려질때 Spring.NET 스터디 진행방법도 결정될 것으로 보인다.

Posted by dalbong2