태터데스크 관리자

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

태터데스크 메시지

저장하였습니다.

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

 

■ 서비스 참조 추가하기

 

우선 서비스에 대한 참조를 클라이언트 프로젝트에서 추가하자. 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