Silverlight

Silverlight를 사용한 기간 업무(LOB) 엔터프라이즈 응용 프로그램 구축, 2부

Hanu Kommalapati

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

  • Silverlight 런타임 환경
  • Silverlight 비동기 프로그래밍
  • 도메인 간 정책
  • 샘플 엔터프라이즈 응용 프로그램
이 기사에서 사용하는 기술:
Silverlight 2

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

목차

비즈니스 서비스와의 통합
서비스 호출
동기화된 서비스 호출
메시지 엔터티 변환
서비스 호출 후의 Silverlight 상태 변경
도메인 간 정책
IIS 외부에서 호스트되는 웹 서비스를 위한 도메인 간 정책
IIS 내부에서 호스트되는 서비스를 위한 도메인 간 정책
응용 프로그램 보안
응용 프로그램 분할
생산성 및 그 밖의 사항들

이 연재 기사의 1부에서는 콜 센터 시나리오를 소개하고 Silverlight에 의해 지원되는 비동기 TCP 소켓을 활용하는 연결된 소켓을 통한 화면 채우기(화면 팝) 구현을 살펴보았습니다("Silverlight를 사용한 기간 업무(LOB) 엔터프라이즈 응용 프로그램 구축, 1부" 참조).

화면 팝은 내부 큐에서 호출을 가져오고 서버의 제네릭 목록에 캐시된, 이전에 수락한 소켓 연결을 통해 알림을 밀어넣는 시뮬레이션되는 호출 발송자를 통해 구현됩니다. 여기에서는 응용 프로그램 보안, 비즈니스 서비스와의 통합, 웹 서비스를 위한 도메인 간 정책, 그리고 응용 프로그램 분할에 대해 살펴보겠습니다. 그림 1에는 콜 센터 응용 프로그램의 논리 아키텍처가 나와 있습니다. 인증 서비스는 유틸리티 서비스에 구현되며 비즈니스 서비스인 ICallService와 IUserProfile은 이름처럼 비즈니스 서비스 프로젝트에 내부에 구현됩니다.

그림 1 Silverlight 콜 센터 논리 아키텍처

다이어그램에는 유틸리티 서비스에 대한 이벤트 스트리밍이 나와 있지만 다운로드 가능한 데모에는 시간을 절약하기 위해 이 기능이 포함되지 않았습니다. 이벤트 캡처 서비스 기능의 구현은 비즈니스 서비스 구현과 비슷합니다. 그러나 중요한 오류가 아닌 비즈니스 이벤트는 일괄 처리 모드를 통해 로컬로 격리된 저장소에 캐시하고 서버로 덤프할 수 있습니다. 우선 비즈니스 서비스 구현에 대한 설명부터 시작하고 마지막에는 응용 프로그램 분할에 대해 살펴보겠습니다.

비즈니스 서비스와의 통합

서비스와의 통합은 기간 업무(LOB) 응용 프로그램의 중요한 측면 중 하나이며 Silverlight는 웹 기반 리소스와 서비스에 액세스하기 위한 구성 요소를 풍부하게 제공합니다. HttpWebRequest, WebClient 및 WCF(Windows Communication Foundation) 프록시 인프라는 HTTP 기반 상호 작용에서 자주 사용되는 네트워크 구성 요소입니다. 이 기사에서는 백 엔드 비즈니스 프로세스와의 통합을 위해 WCF 서비스를 사용할 것입니다.

우리는 응용 프로그램 개발 중에 백 엔드 데이터 원본과의 통합을 위해 대부분 웹 서비스를 사용합니다. Silverlight를 사용한 WCF 웹 서비스 액세스는 ASP.NET, WPF(Windows Presentation Foundation) 또는 Windows Forms 응용 프로그램과 같은 기존의 응용 프로그램과 크게 다르지 않습니다. 차이점은 바인딩 지원과 비동기 프로그래밍 모델입니다. Silverlight는 basicHttpBinding 및 PollingDuplexHttpBinding만 지원합니다. HttpBinding은 상호 운용성이 가장 좋은 바인딩이므로 이 기사에서는 모든 통합에 이를 사용할 것입니다.

PollingDuplexHttpBinding은 HTTP를 통해 알림을 밀어넣기 위해 콜백 계약 사용을 허용합니다. 이 콜 센터 예에서도 화면 팝 알림에 이 바인딩을 사용할 수 있습니다. 그러나 이를 구현하려면 서버에서 HTTP 연결을 캐싱해야 하므로 Internet Explorer 7.0과 같은 브라우저에서 허용되는 두 개의 동시 HTTP 연결 중 하나를 독점하게 됩니다. 이 경우 모든 웹 콘텐츠를 단일 연결을 통해 직렬화해야 하므로 성능 문제가 발생할 수 있습니다. Internet Explorer 8.0에서는 도메인당 6개의 동시 연결을 허용하므로 이러한 성능 문제가 해결됩니다. Internet Explorer 8.0이 널리 보급되면 PollingDuplexHttpBinding을 사용한 푸시 알림을 향후 기사로 다룰 수 있을 것입니다.

다시 응용 프로그램으로 돌아가 보겠습니다. 상담원이 전화를 받으면 화면 팝 프로세스가 전화를 건 사람의 정보를 화면에 표시합니다. 여기에서는 전화를 건 사람의 주문 정보가 표시됩니다. 전화를 건 사람의 정보에는 백 엔드 데이터베이스에서 주문을 고유하게 식별하는 데 필요한 정보가 포함되어 있어야 합니다. 이 데모 시나리오에서는 IVR(대화형 음성 응답) 시스템에 주문 번호를 음성으로 전달한다고 가정하겠습니다. Silverlight 응용 프로그램은 주문 번호를 고유 식별자로 사용하여 WCF 웹 서비스를 호출합니다. 서비스 계약 정의와 구현은 그림 2에 나와 있습니다.

그림 2 비즈니스 서비스 구현

ServiceContracts.cs

[ServiceContract]
public interface ICallService
{
    [OperationContract]
    AgentScript GetAgentScript(string orderNumber);
    [OperationContract]
    OrderInfo GetOrderDetails(string orderNumber);
}

[ServiceContract]
public interface IUserProfile    
{
    [OperationContract]
    User GetUser(string userID);
}

CallService.svc.cs

 [AspNetCompatibilityRequirements(RequirementsMode = 
                            AspNetCompatibilityRequirementsMode.Allowed)]
public class CallService:ICallService, IUserProfile
{
  public AgentScript GetAgentScript(string orderNumber)
  {
    ... 
    script.QuestionList = DataUtility.GetSecurityQuestions(orderNumber);
    return script;
  }

  public OrderInfo GetOrderDetails(string orderNumber)
  {
    ... 
    oi.Customer = DataUtility.GetCustomerByID(oi.Order.CustomerID);
    return oi;
  }

  public User GetUser(string userID)
  {
    return DataUtility.GetUserByID(userID);
  }
 }

Web.Config

