ASP.NET Workflow

장기 실행 작업을 지원하는 웹 응용 프로그램

Michael Kennedy

코드는 MSDN 코드 갤러리에서 다운로드할 수 있습니다.
온라인으로 코드 찾아보기

이 기사에서는 다음 내용에 대해 설명합니다.

  • 프로세스 독립적 워크플로
  • 동기 및 비동기 작업
  • 워크플로, 작업 및 지속성
  • ASP.NET과의 통합
이 기사에서 사용하는 기술:
Windows Workflow Foundation, ASP.NET

목차

워크플로 강화
동기 및 비동기 작업
유휴의 정확한 의미
동기 작업을 비동기 작업으로 만들기
워크플로 및 작업
지속성
구현
ASP.NET과의 통합
몇 가지 고려할 사항들
결론

소프트웨어 개발자는 장기 실행 작업을 지원하는 웹 응용 프로그램을 개발해야 하는 경우가 많습니다. 한 가지 예로 완료되는 데 몇 분이 걸릴 수 있는 온라인 상점의 체크 아웃 프로세스가 있습니다. 일부 표준에 의하면 이러한 작업도 장기 실행 작업이지만 이 기사에서는 이와는 차원이 다르게 완료되는 데 며칠이나 몇 주 또는 몇 달이 걸리는 장기 실행 작업에 대해 알아보겠습니다. 이러한 작업의 한 예로 여러 사람 간의 상호 작용과 실제로 여러 문서 교환이 포함되는 입사 원서 처리 과정이 있습니다.

첫 번째로 ASP.NET 관점에서 덜 심각한 문제부터 시작하여 온라인 상점에서 체크 아웃 작업을 위한 솔루션을 설계해야 한다고 가정해 보겠습니다. 이 작업의 기간 때문에 이 솔루션에 대한 특별한 고려가 필요합니다. 예를 들어 쇼핑 카트 데이터를 ASP.NET 세션에 저장하도록 선택할 수 있습니다. 사이트 업데이트와 부하 분산을 위해 세션 상태를 out-of-process 상태 서버나 데이터베이스로 이동하도록 선택할 수도 있습니다. 이 경우에도 문제를 해결하는 데 필요한 모든 도구를 ASP.NET 자체에서 찾을 수 있습니다.

그러나 작업이 일반적인 ASP.NET 세션 기간인 20분보다 오래 지속되거나 앞부분의 소개한 입사 원서의 예처럼 여러 관계자가 참여하는 경우에는 ASP.NET에서 제공하는 기능으로는 충분하지 않습니다. ASP.NET 작업자 프로세스는 유휴 상태가 되면 자동으로 종료되고 주기적으로 재활용된다는 것을 기억할 것입니다. 장기 실행 작업의 경우 이러한 프로세스 안에 저장된 상태가 손실되기 때문에 심각한 문제입니다.

이러한 장기 실행 작업을 단일 프로세스 내에 호스트한다고 가정해 보십시오. 앞에서 설명한 이유 때문에 ASP.NET 작업자 프로세스는 이러한 작업에 적합하지 않다는 것을 알 수 있습니다. 즉, 이러한 작업을 실행하는 것이 유일한 목표인 Windows 서비스를 작성하는 것을 생각해 볼 수 있습니다. 이 서비스를 다시 시작하지 않는다면 ASP.NET을 직접 사용하는 것이 비해 문제 해결에 가까워질 것입니다. 서비스 프로세스가 자동으로 다시 시작하지 않도록 한다면 이론적으로 장기 실행 작업의 상태가 손실되지 않을 것이기 때문입니다.

그러나 이 방법으로 문제가 해결될까요? 그렇지는 않습니다. 예를 들어 서버에 부하 분산이 필요한 경우에는 어떻게 해야 할까요? 단일 프로세스에 묶여 있는 경우에는 매우 까다로운 문제가 됩니다. 서버를 다시 부팅해야 하거나 프로세스에서 충돌이 발생하면 문제가 더 심각해지며, 이 경우에는 실행 중이던 모든 작업이 손실됩니다.

실제로 작업이 완료되는 데 며칠이나 몇 주가 걸리는 경우에는 이를 실행하는 프로세스의 수명 주기와는 독립적인 솔루션이 필요합니다. 이것은 일반적인 사항이지만 ASP.NET 웹 응용 프로그램에는 특히 중요합니다.

워크플로 강화

Windows WF(Workflow Foundation)는 웹 응용 프로그램을 개발하기 위한 기술로 먼저 생각나는 기술은 아닙니다. 그러나 이러한 워크플로 솔루션에서 고려할 만한 몇 가지 핵심적인 WF의 기능이 있습니다. WF에는 유휴 워크플로를 프로세스 공간에서 완전히 언로드하고 더 이상 유휴 상태가 아니면 이를 활성 프로세스로 자동으로 다시 로드(그림 1 참조)하는 기능이 있으며 이를 통해 장기 실행 작업을 위한 프로세스 독립성을 달성할 수 있습니다. WF를 사용하면 ASP.NET 작업자 프로세스의 비결정적 수명 주기를 극복하여 웹 응용 프로그램 내에서 장기 실행 작업을 제공할 수 있습니다.

