Windows Workflow Foundation 및 Windows Communication Foundation 통합

2007. 3. 7. 14:29 IT 및 개발/.NET FX & Visual C#
요약 : 이 기사에서는 Windows WF(Workflow Foundation)를 사용하여 작성한 워크플로가 WCF(Windows Communication Foundation)를 사용하여 만든 서비스 내에서 어떻게 호스팅되는지에 대한 개요를 제공합니다. 또한 WCF에서 제공되는 광범위한 기능 중 일부를 활용하여 이중 채널을 통해 클라이언트-이벤트 콜백을 용이하게 하는 방법에 대해서도 설명합니다.

"Windows Workflow Foundation Sample Integrating WF and WCF"를 다운로드하려면 여기 (영문)를 클릭하십시오.

소개

Microsoft는 Windows WF(Workflow Foundation) 출시에 맞춰 .NET 개발자 플랫폼에 워크플로 기능을 도입하고 있습니다. 개발자는 이 기능을 사용하여 간단한 순차 워크플로부터 정교한 사용자 상호 작용을 포함하는 복잡한 상태 컴퓨터 기반 워크플로에 이르기까지 광범위한 시나리오를 충족하는 워크플로를 구축할 수 있습니다.

이와 동시에 캡슐화된 서비스 끝점을 통해 비즈니스 기능을 제공하여 비즈니스 기능과 프로세스의 재사용 및 결합을 가능하게 함으로써 서비스 지향 아키텍처를 구성하는 경향이 늘어나고 있습니다. WCF(Windows Communication Foundation)는 일관성 있는 개발자 API, 강력한 호스팅 런타임, 그리고 배포 작업을 돕는 유연한 구성 기반 솔루션을 통해 개발자가 손쉽게 연결된 시스템을 개발할 수 있는 기능을 제공합니다.

이 문서의 마지막에는 WF 및 WCF에 대해 더 배우고자 할 때 참조할 수 있는 추가 리소스 목록이 있습니다.


비용 보고 샘플

이 기사의 코드 샘플은 직원의 비용 청구를 제출 및 승인하는 표준 비즈니스 프로세스를 모델링하는 비용 보고 워크플로 샘플을 기반으로 합니다. 이 원본 샘플은 WCF 및 .NET 3.0 Framework를 활용하여 이 시나리오를 더욱 효과적으로 호스팅하는 방법을 보여 주도록 업데이트되었습니다.

비용 보고 샘플의 첫 릴리스에서는 워크플로 런타임 인스턴스가 포함된 호스트 응용 프로그램과 클라이언트 응용 프로그램 간의 통신 기능을 제공하기 위해 .NET Remoting을 사용했습니다.

그러나 이번에는 비용 보고 구현을 리팩터링하여 클라이언트와 서비스 간 통신에 WCF를 사용하도록 만들었습니다. 또한 솔루션 내의 다양한 고려 사항을 분류하기 위해 논리적인 솔루션 구조를 사용했습니다.


그림 1. 리팩터링된 솔루션의 구조

설계에 메시지를 통합하기 위해서는 이러한 메시지가 비즈니스 프로세스 컨텍스트 내에서 어떻게 사용되는지 이해하는 것이 중요합니다. 비용 보고의 수명 주기에는 여러 개의 상호 작용 지점이 있습니다. 이러한 상호 작용 지점을 간단히 검토해 보겠습니다.

  • 프로세스에는 '클라이언트', '관리자'및 비용 보고 '호스트' 시스템이라는 세 가지 관계 요소가 있습니다.
  • '클라이언트'가 새 비용 청구를 제출하면 프로세스가 시작됩니다.
  • 규칙의 '정책'을 사용하여 비용 청구를 자동으로 승인할 수 있는지 판단합니다.
  • 비용 청구가 자동으로 승인되지 않은 경우 요청을 승인할 '관리자'가 필요합니다. 관리자는 승인해야 하는 새로운 보고가 있는지 확인하거나 알림을 받아야 합니다.
  • 관리자가 '대기' 시간(조정 가능) 동안 승인하지 않으면 자동으로 청구가 거부됩니다.
  • 청구를 검토한 후 '클라이언트'와 '관리자'는 검토 결과에 따라 업데이트되어야 합니다.

WF를 사용하면 프레임워크에서 제공하는 표준 작업을 사용하여 이러한 프로세스를 모델링할 수 있습니다. DelayActivity를 사용하면 일정 기간이 지난 뒤의 이벤트 트리거를 관리할 수 있고, 규칙 엔진 및 PolicyActivity를 사용하면 결과에 대한 질문을 받는 융통성 있는 규칙 집합을 관리할 수 있습니다.

이 프로세스는 사용자 지향 프로세스이므로 개발자는 최종 사용자와 상호 작용하고 이러한 상호 작용이 발생하도록 워크플로를 구성해야 합니다. WF는 로컬 서비스인 HandleExternalEventActivityCallExternalMethodActivity를 통해 호스트 및 워크플로 간 통신을 허용하는 포괄적인 프로그래밍 모델을 제공합니다.

이 부분은 대화형 워크플로를 구축하는 데 있어 중요한 개념이므로 WF 설계에 어떻게 반영되었는지 간략하게 살펴보겠습니다.

