출처: http://www.microsoft.com/korea/msdn/msdnmag/issues/06/10/WCFEssentials/default.aspx#S1

목차
단방향 작업
콜백 작업
클라이언트 콜백 설정
콜백 재입력
이벤트
게시-구독 프레임워크
영구 구독자 관리
이벤트 게시
영구 구독자 관리
대기 중인 게시자 및 구독자
결론

기존의 개체 및 구성 요소 지향 프로그래밍 모델은 클라이언트에서 메서드를 호출하는 방법을 하나만 제공합니다. 클라이언트가 호출을 발행하고 호출이 진행되는 동안에는 차단된 다음, 해당 메서드가 반환되면 실행을 계속하는 방식입니다. WCF(Windows® Communication Foundation)는 이러한 기존 호출 모델을 지원하는 동시에 두 가지 추가 작업 유형도 기본으로 지원합니다. 즉, 실행 후 더 이상 추적하지 않는 형태의 작업을 위한 단방향 호출과 서비스에서 클라이언트로의 콜백이 가능한 이중 콜백을 지원합니다.

Windows Presentation Foundation 응용 프로그램 모델은 독립 실행형 응용 프로그램과 브라우저 응용 프로그램이라는 두 가지 응용 프로기본적으로 Windows Communication Foundation 작업은 요청-응답 형식입니다. 이는 클라이언트가 메시지 형식으로 요청을 발행한 다음 응답 메시지를 받을 때까지 차단되는 방식입니다. 기본 시간 제한인 1분 이내에 서비스에서 응답하지 않으면 클라이언트는 TimeoutException을 받게 됩니다. 또한 통신 또는 서비스 쪽에서 예외가 발생한 경우 프록시에서 클라이언트 쪽에 예외를 일으킵니다. NetPeerTcpBinding 및 NetMsmqBinding을 제외한 모든 바인딩에서 요청-응답 작업을 지원합니다.

먼저 Windows Communication Foundation 작업 호출 방법 및 관련 디자인 지침을 제시한 다음, 이벤트를 게시하고 구독하기 위한 사용자 지정 프레임워크의 구성 요소로 이러한 작업 형식을 사용하는 방법에 대해 설명합니다. 이 과정에서 다양한 고급 Microsoft® .NET Framework 및 Windows Communication Foundation 프로그래밍 기법을 소개합니다.

단방향 작업

작업에 반환되는 값이 없고 클라이언트에서 호출의 성공 또는 실패에 신경 쓰지 않는 경우가 있습니다. Windows Communication Foundation에서는 이처럼 실행 후 더 이상 추적하지 않는 형식의 호출을 지원하기 위해 단방향 작업을 제공합니다. 클라이언트에서 호출을 발행하면 Windows Communication Foundation에서 요청 메시지를 생성하지만 이와 연결된 어떤 응답 메시지도 클라이언트에 반환되지 않습니다. 따라서 단방향 작업에서는 값이 반환될 수 없으며 서비스 쪽에 발생한 예외도 클라이언트로 보내지지 않습니다. 단방향 호출이 비동기 호출과 동일한 것은 아닙니다. 단방향 호출은 서비스에 도달할 때 모든 호출이 한 번에 발송되지 않고 서비스 쪽의 대기열에 있으면서 해당 서비스에 구성된 동시성 모드 동작 및 세션 모드에 따라 한 번에 하나씩 발송됩니다. 단방향 또는 요청-응답에 관계없이 서비스에서 대기열에 포함하는 메시지의 수는 구성된 채널과 안정성 모드에 따라 결정됩니다. 대기 중인 메시지 수가 대기열의 용량을 초과하는 경우 단방향 호출을 발행하는 경우라도 클라이언트가 차단됩니다. 일단 호출이 대기열에 포함되면 클라이언트 차단이 해제되어 계속 실행할 수 있으며, 서비스에서는 백그라운드로 해당 작업을 처리합니다. 이러한 특징으로 인해 비동기 호출과 비슷한 것처럼 보이게 됩니다. 모든 Windows Communication Foundation 바인딩은 단방향 작업을 지원합니다.

OperationContract 특성은 IsOneWay 부울 속성을 제공합니다. 이 속성의 기본값은 false로, 이는 요청-응답 작업을 의미합니다. 그러나 다음과 같이 IsOneWay 속성을 true로 설정하면 해당 메서드가 단방향 작업으로 구성됩니다.

[ServiceContract]
interface IMyContract
{
   [OperationContract(IsOneWay = true)]
   void MyMethod()
}

단방향 작업에 연결된 응답이 없기 때문에 반환되는 값이나 결과를 갖는 지점이 존재하지 않으며 작업은 보내는 매개 변수가 없는 void 반환 형식을 가져야 합니다. Windows Communication Foundation에서는 언어 컴파일러에 대한 연결이 없고 컴파일 시 올바른 사용법을 확인할 수 없으므로 런타임 시 호스트를 로드할 때 및 일치하지 않을 경우 InvalidOperationException을 발생시킬 때 메서드 서명을 확인하여 이를 적용합니다.

클라이언트가 호출의 결과에 신경쓰지 않는다는 것이 호출이 발생했는지에 대해 전혀 관심을 갖지 않는다는 의미는 아닙니다. 일반적으로 단방향 호출에 대해서도 서비스에 대한 안정성을 설정해야 합니다. 이렇게 하면 요청이 서비스로 배달되도록 보장하게 됩니다. 그러나 단방향 호출의 경우 클라이언트에서 단방향 작업의 호출 순서에 신경쓸 수도 있고 신경쓰지 않을 수도 있습니다. 이는 Windows Communication Foundation을 사용하여 순서에 따른 배달 및 실행을 설정하는 것과 안정적인 배달을 설정하는 것을 분리할 수 있도록 한 주요 이유 중 하나입니다.

 