fig01.gif

그림 1 프로세스 인스턴스 간에 작업을 보존하는 워크플로

이 기능을 제공하기 위해 WF의 두 기능을 조합하여 사용하게 됩니다. 첫째, 비동기 작업은 외부 이벤트를 기다리는 동안 워크플로가 유휴 상태라는 신호를 워크플로 런타임에 보냅니다. 둘째, 지속성 서비스는 유휴 워크플로를 프로세스로부터 언로드하고, 데이터베이스와 같은 지속성 저장소 위치에 이를 저장한 다음, 다시 실행할 준비가 되면 워크플로를 다시 로드합니다.

이러한 프로세스 독립성에는 다른 혜택이 있습니다. 즉, 부하 분산을 손쉽게 수행할 수 있는 방법은 물론 영속성을 부여하여 프로세스나 서버 오류 발생 시에 내결함성을 구현할 수 있습니다.

동기 및 비동기 작업

작업은 WF의 원자성 요소입니다. 모든 워크플로는 복합 디자인 패턴과 비슷한 패턴의 작업으로부터 작성됩니다. 실제로 워크플로 자체도 전문화된 작업이라고 할 수 있습니다. 이러한 작업은 동기 또는 비동기로 분류할 수 있습니다. 동기 작업은 포함된 모든 명령을 처음부터 끝까지 실행합니다.

동기 작업의 예로 온라인 상점에서 주문에 대한 세금을 계산하는 작업이 있습니다. 이러한 작업을 구현하는 방법을 생각해 보겠습니다. 대부분의 WF 작업과 마찬가지로 수행되는 작업은 대부분 재정의된 Execute 메서드 내에서 이루어집니다. 이 메서드는 다음과 비슷한 단계를 수행할 것입니다.

  1. 이전 작업에서 주문 데이터를 얻습니다. 일반적으로 이 작업은 데이터 바인딩을 통해 수행되며 조금 뒤에 이에 대한 예를 살펴보겠습니다.
  2. 주문 고객에 대한 정보를 데이터베이스에서 조회합니다.
  3. 데이터베이스에서 고객의 주소에 따라 적용되는 세율을 조회합니다.
  4. 세율과 주문 상품 정보를 사용하여 간단한 계산을 수행합니다.
  5. 후속 작업에서 바인드할 수 있는 속성에 세금을 저장하여 체크 아웃 프로세스를 완료합니다.
  6. Execute 메서드에서 Completed 상태 플래그를 반환하여 이 작업이 완료되었다는 신호를 워크플로 런타임으로 보냅니다.

중요한 것은 대기하는 부분이 없고 연속적으로 작업을 수행한다는 것입니다. Execute 메서드는 단계를 순서대로 수행하고 간단한 주문을 완료합니다. 모든 작업이 Execute 메서드 내에서 수행된다는 것이 동기 작업의 핵심입니다.

비동기 작업은 이와는 다르게 일정 시간 동안 작업을 실행한 다음 외부 자극을 기다리면서 실행을 중지합니다. 대기하는 동안 작업은 유휴 상태가 됩니다. 이벤트가 발생하면 작업을 재개하고 실행을 완료합니다.

비동기 작업의 예로는 지원자가 제출한 입사 원서를 관리자가 검토해야 하는 고용 과정이 있습니다. 관리자가 휴가 중이기 때문에 다음 주까지는 원서를 검토할 수 없다고 가정해 보십시오. Execute 메서드라면 실행 중에 이러한 응답을 기다리기 위해 실행을 중단하는 것이 현실적으로 불가능합니다. 소프트웨어가 사람을 기다려야 하는 경우에는 오랫동안 기다려야 합니다. 여러분의 디자인에도 이를 고려해야 합니다.

유휴의 정확한 의미

이 시점에서 영어의 의미 체계와 아키텍처의 의미 체계가 달라집니다. WF에서 잠시 벗어나서 유휴가 의미하는 것을 조금 더 일반적으로 생각해 보겠습니다.

웹 서비스를 사용하여 암호를 변경하는 다음 클래스를 살펴보겠습니다.

public class PasswordOperation : Operation {
  Status ChangePassword(Guid userId, string pw) {
    // Create a web service proxy:
    UserService svc = new UserService();

    // This can take up to 20 sec for 
    // the web server to respond:
    bool result = svc.ChangePassword( userId, pw );

    Logger.AccountAction( "User {0} changed pw ({1}).",
      userId, result);
    return Status.Completed;
  }
}

ChangePassword 메서드가 유휴 상태가 되는 경우가 있을까요? 있다면 어디일까요?