WF에서 상호 작용을 모델링하기 위해서는 여러 이벤트와 메서드를 제공하는 계약을 설계해야 합니다. 이 계약은 워크플로 및 호스트 프로세스 양쪽에서 모두 인식됩니다. 구축하려는 계약/인터페이스는 워크플로-데이터 교환을 위해 설계되었음을 식별하는 [ExternalDataExchange()] 특성으로 표시해야 합니다. 샘플에서는 워크플로에 IExpenseLocalService 인터페이스를 사용합니다.

그런 다음 워크플로 런타임으로 해당 인터페이스를 구현하는 클래스(로컬 서비스라고 함)를 등록합니다. 워크플로 작업은 이벤트에 등록하거나, 인터페이스 유형에 대해 정의된 메서드를 사용할 수 있으며, 등록한 로컬 서비스로 연결됩니다. 여기에서는 로컬 서비스의 기반 형식과 워크플로 간의 밀결합을 제거하는 제어 반전(Inversion of Control)이라고 하는 패턴을 사용합니다. 샘플에서는 ExpenseLocalService 클래스가 IExpenseLocalService 계약을 구현합니다.

워크플로는 처음 실행될 때 작업할 초기 데이터를 받을 수 있습니다. 워크플로가 외부 상호 작용이 필요한 지점에 이르면 워크플로 내에서 HandleExternalEventActivity에 바인딩할 수 있는 이벤트를 발생시킬 수 있습니다. 이 작업은 인터페이스 유형과 이벤트를 인수로 취하며, 이벤트가 발생하여 실행을 계속하도록 허용하면 워크플로가 가동을 시작합니다.

워크플로에서 로컬 서비스를 콜백해야 하는 경우 CallExternalMethodActivity를 사용하고 인터페이스 및 메서드 이름을 인수로 제공하면 됩니다.

이러한 작업을 사용하여 실행 중인 워크플로가 있는 호스트 프로세스 내에서 양방향 통신을 수행할 수 있으며 WF 내에서 제어 반전 패턴을 사용하여 워크플로와 로컬 서비스 간의 밀결합을 방지할 수 있습니다.

그러나 호스트 프로세스보다 범위를 넓혀 다른 시스템 또는 사용자에 의한 상호 작용을 허용해야 합니다. 다른 서비스나 사용자 기반 응용 프로그램에서 호출할 수 있는 모든 서비스에 대화식 작업을 분산하면 이러한 수준의 상호 작용을 달성할 수 있습니다. WCF는 이러한 메시징 기능을 유연하게 구축할 수 있는 프레임워크입니다.

이번 시나리오에서 WCF와의 통합이 주는 주요 이점은 다음과 같습니다.

  • 서비스 구현을 메시징 내부 작업 코드에서 분리할 수 있습니다.
  • 시스템 연결을 위한 코드 및 복잡성이 훨씬 줄어듭니다.
  • 배포 유연성을 확보할 수 있습니다.
  • 호스트에서 클라이언트로 직접 콜백을 사용할 수 있으므로 더 빠르고 오버헤드가 적은 방법으로 정보를 업데이트할 수 있습니다.

통합을 위한 검사 목록

WF와 WCF의 통합을 완료하려면 사용자가 워크플로를 시작하거나 실행 중인 워크플로와 상호 작용할 수 있는 여러 인터페이스 지점을 제공하는 서비스 인터페이스를 노출해야 합니다. 서비스는 비즈니스 프로세스가 외부 엔터티(예: 프로세스에 관련된 사용자)와 상호 작용하는 지점을 중심으로 모델링해야 합니다.


그림 2. 비용 보고 시나리오의 상호 작용 지점

이를 위해서는 다음을 수행해야 합니다.

  • 서비스 계약을 정의합니다.
  • 이벤트를 통해 새로운 워크플로를 만들거나 기존 워크플로와 상호 작용하는 서비스 작업을 구현합니다.
  • 서비스 호스트 내에서 워크플로 런타임 인스턴스를 호스팅합니다.

단순히 워크플로를 호스팅하는 것 이외에도 WCF 이중 채널을 활용하여 워크플로의 이벤트를 소비 클라이언트에서 발생시킬 수 있습니다. 비용 보고의 경우 솔루션이 정기적인 데이터 업데이트를 위해 서비스를 폴링하는 클라이언트에 의존하므로 이러한 기능이 도움이 됩니다. 클라이언트는 이러한 방법 대신 서비스에서 직접 알림을 받을 수 있습니다.

이를 위해서는 다음을 수행해야 합니다.

  • 콜백 계약을 정의합니다.
  • 이중 채널을 지원하는 바인딩을 사용합니다.

서비스 계약 정의

WCF(Windows Communication Foundation)는 서비스의 기능과 데이터 교환을 추상적으로 정의하는 공식 계약을 선언하도록 요구합니다. 이 계약은 코드에서 인터페이스 선언을 통해 정의합니다.