콜백 작업

Windows Communication Foundation은 서비스가 클라이언트를 다시 호출할 수 있도록 지원합니다. 콜백하는 동안에는 많은 점에서 양상이 바뀌게 됩니다. 즉, 서비스가 클라이언트가 되고 클라이언트가 서비스가 됩니다(그림 1 참조).

그림 1 콜백 작업
그림 1 콜백 작업

또한 클라이언트는 콜백 개체를 쉽게 호스팅할 수 있도록 해야 합니다. 모든 바인딩이 콜백 작업을 지원하는 것은 아닙니다. HTTP는 기본적으로 비연결형 프로토콜이기 때문에 콜백에 사용할 수 없으므로 BasicHttpBinding 또는 WSHttpBinding에서 콜백을 사용할 수 없습니다. Windows Communication Foundation은

NetTcpBinding 및 NetNamedPipeBinding에 대한 콜백을 지원하는데, 이는 기본 전송이 양방향이기 때문입니다. Windows Communication Foundation에서는 HTTP를 통한 콜백을 지원하기 위해 실제로 두 개의 HTTP 채널을 설정하는 WSDualHttpBinding을 제공합니다.

한 채널은 클라이언트에서 서비스로의 호출을 위한 것이며, 다른 한 채널은 서비스에서 클라이언트로의 호출을 위한 것입니다.

서비스 계약에는 최대 하나의 콜백 계약이 있을 수 있습니다. 일단 콜백 계약이 정의되면 클라이언트에서 콜백을 지원해야 하고 모든 호출에서 서비스에 대한 콜백 끝점도 제공해야 합니다. ServiceContract 특성은 Type 형식의 CallbackContract 속성을 제공합니다. 이 속성을 콜백 계약 형식으로 설정하고 다음과 같이 콜백 계약의 정의를 제공해야 합니다.

interface IMyContractCallback
{
   [OperationContract] 
   void OnCallback();
}

[ServiceContract(CallbackContract = typeof(IMyContractCallback))] 
interface IMyContract
{
   [OperationContract] 
   void DoSomething();
}

콜백 계약은 ServiceContract 특성을 내포하고 있으므로 이 특성으로 표시될 필요가 없습니다.

클라이언트 쪽의 가져온 콜백 인터페이스의 이름이 원래의 서비스 쪽 정의의 이름과 같을 필요는 없습니다. 대신 Callback이라는 접미사가 붙은 서비스 계약 인터페이스 이름을 갖게 됩니다.

 

클라이언트 콜백 설정

콜백 개체를 호스트하고 콜백 끝점을 노출하는 것은 클라이언트에 달려 있습니다. 서비스 인스턴스의 실행 범위에서 가장 안쪽은 다음과 같은 인스턴스 컨텍스트입니다.

public sealed class InstanceContext : CommunicationObject, ...
{
   public InstanceContext(object implementation);
   ... // 기타 멤버 
}

클라이언트에서 콜백 개체를 호스트하기 위해 해야 할 일은 다음과 같이 해당 콜백 개체를 인스턴스화하고 이를 둘러싼 컨텍스트를 만드는 것입니다.

class MyCallback : IMyContractCallback 
{
   public void OnCallback() {...}
}
IMyContractCallback callback = new MyCallback();
InstanceContext context = new InstanceContext(callback);

콜백 계약을 정의하는 서비스 끝점의 계약과 상호 작용할 때마다 클라이언트는 프록시를 사용하여 양방향 통신을 설정하고 서비스에 콜백 끝점 참조를 전달해야 합니다. 클라이언트에서 사용하는 프록시는 그림 2에서 볼 수 있는 DuplexClientBase<T> 특수 프록시 클래스에서 파생되어야 합니다.

클라이언트는 DuplexClientBase<T> 생성자에게 일반 프록시에서처럼 서비스 끝점 정보뿐만 아니라 콜백 개체를 호스트하는 인스턴스 컨텍스트도 제공합니다. 프록시는 콜백 컨텍스트 주변의 끝점을 만드는 한편 서비스 끝점 구성에서 콜백 끝점의 세부 정보를 유추합니다. 콜백 끝점 계약은 서비스 계약 콜백 형식에 의해 정의됩니다. 콜백 끝점에는 보내는 호출과 동일한 바인딩(실제로는 전송)이 사용됩니다. Windows Communication Foundation에서는 주소에 클라이언트의 컴퓨터 이름을 사용하며 HTTP를 사용할 때 포트까지 선택합니다. 인스턴스 컨텍스트를 이중 프록시에 전달하고 이 프록시를 사용하여 서비스를 호출함으로써 해당 클라이언트 쪽 콜백 끝점을 노출할 수 있습니다.

SvcUtil 또는 Visual Studio® 2005를 사용하여 프록시 클래스를 생성하는 경우 이 도구에서는 그림 3과 같이 DuplexClientBase<T>에서 파생되는 클래스를 생성합니다. 클라이언트는 콜백 인스턴스를 만들어 컨텍스트에서 이 인스턴스를 호스트하고, 프록시를 만든 다음 서비스를 호출함으로써 콜백 끝점 참조를 전달합니다.