<system.servicemodel> 
   <services>
     <endpoint binding="basicHttpBinding"                contract="AdvBusinessServices.ICallService"/>
     <endpoint binding="basicHttpBinding"                contract="AdvBusinessServices.IUserProfile"/>
   </services>       
   <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<system.servicemodel>

이러한 서비스 끝점의 구현은 단순한 WCF 구현이므로 그다지 흥미롭지 않습니다. 이해하기 쉽도록 비즈니스 엔터티를 위한 데이터베이스를 사용하지 않고 메모리 내의 List 개체를 사용하여 Customer, Order 및 User 개체를 저장하겠습니다. DataUtil 클래스(기사에는 나와 있지 않으므로 코드 다운로드 참조)는 이러한 메모리 내의 List 개체에 대한 액세스를 캡슐화합니다.

fig03.gif

그림 3 보안 질문이 포함된 상담원 대본

Silverlight 사용을 위한 WCF 서비스 끝점은 ASP.NET 파이프라인에 대한 액세스가 필요하므로 CallService 구현에 AspNetCompatibilityRequirements 특성이 요구됩니다. 이 특성은 web.config 파일의 <serviceHostingEnvironment/> 설정과 일치해야 합니다.

앞에서 언급했듯이 Silverlight는 basicHttpBinding과 PollingDuplexHttpBinding만 지원합니다. WCF 서비스 Visual Studio 템플릿을 사용하는 경우 끝점 바인딩이 wsHttpBinding으로 구성되며 이를 수동으로 basicHttpBinding으로 변경해야 Silverlight가 프록시 생성을 위해 서비스 참조를 추가할 수 있습니다. Silverlight 지원 WCF Service Visual Studio 템플릿을 사용하여 AdvBusinessServices 프로젝트에 CallService.svc를 추가하는 경우 ASP.NET 호스팅 호환성 변경과 바인딩 변경이 자동으로 처리됩니다.

서비스 호출

Silverlight 호출 가능 서비스를 구현한 다음에는 서비스 프록시를 만들고 이를 사용하여 UI를 백 엔드 서비스 구현에 연결할 차례입니다. Visual Studio에서 서비스 참조 | 서비스 참조 추가를 사용해야 안정적으로 WCF용 프록시를 생성할 수 있습니다. 필자의 데모에서 프록시는 CallBusinessProxy 네임스페이스에 생성됩니다. Silverlight는 네트워크 리소스에 대한 비동기 호출만 허용하는데, 서비스 호출도 예외는 아닙니다. 고객의 전화가 걸려 오면 Silverlight 클라이언트는 알림을 수신하고 허용/거부 대화 상자를 표시합니다.

상담원이 전화를 받는 경우 프로세스의 다음 단계는 호출 상황에 해당하는 상담원 대본을 검색하는 웹 서비스를 호출하는 것입니다. 이 데모에서는 그림 3에 나와 있는 한 대본만 사용할 것입니다. 표시된 대본에는 인사말과 보안 질문 목록이 포함되어 있습니다. 상담원은 지원을 제공하기에 앞서 최소한의 질문을 하고 답변을 확인합니다.

상담원 대본은 주문 번호를 입력으로 제공하고 ICallService.GetAgentScript()에 액세스하여 검색할 수 있습니다. GetAgentScript()는 Silverlight 웹 서비스 스택에 의해 적용되는 비동기 프로그래밍 모델에 부합하여 CallServiceClient.BeginGetAgentScript()로 사용할 수 있습니다. 서비스 호출을 수행하는 동안 그림 4에 나와 있는 것처럼 콜백 처리기 GetAgentScriptCallback을 제공해야 합니다.

그림 4 서비스 호출 및 Silverlight UI 변경

class Page:UserControl
{   
   ... 
   void _notifyCallPopup_OnAccept(object sender, EventArgs e)
   {
     AcceptMessage acceptMsg = new AcceptMessage();
     acceptMsg.RepNumber = ClientGlobals.currentUser.RepNumber;
     ClientGlobals.socketClient.SendAsync(acceptMsg);
     this.borderCallProgressView.DataContext = ClientGlobals.callInfo;
     ICallService callService = new CallServiceClient();
     IAsyncResult result = 
        callService.BeginGetAgentScript(ClientGlobals.callInfo.OrderNumber, 
                     GetAgentScriptCallback, callService);
     //do a preemptive download of user control
     ThreadPool.QueueUserWorkItem(ExecuteControlDownload);
     //do a preemptive download of the order information
     ThreadPool.QueueUserWorkItem(ExecuteGetOrderDetails, 
                ClientGlobals.callInfo.OrderNumber);
   }

   void GetAgentScriptCallback(IAsyncResult asyncReseult)
   {

     ICallService callService = asyncReseult.AsyncState as ICallService;
     CallBusinessProxy.AgentScript svcOutputAgentScript = 
                     callService.EndGetAgentScript(asyncReseult);
     ClientEntityTranslator astobas =  
                               SvcScriptToClientScript.entityTranslator;
     ClientEntities.AgentScript currentAgentScript =  
                             astobas.ToClientEntity(svcOutputAgentScript)
                             as ClientEntities.AgentScript;
     Interlocked.Exchange<ClientEntities.AgentScript>(ref 
                   ClientGlobals.currentAgentScript, currentAgentScript);
     if (this.Dispatcher.CheckAccess())
     {
       this.borderAgentScript.DataContext = ClientGlobals.agentScript;
       ... 
       this.hlVerifyContinue.Visibility = Visibility.Visible;
     }
     else
     {
       this.Dispatcher.BeginInvoke(
        delegate()
        {
          this.borderAgentScript.DataContext = ClientGlobals.agentScript;
          ...
          this.hlVerifyContinue.Visibility = Visibility.Visible;

        } );
       }
     }
   private void ExecuteControlDownload(object state)
   {
     WebClient webClient = new WebClient();
     webClient.OpenReadCompleted += new   
       OpenReadCompletedEventHandler(OrderDetailControlDownloadCallback);
     webClient.OpenReadAsync(new Uri("/ClientBin/AdvOrderClientControls.dll", 
                                                     UriKind.Relative));
   }
   ... 
}

서비스 호출의 결과는 콜백 처리기에서만 검색할 수 있으므로 Silverlight 응용 프로그램 상태에 대한 변경은 모두 콜백 처리기에서 수행되어야 합니다. CallServiceClient.BeginGetAgentScript()는 UI 스레드에서 실행되는 _notifyCallPopup_OnAccept에 의해 호출되며 비동기 요청을 큐에 저장하고 즉시 다음 문으로 반환됩니다. 아직은 상담원 대본을 사용할 수 없기 때문에 스크립트를 캐시하고 UI로 데이터 바인딩하려면 콜백이 트리거될 때까지 기다려야 합니다.