비즈니스 서비스를 설계할 때는 일반적으로 요청/응답 공동 작업 패턴을 사용하게 됩니다. 이 패턴을 사용하는 경우 제공하려는 계약에서 다음과 같은 세 가지 측면을 준비해야 합니다.

  • 게시되는 작업. 서비스가 해당 소비자에게 게시하는 기능이며 인터페이스상의 메서드입니다.
  • 각 요청 및 응답에 대해 구조화된 데이터를 캡슐화하는 메시지. 각 메서드에 대한 인수 및 반환 형식입니다. WCF 용어로는 대개 메시지 계약이지만 비교적 간단한 시나리오에서는 데이터 계약이 됩니다.
  • 서비스를 통해 교환할 수 있는 핵심 비즈니스 엔터티의 데이터 정의. 메시지의 일부를 구성하는 요소로, WCF 용어로는 데이터 계약이 됩니다.

서비스 계약은 작업을 노출하는 계약을 정의한 후 연결을 통해 게시되는 특정 작업을 정의하는 특성 기반 태그를 사용하여 정의됩니다.

각 서비스 계약은 [ServiceContract] 특성으로 명시적으로 표시됩니다. 이 특성은 다음과 같은 매개 변수와 함께 선언할 수 있습니다.

  • Name. WSDL <portType> 요소에 선언된 계약 이름을 제어합니다.
  • Namespace. WSDL <portType> 요소에 선언된 계약의 네임스페이스를 제어합니다.
  • SessionMode. 계약에 세션을 지원하는 바인딩이 필요한지 여부를 지정합니다.
  • CallbackContract. 클라이언트 콜백에 사용될 계약을 지정합니다.
  • ProtectionLevel. 계약에 ProtectionLevel 속성을 지원하는 바인딩이 필요한지 여부를 지정합니다. 이 속성은 암호화 및 디지털 서명을 위한 요구 사항을 선언하는 데 사용됩니다.
작업 선언

이제 서비스는 여러 개의 게시된 작업으로 구성됩니다. 작업은 [OperationContract] 특성으로 표시되어 명시적으로 계약에 포함됩니다. ServiceContract와 마찬가지로 OperationContract에는 끝점에 바인딩되는 방식을 제어하는 다음과 같은 여러 매개 변수가 있습니다.

  • Action. 이 작업을 고유하게 식별하는 이름을 제어합니다. 메시지가 끝점에 수신되면 발송자는 제어 및 작업을 사용하여 호출할 메서드를 결정합니다.
  • IsOneWay. 작업이 요청 메시지를 받지만 응답은 생성하지 않음을 나타냅니다. 이는 단순히 void 반환 형식을 반환하는 것(이 경우 결과 메시지가 생성됨)과는 다릅니다.
  • ProtectionLevel. 작업에 필요한 암호화 또는 서명 요구 사항을 지정합니다.

다음은 코드에서 서비스 계약 부분을 보여 주는 예제입니다.

[ServiceContract]
public interface IExpenseService
{
    [OperationContract]
    GetExpenseReportsResponse GetExpenseReports();

    [OperationContract]
    GetExpenseReportResponse GetExpenseReport(GetExpenseReportRequest getExpenseReportRequest);
}

메시지 및 데이터 엔터티 선언

메시지는 전송할 각 메시지에 대한 페이로드 또는 본문을 정의하는 클래스로 모델링하는 것이 좋습니다. 이는 ASP.NET을 사용하여 웹 서비스를 구축할 때 WSCF(WS Contract First)와 같은 도구를 사용하여 메시지를 모델링하는 방식과 비슷합니다.

WCF는 기본적으로 DataContractSerializer라는 serialization 엔진을 사용하여 데이터를 serialize(데이터를 XML로 변환) 및 deserialize(XML을 데이터로 변환)합니다. DataContractSerializer를 사용하기 위해 System.Runtime.Serialization 네임스페이스에 대한 참조를 추가한 후 클래스에 [DataContract] 특성을 표시하고 게시할 멤버를 [DataMember]로 표시합니다.

[DataContract]
public class GetExpenseReportsResponse
{
    private List<ExpenseReport> reports;

    [DataMember]
    public List<ExpenseReport> Reports
    {
        get { return reports; }
        set { reports = value; }
    }
}

메시지 내에서 사용되는 데이터 엔터티는 비즈니스 영역 내의 엔터티를 나타냅니다. 메시지 계약과 마찬가지로 DataContractSerializer 및 특성을 사용하여 배포되는 멤버를 명시적으로 포함할 수 있습니다. 또는 데이터 모델링만 수행하려는 경우에는 공용 필드 방식을 사용하고 클래스를 serialize 가능한 것으로 표시할 수 있습니다.

샘플에서는 메시징 태그 처리를 위해 데이터 계약 방식을 사용했습니다. 실제 시나리오에서는 복잡한 스키마, 스키마에서의 속성 사용, 그리고 SOAP 헤더 사용 요구 사항을 처리해야 하는 경우가 많습니다. WCF는 이러한 극한의 상황을 위해 본문만이 아닌 전체 SOAP Envelope를 기술하는 [MessageContract] 특성으로 표시되는 클래스를 정의하는 기능을 제공합니다.

데이터 계약과 메시지 계약에 대한 자세한 내용은 이 기사의 마지막 부분에 있는 추가 정보에서 각 해당 MSDN Library 기사를 참조하십시오.


워크플로 런타임 호스팅