class MyCallback : IMyContractCallback 
{
   public void OnCallback() {...}
}
IMyContractCallback callback = new MyCallback();
InstanceContext context = new InstanceContext(callback);

MyContractClient proxy = new MyContractClient(context);
proxy.DoSomething();

클라이언트는 콜백을 기다리고 있는 동안에는 프록시를 닫을 수 없습니다. 이 경우 프록시를 닫으면 콜백 끝점이 닫히므로 서비스에서 콜백을 시도할 때 서비스 쪽에서 오류가 발생하게 됩니다. 클라이언트 자체에서 종종 콜백 계약을 구현하기도 하는데, 이 경우 대개 클라이언트는 그림 4와 같이 프록시에 멤버 변수를 사용하며 클라이언트가 삭제될 때 이를 닫습니다.

클라이언트 쪽 콜백 끝점 참조는 클라이언트에서 서비스로 보내는 모든 호출과 함께 전달되며 들어오는 메시지의 일부입니다. OperationContext 클래스는 GetCallbackChannel<T> generic 메서드를 통해 서비스에서 콜백 참조에 쉽게 액세스할 수 있도록 합니다. 서비스에서 콜백 참조를 사용하여 수행하는 작업과 콜백 참조를 사용하도록 결정하는 시기는 서비스의 재량에 달려 있습니다. 서비스는 작업 컨텍스트에서 콜백 참조를 추출한 다음 나중에 사용하도록 저장하거나 서비스 작업 중에 이 참조를 사용하여 클라이언트로 콜백할 수 있습니다. 그림 5는 첫 번째 옵션을 보여 줍니다.

앞에서 본 것과 같은 콜백 계약 정의를 사용할 경우 서비스에서 정적 generic 목록을 사용하여 IMyContractCallback 형식의 인터페이스에 대한 참조를 저장합니다. 서비스에서는 어느 클라이언트가 호출하고 있는지, 또 해당 클라이언트가 이미 서비스를 호출했는지 여부를 알 수 없으므로 모든 호출을 검사하여 해당 콜백 참조가 이미 목록에 있는지 확인합니다. 목록에 참조가 없으면 서비스에서 해당 콜백을 목록에 추가합니다. 서비스 클래스도 정적 메서드인 CallClients를 제공합니다. 호스트 쪽에서는 다음과 같이 이 메서드만 사용하여 클라이언트로 콜백할 수 있습니다.

MyService.CallClients();

이렇게 호출되는 경우 호출하는 측에서는 콜백 호출에 일부 호스트 쪽 스레드를 사용합니다. 이러한 스레드는 들어오는 서비스 호출을 실행하는 스레드와는 관계가 없습니다.

 

콜백 재입력

서비스에서는 전달된 콜백 참조를 호출해야 하거나 계약 작업을 실행하는 동안 콜백 목록을 호출해야 할 수도 있습니다. 그러나 서비스 클래스는 기본적으로 단일 스레드 액세스로 구성되므로 이러한 호출은 허용되지 않습니다. 서비스 인스턴스는 잠금과 연결되며, 한 번에 하나의 스레드만 잠금을 소유하여 해당 서비스 인스턴스에 액세스할 수 있습니다. 작업 중에 클라이언트를 호출하는 경우 콜백을 호출하는 동안 서비스 스레드를 차단해야 합니다. 그러나 문제는 콜백이 반환되면 클라이언트에서 응답 메시지를 처리할 때 동일한 잠금에 대한 소유권이 필요하게 되고 따라서 교착 상태가 발생한다는 점입니다.

Windows Communication Foundation에서는 교착 상태를 방지하기 위해 단일 스레드 서비스 인스턴스에서 클라이언트로 콜백하려고 시도할 때 InvalidOperationException을 발생시킵니다. 가능한 해결 방법은 세 가지입니다. 첫 번째 방법은 다중 스레드 액세스에 대한 서비스를 구성하는 것입니다. 이 경우 잠금과 연결되지 않으므로 콜백이 허용됩니다. 그러나 이렇게 하면 서비스에 동기화를 제공해야 하므로 서비스 개발자의 부담이 가중됩니다.

두 번째 해결 방법은 재입력에 대해 서비스를 구성하는 것입니다. 서비스 인스턴스는 재입력에 대해 구성되는 경우에도 여전히 잠금과 연결되며 단일 스레드 액세스만 허용됩니다. 그러나 서비스가 클라이언트로 콜백하는 경우 Windows Communication Foundation에서 먼저 자동으로 잠금을 해제합니다.

다음 코드에서는 재입력에 대해 구성된 서비스를 보여 줍니다. 서비스는 작업 실행 중에 작업 컨텍스트에 도달하여 콜백 참조를 가져온 다음 호출합니다. 컨트롤은 콜백이 반환된 후에만 서비스에 반환되며 서비스 자체의 스레드에서 잠금을 다시 확보해야 합니다.

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
class MyService : IMyContract
{
   public void DoSomething()
   {
      IMyContractCa llback callback = OperationContext.Current.
         GetCallbackChannel<IMyContractCallback>();
      callback.OnCallback();
   }
}

서비스가 안전하게 클라이언트로 콜백할 수 있도록 해 주는 세 번째 방법은 콜백 계약 작업을 단방향 작업으로 구성하는 것입니다. 이렇게 하면 잠금을 얻기 위해 경합하는 응답 메시지가 없기 때문에 동시성이 단일 스레드로 설정된 경우에도 서비스에서 콜백이 가능하게 됩니다.

 

이벤트