이 메서드의 스레드는 UserService에서 HTTP 응답을 기다리는 동안 차단됩니다. 즉, 개념상으로 유휴 상태가 될 수 있으며 서비스 응답을 기다리는 동안 스레드가 유휴 상태가 됩니다. 그러나 서비스를 기다리는 동안 스레드는 다른 작업을 할 수 있을까요? 그렇지는 않습니다. 스레드가 현재 사용 중이기 때문에 다른 작업을 할 수 없습니다. 즉, WF 관점에서는 이 "워크플로"가 유휴 상태가 아닌 것입니다.

유휴 상태가 아닌 이유는 무엇일까요? ChangePassword와 같은 작업을 효율적으로 실행하기 위해 큰 규모의 스케줄러 클래스를 실행했다고 가정해 보겠습니다. 여기서 효율적이라는 것은 여러 작업을 병렬로 실행하고, 완전 병렬 처리를 위한 최소한의 스레드를 사용하는 등의 특성을 의미합니다. 이러한 효율을 달성하는 핵심은 언제 작업이 실행 중이고 유휴 상태인지 아는 것입니다. 스케줄러는 작업이 유휴 상태인 경우 작업을 실행하던 스레드를 다시 작업을 실행할 준비가 될 때까지 다른 작업을 하는 데 사용할 수 있기 때문입니다.

아쉽게도 스케줄러는 ChangePassword 메서드를 완벽하게 볼 수 없습니다. 이 메서드는 사실상 유휴 상태인 기간이 있지만 스케줄러가 외부에서 보았을 때는 차단된 단일 작업 단위일 뿐입니다. 스케줄러에는 작업 단위를 분리하여 유휴 상태인 동안 스레드를 다시 사용하는 기능이 없습니다.

동기 작업을 비동기 작업으로 만들기

작업이 잠재적으로 유휴 상태가 될 수 있는 부분까지의 코드와 유휴 상태 이후에 실행되는 코드의 두 부분으로 나누면 작업에 필요한 스케줄링 투명성을 추가할 수 있습니다.

앞서 가상의 예에서는 웹 서비스 프록시 자체에서 제공되는 비동기 기능을 사용할 수 있습니다. 조금 뒤에 살펴보겠지만 이러한 간략한 설명과 WF가 실제로 작동하는 방식에는 약간의 차이가 있습니다.

그림 2에는 ChangePasswordImproved라는 향상된 버전의 암호 변경 메서드가 있습니다. 이 메서드에서는 먼저 이전과 마찬가지로 웹 서비스 프록시를 만듭니다. 그런 다음 서버가 응답할 때 알림을 받을 콜백 메서드를 등록합니다. 그리고 서비스 호출을 비동기적으로 실행하고 Status.Executing을 반환하여 작업이 유휴 상태이며 완료되지 않았다는 것을 스케줄러에 보고합니다. 이 단계는 이 코드가 유휴 상태인 동안 스케줄러가 다른 작업을 수행할 수 있도록 하는 중요한 단계입니다. 마지막으로 완료 이벤트가 발생하면 스케줄러를 실행하여 작업이 완전히 완료되었다는 신호를 보냅니다.

그림 2 간단한 암호 변경 서비스 호출

public class PasswordOperation : Operation {
  Status ChangePasswordImproved(Guid userId, string pw) {
    // Create a web service proxy:
    UserService svc = new UserService();

    svc.ChangePasswordComplete += svc_ChangeComplete;
    svc.ChangePasswordAsync( userId, pw );
    return Status.Executing;
  }

  void svc_ChangeComplete(object sender, PasswordArgs e) {
    Logger.AccountAction( "User {0} changed pw ({1}).",
      e.UserID, e.Result );

    Scheduler.SignalCompleted( this );
  }
}

워크플로 및 작업

이번에는 작업 유휴 상태의 개념을 WF를 사용하여 작업을 작성하는 데 적용해보겠습니다. 앞서 살펴본 내용과 매우 비슷하지만 이번에는 WF 모델 내에서 작업해야 합니다.

WF에는 다양한 기본 제공 작업이 포함되어 있습니다. 그러나 WF를 사용하여 실제 시스템 구축을 시작하면 머지 않아서 재사용 가능한 사용자 지정 작업을 작성하기를 원하게 됩니다. 이렇게 하려면 보편적인 Activity 클래스에서 파생되는 클래스를 정의하기만 하면 됩니다. 다음은 기본적인 예입니다.

class MyActivity : Activity {
  override ActivityExecutionStatus 
    Execute(ActivityExecutionContext ctx) {

    // Do work here.
    return ActivityExecutionStatus.Closed;
  }
}

작업에서 유용한 일을 하려면 Execute 메서드를 재정의해야 합니다. 수명이 짧은 동기 작업을 작성하는 경우에는 이 메서드 내에서 작업의 동작을 구현하고 Closed 상태를 반환하면 됩니다.