서비스에서는 일반적으로 서비스 형식의 새 인스턴스가 생성되고 세션 수명 주기 동안 유지 관리되는 동시 동작을 허용합니다. 이러한 상황에서 워크플로를 사용하려면 호출별로 실행하는 대신 워크플로 런타임의 인스턴스를 만들어 서비스-호스트 인스턴스의 수명 동안 이를 유지 관리해야 합니다.

이를 위해 권장되는 방식은 호스트 서비스 생성 시에 활성화되는 확장 클래스를 사용하는 것입니다. 이 확장은 워크플로 런타임의 전역 인스턴스를 만들고 유지 관리하며, 각 독립 서비스 인스턴스에서 이를 액세스할 수 있도록 허용합니다.

ServiceHost에 확장을 구현하려면 IExtension<ServiceHostBase>를 구현하는 클래스를 만듭니다. 솔루션에서는 WcfExtensions 코드 프로젝트 아래에 있는 WfWcfExtension 클래스를 통해 이에 대한 예제를 볼 수 있습니다.

여기에서는 두 가지 메서드, 즉 확장이 해당 부모 개체에 연결될 때 호출되는 Attach와 부모 개체가 언로드될 때 호출되는 Detach를 구현해야 합니다.

다음에서 볼 수 있듯이 Attach 메서드는 WorkflowRuntime의 새 인스턴스를 만들어 필요한 서비스로 인스턴스화합니다. 이 인스턴스는 workflowRuntime이라는 로컬 개인 필드에 저장합니다.

void IExtension<ServiceHostBase>.Attach(ServiceHostBase owner)
{
    workflowRuntime = new WorkflowRuntime(workflowServicesConfig);
    ExternalDataExchangeService exSvc = new ExternalDataExchangeService();
    workflowRuntime.AddService(exSvc);
    workflowRuntime.StartRuntime();
}

여기에서 볼 수 있듯이 워크플로 런타임의 초기화에는 시작하기 전에 서비스 인스턴스를 런타임에 추가하는 작업이 포함됩니다. 솔루션을 구축할 때는 일반적으로 런타임을 시작하기 전에 모든 서비스를 추가하는 것이 좋습니다. 그러나 결합이 문제시되는 경우에는 후기 바인딩 방식을 사용하는 것이 더 적합할 수 있습니다.

샘플에서는 WorkflowRuntime이 시작된 후에 ExpenseService 클래스에 있는 SetUpWorkflowEnvironment 메서드의 일부로 ExpenseLocalService 인스턴스를 ExternalDataExchangeService에 추가합니다.

다음에서 볼 수 있는 Detach 메서드는 StopRuntime을 호출하여 런타임을 종료합니다.

void IExtension<ServiceHostBase>.Detach(ServiceHostBase owner)
{
    workflowRuntime.StopRuntime();
}

WorkflowRuntime이 서비스-호스트 시작의 일부로 생성 및 초기화되므로 모든 기존 워크플로는 서비스 호출이 수행되기 전에 진행할 수 있게 됩니다. 서비스 호스트가 종료되면 워크플로 런타임도 깔끔하게 종료됩니다.

참고   워크플로를 호스팅하고 장시간 실행되는 워크플로를 모델링하는 경우에는 일반적인 방법인 워크플로 지속성 서비스(예: SqlWorkflowPersistenceService)를 사용하는 것이 좋습니다. 이는 응용 프로그램이나 프로세스가 다시 시작되더라도 상태 지속성을 유지하는 메커니즘을 제공합니다.
서비스 작업 만들기

서비스 동작을 포함하는 클래스를 만들려면 서비스 계약을 정의하는 인터페이스를 한 개 이상 구현해야 합니다.

public class ExpenseService :
        IExpenseService,
        IExpenseServiceClient,
        IExpenseServiceManager

워크플로와의 통합을 위해 서비스 메서드는 비즈니스 논리를 포함하지 않지만 대신 이벤트를 제어하거나 비즈니스 프로세스를 캡슐화하는 실행 중인 워크플로에 이벤트를 발생시키는 코드를 포함합니다.

워크플로를 처리하는 작업에서는 새 워크플로를 시작하거나 이미 실행 중인 워크플로와 상호 작용하게 됩니다.

새 워크플로 인스턴스를 만들려면 WorkflowRuntime을 사용하여 원하는 워크플로 유형의 새 인스턴스를 인스턴스화해야 합니다. ServiceHost 확장 클래스에서 이미 이러한 인스턴스를 만들었습니다. 이 인스턴스에 대한 참조를 얻으려면 OperationContext를 사용하여 사용자 지정 확장을 찾아야 합니다.

WfWcfExtension extension = OperationContext.Current.Host.Extensions.Find<WfWcfExtension>();
workflowRuntime = extension.WorkflowRuntime;

OperationContext는 서비스 메서드의 실행 컨텍스트에 대한 액세스를 제공하는 클래스입니다. 앞에 있는 코드를 보면 알 수 있듯이 이 클래스는 현재 서비스 메서드에 대한 컨텍스트를 제공하는 Current라는 단일 항목을 제공합니다. Host 속성을 호출하여 실행 중인 ServiceHost로 인스턴스를 가져온 다음 그 유형을 기반으로 확장을 찾습니다.