기본적으로 이중 콜백은 이벤트와 함께 사용합니다. 이벤트를 사용하면 서비스 쪽에 발생한 상황을 클라이언트에 알릴 수 있습니다. 이벤트는 직접적인 클라이언트 호출로 인해 발생하거나 서비스의 모니터링 대상으로 인해 발생할 수 있습니다. 그림 6과 같이 이벤트를 발생시키는 서비스를 게시자라고 하며, 이벤트를 받는 클라이언트를 구독자라고 합니다.

그림 6 게시자와 구독자
그림 6 게시자와 구독자

Windows Communication Foundation의 이벤트는 콜백 작업에 지나지 않지만 본질적인 특성 탓에 일반적으로 클라이언트와 서비스 간의 관계보다 더 느슨한 게시자와 구독자 간의 관계를 의미합니다. 서비스는 이벤트를 처리할 때 대개 여러 구독자에게 동일한 이벤트를 게시합니다. 일반적으로 게시자는 구독자의 호출 순서, 또는 이벤트를 처리할 때 구독자에게 발생할 수 있는 오류에 대해 신경 쓰지 않습니다. 또한 서비스는 구독자로부터 반환되는 결과에 대해서도 신경 쓰지 않습니다. 따라서 이벤트 처리 작업은 void 반환 형식을 가져야 하며 보내는 매개 변수가 없어야 하고 단방향 작업으로 표시되어야 합니다. 이벤트를 개별 콜백 계약으로 분류하고, 동일한 계약에서 이벤트와 일반 콜백을 혼합하지 않는 것이 좋습니다.

interface IMyEvents
{
   [OperationContract(IsOneWay = true)]
   void OnEvent1();

   [OperationContract(IsOneWay = true)]
   void OnEvent2(int number);

   [OperationContract(IsOneWay = true)]
   void OnEvent3(int number, string text);
}

그러나 이벤트에 이중 콜백을 그대로 사용하는 경우 종종 게시자와 구독자 간에 너무 많은 연결이 발생하게 됩니다. 구독자는 응용 프로그램에서 게시되고 있는 모든 서비스의 위치를 알고 해당 서비스에 연결해야 합니다. 구독자가 알지 못하는 게시자는 해당 구독자에게 이벤트를 알릴 수 없습니다. 이 경우 이미 배포된 응용 프로그램에 새 구독자를 추가하거나 기존 구독자를 제거하기가 어렵게 됩니다. 구독자가 응용 프로그램에서 특정 형식의 이벤트가 발생할 때마다 자신에게 알리도록 요청할 방법은 없습니다. 또한 구독자는 구독하거나 구독을 취소하기 위해서는 각 게시자에 대한 호출을 여러 번 수행해야 하는데, 여기에는 많은 비용이 소요될 수 있습니다. 여러 명의 게시자가 동일한 이벤트를 발생시키지만 이들이 제공하는 구독 및 구독 취소 방법은 조금씩 다를 수 있습니다. 이 경우 당연히 구독자는 이러한 메서드에 연결됩니다.

마찬가지로, 게시자는 자신이 알고 있는 사항만 구독자에게 알릴 수 있습니다. 게시자가 어떤 이벤트를 받기를 원하는 모든 구독자에게 해당 이벤트를 전달할 방법은 없으며 이벤트를 브로드캐스트할 수도 없습니다. 또한 모든 게시자에게는 구독자 목록과 게시 작업을 직접 관리하는 데 필요한 코드가 있어야 합니다. 이 코드는 서비스가 해결해야 하는 비즈니스 문제와는 거의 관계가 없으며, 동시 게시와 같은 고급 기능을 사용하려는 경우 상당히 복잡하게 될 수 있습니다.

더욱이 이중 콜백에는 게시자와 구독자의 수명선 간에 연결이 발생하게 됩니다. 구독자는 이벤트를 구독하고 받으려면 가동되어 실행 중이어야 합니다. 구독자는 이벤트가 발생하면 응용 프로그램에서 구독자의 인스턴스를 만들어 해당 이벤트를 처리하도록 요청할 수 없습니다.

끝으로 구독은 프로그래밍 방식으로 설정해야 합니다. 시스템이 실행 중일 때 응용 프로그램에서 구독을 구성하거나 구독자의 기본 설정을 변경하기 위한 손쉬운 관리 방법은 없습니다.

이러한 문제에 대한 해결 방법은 그림 7에서 볼 수 있듯이 게시-구독 디자인 패턴을 사용하여 주변을 디자인하고 그 사이에 전용 구독 서비스와 전용 게시 서비스를 도입하여 구독자와 게시자를 분리하는 것입니다.

그림 7 게시-구독 시스템
그림 7 게시-구독 시스템

이벤트를 구독하려는 구독자는 구독 서비스에 등록하며, 구독 서비스는 구독자 목록을 관리하고 구독 취소에서도 이와 비슷한 기능을 제공합니다. 마찬가지로, 모든 게시자는 게시자 서비스를 통해 이벤트를 발생시키며 구독자에게 이벤트가 직접 전달되지 않도록 합니다. 가입 및 게시 서비스는 시스템을 분리하는 우회 계층을 제공합니다. 구독자는 게시자 ID에 대해 더 이상 알 필요가 없습니다. 구독 메커니즘이 모든 게시자에 대해 일정하기 때문에 구독자는 이벤트 형식을 구독하여 게시자로부터 이벤트를 받을 수 있습니다. 실제로 게시자는 구독 목록을 관리하지 않아도 되며 구독자가 누구인지 알지도 못 합니다. 게시자는 관심 있는 구독자가 받을 수 있도록 이벤트를 게시 서비스에 배달합니다.