실제 작업에서는 작업이 워크플로 및 워크플로를 호스트하는 큰 응용 프로그램 내의 다른 작업과 통신하는 방법과 데이터베이스 시스템, UI 상호 작용과 같은 서비스에 액세스하는 방법 등과 같이 조금 더 큰 문제들을 고려해야 할 가능성이 많습니다. 동기 작업을 작성하는 경우 이러한 문제는 비교적 간단하게 해결할 수 있습니다.

비동기 작업을 작성하는 경우에는 조금 더 복잡한 문제를 해결해야 하지만 다행스럽게도 대부분의 비동기 작업에 동일한 패턴이 반복적으로 사용됩니다. 조금 뒤에 살펴보겠지만 이 패턴을 손쉽게 기본 클래스에 캡처하여 사용할 수 있습니다.

다음은 대부분의 비동기 작업에 필요한 기본적인 단계입니다.

  1. Activity에서 파생되는 클래스를 만듭니다.
  2. Execute 메서드를 재정의합니다.
  3. 기다리고 있는 비동기 이벤트 완료에 대한 알림을 받는 데 사용되는 워크플로 큐를 만듭니다.
  4. 큐의 QueueItemAvailable 이벤트를 구독합니다.
  5. 장기 실행 작업을 시작합니다(예: 구직 원서 검토를 부탁하는 전자 메일을 관리자에게 전송).
  6. 외부 이벤트가 발생하기를 기다립니다. 이렇게 하면 작업이 유휴 상태가 되었다는 신호가 발생합니다. ExecutionActivityStatus.Executing을 반환하여 워크플로 런타임에 이를 알릴 수 있습니다.
  7. 이벤트가 발생하면 QueueItemAvailable 이벤트를 처리하는 메서드는 큐에서 항목을 제거하고 이를 예상되는 데이터 형식으로 변환한 다음 결과를 처리합니다.
  8. 일반적으로 이 시점에 작업 실행이 완료됩니다. ActivityExecutionContext.CloseActivity를 반환하여 워크플로 런타임에 신호를 보냅니다.

지속성

이 기사의 처음 부분에서 필자는 워크플로를 통해 프로세스 독립성을 얻기 위해 필요한 두 가지 기본적인 요소로 비동기 작업과 종속성 서비스를 들었습니다. 비동기 작업 부분은 방금 살펴보았으며 다음은 종속성을 위한 기술인 워크플로 서비스에 대해 알아보겠습니다.

워크플로 서비스는 WF의 핵심 확장 지점입니다. WF 런타임은 실행되는 모든 워크플로를 호스트하기 위해 응용 프로그램에서 인스턴스화하기 위한 클래스입니다. 이 클래스에는 워크플로 서비스의 개념을 통해 동시에 달성할 수 있는 두 가지 상반되는 디자인 목표를 가지고 있습니다. 첫 번째 목표는 이 워크플로 런타임이 여러 위치에서 사용될 수 있는 간소한 개체가 되도록 하는 것입니다. 두 번째 목표는 이 런타임이 실행 중에 런타임에 강력한 기능을 제공하는 것입니다. 예를 들어 유휴 상태인 워크플로를 자동으로 유지하고, 워크플로 진행 상황을 추적하며, 다른 사용자 지정 기능을 지원할 수 있습니다.

이러한 기능 중에서 두 가지만 기본 제공되므로 워크플로 런타임은 기본적으로 간소합니다. 지속성이나 추적과 같은 복잡한 서비스는 서비스 모델을 통해 선택적으로 설치할 수 있습니다. 실제로 서비스의 정의는 워크플로에 제공하고자 하는 전역 기능을 의미합니다. WorkflowRuntime 클래스의 AddService 메서드를 호출하여 런타임에 이러한 서비스를 설치할 수 있습니다.

void AddService(object service)

AddService는 System.Object 참조를 받으므로 워크플로에 필요한 어떤 것이라도 추가할 수 있습니다.

여기에서는 두 가지 서비스를 사용할 것입니다. 첫 번째로 비동기 작업을 만드는 데 필수적인 워크플로 큐에 액세스하기 위해 WorkflowQueuingService를 사용할 것입니다. 이 서비스는 기본적으로 설치되며 사용자 지정할 수 없습니다. 다른 서비스는 SqlWorkflowPersistenceService입니다. 이 서비스는 지속성 기능을 제공하며 기본적으로 설치되지는 않지만 WF에 포함되어 있으므로 런타임에 추가하기만 하면 됩니다.