서비스를 성공적으로 완료하면 GetAgentScriptCallback이 트리거되며, 이는 상담원 대본을 검색하고, 전역 변수를 채우고, 해당 UI 요소에 상담원 스크립트를 데이터 바인딩하여 UI를 조정합니다. UI를 조정하는 동안 GetAgentScriptCallback은 Dispatcher.CheckAccess()를 사용하여 UI 스레드에서 업데이트되도록 합니다.

UIElement.Dispatcher.CheckAccess()는 UI 스레드 ID를 작업자 스레드 ID와 비교하여 두 스레드가 동일하면 true를 반환하고 그렇지 않으면 false를 반환합니다. 작업자 스레드에서 GetAgentScriptCallback이 실행되면(실제로는 항상 작업자 스레드에서 실행되므로 간단히 Dispatcher.BeginInvoke를 호출할 수 있음) CheckAccess()는 false를 반환하며 UI는 Dispatcher.Invoke()를 통해 익명 대리자를 발송함으로써 업데이트됩니다.

동기화된 서비스 호출

Silverlight 네트워킹 환경의 비동기적인 특성으로 인해 UI 스레드에서 비동기 서비스 호출을 수행하고 호출의 결과를 바탕으로 응용 프로그램 상태를 변경할 목적으로 호출이 완료되기를 기다리기란 거의 불가능합니다. 그림 4에서 _notifyCallPopup_OnAccept는 주문 세부 정보를 검색하고, 출력 메시지를 클라이언트 엔터티로 변환하며, 이를 스레드에 안전한 방법으로 전역 변수에 저장해야 합니다. 이를 위해 처리기 코드를 다음과 같이 작성하려는 생각이 들 수도 있습니다.

CallServiceClient client = new CallServiceClient();
client.GetOrderDetailsAsync(orderNumber);
this._orderDetailDownloadHandle.WaitOne();
//do something with the results

그러나 이 코드를 실행하면 this._orderDetailDownloadHandle.WaitOne() 문에서 응용 프로그램이 중단됩니다. WaitOne() 문이 UI 스레드가 다른 스레드에서 발송된 메시지를 수신하지 못하도록 차단하기 때문입니다. 대신 작업자 스레드가 서비스 호출을 실행하도록 예약하고 호출이 완료되기를 기다린 다음 작업자 스레드에서 서비스 출력의 후처리 전체를 완료할 수 있습니다. 이 방법은 그림 5에서 볼 수 있습니다. UI 스레드에서 의도하지 않은 호출 차단 사용을 방지하기 위해 사용자 지정 SLManualResetEvent 내부에 ManualResetEvent를 래핑하고 WaitOne()에 대한 호출이 수행될 때 UI 스레드를 테스트하도록 했습니다.

그림 5 주문 세부 정보 검색

void _notifyCallPopup_OnAccept(object sender, EventArgs e)
{
  ... 
  ThreadPool.QueueUserWorkItem(ExecuteGetOrderDetails, 
        ClientGlobals.callInfo.OrderNumber);
}
private SLManualResetEvent _ orderDetailDownloadHandle = new 
        SLManualResetEvent();
  private void ExecuteGetOrderDetails(object state)
{
  CallServiceClient client = new CallServiceClient();
  string orderNumber = state as string;
  client.GetOrderDetailsCompleted += new
        EventHandler<GetOrderDetailsCompletedEventArgs>
        (GetOrderDetailsCompletedCallback);
  client.GetOrderDetailsAsync(orderNumber);
  this._orderDetailDownloadHandle.WaitOne();
  //translate entity and save it to global variable
  ClientEntityTranslator oito = SvcOrderToClientOrder.entityTranslator;
  ClientEntities.Order currentOrder = 
        oito.ToClientEntity(ClientGlobals.serviceOutputOrder)
        as ClientEntities.Order;
  Interlocked.Exchange<ClientEntities.Order>(ref ClientGlobals.
       currentOrder, currentOrder);
}

void GetOrderDetailsCompletedCallback(object sender, 
        GetOrderDetailsCompletedEventArgs e)
  {
    Interlocked.Exchange<OrderInfo>(ref ClientGlobals.serviceOutputOrder, 
         e.Result);
    this._orderDetailDownloadHandle.Set();
  }

SLManualResetEvent는 범용 클래스이므로 특정 컨트롤의 Dispatcher.CheckAccess()에 의존할 수는 없습니다. ApplicationHelper.IsUiThread()가 Application.RootVisual.Dispatcher.CheckAccess()를 확인할 수 있지만 이 메서드에 액세스하면 잘못된 스레드 간 액세스 예외가 트리거됩니다. 따라서 UIElement 인스턴스에 액세스할 수 없을 때 작업자 스레드에서 이를 안정적으로 테스트하는 유일한 방법은 다음과 같이 Deployment.Current.Dispatcher.CheckAccess()를 사용하는 것입니다.

public static bool IsUiThread()
    {
        if (Deployment.Current.Dispatcher.CheckAccess())
            return true;
        else
            return false;
    }

작업을 백그라운드로 실행하려면 ThreadPool.QueueUserWorkItem을 사용하는 대신 BackGroundWorker를 사용할 수 있습니다. BackGroundWorker 역시 ThreadPool.QueueUserWorkItem을 사용하지만 UI 스레드에서 실행할 수 있는 처리기를 연결하도록 허용합니다. 이 패턴을 사용하면 여러 서비스 호출을 동시에 실행할 수 있으며 추가적인 처리를 위해 결과를 집계하기 전에 SLManualResetEvent.WaitOne()을 사용하여 모든 호출이 완료되기를 기다릴 수 있습니다.

메시지 엔터티 변환

GetAgentScriptCallback도 출력 메시지 엔터티(DataContracts라고도 함)를 서비스에서 클라이언트 쪽 사용 의미 체계를 나타내는 클라이언트 쪽 엔터티로 변환합니다. 예를 들어 서버 쪽 메시지 엔터티의 디자인이 데이터 바인딩에는 신경을 쓰지 않지만 콜 센터뿐 아니라 광범위한 사용자를 지원해야 하는 서비스의 다중 사용 특성에는 관심을 가질 수 있습니다.

또한 메시지 엔터티에 대한 변경은 클라이언트의 제어권을 벗어나기 때문에 메시지 엔터티와의 긴밀한 결합은 피하는 것이 좋습니다. 메시지 엔터티를 클라이언트 쪽 엔터티로 변환하는 방법은 Silverlight에만 적용되는 것은 아니며 디자인 타임의 긴밀한 결합을 방지하려고 할 때 모든 웹 서비스 소비자에 일반적으로 적용됩니다.

여기에서는 특별한 중첩 제네릭, 람다 식 또는 제어 반전 컨테이너를 사용하지 않고 엔터티 변환기의 구현을 매우 간단하게 유지하기로 했습니다. ClientEntityTranslator는 모든 하위 클래스가 다시 정의해야 하는 ToClientEntity() 메서드를 정의하는 추상 클래스입니다.

public abstract class ClientEntityTranslator
{
  public abstract ClientEntities.ClientEntity ToClientEntity(object 
                                                 serviceOutputEntity);
}