확장 인스턴스에 대한 참조를 확보한 다음에는 공용 속성을 통해 WorkflowRuntime을 반환하고 이를 사용하여 SequentialWorkflow의 새 인스턴스를 만들 수 있습니다.

Guid workflowInstanceId = submitExpenseReportRequest.Report.ExpenseReportId;

Assembly asm = Assembly.Load("ExpenseWorkflows");
Type workflowType = asm.GetType("ExpenseWorkflows.SequentialWorkflow");

WorkflowInstance workflowInstance = workflowRuntime.CreateWorkflow(workflowType, null, workflowInstanceId);
workflowInstance.Start();

expenseLocalService.RaiseExpenseReportSubmittedEvent(workflowInstanceId, submitExpenseReportRequest.Report);

위의 코드에서는 미리 정의된 유형을 바탕으로 새 워크플로 인스턴스를 만듭니다. 이는 직접적인 유형 인스턴스화를 통해서도 가능하지만 여기에서는 엄격하게 형식이 지정된 바인딩 대신 런타임에 동적인 규칙을 바탕으로 워크플로를 만드는 유연성을 발휘할 수 있음을 보여 줍니다.

마지막 줄은 워크플로의 첫 번째 HandleExternalEventActivity에서 처리되는 이벤트를 발생시켜 워크플로의 시작을 알립니다. 이 이벤트는 ExpenseLocalService 클래스의 인스턴스를 통해 발생시킵니다. 샘플에서는 ExpenseLocalService를 사용하여 새 워크플로를 시작하거나 기존 워크플로에 이벤트를 발생시켜 워크플로와 상호 작용합니다. 이 클래스는 비즈니스 프로세스를 캡슐화하는 메커니즘으로 사용되며 내부적으로는 WF를 사용하여 구현됩니다.


그림 3. 워크플로는 HandleExternalEventActivity로 시작됩니다.

처리해야 할 다른 유형의 상황은 기존 워크플로로 콜백하고 이벤트를 발생시키는 것입니다. 워크플로 엔진으로 이벤트를 발생시켜 기존 워크플로가 이벤트를 받고 처리를 계속할 수 있도록 해야 합니다.

비용 보고 흐름 내에서 이러한 상황이 발생하는 예로는 관리자 승인이 필요한 경우를 들 수 있습니다. 워크플로는 RequestManagerApproval에 대한 외부 메서드를 호출하고 이 메서드는 관리자에게 새 비용 보고서를 승인하거나 거부해야 한다는 알림을 생성합니다.

워크플로에는 가능한 이벤트 중 하나가 발생할 때까지 차단하는 ListenActivity가 포함되어 있습니다. 이 경우에는 관리자가 보고서를 검토했거나 DelayActivity에 따라 시간이 초과되었음을 알리는 이벤트를 수신하게 됩니다.


그림 4. ManagerApproval 사용자 지정 작업 흐름

Guid workflowInstanceId =
submitReviewedExpenseReportRequest.Report.ExpenseReportId;

ExpenseReportReviewedEventArgs e =
   new ExpenseReportReviewedEventArgs(workflowInstanceId, report, review);

if (ExpenseReportReviewed != null)
{
   ExpenseReportReviewed(null, e);
}

관리자가 ManagerApplication을 사용하여 보고서를 검토하면 호스트로 서비스 호출이 다시 수행되어 ExpenseReportReviewed 이벤트를 발생시키는 SubmitReviewedExpenseReport 메서드가 호출됩니다.

워크플로에서 HandleExternalEventActivity로 이벤트를 발생시키는 경우 이벤트를 라우팅할 수 있도록 현재 처리하고 있는 워크플로 인스턴스의 GUID를 알아야 합니다.

각 이벤트는 EventArgs와 함께 생성되는데, EventArgs를 사용하면 이벤트 모델을 통해 워크플로로 데이터를 다시 전달할 수 있습니다. 이 예에서는 검토 작업의 컨텍스트를 제공하는 데이터와 보고서의 현재 상태에 대한 세부 정보를 모두 전달할 수 있습니다.

워크플로에서 이벤트는 HandleExternalEventActivity의 속성을 통해 자동으로 워크플로에 연결됩니다.


그림 5. HandleExternalEventActivity를 IExpenseLocalService 인터페이스에 연결합니다.

[ExternalDataExchange] 특성으로 표시해야 하는 인터페이스 유형을 지정한 후 HandleExternalEventActivity에서 구독할 해당 인터페이스의 이벤트를 지정합니다.

이벤트 인수는 ExternalDataEventArgs 클래스에서 파생되어야 합니다. 이는 최소한 각 이벤트가 워크플로의 InstanceId와 같은 컨텍스트를 포함하게 된다는 것을 의미합니다. 그러면 워크플로 런타임은 올바른 워크플로 인스턴스로 이벤트를 라우팅하여 계속 진행되도록 합니다. 지속성 서비스를 사용하는 경우에는 런타임은 실행 기간 동안 워크플로에 대한 실행 상태의 하이드레이션 및 리하이드레이션 작업도 관리하게 됩니다.

서비스 호스팅

WCF 서비스를 호스팅하려면 ServiceHost 컨테이너 내에서 실행해야 합니다.