SqlWorkflowPersistenceService라는 이름을 보면 어디에서인가는 데이터베이스가 필요하다는 것을 짐작할 수 있을 것입니다. 여기에 사용할 빈 데이터베이스를 만들거나 기존 데이터베이스에 약간의 테이블을 추가할 수 있습니다. 개인적으로 필자는 지속성 데이터를 다른 데이터와 혼합하기보다는 전용 데이터베이스를 만드는 것을 선호합니다. 그래서 SQL Server에 WF_Persist라는 빈 데이터베이스를 만들었습니다. 그런 다음 스크립트 두 개를 실행하여 필요한 데이터베이스 스키마와 저장 프로시저를 만들었습니다. 이러한 스크립트는 Microsoft .NET Framework의 일부로 설치되며 다음 폴더에 있습니다.

C:\Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN\

SqlPersistenceService_Schema.sql 스크립트를 먼저 실행한 다음 SqlPersistenceService_Logic.sql 스크립트를 실행하십시오. 이제 연결 문자열을 지속성 서비스로 전달하여 이 데이터베이스를 지속성에 사용할 수 있습니다.

SqlWorkflowPersistenceService sqlSvc = 
    new SqlWorkflowPersistenceService(
  @"server=.;database=WF_Persist;trusted_connection=true",
  true, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));

wfRuntime.AddService(sqlSvc);

유휴 워크플로를 언로드하고 데이터베이스에 저장하며 다시 필요할 때 복원하려면 이 간단한 AddService 메서드만 호출하면 됩니다. WF 런타임이 나머지 모든 부분을 처리합니다.

구현

이제 기술적 기반은 모두 준비되었으므로 이를 하나로 엮어서 장기 실행 작업을 지원하는 ASP.NET 웹 사이트를 구축하기만 하면 됩니다. 이 예에서 살펴볼 세 가지 주요 요소는 비동기 작업 작성, 웹 응용 프로그램에 워크플로 런타임 통합, 그리고 웹 페이지에서 워크플로와의 통신이 있습니다.

여기에서는 Trey Research라는 가상의 .NET 컨설팅 회사를 위한 응용 프로그램을 작성해 보겠습니다. 이 회사에서는 컨설턴트 모집과 고용 프로세스를 자동화하려고 하며, 이러한 고용 프로세스를 지원하는 ASP.NET 웹 사이트를 구축하고 있습니다. 모든 것을 최대한 간단하게 하겠지만 수행해야 할 몇 가지 단계가 있습니다.

  1. 입사 지원자가 Trey Research 웹 사이트에 방문하고 입사 의사를 전달합니다.
  2. 새로운 지원자가 있다는 것을 알리는 전자 메일이 관리자에게 전송됩니다.
  3. 관리자는 입사 원서를 검토하고 지원자에게 특정 직무를 승인합니다.
  4. 지원자에게 제안된 직무에 대한 정보가 포함된 전자 메일이 전송됩니다.
  5. 지원자는 웹 사이트에 방문하여 제안된 직무를 받아들이거나 거부합니다.

이 과정은 간단하지만 지원자가 다시 방문하여 추가 정보를 입력하기 위해 사람을 기다려야 하는 몇 가지 단계가 있습니다. 이러한 유휴 지점에서 시간이 오래 걸릴 수 있습니다. 따라서 이러한 부분에 장기 실행 프로세스를 설명하는 데 적합합니다.

이 웹 응용 프로그램은 기사의 소스 코드에 포함되어 있습니다. 그러나 전체 효과를 보려면 지속성 데이터베이스를 만들고 사용할 샘플을 구성해야 합니다. 이 예에서는 지속성 서비스를 설정 또는 해제할 수 있는 스위치를 만들었으며 기본적으로 이를 해제했습니다. 이를 설정하려면 web.config의 AppSettings 섹션에서 usePersistDB를 true로 설정하면 됩니다. 이 기사를 읽는 동안 작동하는 예를 보려면 필자의 웹 사이트에서 async work viewer를 확인하십시오.

그림 3 고용 프로세스 워크플로

완전하게 ASP.NET으로부터 독립적으로 워크플로를 디자인하는 것으로 작업을 시작하겠습니다. 워크플로를 작성하려면 사용자 지정 작업 네 개를 만들어야 합니다. 첫 번째는 전자 메일 전송 작업으로 간단한 동기 작업입니다. 나머지는 위에서 살펴본 1, 3 및 5번 단계를 수행하며 비동기 작업입니다. 이러한 작업은 장기 실행 작업의 성공에 필수적입니다. 이러한 작업을 각각 GatherEmployeeInfoActivity, AssignJobActivity 및 ConfirmJobActivity라고 부르겠습니다. 그런 다음 이러한 작업을 사실상 기본만 갖추고 있는 그림 3의 워크플로에 조합할 것입니다.

전자 메일 전송 작업은 단순하므로 이 기사에서 자세히 다루지는 않을 것입니다. 이 작업은 앞서 살펴본 MyActivity 클래스와 같은 동기 작업입니다. 자세한 내용은 코드 다운로드를 참조하십시오.