각 자식 클래스는 서비스 교환 형식에 있어 고유하므로 필요한 만큼 변환기를 만들 것입니다. 이 데모에서는 IUserProfile.GetUser(), ICallService.GetAgentScript() 및 ICallService.GetOrderDetails()의 세 가지 서비스 호출 형식을 만들었으므로 그림 6에 나오는 것과 같이 세 가지 변환기를 만들었습니다.

그림 6 메시지 엔터티를 클라이언트 쪽 엔터티로 변환하는 변환기

public class SvcOrderToClientOrder : ClientEntityTranslator
{
  //singleton
  public static ClientEntityTranslator entityTranslator = new                 
                                           SvcOrderToClientOrder();
  private SvcOrderToClientOrder() { }
  public override ClientEntities.ClientEntity ToClientEntity(object                   
                                                  serviceOutputEntity)
  {
    CallBusinessProxy.OrderInfo oi = serviceOutputEntity as 
                                         CallBusinessProxy.OrderInfo;
    ClientEntities.Order bindableOrder = new ClientEntities.Order();
    bindableOrder.OrderNumber = oi.Order.OrderNumber;
    //code removed for brevity  ... 
    return bindableOrder;
  }
}

public class SvcUserToClientUser : ClientEntityTranslator
{
    //code removed for brevity  ... 
}

public class SvcScriptToClientScript : ClientEntityTranslator
{
    //code removed for brevity  ...
    }
}

위쪽의 변환기는 상태를 저장하지 않으며 단일 패턴을 적용합니다. 변환기는 일관성을 위해 ClientEntityTranslator로부터 상속할 수 있어야 하며 가비지 수집 변동을 피하기 위해 단일 항목이어야 합니다.

필자는 해당 서비스 호출이 수행될 때마다 동일한 인스턴스를 다시 사용했습니다. 트랜잭션 방식 서비스 호출에 일반적으로 적용되는 것처럼 규모가 큰 입력 메시지가 필요한 서비스 상호 작용에는 다음과 같은 클래스 정의를 사용하여 ServiOutputEntityTranslator를 만들 수도 있습니다.

public abstract class ServiOutputEntityTranslator
{
  public abstract object ToServiceOutputEntity(ClientEntity  
                                                      clientEntity);
}

메시지 엔터티의 기본 클래스를 제어하지 않으므로(이 데모에서는 가능하지만 실제로는 불가능함) 위의 함수 반환 값이 "object"임을 알 수 있습니다. 형식 안전성은 해당 변환기에 의해 구현됩니다. 이 데모에서는 간단하게 작성하기 위해 서버로 데이터를 저장하지 않으므로 클라이언트 엔터티를 메시지 엔터티로 변환하기 위한 변환기는 포함되지 않았습니다.

서비스 호출 후의 Silverlight 상태 변경

Silverlight 시각적 상태 변경은 UI 스레드에서 실행되는 코드에서만 수행될 수 있습니다. 서비스 호출의 비동기 실행은 항상 콜백 처리기에 결과를 반환하므로 처리기는 응용 프로그램의 시각적 또는 비시각적 상태 변경을 수행하기에 알맞은 장소입니다.

여러 서비스가 비동기적으로 공유 상태를 수정하려고 시도할 수 있는 경우 비시각적인 상태 변경은 스레드에 안전한 방법으로 교환되어야 합니다. UI를 수정하기 전에 Deployment.Current.Dispatcher.CheckAccess()를 확인하는 것이 좋습니다.

도메인 간 정책

미디어 응용 프로그램 및 배너 광고를 보여 주는 응용 프로그램과 달리 실제 엔터프라이즈급 LOB 응용 프로그램에는 다양한 서비스 호스팅 환경과의 통합이 필요합니다. 예를 들어 기사 전체에서 언급된 콜 센터 응용 프로그램은 전형적인 엔터프라이즈 응용 프로그램입니다. 웹 사이트에 호스트되는 이 응용 프로그램은 화면 팝을 위해 상태 저장 소켓 서버에, LOB 데이터 액세스를 위해 WCF 기반 웹 서비스에 액세스하며 다른 도메인에서 추가 XAP 패키지(압축된 Silverlight 배포 패키지)를 다운로드할 수 있습니다. 또한 계측 데이터를 전송하는 데도 다른 도메인을 사용합니다.

Silverlight 샌드박스는 그림 1에 나와 있는 것처럼 기본적으로 원래 도메인인 advcallclientweb(localhost:1041)을 제외하고는 다른 도메인으로의 네트워크 액세스를 허용하지 않습니다. Silverlight 런타임은 응용 프로그램이 원래 도메인 외의 다른 도메인에 액세스하면 옵트인 정책을 확인합니다. 다음은 클라이언트에서 요청하는 도메인 간 정책을 지원할 필요가 있는 일반적인 서비스-호스팅 시나리오의 목록입니다.

  • 서비스 프로세스(또는 단순하게 하기 위해 콘솔 응용 프로그램)에서 호스트되는 웹 서비스
  • IIS나 다른 웹 서버에서 호스트되는 웹 서비스
  • 서비스 프로세스(또는 콘솔 응용 프로그램)에서 호스트되는 TCP 서비스

지난 달에는 TCP 서비스를 위한 도메인 간 정책 구현에 대해 알아보았으므로 이달에는 사용자 지정 프로세스와 IIS 내부에서 호스트되는 웹 서비스를 중점적으로 살펴보겠습니다.

IIS에서 호스트되는 웹 서비스 끝점을 위한 도메인 간 정책을 구현하기는 간단하지만 다른 경우에는 정책 요청 및 응답의 특성에 대한 지식이 필요합니다.

IIS 외부에서 호스트되는 웹 서비스를 위한 도메인 간 정책

상태를 효과적으로 관리하기 위해 서비스를 IIS 외부의 OS 프로세스에서 호스트하려는 경우가 있을 것입니다. 프로세스는 이러한 WCF 서비스의 도메인 간 액세스를 위해 HTTP 끝점의 루트에 정책을 호스트해야 합니다. 도메인 간 웹 서비스가 호출되면 Silverlight는 clientaccesspolicy.xml에 대한 HTTP Get 요청을 수행합니다. 서비스가 IIS 내에서 호스트되는 경우 clientaccesspolicy.xml 파일을 웹 사이트의 루트로 복사할 수 있으며 파일을 제공하는 나머지 작업은 IIS가 처리합니다. 로컬 시스템에서 수행하는 사용자 지정 호스팅의 경우 http://localhost:<port>/clientaccesspolicy.xml이 유효한 URL입니다.

콜 센터 데모에서는 호스트된 사용자 지정 웹 서비스를 전혀 사용하지 않으므로 콘솔 응용 프로그램에서 간단한 TimeService를 사용하여 개념을 설명하겠습니다. 콘솔은 Microsoft .NET Framework 3.5의 새로운 REST(REpresentational State Transfer) 기능을 사용하여 REST 끝점을 공개합니다. UriTemplate 속성은 그림 7에 나와 있는 리터럴로 정확하게 설정해야 합니다.