임시 구독자와 영구 구독자 두 가지 종류의 구독자를 정의할 수 있습니다. 임시 구독자는 메모리에 있는 실행 중인 구독자이며, 영구 구독자는 디스크에 존속하는 구독자로 이벤트 발생 시에 호출할 서비스를 나타냅니다. 임시 구독자의 경우 실행 중인 서비스에 콜백 참조를 전달하기 위한 간단한 방법으로 이중 콜백 메커니즘을 사용할 수 있습니다. 영구 구독자의 경우 구독자 주소만 참조로 기록하면 됩니다. 이벤트가 발생하면 게시 서비스에서 영구 구독자 주소를 호출하여 이벤트를 배달하게 됩니다. 두 가지 구독 형식 간의 또 다른 중요한 차이점은 영구 구독의 경우 디스크나 데이터베이스에 저장할 수 있다는 것입니다. 이렇게 저장하면 응용 프로그램 종료 또는 시스템 충돌 및 재시작 시에도 구독이 유지되므로 관리자가 구독을 구성할 수 있게 됩니다. 응용 프로그램 종료 시에는 임시 구독을 저장하지 못하므로 응용 프로그램을 시작할 때마다 명시적으로 임시 구독을 설정해야 합니다.

 

게시-구독 프레임워크

이 기사에서 제공되는 소스 코드에는 완전한 게시-구독 예제가 포함되어 있습니다. 필자는 샘플 게시-구독 서비스 및 클라이언트뿐만 아니라, 이러한 서비스의 구현과 응용 프로그램에 대한 지원 추가를 자동화하는 범용 프레임워크도 제공하도록 이 코드를 작성했습니다. 프레임워크를 만들 때 첫 번째 단계는 게시-구독 관리 인터페이스를 분류하고 임시 구독과 영구 구독에 대해 각각 별도의 게시 계약을 제공하는 것이었습니다.

임시 구독 관리를 위해 다음과 같이 ISubscriptionService 인터페이스를 정의했습니다.

 [ServiceContract]
public interface ISubscriptionService
{
   [OperationContract]
   void Subscribe(string eventOperation);

   [OperationContract]
   void Unsubscribe(string eventOperation);
}

ISubscriptionService는 구현하는 끝점에서 예상되는 콜백 계약을 식별하지 않습니다. 콜백 인터페이스는 다음과 같이 ISubscriptionService로부터 파생되어 원하는 콜백 계약을 지정함으로써 응용 프로그램에서 제공됩니다.

[ServiceContract(CallbackContract = typeof(IMyEvents))]
interface IMySubscriptionService : ISubscriptionService {}

ISubscriptionService의 하위 인터페이스는 작업을 추가하지 않아도 됩니다. 임시 구독 관리 기능은 ISubscriptionService에 의해 제공됩니다. Subscribe 또는 Unsubscribe에 대한 각각의 호출에서 구독자는 구독하거나 구독 취소할 작업(이벤트)의 이름을 제공해야 합니다. 호출자는 모든 이벤트에서 구독하거나 구독 취소하려면 빈 문자열이나 null 문자열을 전달하면 됩니다.

필자의 프레임워크는 다음과 같이 SubscriptionManager<T> generic 추상 클래스에 ISubscriptionService의 메서드에 대한 구현을 제공합니다.

public abstract class SubscriptionManager<T> where T : class
{
   public void Subscribe(string eventOperation);
   public void Unsubscribe(string eventOperation);
   ... // 기타 멤버 
}

SubscriptionManager<T>에 대한 generic 형식 매개 변수는 이벤트 계약입니다. SubscriptionManager<T>는 ISubscriptionService에서 파생되지 않습니다.

응용 프로그램에서는 ISubscriptionService의 특정 하위 인터페이스를 지원하는 끝점 형식으로 자체의 임시 구독 서비스를 노출해야 합니다. 이렇게 하려면 응용 프로그램은 SubscriptionManager<T>에서 파생되는 서비스 클래스를 제공하고 콜백 계약을 형식 매개 변수로 지정하고 ISubscriptionService의 특정 하위 인터페이스에서 파생되어야 합니다.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MySubscriptionService : 
   SubscriptionManager<IMyEvents>,IMySubscriptionService {}

IMySubscriptionService는 새로운 작업을 추가하지 않고 SubscriptionManager<T>가 ISubscriptionService의 메서드를 이미 구현하고 있으므로 MySubscriptionService에는 코드가 필요하지 않습니다.

끝으로 응용 프로그램에서 IMySubscriptionService에 대한 끝점을 정의해야 합니다.

<services>
   <service name ="MySubscriptionService">
      <endpoint
         address = "..."
         binding = "..."
         contract= "IMySubscriptionService"
      />
   </service>
</services>

그림 8은 SubscriptionManager<T>에서 임시 구독을 관리하는 방식을 보여 줍니다. SubscriptionManager<T>는 다음과 같이 m_TransientStore라고 하는 정적 generic 사전에 임시 구독자를 저장합니다.

static Dictionary<string,List<T>> m_TransientStore;