이제 세 가지 비동기 작업을 만들어야 합니다. 비동기 작업을 만들기 위한 8단계 프로세스를 하나의 공통 기본 클래스로 캡슐화할 수 있다면 수고를 덜 수 있을 것입니다. 이를 위해 AsyncActivity(그림 4 참조)라는 클래스를 정의할 것입니다. 이 기사의 코드에는 실제 코드에는 있는 몇 가지 내부 도우미 메서드와 오류 처리가 생략되어 있습니다. 이러한 세부 사항은 간소함을 위해 생략되었습니다.

그림 4 AsyncActivity

public abstract class AsyncActivity : Activity {
  private string queueName;

  protected AsyncActivity(string queueName) {
    this.queueName = queueName;
  }

  protected WorkflowQueue GetQueue(
      ActivityExecutionContext ctx) {
    var svc = ctx.GetService<WorkflowQueuingService>();
    if (!svc.Exists(queueName))
      return svc.CreateWorkflowQueue(queueName, false);

    return svc.GetWorkflowQueue(queueName);
  }

  protected void SubscribeToItemAvailable(
      ActivityExecutionContext ctx) {
    GetQueue(ctx).QueueItemAvailable += queueItemAvailable;
  }

  private void queueItemAvailable(
      object sender, QueueEventArgs e) {
    ActivityExecutionContext ctx = 
      (ActivityExecutionContext)sender;
    try { OnQueueItemAvailable(ctx); } 
    finally { ctx.CloseActivity(); }
  }

  protected abstract void OnQueueItemAvailable(
    ActivityExecutionContext ctx);
}

이 기본 클래스에서 비동기 작업을 만들기 위한 몇 가지 지루하고 반복적인 부분을 래핑했다는 것을 알 수 있습니다. 이 클래스를 처음부터 자세하게 살펴보겠습니다. 생성자에서는 큐 이름에 대한 문자열을 전달했습니다. 워크플로 큐는 느슨한 연결을 유지하면서 호스트 응용 프로그램(웹 페이지)에 데이터를 전달하기 위한 입력 지점입니다. 이러한 큐는 이름과 워크플로 인스턴스로 참조되므로 모든 비동기 작업에는 고유한 자체 큐 이름이 필요합니다.

다음은 GetQueue 메서드를 정의합니다. 여기에서 알 수 있듯이 워크플로 큐를 만들고 액세스하기는 쉽지만 다소 지루하기 때문에 이 클래스와 파생 클래스에서 도우미 메서드로 사용하도록 이 메서드를 만들었습니다.

그런 다음에는 SubscribeToItemAvailable이라는 메서드를 정의했습니다. 이 메서드는 워크플로 큐에 항목이 도착했을 때 발생하는 이벤트를 구독하기 위한 세부 사항을 캡슐화합니다. 이것은 워크플로가 유휴 상태인 동안 장기 대기 기간의 완료를 나타내는 경우가 대부분입니다. 작동 과정은 다음과 비슷합니다.

  1. 장기 실행 작업을 시작하고 SubscribeToItemAvailable을 호출합니다.
  2. 작업이 유휴 상태임을 워크플로 런타임에 알립니다.
  3. 지속성 서비스에 의해 워크플로 인스턴스가 데이터베이스로 직렬화됩니다.
  4. 작업이 완료되면 워크플로 큐로 항목이 전송됩니다.
  5. 이에 따라 데이터베이스로부터 워크플로 복원이 트리거됩니다.
  6. 기본 AsyncActivity에 의해 템플릿 메서드 OnQueueItemAvailable가 실행됩니다.
  7. 작업이 작동을 완료합니다.

AsyncActivity 클래스 작동을 확인하기 위해 AssignJobActivity 클래스를 구현해 보겠습니다. 다른 두 비동기 작업은 서로 비슷하며 코드 다운로드에 포함되어 있습니다.

그림 5에서는 AssignJobActivity가 AsyncActivity 기본 클래스에서 제공하는 템플릿을 사용하는 방법을 볼 수 있습니다. 장기 실행 작업을 시작하기 위한 준비 작업을 위해 Execute를 재정의했지만 이 예에서는 아무것도 하지 않았습니다. 그런 다음 추가 데이터 있을 때에 대한 이벤트를 구독합니다.

그림 5 AssignJobActivity

public partial class AssignJobActivity : AsyncActivity {
  public const string QUEUE NAME = "AssignJobQueue";

  public AssignJobActivity()
    : base(QUEUE_NAME) 
  {
    InitializeComponent();
  }

  protected override ActivityExecutionStatus Execute(
      ActivityExecutionContext ctx) {
    // Runs before idle period:
    SubscribeToItemAvailable(ctx);
    return ActivityExecutionStatus.Executing;
  }

  protected override void OnQueueItemAvailable(
      ActivityExecutionContext ctx) {
    // Runs after idle period:
    Job job = (Job)GetQueue(ctx).Dequeue();

    // Assign job to employee, save in DB.
    Employee employee = Database.FindEmployee(this.WorkflowInstanceId);
    employee.Job = job.JobTitle;
    employee.Salary = job.Salary;
  }
}