그림 7 호스트된 사용자 지정 WCF 서비스의 구현

[ServiceContract]
public interface IPolicyService
{
    [OperationContract]            
    [WebInvoke(Method = "GET", UriTemplate = "/clientaccesspolicy.xml")]  
    Stream GetClientAccessPolicy();
}
public class PolicyService : IPolicyService
{
    public Stream GetClientAccessPolicy()
    {
        FileStream fs = new FileStream("PolicyFile.xml", FileMode.Open);
        return fs;
    }
}

인터페이스 이름이나 메서드 이름은 결과에 아무 영향도 주지 않으므로 원하는 이름을 선택할 수 있습니다. WebInvoke에는 기본적으로 XML로 설정되는 RequestFormat 및 ResponseFormat과 같은 다른 속성이 있으며 이러한 속성을 명시적으로 설정할 필요는 없습니다. 또한 BodyStyle 속성은 기본값인 BodyStyle.Bare이어야 하며 이것은 응답이 래핑되지 않는다는 것을 의미합니다.

서비스 구현은 매우 간단하여 Silverlight 클라이언트 요청에 대한 응답으로 clientaccesspolicy.xml을 스트리밍하는 것이 전부입니다. 정책 파일 이름은 원하는 것으로 선택할 수 있습니다. 정책 서비스 구현은 그림 7에 나와 있습니다.