사전의 각 항목에는 이벤트 작업의 이름과 해당되는 모든 구독자가 연결된 목록 형식으로 들어 있습니다. SubscriptionManager<T>의 정적 생성자는 리플렉션을 사용하여 콜백 인터페이스(SubscriptionManager<T>에 대한 형식 매개 변수)의 모든 작업을 가져오고 사전을 초기화하여 모든 작업의 목록이 비어 있도록 합니다. Subscribe 메서드는 작업 호출 컨텍스트에서 콜백 참조를 추출합니다. 호출자가 작업 이름을 지정하면 Subscribe에서 AddTransient 도우미 메서드를 호출하여 저장소에서 해당 이벤트에 대한 구독자 목록을 검색합니다. 목록에 구독자가 없으면 AddTransient가 추가합니다.

호출자가 작업 이름으로 빈 문자열이나 null 문자열을 지정한 경우 Subscribe는 콜백 계약의 각 작업에 대해 AddTransient를 호출합니다. 구독 취소도 이와 비슷한 방식으로 수행됩니다. 호출자는 모든 이벤트를 구독한 다음 특정 이벤트를 구독 취소할 수 있습니다.

 

영구 구독자 관리

영구 구독자를 관리하기 위해 그림 9의 IPersistentSubscriptionService 인터페이스를 정의했습니다.

영구 구독자를 추가하려면 호출자가 PersistSubscribe를 호출하여 구독자 주소, 이벤트 계약 이름 및 관련 이벤트 작업을 직접 제공해야 합니다. 구독을 취소하려면 이와 동일한 정보로 PersistUnsubscribe를 사용합니다. IPersistentSubscriptionService는 어떤 방식으로도 서비스 쪽에서의 구독자 위치를 나타내지 않습니다. 이는 구현 세부 사항입니다.

앞에서 나온 SubscriptionManager<T> 클래스도 IPersistentSubscriptionService의 메서드를 구현합니다.

public abstract class SubscriptionManager<T> where T : class
{
   public void PersistUnsubscribe(
      string address, string eventsContract, string eventOperation);
   public void PersistSubscribe(
      string address, string eventsContract, string eventOperation);
   ... // 기타 멤버 
}

SubscriptionManager<T>는 SQL Server™에 영구 구독자를 저장합니다. SubscriptionManager<T>는 IPersistentSubscriptionService에서 파생되지 않습니다. 사용하는 응용 프로그램에서는 자체 영구 구독 서비스를 노출해야 하지만 콜백 참조가 필요하지 않으므로 IPersistentSubscriptionService에서 새 계약을 파생시킬 필요는 없습니다. 응용 프로그램은 SubscriptionManager<T>에서 파생되어 형식 매개 변수로 이벤트 계약을 지정하고 IPersistentSubscriptionService의 파생물을 추가하면 됩니다.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MySubscribtionService : SubscriptionManager<IMyEvents>,
   IPersistentSubscriptionService {} 

SubscriptionManager<T>에서 이미 IPersistentSubscriptionService의 메서드를 구현하고 있으므로 MySubscriptionService에는 코드가 필요하지 않습니다.

끝으로 응용 프로그램에서 IPersistentSubscriptionService에 대한 끝점을 정의해야 합니다.

<services>
   <service name ="MySubscriptionService">
      <endpoint
         address = "..."
         binding = "..."
         contract= "IPersistentSubscriptionService"
      />
   </service>
</services>

그림 10에서는 SubscriptionManager<T>의 IPersistentSubscriptionService 메서드 구현을 보여 주는데, 이는 구독자가 사전의 메모리 내부가 아닌 SQL Server에 저장된다는 것을 제외하고는 그림 8과 비슷합니다.

응용 프로그램에서 동일한 이벤트 계약에 대해 임시 구독자와 영구 구독자를 모두 지원해야 하는 경우 ISubscriptionService의 특수 하위 인터페이스 및 IPersistentSubscriptionService에서 구독 서비스 클래스를 파생시키고 일치하는 두 끝점을 노출하기만 하면 됩니다.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MySubscriptionService : SubscriptionManager<IMyEvents>,
   IMySubscriptionService,IPersistentSubscriptionService {}

 

이벤트 게시

필자의 프레임워크를 사용하면 게시 서비스를 손쉽게 구현할 수 있습니다. 이 서비스는 구독자와 동일한 이벤트 계약을 지원해야 하고 응용 프로그램의 게시자에게 알려지는 유일한 계약 지점이어야 합니다. 게시 서비스가 끝점의 이벤트 계약을 노출하기 때문에 이벤트 계약을 서비스 계약으로 표시해야 합니다. 이는 임시 구독자와의 이중 콜백을 위해서만 이벤트 계약을 사용하는 경우에도 마찬가지입니다.