여기에는 호스트 응용 프로그램인 웹 페이지가 관리자로부터 해당 정보를 수집하면 새로운 Job 개체를 작업의 큐로 전송한다는 암시적인 계약이 있습니다. 이는 작업에 진행해도 좋다는 신호를 보냅니다. 그러면 데이터베이스에 있는 직원을 업데이트합니다. 워크플로의 다음 작업은 합격자에게 제안된 직무를 알리는 전자 메일을 전송합니다.

ASP.NET과의 통합

여기까지가 워크플로 내부에서 작업이 수행되는 방법입니다. 그렇다면 워크플로를 어떻게 시작할 수 있을까요? 웹 페이지가 관리자에게서 취업 정보를 받으려면 어떻게 해야 할까요? 그리고 웹 페이지가 작업에 Job 개체를 전달하려면 어떻게 해야 할까요?

우선 워크플로를 시작하는 방법부터 살펴보겠습니다. 웹 사이트의 시작 페이지를 보면 Apply Now 링크가 있습니다. 지원자가 이 링크를 클릭하면 워크플로를 시작하고 동시에 사용자에게 단계적으로 인터페이스를 안내합니다.

protected void LinkButtonJoin_Click(
    object sender, EventArgs e) {
  WorkflowInstance wfInst = 
    Global.WorkflowRuntime.CreateWorkflow(typeof(MainWorkflow));

  wfInst.Start();
  Response.Redirect(
    "GatherEmployeeData.aspx?id=" + wfInst.InstanceId);
}

간단하게 워크플로 런타임에서 CreateWorkflow를 만들고 워크플로 인스턴스를 시작했습니다. 그 다음에는 모든 후속 웹 페이지에 쿼리 매개 변수로서 인스턴스 ID를 전달하여 워크플로 인스턴스를 추적합니다.

웹 페이지에서 워크플로로 데이터를 전송하려면 어떻게 해야 할까요? 관리자가 지원자를 위한 직무를 선택하는 그림 6의 직무 할당 페이지를 보겠습니다.

그림 6 직무 할당

public class AssignJobPage : System.Web.UI.Page {
  /* Some details omitted */
  void ButtonSubmit_Click(object sender, EventArgs e) {
    Guid id = QueryStringData.GetWorkflowId();
    WorkflowInstance wfInst = Global.WorkflowRuntime.GetWorkflow(id);

    Job job = new Job();
    job.JobTitle = DropDownListJob.SelectedValue;
    job.Salary = Convert.ToDouble(TextBoxSalary.Text);

    wfInst.EnqueueItem(AssignJobActivity.QUEUE_NAME, job, null, null);

    buttonSubmit.Enabled = false;
    LabelMessage.Text = "Email sent to new recruit.";
  }
}

직무 할당 웹 페이지는 간단한 입력 폼이라고 할 수 있습니다. 여기에는 선택 가능한 직무에 대한 드롭다운 목록과 제안된 급여에 대한 텍스트 상자가 있습니다. 이 기사에는 코드가 나와 있지 않지만 현재 지원자도 표시합니다. 관리자가 지원자에 대한 직무와 급여를 할당하고 전송 단추를 클릭하면 그림 6에 나와 있는 코드가 실행됩니다.

이 페이지에서는 워크플로 인스턴스 ID를 쿼리 문자열 매개 변수로 사용하여 연결된 워크플로 인스턴스를 조회합니다. 그런 다음 Job 개체가 생성되고 폼에 있던 값으로 초기화됩니다. 마지막으로 Job을 작업의 큐에 저장하여 이 정보를 작업으로 다시 보냅니다. 이것은 유휴 워크플로를 다시 로드하고 실행을 계속할 수 있도록 하는 핵심 단계입니다. AssignJobActivity는 이 Job을 이전에 수집된 직원과 연결하고 데이터베이스에 저장합니다.

이러한 마지막 두 코드에서는 비동기 작업을 원활하게 수행하고 외부 호스트와 워크플로 통신을 수행하는 데 워크플로 큐가 필수적이라는 것을 보여 주고 있습니다. 워크플로를 사용하더라도 페이지 흐름에는 영향이 없다는 것도 중요합니다. WF를 사용하여 페이지 흐름을 제어할 수도 있지만 이 기사의 초점은 아닙니다.

그림 6에서는 다음과 같이 전역 응용 프로그램 클래스를 통해 워크플로 런타임에 액세스했다는 것을 알 수 있습니다.

WorkflowInstance wfInst = 
  Global.WorkflowRuntime.GetWorkflow(id);