이제 HTTP 요청을 REST 스타일로 제공하기 위해 IPolicyService를 구성해야 합니다. 콘솔 응용 프로그램(ConsoleWebServices)의 App.Config는 그림 8에 나와 있습니다. 특수한 구성이 필요한 몇 가지 사항이 있습니다. 우선 ConsoleWebServices.IPolicyServer 끝점의 바인딩은 webHttpBinding으로 설정해야 하며, IPolicyService 끝점 동작은 구성 파일에 나오는 것처럼 WebHttpBehavior로 구성해야 합니다. PolicyService의 기본 주소는 루트 URL(예: http://localhost:3045/)로 설정해야 하며 끝점 주소는 비워 둬야 합니다(예: <endpoint address="" … contract="ConsoleWebServices.IPolicyService" />).

그림 8 사용자 지정 호스팅 환경을 위한 WCF 설정

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <!-- IPolicyService end point should be configured with 
           webHttpBinding-->
      <service name="ConsoleWebServices.PolicyService">
         <endpoint address="" 
               behaviorConfiguration="ConsoleWebServices.WebHttp"
               binding="webHttpBinding" 
               contract="ConsoleWebServices.IPolicyService" />
         <host>
           <baseAddresses>
             <add baseAddress="http://localhost:3045/" />
           </baseAddresses>
         </host>
      </service>
      <service behaviorConfiguration="ConsoleWebServices.TimeServiceBehavior"
               name="ConsoleWebServices.TimeService">
         <endpoint address="TimeService" binding="basicHttpBinding" 
               contract="ConsoleWebServices.ITimeService">
         </endpoint>
         <host>
            <baseAddresses>
              <add baseAddress="http://localhost:3045/TimeService.svc" />
            </baseAddresses>
         </host>
       </service>
     </services>
     <behaviors>
        <endpointBehaviors>
          <!--end point behavior is used by REST endpoints like 
              IPolicyService described above-->
          <behavior name="ConsoleWebServices.WebHttp">
            <webHttp />
          </behavior>
        </endpointBehaviors>
       ... 
      </behaviors>
    </system.serviceModel>
</configuration>

마지막으로 코드 샘플에 나온 TimeService 및 구성과 같이 콘솔에서 호스트되는 서비스는 IIS 해당 부분과 비슷한 URL을 가지도록 구성해야 합니다. 예를 들어 IIS에서 기본 HTTP로 호스트되는 TimeService 끝점의 URL은 http://localhost/TimeService.svc와 비슷합니다. 이 경우에는 http://localhost/TimeService.svc?WSDL에서 메타데이터를 가져올 수 있습니다.

그러나 콘솔 호스팅의 경우 서비스 호스트의 기본 주소에 "?WSDL"을 붙여서 메타데이터를 가져올 수 있습니다. 그림 8에 나온 구성에서는 TimeService의 기본 주소가 http://localhost:3045/TimeService.svc로 설정되었으므로 http://localhost:3045/TimeService.svc?WSDL에서 메타데이터를 가져올 수 있습니다.

이 URL은 IIS 호스팅에서 사용하는 것과 비슷합니다. 호스트 기본 주소를 http://localhost:3045/TimeService.svc/로 설정하면 메타데이터 URL은 조금 이상해 보이는 http://localhost:3045/TimeService.svc/?WSDL이 됩니다. 이러한 동작을 알아두면 메타데이터 URL을 알아낼 때 시간을 절약할 수 있습니다.

IIS 내부에서 호스트되는 서비스를 위한 도메인 간 정책

앞에서 설명한 것처럼 IIS에서 호스트되는 서비스의 도메인 간 정책을 배포하는 과정은 간단합니다. 웹 서비스가 호스트되는 사이트의 루트에 clientaccesspolicy.xml 파일을 복사하기만 하면 됩니다. 그림 1에 나와 있는 것처럼 Silverlight 응용 프로그램은 advcallclientweb(localhost:1041)에서 호스트되며 AdvBusinessServices(localhost:1043)에서 비즈니스 서비스에 액세스합니다. Silverlight 런타임을 사용하려면 그림 9에 나오는 코드가 포함된 clientaccesspolicy.xml을 AdvBusinessServices 웹 사이트의 루트에 배포해야 합니다.

그림 9 IIS에서 호스트되는 웹 서비스의 Clientaccesspolicy.xml

<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <!--allows the access of Silverlight application with localhost:1041
           as the domain of origin-->  
        <domain uri="http://localhost:1041"/>
        <!--allows the access of call simulator Silverlight application
           with localhost:1042 as the domain of origin-->  
        <domain uri="http://localhost:1042"/>
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>

연재 기사의 1부에서는 소켓 서버(advpolicyserver)의 도메인 간 정책 형식을 설명했었는데 이를 기억한다면 <allow-from> 형식이 비슷하다는 것을 알 수 있을 것입니다. 차이점은 <grant-to> 섹션이며 여기에 소켓 서버는 다음에 나와 있는 것과 같이 포트 범위와 프로토콜 특성을 포함하는 <socket-resource> 설정을 요구합니다.

<grant-to>
  <socket-resource port="4530" protocol="tcp" />
</grant-to>

ASP.NET 웹 사이트 템플릿을 사용하여 WCF 서비스 호스팅 사이트를 만들고 나중에 WCF 끝점을 추가하는 경우 테스트 웹 서버는 가상 디렉터리를 "/AdvBusinessServices"와 같이 프로젝트 이름으로 매핑합니다. 이를 프로젝트의 속성 페이지에서 "/"로 변경해야 루트에서 clientaccesspolicy.xml을 제공할 수 있습니다. 이를 변경하지 않으면 clientaccesspolicy.xml이 루트에 있지 않게 되고 서비스에 액세스할 때 Silverlight 응용 프로그램에 서버 오류가 발생합니다. WCF 웹 서비스 프로젝트 템플릿을 사용하여 만든 웹 사이트의 경우에는 이것이 문제가 되지 않습니다.

그림 10 PasswordBox를 사용하는 Login 컨트롤

<UserControl x:Class="AdvCallCenterClient.Login">
  <Border x:Name="LayoutRoot" ... >
    <Grid x:Name="gridLayoutRoot">
     <Border x:Name="borderLoginViw" ...>
       <TextBlock Text="Pleae login.." Style="{StaticResource headerStyle}"/>
       <TextBlock Text="Rep ID" Style="{StaticResource labelStyle}"/>
       <TextBox x:Name="txRepID" Style="{StaticResource valueStyle}"/>
       <TextBlock Text="Password" Style="{StaticResource labelStyle}"/>
       <PasswordBox x:Name="pbPassword" PasswordChar="*"/>
       <HyperlinkButton x:Name="hlLogin" Content="Click to login"  
            ToolTipService.ToolTip="Clik to login" Click="hlLogin_Click" />
     </Border>
     <TextBlock x:Name="tbLoginStatus" Foreground="Red" ... />
      ...
</UserControl>

public partial class Login : UserControl
{
  public Login()
  {
    InitializeComponent();
  }
  public event EventHandler<EventArgs> OnSuccessfulLogin;
  private void hlLogin_Click(object sender, RoutedEventArgs e)
  {
    //validate the login
    AuthenticationProxy.AuthenticationServiceClient authService 
                  = new AuthenticationProxy.AuthenticationServiceClient();
    authService.LoginCompleted += new 
                EventHandler< AuthenticationProxy.LoginCompletedEventArgs>
                                           (authService_LoginCompleted);
    authService.LoginAsync(this.txRepID.Text, this.pbPassword.Password, 
                                                          null, false);     
  }

  void authService_LoginCompleted(object sender, 
                           AuthenticationProxy.LoginCompletedEventArgs e)
  {
    if (e.Result == true)
    {
       if (OnSuccessfulLogin != null)
          OnSuccessfulLogin(this, null);
    }
    else
    {
      this.tbLoginStatus.Text = "Invalid user id or password";
    }

  }
}

응용 프로그램 보안

인증은 LOB 응용 프로그램의 중요한 요건 중 하나입니다. 콜 센터 상담원은 교대를 시작하기 전에 사용자 ID와 암호를 입력하여 인증해야 합니다. ASP.NET 웹 응용 프로그램에서는 멤버 자격 공급자와 서버 쪽 ASP.NET 로그인 컨트롤을 사용하여 손쉽게 인증을 구현할 수 있습니다. Silverlight에서 인증을 적용하는 데는 외부 인증과 내부 인증의 두 가지 방법이 있습니다.

외부 인증은 매우 간단하며 ASP.NET 응용 프로그램의 인증 구현과 비슷합니다. 이 방식에서는 Silverlight 응용 프로그램이 표시되기 전에 ASP.NET 기반 웹 페이지에서 인증이 수행됩니다. 인증 컨텍스트는 Silverlight 응용 프로그램이 로드되기 전에 InitParams 매개 변수를 통해 Silverlight 응용 프로그램으로 전송되거나 응용 프로그램이 로드된 후에 사용자 지정 웹 서비스 호출(인증 상태 정보 추출을 위해)을 통해 Silverlight 응용 프로그램으로 전송됩니다.

이 방식은 Silverlight 응용 프로그램이 규모가 큰 ASP.NET/HTML 기반 시스템의 일부인 경우에 많이 사용됩니다. 그러나 Silverlight가 응용 프로그램의 주요 요소인 경우에는 Silverlight 내에서 인증을 수행하는 것이 자연스럽습니다. 여기에서는 Silverlight 2 PasswordBox 컨트롤을 사용하여 암호를 캡처하고 사용자 자격 증명 유효성 검사에는 ASP.NET AuthenticationService WCF 끝점을 사용하여 인증할 것입니다. AuthenticationService, ProfileService 및 RoleService는 .NET Framework 3.5에 새로 추가된 네임스페이스인 System.Web.ApplicationServices의 일부입니다. 그림 10에는 이러한 목적을 위해 만든 Login 컨트롤의 XAML이 나와 있습니다. Login 컨트롤은 입력된 사용자 ID와 암호를 전달하고 ASP.NET AuthenticationService.LoginAsync()를 호출합니다.

fig11.gif

그림 11 로그인 사용자 지정 Silverlight 컨트롤

그림 11에 나오는 콜 센터 응용 프로그램의 로그인 화면은 정교하지는 않지만 데모에 사용하기에는 충분합니다. 필자는 잘못된 로그인 메시지와 암호 재설정 대화 상자를 표시하는 데 필요한 모든 요소를 갖추도록 컨트롤 내에서 LoginCompleted 이벤트를 처리하기 위한 처리기를 구현했습니다. 로그인이 성공하면 사용자 정보를 포함하는 첫 번째 응용 프로그램 화면을 표시하도록 부모 컨트롤(이 경우에는 Application.RootVisual)에 지시하는 OnSuccessfulLogin 이벤트가 트리거됩니다.

그림 12에 나오는 것처럼 기본 Silverlight 페이지에 있는 LoginCompleted(ctrlLoginView_OnSuccessfulLogin) 처리기는 비즈니스 서비스 웹 사이트에서 호스트되는 프로필 서비스를 호출합니다. AuthenticationService는 기본적으로 어떤 .svc 끝점에도 매핑되지 않으므로 다음과 같이 .svc 파일을 실제 구현에 매핑할 것입니다.

<!-- AuthenticationService.svc -->
<%@ ServiceHost Language="C#" Service="System.Web.ApplicationServices.  
    AuthenticationService" %>

그림 12 Page.xaml 내에서 Login.xaml 사용

<!-- Page.xaml of the main UserControl attached to RootVisual-->
<UserControl x:Class="AdvCallCenterClient.Page" ...>
   <page:Login x:Name="ctrlLoginView" Visibility="Visible"   
         OnSuccessfulLogin="ctrlLoginView_OnSuccessfulLogin"/>
   ...
</UserControl>
<!-- Page.xaml.cs of the main UserControl attached to RootVisual-->
public partial class Page : UserControl
{       
   ... 

   private void ctrlLoginView_OnSuccessfulLogin(object sender, EventArgs e)
   {
     Login login = sender as Login;
     login.Visibility = Visibility.Collapsed;
     CallBusinessProxy.UserProfileClient userProfile 
                           = new CallBusinessProxy.UserProfileClient();
     userProfile.GetUserCompleted += new  
     EventHandler<GetUserCompletedEventArgs>(userProfile_GetUserCompleted);
     userProfile.GetUserAsync(login.txRepID.Text);
   }
   ... 
   void userProfile_GetUserCompleted(object sender, 
                                             GetUserCompletedEventArgs e)
   {
     CallBusinessProxy.User user = e.Result;
     UserToBindableUser utobu = new UserToBindableUser(user);
     ClientGlobals.currentUser = utobu.Translate() as ClientEntities.User;
     //all the time the service calls will be complete on a worker thread 
     //so the following check is redunant but done to be safe
     if (!this.Dispatcher.CheckAccess())
     {
       this.Dispatcher.BeginInvoke(delegate()
       {
         this.registrationView.DataContext = ClientGlobals.currentUser;
         this.ctrlLoginView.Visibility = Visibility.Collapsed;
         this.registrationView.Visibility = Visibility.Visible;
       });
      }
    }
}

Silverlight는 AJAX와 같은 스크립팅 환경에서 호출할 수 있도록 구성된 웹 서비스만 호출할 수 있습니다. AuthenticationService 서비스는 다른 모든 AJAX 호출 가능 서비스와 마찬가지로 ASP.NET 런타임 환경에 대한 액세스를 필요로 합니다. <system.servicemodel> 노드 바로 아래에서 <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/> 설정을 사용하여 이러한 액세스를 제공할 수 있습니다. Silverlight 로그인 프로세스(또는 AJAX)가 인증 서비스를 호출할 수 있도록 하려면 "방법: WCF 인증 서비스 활성화"에 나오는 설명에 따라 web.config를 설정해야 합니다. Silverlight 범주에 있는 Silverlight 사용 WCF 서비스 템플릿을 사용하여 만든 서비스의 경우에는 자동으로 Silverlight에 맞게 구성됩니다.

그림 13에는 인증 서비스에 필요한 중요 요소를 포함하는 편집된 구성이 나와 있습니다. 서비스를 구성한 다음에는 인증 정보를 저장하는 aspnetdb의 SQL Server 구성 설정도 대체했습니다. Machine.config는 웹 사이트의 App_Data 디렉터리에 aspnetdb.mdf가 삽입되는 것으로 예상하는 LocalSqlServer 설정을 정의합니다. 이 구성 설정은 기본 설정을 제거하고 SQL Server 인스턴스에 연결된 aspnetdb를 가리키도록 합니다. 별도의 시스템에서 실행 중인 데이터베이스 인스턴스를 가리키도록 손쉽게 변경할 수 있습니다.

그림 13 ASP.NET 인증 서비스를 위한 설정

//web.config
<Configuration>  
  <connectionStrings>
  <!-- removal and addition of LocalSqlServer setting will override the   
   default asp.net security database used by the ASP.NET Configuration tool
   located in the Visul Studio Project menu-->
  <remove name="LocalSqlServer"/>
    <add name="LocalSqlServer" connectionString="Data 
             Source=localhost\SqlExpress;Initial Catalog=aspnetdb; ... />
</connectionStrings>
<system.web.extensions>
   <scripting>
     <webServices>
   <authenticationService enabled="true" requireSSL="false"/>
     </webServices>
   </scripting>
</system.web.extensions>
... 
<authentication mode="Forms"/>
... 
<system.serviceModel>
   <services>
     <service name="System.Web.ApplicationServices.AuthenticationService" 
              behaviorConfiguration="CommonServiceBehavior">
    <endpoint 
              contract="System.Web.ApplicationServices.AuthenticationService" 
              binding="basicHttpBinding" bindingConfiguration="useHttp" 
              bindingNamespace="https://asp.net/ApplicationServices/v200"/>
     </service>
   </services>
   <bindings>
     <basicHttpBinding>
    <binding name="useHttp">
          <!--for production use mode="Transport" -->
      <security mode="None"/>
     </binding>
     </basicHttpBinding>
   </bindings>
   ... 
   <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
</system.serviceModel>
</configuration>

Login 컨트롤의 캡슐화를 보존하고 부모 컨트롤과의 느슨한 디자인 타임 결합을 유지하기 위해 로그인 프로세스의 성공은 OnSuccessfulLogin 이벤트를 트리거하여 전달됩니다. Page 클래스인 Application.RootVisual은 로그인 성공 시에 첫 번째 화면을 표시하는 데 필요한 비즈니스 프로세스를 실행합니다. 그림 12의 userProfile_GetUserCompleted 메서드에 나와 있는 것처럼 로그인이 성공한 후에 처음 표시되는 화면은 registrationView입니다. 이 뷰가 표시된 다음에는 CallBusinessProxy.UserProfileClient.GetUserAsync()를 호출하여 사용자 정보를 검색합니다. 뒤에 설명할 비즈니스 서비스 통합과 비슷한 비동기 서비스 호출 부분을 살펴보십시오.

이전 구성에서는 SSL(Secure Sockets Layer)을 사용하지 않았지만 프로덕션 시스템을 구축할 때는 SSL을 사용하도록 수정해야 합니다.

fig14.gif

그림 14 주문 세부 정보로 채워진 OrderDetails.xaml 컨트롤

응용 프로그램 분할

Silverlight 응용 프로그램의 시작 시간에 영향을 주는 요인 중 하나는 초기 패키지의 크기입니다. XAP 패키지의 크기에 대한 지침은 웹 응용 프로그램의 페이지 크기에 적용되는 지침과 동일합니다. 대역폭은 제한된 리소스입니다. 웹 응용 프로그램의 응답 시간을 엄격하게 제한하려면 Silverlight 응용 프로그램 시작 시간에 주의를 기울여야 합니다.

첫 번째 UserControl이 표시되기 전까지 소비되는 처리 시간 외에 응용 프로그램 패키지의 크기도 이러한 응용 프로그램의 중요한 특성에 직접적인 영향을 줍니다. 시작 속도를 개선하려면 복잡한 응용 프로그램에서 단일 XAP 파일이 수십 MB까지 커지지 않도록 해야 합니다.

Silverlight 응용 프로그램은 XML 파일의 컬렉션, 개별 DLL 또는 개별 XML 파일, 이미지, 그리고 인식되는 MIME 형식이 있는 다른 모든 형식으로 분리할 수 있습니다. 콜 센터 시나리오에서는 세분화된 응용 프로그램 분할을 설명하기 위해 OrderDetail Silverlight 컨트롤을 개별 DLL(AdvOrderClientControls.dll)로 배포하고 AdvCallCenterClient.xap를 AdvCallClientWeb 프로젝트의 ClientBin 디렉터리에 배포할 것입니다(그림 1 참조).

DLL은 상담원이 걸려 오는 전화를 수락하면 작업자 스레드에서 선점형으로 다운로드됩니다. 그림 4에 나오는 ThreadPool.QueueUserWorkItem(ExecuteControlDownload) 호출이 이 작업을 담당합니다. 전화를 건 사람이 보안 질문에 대답하면 리플렉션을 사용하여 OrderDetail 컨트롤의 인스턴스를 만들고 화면에 표시하기 전에 이를 컨트롤 트리에 추가할 것입니다. 그림 14를 보면 주문 상세 정보로 채워진 OrderDetail.xaml이 컨트롤 트리에 로드되어 있음을 알 수 있습니다.

OrderDetail 컨트롤을 포함하는 DLL은 콜 센터 클라이언트와 동일한 웹 사이트에 배포되며, 이것은 동일한 응용 프로그램에 속하는 DLL에는 일반적인 것이므로 이 경우에는 도메인 간 문제가 전혀 발생하지 않습니다. 그러나 Silverlight 응용 프로그램은 아키텍처 다이어그램(그림 1 참조)에 나와 있는 것처럼 로컬 서비스와 클라우드에 있는 서비스를 포함하여 여러 도메인에 배포된 서비스에 액세스할 수 있으므로 서비스에는 해당되지 않습니다.

ExecuteControlDownload 메서드는 백그라운드 작업자 스레드에서 실행되며 DLL을 다운로드하는 데 WebClient 클래스를 사용합니다. WebClient는 기본적으로 다운로드가 원본의 도메인에서 수행된다고 가정하므로 상대 URI만 사용합니다.

OrderDetailControlDownloadCallback 처리기는 DLL 스트림을 받고 그림 15에 나와 있는 ResourceUtility.GetAssembly()를 사용하여 어셈블리를 만듭니다. 어셈블리 생성은 UI 스레드에서 수행되어야 하므로 GetAssembly()와 전역 변수에 대한 스레드에 대해 안전한 어셈블리 할당을 UI 스레드에 발송할 것입니다.

void OrderDetailControlDownloadCallback(object sender,
       OpenReadCompletedEventArgs e)
  {
    this.Dispatcher.BeginInvoke(delegate() {
    Assembly asm = ResourceUtility.GetAssembly(e.Result);
    Interlocked.Exchange<Assembly>(ref 
        ClientGlobals.advOrderControls_dll, asm ); });
  }

그림 15 리소스를 추출하기 위한 유틸리티 함수

public class ResourceUtility
{ 
  //helper function to retrieve assembly from a package stream
  public static Assembly GetAssembly(string assemblyName, Stream 
                                                        packageStream)
  {
    StreamResourceInfo srInfo =
    Application.GetResourceStream(
              new StreamResourceInfo(packageStream, "application/binary"),
              new Uri(assemblyName, UriKind.Relative));
    return GetAssembly(srInfo.Stream);
  }
  //helper function to retrieve assembly from a assembly stream
  public static Assembly GetAssembly(Stream assemblyStream)
  {
    AssemblyPart assemblyPart = new AssemblyPart();
    return assemblyPart.Load(assemblyStream);
  }
  //helper function to create an XML document from the stream
  public static XElement GetXmlDocument(Stream xmlStream)
  {
    XmlReader reader = XmlReader.Create(xmlStream);
    XElement element = XElement.Load(reader);
    return element;
  }
  //helper function to create an XML document from the default package
  public static XElement GetXmlDocumentFromXap(string fileName)
  {
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.XmlResolver = new XmlXapResolver();
    XmlReader reader = XmlReader.Create(fileName);
    XElement element = XElement.Load(reader);
    return element;
  }
  //gets the UIElement from the default package
  public static UIElement GetUIElementFromXaml(string xamlFileName)
  {
    StreamResourceInfo streamInfo = Application.GetResourceStream(new 
                                  Uri(xamlFileName, UriKind.Relative));
    string xaml = new StreamReader(streamInfo.Stream).ReadToEnd();
    UIElement uiElement = null;
    try
    {
      uiElement = (UIElement)XamlReader.Load(xaml);
    }
    catch
    {
      throw new SLApplicationException(string.Format("Can't create 
                                  UIElement from {0}", xamlFileName));
    }
    return uiElement;
  }
}

발송된 대리자는 콜백 처리기와는 다른 스레드에서 실행되므로 익명 대리자에서 액세스되는 개체의 상태에 대해 인식해야 합니다. 이전 코드에서는 다운로드된 DLL 스트림의 상태가 매우 중요합니다. 스트림의 리소스를 되찾는 코드를 OrderDetailControlDownloadCallback 함수 내에 작성하지는 않습니다. 이러한 코드를 사용하면 UI 스레드가 어셈블리를 생성할 기회를 갖기 전에 다운로드한 스트림이 성급하게 제거될 수 있습니다. 다음에 나와 있는 것과 같이 리플렉션을 사용하여 OrderDetail 사용자 컨트롤을 만들고 이를 Panel에 추가할 것입니다.

_orderDetailContol = ClientGlobals.advOrderControls_dll.CreateInstance
                  ("AdvOrderClientControls.OrderDetail") as UserControl;
spCallProgressPanel.Children.Add(_orderDetailContol);

그림 15의 ResourceUtility에는 XAML에서 UIElement을 추출하고 다운로드한 스트림과 기본 패키지에서 XAML 문서를 추출하는 다양한 유틸리티 함수가 나와 있습니다.

생산성 및 그 밖의 사항들

지금까지 기존 엔터프라이즈 응용 프로그램의 관점에서 Silverlight를 살펴보고 응용 프로그램의 몇 가지 구조적인 측면에 대해 알아보았습니다. Silverlight 소켓을 사용한 밀어넣기 알림 구현은 콜 센터와 같은 LOB 시나리오를 가능하게 하는 핵심입니다. 호스트당 6개의 동시 HTTP 연결을 포함할 것으로 예정되어 있는 향후 Internet Explorer 8.0 릴리스에서는 이중 WCF 바인딩을 사용할 때 인터넷을 통한 밀어넣기 알림 구현이 더욱 좋아질 것입니다. LOB 데이터 및 프로세스와의 통합은 기존 데스크톱 응용 프로그램에서처럼 쉬워질 것입니다.

이것은 AJAX와 다른 RIA(다기능 인터넷 응용 프로그램) 플랫폼과 비교하면 상당히 큰 성능 향상이 될 것입니다. ASP.NET 최신 릴리스에서 제공하는 WCF 인증 및 권한 부여 끝점을 사용하여 Silverlight 응용 프로그램을 보호할 수 있습니다. Silverlight를 사용한 LOB 응용 프로그램 개발을 간단히 살펴본 이번 기사를 통해 미디어와 광고 시나리오 외의 영역에서도 Silverlight를 활용할 수 있음을 확인했습니다.

Hanu Kommalapati는 Microsoft 플랫폼 전략 관리자이며 엔터프라이즈 고객에게 Silverlight와 Azure Services 플랫폼 기반의 기간 업무(LOB) 응용 프로그램을 개발하도록 조언하는 업무를 맡고 있습니다.