본문 바로가기

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

개발 프레임워크 만들기 대장정 43 - WCF 확장하기 II (사용자정보 전달하기)

이제 클라이언트측 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의 컨트롤과 관련되어 있다. 이것에 대해서는 어떻게 설명해야 할지 정리를 좀 해야겠다.