이제 Windows 워크플로를 웹 응용 프로그램으로 통합하는 마지막 단계입니다. 즉, 워크플로 런타임 내에서 실행되는 모든 워크플로를 통합해야 합니다. AppDomain에는 원하는 만큼 여러 개의 워크플로 런타임을 사용할 수 있지만 단일 워크플로 런타임을 사용하는 것이 가장 적절합니다. 이 때문에, 그리고 WF 런타임 개체는 스레드로부터 안전하기 때문에 이를 전역 응용 프로그램 클래스의 공용 정적 속성으로 만들었습니다. 또한 응용 프로그램 시작 이벤트에서 워크플로 런타임을 시작하고 응용 프로그램 중지 이벤트에서 워크플로 런타임을 중지했습니다. 그림 7에는 전역 응용 프로그램 클래스의 간소화된 버전이 나와 있습니다.

그림 7 워크플로 런타임 시작

public class Global : HttpApplication {
  public static WorkflowRuntime WorkflowRuntime { get; set; }

  protected void Application_Start(object sender, EventArgs e) {
    WorkflowRuntime = new WorkflowRuntime();
    InstallPersistenceService();
    WorkflowRuntime.StartRuntime();
    // ...
  }

  protected void Application_End(object sender, EventArgs e) {
    WorkflowRuntime.StopRuntime();
    WorkflowRuntime.Dispose();
  }

  void InstallPersistenceService() {
    // Code from listing 4.
  }
}

응용 프로그램 시작 이벤트에서 런타임을 만들고, 지속성 서비스를 설치한 다음, 런타임을 시작했습니다. 그리고 응용 프로그램 종료 이벤트에서는 런타임을 중지합니다. 이는 중요한 단계입니다. 실행 중인 워크플로가 있는 경우 이러한 워크플로가 언로드될 때까지 차단합니다. 런타임을 중지한 다음에는 Dispose를 호출합니다. StopRuntime을 호출한 다음 Dispose를 호출하는 것이 중복되는 작업처럼 보이지만 그렇지 않으며 두 메서드를 순서에 맞게 호출해야 합니다.

몇 가지 고려할 사항들

이 기사에서 직접 다루지 않은 몇 가지 항목을 질문과 답 형식으로 알아보겠습니다. ManualWorkflowSchedulerService를 사용하지 않은 이유는 무엇일까요? WF과 ASP.NET의 통합에 대한 이야기를 하다 보면 스레드 풀을 사용하는 워크플로의 기본 스케줄러를 ManualWorkflowSchedulerService라는 서비스로 대체하는 것이 중요하다고 강조하는 사람들을 볼 수 있습니다. 그 이유는 장기 실행 목적에는 필요하지 않거나 적절하지 않기 때문입니다. 단일 워크플로가 해당 요청 내에서 완료되는 경우에는 수동 스케줄러로 충분합니다. 그러나 워크플로가 여러 요청은 물론 여러 프로세스 수명 주기에 걸쳐 실행되는 경우에는 적합하지 않습니다.

지정된 워크플로 인스턴스의 현재 상태를 추적하는 방법이 있습니까? 가능합니다. WF에 전체 추적 서비스가 기본 제공되고 있으며 SQL 지속성 서비스와 비슷한 방법으로 사용할 수 있습니다. Matt Milner의 2007년 3월 Foundations 칼럼, "Windows Workflow Foundation의 추적 서비스"를 참조하십시오.

결론

이 기사에서 설명된 기술을 두 단계로 요약할 수 있습니다. 먼저 ASP.NET 작업자 프로세스와 프로세스 모델이 장기 실행 작업에 적합하지 않은 이유를 설명했습니다. 이러한 한계를 극복하기 위해 비동기 작업과 워크플로 지속성이라는 WF의 두 가지 기능을 활용하여 프로세스 독립성을 달성했습니다.

비동기 작업을 작성하기는 다소 까다롭기 때문에 이 기사에서 소개한 AsyncActivity 기본 클래스에 세부 사항을 캡슐화했습니다. 그런 다음 비동기 작업으로 작성한 순차적 워크플로로 장기 실행 작업을 만들었으며 이를 웹 응용 프로그램에 통합하면 손쉽게 프로세스 독립성을 달성할 수 있습니다.

마지막으로 워크플로를 ASP.NET에 통합하는 데는 워크플로 큐를 통해 작업과 통신하는 것과 전역 응용 프로그램 클래스에서 런타임을 호스팅하는 두 가지 기본적인 부분이 있다는 것을 설명했습니다.

이제 WF를 ASP.NET에 통합하여 장기 실행 작업을 지원하는 방법을 배웠으므로 .NET Framework 기반의 솔루션을 구축하기 위한 더 강력한 도구가 하나 더 마련된 것입니다.

Michael Kennedy 는 DevelopMentor에서 강사로 일하고 있으며 이곳에서 핵심 .NET 기술과 Agile 및 TDD 개발 방법론을 전문적으로 가르치고 있습니다. 문의 사항이 있으면 michaelckennedy.net에 있는 웹 사이트와 블로그에 방문하십시오.