WCF를 사용하여 호스팅하는 방법을 검토하기에 앞서 선택할 수 있는 대안에 대해 살펴보겠습니다.

  • 표준 Windows 프로세스에서는 ServiceHost 인스턴스를 수동으로 만들고 열 수 있습니다.
  • Microsoft 인터넷 정보 서비스(IIS) 6.0을 통해 웹 끝점(웹 서비스)을 호스팅하는 경우 System.ServiceModel 네임스페이스에서 제공되는 사용자 지정 HttpHandler를 사용합니다.
  • IIS 7에서 호스팅하는 경우에는 WAS(Windows Activation Service)를 사용하여 끝점을 호스팅할 수 있습니다.

일반적으로 웹 서비스를 구축하는 경우에는 인터넷 정보 서비스를 사용하여 호스팅하는 방법을 선택하고, 디먼 역할을 하는 단일 인스턴스 끝점을 구축하는 경우에는 Windows 서비스를 사용하여 호스팅하는 방법을 선택합니다.

예제에서는 Windows 콘솔 응용 프로그램 내에서 주 서비스 인스턴스를 호스팅하는데, 이는 Windows 서비스를 호스팅하는 방식과 비슷합니다.

서비스를 배포하려면 ServiceHost 클래스의 인스턴스를 만들고 게시하려는 각 서비스 유형에 대해 이 인스턴스의 끝점을 엽니다. ServiceHost는 생성자의 일부로 여러 인수를 취하지만 가장 중요한 인수는 Type 인수 또는 ServiceContract를 구현하는 클래스의 인스턴스입니다.

  • PerCall 또는 PerSession 인스턴스를 사용하려는 경우에는 Type을 사용합니다.
  • Single 인스턴스를 사용하는 경우에는 단일 인스턴스를 사용하십시오.

WCF 내의 인스턴스 및 동시성에 대한 자세한 내용은 MSDN Library의 Sessions, Instancing, and Concurrency (영문)를 참조하십시오.

호스트가 설정된 후에는 사용할 수 있는 모든 구성을 구문 분석하고(이에 대한 자세한 내용은 배포 구성 섹션 참조) 이를 명시적으로 추가된 구성과 병합하여 사용할 수 있는 끝점을 확인한 다음 게시를 위해 이 끝점을 엽니다. 클라이언트에서 호출을 수신하면 요청은 새로운 백그라운드 작업자 스레드에서 처리되며, 메시지의 SOAP 계약 이름 및 작업에서 지정된 대로 적절한 서비스 작업으로 라우팅됩니다.

using (ServiceHost serviceHost = new ServiceHost(new ExpenseService()))
{
    WfWcfExtension wfWcfExtension = new WfWcfExtension("WorkflowRuntimeConfig");
    serviceHost.Extensions.Add(wfWcfExtension);
    serviceHost.Open();

    // 이 시점에서 프로세스를 차단합니다(예:Console.ReadLine();).

    serviceHost.Close();
}

ServiceHost 구성은 연결에 대한 끝점을 열기 전에 수행해야 합니다. 앞에서 보았듯이 이를 위해서는 .Open()을 호출하기 전에 호스트 개체와 상호 작용합니다. using 범위를 사용하여 ServiceHost를 사용하기 전에 삭제되도록 하고, 이 범위 끝에 명시적으로 Close()를 호출하여 모든 활성 연결 및 끝점을 깔끔하게 종료하는 것이 좋습니다.


배포 구성

WCF는 XML 구성을 통해 끝점을 구성할 수 있도록 함으로써 구현에서 배포 문제를 분리하는 메커니즘을 제공합니다. 이로써 관리자는 코드를 다시 개발하지 않고도 서비스 정책을 수정할 수 있습니다.

각 서비스는 한 개 이상의 끝점에 배포됩니다. 간단히 말해 끝점은 클라이언트가 서비스를 사용할 수 있는, 주소 지정이 가능한 연결 지점입니다. WCF에서 각 끝점은 WCF의 기초로 잘 알려진 세 가지 특성으로 선언됩니다.

이 세 특성은 Address, BindingContract입니다.

