본문 바로가기

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

개발 프레임워크 만들기 대장정 42 - WCF 확장하기 I (사용자 정보 받기 )

앞의 포스트까지는 메뉴를 클릭했을 때 해당 업무 화면을 로딩하는 것까지 진행했다. 이제 업무 화면에서 서비스를 호출하는 기능을 구현하도록 하자. 이때 개발 프레임워크단에서는 흔히 서비스를 호출할 수 있는 프락시 객체를 제공한다. 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에 생성되어 있어야 샘플코드가 실행된다.