[ServiceContract]
interface IMyEvents {...

게시-구독 프레임워크에는 다음과 같이 정의된 PublishService<T> 도우미 클래스가 있습니다.

public abstract class PublishService<T> where T : class
{
   protected static void FireEvent(params object[] args);
}

PublishService<T>에는 형식 매개 변수로서 이벤트 계약 형식이 필요합니다. 자신만의 게시 서비스를 제공하려면 PublishService<T>에서 파생시키고 FireEvent 메서드를 사용하여 임시 또는 영구 구독자에 관계없이 모든 구독자에게 이벤트를 배달합니다.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyPublishService : PublishService<IMyEvents>,IMyEvents
{
   public void OnEvent1() { FireEvent(); }
   public void OnEvent2(int number) { FireEvent(number); }
   public void OnEvent3(int number,string text) {
      FireEvent(number,text); 
   }
}

params 개체 배열을 사용하므로 매개 변수에 상관없이 FireEvent를 통해 모든 형식의 이벤트를 발생시킬 수 있습니다.

끝으로 응용 프로그램은 이벤트 계약이 있는 게시 서비스에 대한 끝점을 노출해야 합니다.

<services>
   <service name ="MyPublishService">
      <endpoint
         address = "..."
         binding = "..."
         contract="IMyEvents"
      />
   </service>
</services>

그림 11은 PublishService<T>의 구현을 보여 줍니다.

FireEvent 메서드는 게시 서비스의 이벤트 발생을 간소화하기 위해 구독자에게 전달할 매개 변수를 받아들이지만, 해당 호출자는 구독자에서 호출할 작업의 이름을 제공하지 않습니다. 이러한 이유로 FireEvent는 스택 프레임에 액세스하여 호출하는 메서드의 이름을 추출합니다. 그런 다음 PublishPersistent 도우미 메서드를 사용하여 모든 영구 구독자에 게시하고 PublishTransient 도우미 메서드를 사용하여 모든 임시 구독자에 게시합니다. 두 가지 게시 메서드는 거의 동일한 방식으로 작동합니다. 즉, SubscriptionManager<T>에 액세스하여 각각의 구독자 목록을 검색한 다음 Publish 메서드를 사용하여 이벤트를 발생시킵니다. 다음으로 구독자에 대한 프록시 배열 형식으로 구독자가 반환됩니다. 이 배열은 Publish 메서드에 전달됩니다.

Publish는 이 시점에서 단순히 구독자를 호출할 수도 있습니다. 그러나 필자는 미숙한 구독자가 이벤트를 처리하는 데 많은 시간을 소모하는 경우 이로 인해 다른 구독자가 적시에 이벤트를 받는 데 지장이 없도록 하기 위해 이벤트의 동시 게시를 지원하고자 했습니다. 이벤트 작업이 단방향으로 표시되어 있다고 해서 비동기 호출이 보장되는 것은 아닙니다. 아울러 필자는 이벤트 작업이 단방향으로 표시되어 있지 않은 경우에도 동시 게시를 지원하고자 했습니다.

게시 메서드는 두 가지 익명 메서드를 정의합니다. 첫 번째 익명 메서드는 Invoke 도우미 메서드를 호출하여 제공된 개별 구독자에게 이벤트를 발생시킨 다음, 프록시를 닫도록 지정된 경우 프록시를 닫습니다. Invoke는 특정 구독자 형식에 대해 컴파일되지 않았기 때문에 호출에 대해 리플렉션 및 후기 바인딩을 사용합니다. 또한 Invoke는 게시 쪽에 관심이 없기 때문에 호출에서 발생된 모든 예외도 표시하지 않습니다. 두 번째 익명 메서드는 스레드 풀의 스레드에 의해 실행될 첫 번째 익명 메서드를 대기열에 넣습니다. 끝으로 Publish는 제공된 배열에 속한 모든 구독자에 대해 두 번째 익명 메서드를 호출합니다.

PublishService<T>가 구독자를 동일하게 취급하는 방식에 유의합니다. 구독자가 임시 구독자인지 영구 구독자인지는 거의 문제가 되지 않습니다. 유일한 차이점은 영구 구독자에게 게시한 후에는 프록시를 닫아야 한다는 것입니다. 이러한 일률성은 SubscriptionManager<T>의 GetTransientList 및 GetPersistentList 도우미 메서드에 의해 확보됩니다. 두 메서드 중 GetTransientList 메서드가 더 간단합니다.

public abstract class SubscriptionManager<T> where T : class
{
   internal static T[] GetTransientList(string eventOperation)
   {
      lock(typeof(SubscriptionManager<T>))
      {
         List<T> list = m_TransientStore[eventOperation];
         return list.ToArray();
      }
   }
   ... // 기타 멤버 
}

GetTransientList는 지정한 작업에 대한 모든 구독자를 임시 저장소에서 찾아 배열로 반환합니다. GetPersistentList는 난제에 직면하게 됩니다. 영구 구독자에 대해 준비된 프록시 목록이 없다는 것입니다. 이들에 대해 알려진 것이라고는 주소가 전부입니다. 따라서 GetPersistentList는 그림 12와 같이 영구 구독자 프록시를 인스턴스화해야 합니다.

각 구독자에 대한 프록시를 만들기 위해 GetPersistentList에는 구독자의 주소, 바인딩 및 계약이 필요합니다. 여기에서 계약은 물론 SubscriptionManager<T>에 대한 형식 매개 변수입니다. 주소를 얻기 위해 GetPersistentList는 GetSubscribersToContractEventOperation을 호출하여 데이터베이스를 쿼리하고, 지정한 이벤트를 구독하는 영구 구독자의 모든 주소를 배열로 반환합니다. 이제 GetPersistentList에 필요한 것은 각 구독자가 사용하는 바인딩입니다. 바인딩을 얻기 위해 GetPersistentList는 GetBindingFromAddress 도우미 메서드를 호출하여 주소 스키마에서 사용할 바인딩을 유추합니다. GetBindingFromAddress는 모든 HTTP 주소를 WSHttpBinding으로 취급합니다.

 

영구 구독자 관리

그림 9에서 볼 수 있는 IPersistentSubscriptionService의 메서드를 사용하여 런타임 시 영구 구독을 추가하거나 제거할 수 있지만, 이러한 구독은 영구적 특성을 갖기 때문에 관리 도구를 통해 관리하게 되는 경우가 많습니다. 이를 위해 IPersistentSubscriptionService는 그림 13과 같이 구독자 저장소의 다양한 쿼리에 응답하는 추가 작업을 정의합니다.

이러한 관리 작업에는 구독자의 주소, 구독 계약 및 이벤트를 포함하는 PersistentSubscription이라는 간단한 데이터 구조가 이용됩니다. 필자의 게시-구독 프레임워크에는 그림 14에서 볼 수 있는 Persistent Subscription Manager라는 샘플 영구 구독 관리 도구가 포함되어 있습니다.

그림 14 Persistent Subscription Manager
그림 14 Persistent Subscription Manager (작게 보려면 이미지를 클릭하십시오.)

그림 14 Persistent Subscription Manager
그림 14 Persistent Subscription Manager (크게 보려면 이미지를 클릭하십시오.)

이 도구는 IPersistentSubscriptionService를 사용하여 구독을 추가하거나 제거합니다. 새 구독을 추가하려면 도구에 이벤트 계약 정의의 메타데이터 교환 주소를 제공해야 합니다. 영구 구독자는 다형적이기 때문에 영구 구독자 자체의 메타데이터 교환 주소 또는 게시 서비스의 메타데이터 교환 주소를 사용할 수 있습니다. MEX Address 입력란에 메타데이터 교환 기준 주소를 입력하고 Lookup 단추를 클릭합니다. 도구에서 프로그래밍 방식으로 이벤트 서비스의 메타데이터를 검색하여 Contract 및 Event 콤보 상자를 채우게 됩니다.

구독하려면 영구 구독자의 주소를 입력하고 Subscribe 단추를 클릭합니다. 그러면 Persistent Subscription Manager에서 구독 서비스, 즉 지금까지 예제에서 설명한 MySubscriptionService 서비스를 호출하여 해당 구독을 추가합니다. 구독 서비스의 주소는 Persistent Subscription Manager 구성 파일에 유지됩니다.

페이지 맨 위로페이지 맨 위로

대기 중인 게시자 및 구독자

이벤트 게시 또는 구독에 동기 바인딩을 사용하는 대신 NetMsmqBinding을 사용할 수 있습니다. 대기 중인 게시-구독 이벤트는 느슨하게 연결된 시스템의 이점과 연결이 끊어진 실행의 유연성을 결합합니다. 대기 중인 이벤트를 사용하는 경우 계약의 모든 이벤트는 당연히 단방향 작업으로 표시되어야 합니다. 그림 15와 같이 어느 쪽 끝에서든 독립적으로 대기열을 사용할 수 있습니다.

그림 15 대기 중인 게시-구독
그림 15 대기 중인 게시-구독

대기 중인 게시자 및 연결된 동기 구독자가 있을 수 있습니다. 대기 중인 구독자에 게시하는 연결된 게시자, 또는 대기 중인 게시자와 대기 중인 구독자가 모두 있을 수 있습니다. 그러나 이중 콜백에는 MSMQ 바인딩이 지원되지 않기 때문에 대기 중인 임시 구독은 있을 수 없습니다. 앞에서 살펴본 것처럼 관리 도구를 사용하여 구독자를 관리할 수 있으며 관리 작업은 여전히 동기적이고 연결되어 있습니다.

대기 중인 게시자를 이용하려면 게시 서비스에서 MSMQ 바인딩을 사용하여 대기 중인 끝점을 노출해야 합니다. 대기 중인 게시자에서 이벤트를 발생시킬 때 게시 서비스가 오프라인이 되거나 게시 클라이언트 자체가 연결이 끊어질 수 있습니다. 대기 중인 게시 서비스에 두 이벤트를 게시할 때는 배달 순서 및 이러한 이벤트에 대한 최종 구독자의 처리가 보장되지 않습니다. 이벤트 계약이 한 세션에 대해서만 구성된 경우, 그리고 단일 게시 서비스를 처리하는 경우에 한해 게시의 순서를 추측만 할 수 있습니다.

대기 중인 구독자를 배포하려면 영구 구독 서비스가 대기 중인 끝점을 노출해야 합니다. 이렇게 하면 게시자가 온라인 상태인 경우에도 구독자는 오프라인 상태가 될 수 있습니다. 구독자는 다시 연결될 때 대기 중인 모든 이벤트를 받게 됩니다. 또한 어떠한 이벤트도 손실되지 않으므로 대기 중인 구독자는 게시 서비스 자체의 연결이 끊어진 경우에 적합합니다. 대기 중인 단일 구독자에 여러 이벤트가 발생하는 경우 이벤트의 배달 순서가 보장되지 않습니다. 구독자는 이벤트 계약이 한 세션에 대해서 구성된 경우 게시의 순서를 추측만 할 수 있습니다. 물론 대기 중인 게시자와 대기 중인 구독자 둘 다 있는 경우에는 동시에 오프라인으로 작동할 수 있습니다.

페이지 맨 위로페이지 맨 위로

결론

Windows Communication Foundation은 요청-응답, 단방향 작업 또는 이중 콜백을 사용하여 강력하고 유용한 프로그래밍 모델을 기본적으로 지원합니다. 작업을 이해하고 올바른 호출 모드를 선택하는 것은 최우선적으로 수행해야 하는 가장 중요한 디자인 결정 사항 중 하나이지만, 각 모드를 적용하는 작업은 무척 쉽습니다. 이러한 작업 형식을 서비스 지향 응용 프로그램에 있는 그대로 사용하거나 게시-구독 패턴처럼 높은 수준의 추상화를 독자적으로 구성할 수 있습니다.

페이지 맨 위로페이지 맨 위로

+ Recent posts