Address: 주소 지정이 가능한 이 끝점의 고유한 위치입니다. 일반적으로 서비스가 요청을 수신하는 위치의 절대 주소를 가리키는 URI입니다(예: http://myhost/myservice 또는 net.tcp://myhost:400/myservice).

Binding: 서비스와 소비자 간의 통신을 위한 프로토콜을 결정하는 정책입니다. Binding은 사용되는 전송 유형, 메시지 인코딩 방법 및 데이터를 serialize하는 방법과 같은 측면을 지정합니다. WCF에는 대부분의 일반적인 시나리오를 지원하는 즉시 사용 가능한 바인딩이 다수 포함되어 있습니다.

Contract: 코드에서 인터페이스를 통해 정의한 대로 게시되는 작업 및 데이터입니다.

서비스를 구성하려면 서비스를 선언하는 구성을 선언하고 해당 서비스에 대한 끝점을 모두 구성해야 합니다. 서비스에서 구현할 수 있는 계약의 수에는 제한이 없기 때문에 사용자가 게시해야 하는 끝점의 수도 여기에 맞춰 결정됩니다.

다음은 구성 예제입니다.

<services>
    <service name="ExpenseServices.ExpenseService">
        <endpoint
            address="http://localhost:8081/ExpenseService/Manager"
            binding="wsHttpBinding"
            contract="ExpenseContracts.IExpenseServiceManager" />
        <endpoint
            address="http://localhost:8081/ExpenseService/Client"
            binding="wsDualHttpBinding"
            contract="ExpenseContracts.IExpenseServiceClient" />
    </service>
</services>

이 구성 예제에서는 ExpenseServices.ExpenseService 유형의 서비스에 대해 구성을 선언합니다. 이렇게 하면 이 유형을 기반으로 새 ServiceHost를 인스턴스화할 때 런타임에서 구성을 찾을 수 있습니다. 바인딩에 대한 자세한 내용은 MSDN Library의 WCF Bindings (영문)를 참조하십시오.


서비스 사용

WCF를 통한 서비스 사용은 ChannelFactory 클래스를 사용하여 수행됩니다. ChannelFactory는 구성에 지정된 끝점에 연결하는 서비스 계약의 프록시 인스턴스를 제공하기 위해 팩토리 패턴을 사용합니다. 메시지 암호화를 위한 보안 자격 증명 및 인증서와 같은 런타임 정보를 사용하거나 동적으로 끝점 정보를 확인하도록 팩토리를 구성할 수 있습니다.

private IExpenseServiceManager CreateChannelExpenseServiceManager()
{
    ChannelFactory<IExpenseServiceManager> factory = new ChannelFactory<IExpenseServiceManager>("ExpenseServiceManager");
    IExpenseServiceManager proxy = factory.CreateChannel();

    return proxy;
}

여기에서 볼 수 있듯이 처음에 팩토리의 인스턴스를 만듭니다. 이 인스턴스는 서비스 계약에 대한 제네릭 인수를 사용하므로 원하는 계약의 인스턴스만 반환하는 더 정확한 팩토리를 만들 수 있습니다. 끝점에 사용될 구성을 결정하는 인수도 지정합니다. 이 경우에는 응용 프로그램 구성 파일에 있는 구성을 참조하는 ExpenseServiceManager라는 이름의 끝점 구성을 사용합니다.

<system.serviceModel>
    <client>
        <endpoint name="ExpenseServiceManager"
            address="http://localhost:8081/ExpenseService/Manager"
            binding="wsHttpBinding"
            contract="ExpenseContracts.IExpenseServiceManager" />
    </client>
</system.serviceModel>

끝점 정의가 호스트의 구성에 선언된 정의와 정확하게 일치하는 것을 알 수 있습니다. 일반적으로 구성이 다른 유일한 경우는 네트워크 구성이나 사용자 지정 동작이 구현되고 있어 클라이언트와 서버 간의 주소가 다를 때입니다.

Windows SDK를 설치했다면 프록시 클래스 및 끝점 구성 생성을 자동화하며 사용자의 솔루션으로 통합 가능한 도구(svcutil)를 사용할 수 있습니다. 이 도구를 활용하려면 대상 서비스가 WSDL 또는 WS-MetadataExchange를 통해 메타데이터 설명을 게시해야 합니다.


이중 채널 구성

지금까지는 통신 흐름이 소비자가 메시지를 보내고 서비스가 이에 응답하는 요청 응답 공동 작업 패턴을 사용한다고 가정했습니다. WCF는 이외에도 단방향(전송한 후 더 이상 추적하지 않음) 또는 양방향 이중 통신과 같은 여러 가지 다른 메시지 흐름을 지원합니다. 어느 쪽에서든 대화를 시작할 수 있는 메시지 흐름을 다루는 경우에는 이중 또는 양방향 채널을 사용해야 합니다. 이중 채널은 어느 쪽에서든 데이터를 전송할 수 있는 강력하게 연결된 시스템에서 매우 효과적입니다. 이 채널을 유용하게 활용할 수 있는 예는 이벤트에서 콜백을 제공하는 경우입니다.

클라이언트 콜백 구현

WCF에서 클라이언트 콜백은 CallbackContracts라고 하는 개념을 통해 구현됩니다. 여기서 게시하는 계약의 경우 클라이언트가 게시할 작업을 정의하는 두 번째 계약을 지명할 수 있으며, 이는 서비스에서 실행되는 코드를 사용하여 콜백할 수 있습니다.

CallbackContract를 선언하려면 인터페이스 유형을 콜백하려는 서비스 계약의 일부로 지정합니다.

[ServiceContract(CallbackContract = typeof(IExpenseServiceClientCallback))]

또한 netTcpBinding 또는 wsDualHttpBinding과 같이 이중 채널을 지원하는 바인딩을 사용해야 합니다. TCP를 통한 이중 통신은 메시지 교환이 이루어지는 동안 설정 및 유지되는 양방향 연결을 통해 이루어집니다. HTTP를 통한 통신은 클라이언트 수신기에 대한 콜백에 의해 수행됩니다. 클라이언트가 반환 경로를 인식하지 못하는 경우도 있고, 사용자가 구성을 통해 이를 엄격하게 정의하려는 경우도 있으므로 사용자 지정 바인딩 구성을 사용하여 대체 clientBaseAddress를 선언할 수 있습니다.

<endpoint binding="wsDualHttpBinding" bindingConfiguration="AlternativeClientCallback"/>
<bindings>
    <wsDualHttpBinding>
        <binding name="AlternativeClientCallback"
            clientBaseAddress="http://localhost:8082/ExpenseService/ClientCallback"/>
    </wsDualHttpBinding>
</bindings>

클라이언트에서 콜백 구현

콜백 계약 구현은 서비스 계약 구현과 완전히 동일합니다. 정의한 인터페이스의 구현을 제공해야 합니다.

class CallbackHandler : IExpenseServiceClientCallback
{
    public void ExpenseReportReviewed(ExpenseReportReviewedRequest expenseReportReviewedRequest)
    {
        // 여기에서 콜백에 응답하기 위한 클라이언트 논리를 구현합니다.
    }
}

호스트가 콜백할 CallbackHandler 클래스 인스턴스를 갖도록 하려면 연결의 이중 특성을 인식하도록 클라이언트 채널을 설정해야 합니다.

우선 앞서 설명한 대로 이중 채널을 지원하는 바인딩을 사용합니다. 다음으로, 서비스 끝점에 대한 연결을 초기화할 때 서비스에 대한 이중 연결을 만드는 DuplexChannelFactory라는 ChannelFactory의 하위 클래스 버전을 사용합니다.

private IExpenseServiceClient CreateChannelExpenseServiceClient()
{
    InstanceContext context = new InstanceContext(new CallbackHandler());

    DuplexChannelFactory<IExpenseServiceClient> factory = new DuplexChannelFactory<IExpenseServiceClient>(context, "ExpenseServiceClient");
    IExpenseServiceClient proxy = factory.CreateChannel();

    return proxy;
}

DuplexChannelFactory를 사용할 때 중요한 차이점은 CallbackHandler 클래스의 인스턴스를 초기화하고 이를 팩토리의 생성자에 전달하여 콜백에 사용될 컨텍스트를 초기화한다는 것입니다.

호스트에서 콜백 구현

호스트의 관점에서는 IExpenseServiceClient 계약에 정의된 콜백 채널을 통해 클라이언트로의 콜백에 대한 참조를 얻을 수 있습니다.

[ServiceContract(CallbackContract = typeof(IExpenseServiceClientCallback))]
public interface IExpenseServiceClient : IExpenseService

CallbackContract 특성은 호스트에서 이루어지는 콜백에 대한 계약을 정의하는 인터페이스를 선언합니다.

콜백을 수행하려면 다음과 같이 OperationContext.Current.GetCallbackChannel을 호출하여 콜백 계약에 대한 참조를 가져옵니다.

IExpenseServiceClientCallback callback = OperationContext.Current.GetCallbackChannel<IExpenseServiceClientCallback>();
callback.ExpenseReportReviewed(new ExpenseReportReviewedRequest(e.Report));

콜백 채널에 대한 참조를 가져온 다음에는 정상적으로 이를 호출할 수 있습니다.


결론

Windows Workflow Foundation은 워크플로 정의를 위한 범용 프레임워크와 실행 중인 워크플로를 호스팅하고 이 워크플로와 상호 작용할 수 있는 강력한 런타임 엔진을 제공합니다.

Windows Communication Foundation은 연결된 시스템을 구축하기 위한 범용 프레임워크를 제공하며, 통신 방법을 정의하기 위한 일관성 있는 API와 광범위한 기능 집합을 개발자에게 제공합니다.

이 두 가지 프레임워크를 함께 사용하면 현재 환경 내에서 분산 비즈니스 프로세스를 구축 및 배포하기 위한 유연하고 포괄적인 응용 프로그램 플랫폼을 제공할 수 있습니다. WF는 비즈니스 논리 및 프로세스를 모델링하고 캡슐화할 수 있도록 하며 WCF는 시스템 분산의 매개체가 되는 메시징 인프라를 제공합니다.

다음은 서비스를 설계할 때 기억해야 할 몇 가지 지침입니다.

  • 장시간 실행되는 워크플로는 지속성 서비스를 사용하여 제공해야 합니다.
  • 서비스 작업은 이벤트 발생을 통해 실행 중인 워크플로에서 상호 작용이 가능합니다. 워크플로는 사용자의 주의가 필요할 때 이벤트를 발생시키고, 외부(예: 외부 서비스 또는 사용자)와 상호 작용할 때 이벤트에 응답하도록 설계되어야 합니다.
  • 워크플로는 서비스 호출과 비동기적으로 실행되므로 어느 시점에 서비스 데이터를 반환해야 하고 해당 시점에서의 데이터 상태는 어떠한지 고려하여 이에 맞게 적절히 설계해야 합니다. 동기적 방식을 원하는 경우에는 ManualWorkflowSchedulerService 클래스를 사용하여 워크플로 실행을 수동으로 예약하도록 할 수 있습니다.

추가 정보
  1. Sessions, Instancing, and Concurrency (영문)
  2. WCF Bindings (영문)
  3. Using Data Contracts (영문)
  4. Using Message Contracts (영문)
  5. .NET Framework 3.0 커뮤니티 사이트 (영문)
  6. Windows Workflow Foundation 포럼 (영문)
출처 : 한국 마이크로소프트 MSDN (2007년 1월)