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

Cutting Edge

Silverlight에서의 동적 콘텐츠 전달 관리, 2부

Dino Esposito

목차

영구 캐시를 사용하는 이유
격리된 저장소 개요
격리된 저장소 API
영구적 패키지 캐시 만들기
만료 정책
전체 과정
몇 가지 결론

지난 칼럼에서는 시작 시 또는 요청 시 시나리오에서 동적으로 생성된 콘텐츠를 Silverlight 응용 프로그램에 제공하는 방법을 설명했습니다. WebClient 클래스와 해당 비동기 호출 모델을 사용하여 URL 기반 리소스를 다운로드하는 여러 가지 예도 살펴보았습니다. 그중에서도 특히 XAML과 관리 코드가 포함된 XAP 패키지를 다운로드하는 데 필요한 내용을 집중적으로 살펴보았습니다. 자세한 내용은 2009년 1월 Cutting Edge 칼럼을 참조하십시오.

압축된 스트림을 다운로드하고 필요한 어셈블리를 압축 해제하는 것이 기본적인 개념입니다. 다음은 어셈블리에 포함되어 있는 클래스를 인스턴스화해야 합니다. 클래스는 XAML 시각적 트리의 전체 조각인 XAML 사용자 컨트롤이며 이를 현재 XAML DOM(문서 개체 모델)에 있는 임의의 자리 표시자에 추가할 수 있습니다.

브라우저는 다운로드한 XAP 리소스를 다른 리소스 유형과 구분할 수 없습니다. 따라서 브라우저는 다운로드한 다른 리소스와 마찬가지로 XAP 패키지를 캐시합니다. 이 기본 제공 메커니즘은 동일한 패키지를 반복적으로 다시 받기 위해 왕복을 반복하는 것을 방지하는 첫 번째 단계의 최적화를 제공합니다. 지난 달 칼럼에서 논의한 다운로드 구성 요소의 핵심인 WebClient 클래스는 브라우저의 연결 엔진에 기반을 두며 로컬로 사용 가능하고 아직 만료되지 않은 리소스는 다운로드하지 않습니다.

결과적으로 필요할 수 있는 외부 패키지 로딩을 연기하는 다운로드 구성 요소가 마련되었습니다. 또한 동적으로 로드한 리소스를 위한 무료 캐싱 기능도 사용할 수 있게 되었습니다. 무엇보다도 시각적 트리에 대한 동적인 변경을 처리할 수 있는 패키지의 영구 로컬 캐시를 추가하기를 원할 것입니다. 그 방법을 알아보겠습니다.

영구 캐시를 사용하는 이유

동적 Silverlight 콘텐츠의 경우에는 추가적인 제어를 원할 두 가지 주요 측면이 있습니다.

첫 번째는 다운로드한 콘텐츠의 만료 정책입니다. 지정한 패키지가 언제 만료되고 언제 다시 다운로드해야 하는지 정확하게 제어하기를 원할 수 있습니다. 또한 특정 사용자 작업과 같은 몇 가지 외부 작업이나 다른 캐시된 리소스에 대한 변경에 만료를 연결하기를 원할 수 있습니다. ASP.NET 캐시의 작동 방법을 알고 있다면 무슨 의미인지 알 수 있을 것입니다.

실제로 ASP.NET 캐시는 파일 변경, 날짜/시간 또는 심지어 다른 캐시된 항목에 대한 변경을 바탕으로 데이터를 캐시하고 캐시된 각 항목에 자체 만료 정책을 할당할 수 있도록 허용합니다. Silverlight 2에는 이와 비슷한 엔진이 없지만 동적이고 자유로운 사용자 지정이 가능한 대규모의 응용 프로그램이라면 이러한 기능이 큰 도움이 될 것입니다.

변경을 원하는 표준 Silverlight 리소스 캐싱의 두 번째 측면은 사용자 작업에 대한 패키지 노출에 대한 것입니다. 다른 말로 하면, 브라우저의 캐시에 저장된 XAP 패키지는 사용자에 의해 좌우됩니다. 사용자가 브라우저의 인터페이스를 사용하여 캐시를 삭제하면 모든 XAP 패키지가 사라집니다.

응용 프로그램 관리 영구 캐시는 두 가지 문제를 모두 해결합니다. 이 영구 캐시에 저장된 XAP 패키지는 사용자가 브라우저의 캐시를 삭제하더라도 영향받지 않습니다. Silverlight XAP 패키지를 영구적으로 저장하려면 로컬 파일 시스템에 대한 액세스가 필요합니다. 그러나 Silverlight는 보안을 위해 응용 프로그램이 전체 로컬 파일 시스템에 액세스하는 것을 허용하지 않습니다. 여기에 격리된 저장소 API가 도움이 될 수 있습니다. Silverlight 보안에 대한 자세한 내용은 "CLR Inside Out: Silverlight 2의 보안"을 참조하십시오.

격리된 저장소 개요

격리된 저장소는 Silverlight를 위해 만들어진 것은 아니며 Microsoft .NET Framework에 버전 1.0부터 포함되어 있었습니다. 격리된 저장소는 부분적으로 신뢰할 수 있는 응용 프로그램을 위한 기능으로서 이를 사용하면 이러한 응용 프로그램이 적용되는 모든 보안 정책을 완전히 준수하면서 로컬 컴퓨터에 데이터를 저장할 수 있습니다. 완전히 신뢰되는 기존 .NET 응용 프로그램의 경우에는 자체 데이터를 저장하기 위해 격리된 저장소 계층을 거칠 필요가 없을 것입니다. 그러나 부분적으로 신뢰되는 응용 프로그램이 클라이언트에 데이터를 저장하려면 격리된 저장소가 유일한 선택 사항입니다.

Silverlight 관점에서 격리된 저장소는 강력한 도구이며 비교적 큰 데이터 청크를 사용 브라우저에 관계없이, 그리고 HTTP 쿠키와 같은 제약을 받지 않고 영구적으로 저장할 수 있는 유일한 방법입니다. 격리된 저장소는 Silverlight에서 로컬 시스템에 데이터를 캐시할 수 있는 유일한 방법이라는 것이 중요합니다. Silverlight 응용 프로그램이 임의의 데이터를 로컬로 저장해야 하는 경우 반드시 격리된 저장소를 사용해야 합니다. 또한 격리된 저장소를 사용하면 각 응용 프로그램이 다른 응용 프로그램이나 사이트 외부의 다른 응용 프로그램으로부터 격리된 자체 데이터를 가질 수 있습니다.

격리된 저장소에 대한 일반적인 NET 기반 소개와 가상 일반적인 사용 시나리오에 대해 알아보려면 격리된 저장소에 대한 .NET Framework 개발자 가이드를 참조하십시오. 기사에서는 격리된 저장소를 사용하는 것이 적절하지 않은 두 가지 시나리오를 언급합니다. 특히 지침에서는 사용자 기본 설정을 제외한 구성 설정, 중요한 정보 또는 코드를 저장하는 데 격리된 저장소를 사용하지 않아야 한다고 명시하고 있습니다. 이러한 지침은 일반적인 보안 인식과 관련된 것으로 격리된 저장소를 사용하는 데 따르는 고유한 위험을 지적하는 것은 아닙니다.

그렇다면 다운로드한 XAP 패키지를 안전하게 Silverlight 격리된 저장소에 저장할 수 있을까요? Silverlight에서는 데스크톱 CLR과는 다르게 모든 실행 코드 조각이 기본적으로 신뢰되지 않으며 중요한 메서드를 실행이나 호출 스택의 사용 권한 상승이 허용되지 않습니다. Silverlight에서는 나중에 실행하기 위해 저장하는 모든 코드는 위험한 작업을 전혀 수행할 수 없습니다. 이것은 다른 Silverlight 코드를 실행하는 것보다 더 위험한 작업이 아닙니다. Silverlight 패키지의 영구적인 캐시를 만들면 현재 의식적으로 실행 중인 Silverlight 응용 프로그램의 세그먼트를 로컬로 저장할 수 있습니다.

Silverlight에서 격리된 저장소의 역할은 지속성과 관련해서는 기존 웹 응용 프로그램에서 HTTP 쿠키의 역할과 비슷합니다. Silverlight에서 격리된 저장소는 실행 가능 코드를 포함하여 모든 종류의 데이터를 포함할 수 있는 보다 큰 쿠키의 집합이라고 할 수 있습니다. 이 경우에 Silverlight 핵심 CLR이 보호를 제공합니다. Silverlight 보안 모델에 따르면 응용 프로그램 코드가 중요한 메서드를 실행하기를 원할 때마다 핵심 CLR은 예외를 발생시킵니다. HTTP 쿠키와는 달리 Silverlight의 격리된 저장소는 네트워크 I/O에 연결되지 않으며 요청 시에 콘텐츠가 전송되지 않습니다.

격리된 저장소의 데이터는 응용 프로그램에 의해 격리되며 다른 Silverlight 응용 프로그램은 저장소에 액세스할 수 없습니다. 그러나 데이터는 로컬 파일 시스템에 저장되므로 해당 시스템의 관리자는 저장소에 액세스할 수 있습니다.

이번에도 역시 전반적인 모델은 HTTP 쿠키가 작동하는 방법과 그다지 다르지 않습니다. 관리자는 쿠키를 찾고 심지어 내용을 변경할 수 있습니다. 해당 컨텍스트에 필요한 경우에는 암호화를 사용하여 추가적인 데이터 보호를 제공할 수 있습니다.

다운로드한 실행 가능 코드가 시스템에 유지된다는 사실 때문에 걱정이 된다면 Silverlight 보안 모델에 대해 더 자세하게 알아둘 필요가 있습니다. 간단히 말해 Silverlight 핵심 CLR은 응용 프로그램 코드가 중요 메서드를 실행하려고 시도할 때마다 예외를 발생시킵니다. Silverlight BCL(기본 클래스 라이브러리)에서 높은 권한이 필요한 작업을 수행하는 메서드와 클래스는 특수한 SecurityCritical 특성으로 표시됩니다. System.IO 네임스페이스에 포함된 대부분의 내용이 이 경우에 해당합니다.

Silverlight 보안 환경에서는 중요 메서드에 대한 일부 플랫폼 클래스의 안전한 호출을 승인합니다. 이러한 클래스와 메서드는 SecuritySafeCritical 특성으로 표시됩니다. System.IO.IsolatedStorage API(그림 1 참조)에 있는 클래스가 여기에 해당합니다. Silverlight 보안의 요점은 응용 프로그램 코드의 조각이 SecurityCritical이나 SecuritySafeCritical 특성으로 표시되는 경우는 없다는 것입니다. 이 특성은 Microsoft가 디지털 방식으로 서명한 어셈블리의 클래스를 위해 예약되었으며 Silverlight 설치 디렉터리에서 메모리로 로드됩니다.

그림 1 격리된 저장소 API 내부 둘러보기

여기에서 알 수 있듯이 악성 사용자가 시스템에 침입하여 다운로드한 Silverlight 콘텐츠를 대체하는 매우 좋지 않은(발생 가능성도 낮은) 상황이라고 하더라도 피해는 투명 모드에서 실행 가능한 일반 작업으로 제한됩니다.

격리된 저장소 API

Silverlight BCL에는 웹 시나리오에 맞게 맞춤 구성된 격리된 저장소의 자체 구현이 제공됩니다. 격리된 저장소는 전체 로컬 파일 시스템의 하위 트리에 대한 액세스를 제공하며 메서드나 속성은 사용자 시스템에서 파일 저장소의 실제 위치를 알아내기 위한 코드를 실행할 수 없습니다. Silverlight 응용 프로그램은 격리된 저장소를 통해 절대 파일 시스템 경로를 사용할 수 없습니다. 비슷하게 드라이브 정보를 사용할 수 없고 지원되지 않으며 다음과 같은 줄임표가 포함된 상대 경로 역시 사용할 수 없습니다.

\..\..\myfile.txt 

격리된 저장소 하위 트리의 루트는 현재 사용자 경로에 있는 폴더에 있습니다. 예를 들어 Windows Vista에서 격리된 저장소 폴더의 루트는 Users 디렉터리 아래에 있습니다.

Silverlight 응용 프로그램은 다음과 같은 메서드 호출을 통해 응용 프로그램별 격리된 저장소 진입점에 대한 액세스를 얻습니다.

using (IsolatedStorageFile iso = 
       IsolatedStorageFile.GetUserStoreForApplication()) 
{
  ...
}

정적 메서드 GetUserStoreForApplication은 격리된 저장소에 대한 향후 액세스에 사용되는 토큰을 반환합니다. GetUserStoreForApplication을 처음 호출하면 아직 없는 경우 응용 프로그램 전용 하위 트리가 생성됩니다.

Silverlight 격리된 저장소 API는 보호되는 파일 시스템 하위 트리 내의 파일 및 디렉터리를 대상으로 작업하는 클래스를 제공합니다. 다행스럽게도 알아야 하는 클래스의 목록은 그림 2에 나오는 것처럼 그리 길지 않습니다.

fig02.gif

IsolatedStorageFile 클래스에는 파일과 디렉터리 만들기 및 삭제, 파일 및 디렉터리 존재 여부 확인, 새 파일 읽기 및 쓰기를 위한 여러 메서드가 있습니다. 파일 작업에는 스트림을 사용하거나 또는 작업하기 훨씬 편리한 개체인 스트림 판독기로 스트림을 래핑할 수도 있습니다. 그림 3에는 스트림 판독기를 사용하여 격리된 저장소 파일을 만드는 방법에 대한 간단한 예가 있습니다.

그림 3 격리된 저장소 파일 만들기

using (IsolatedStorageFile iso = 
      IsolatedStorageFile.GetUserStoreForApplication())
{
    // Open or create the low level stream
    IsolatedStorageFileStream fileStream;
    fileStream = new IsolatedStorageFileStream(fileName, 
        FileMode.OpenOrCreate, iso);

    // Encapsulate the raw stream in a more convenient writer
    StreamWriter writer = new StreamWriter(stream);

    // Write some data
    writer.Write(DateTime.Now.ToString());

    // Clean up
    writer.Close();
    stream.Close();
}

편리한 스트림 작성기나 판독기를 사용하여 저수준 스트림을 래핑한 후에 데이터를 기록하거나 읽는 코드는 기존 .NET 응용 프로그램에 사용되는 코드와 거의 동일합니다. 다운로드한 XAP 패키지를 격리된 저장소 API를 활용하여 로컬로 저장하고 나중에 다시 로드하는 방법을 알아보겠습니다.

영구적 패키지 캐시 만들기

지난 달 칼럼에서는 XAP 패키지를 다운로드하고 어셈블리와 다른 리소스를 추출하기 위해 필요한 상용구 코드 중 일부를 숨기기 위해 다운로더 래퍼 클래스를 사용했습니다. 그러나 Downloader 클래스는 단순한 도우미 클래스가 아닙니다. 개념상 이 클래스는 몇 가지 이유 때문에 나머지 응용 프로그램 코드에서 격리하기를 원하는 중요한 논리의 조각을 나타냅니다.

가장 먼저 떠오르는 이유는 테스트 용이성입니다. 인터페이스를 통해 다운로드 구성 요소의 기능을 공개하면 테스트를 위해 신속하고 효과적으로 다운로더의 모의 개체를 만들 수 있습니다. 또한 단순한 다운로더를 정교한 다운로더로 대체하기 위해 활용하는 도구를 나타내는 인터페이스가 우연하게도 패키지 캐싱을 지원합니다. 그림 4에는 목표로 정해야 하는 디자인의 아키텍처가 나와 있습니다.

fig04.gif

그림 4 다운로더 구성 요소와 응용 프로그램의 나머지 부분

fig05.gif

그림 5 인터페이스 추출

지난 달의 소스 코드에서 Downloader 클래스는 단일 코드 조각이었습니다. 보다 유연한 디자인을 위해서는 여기에서 인터페이스를 추출해야 합니다. 그림 5에 나오는 것처럼 Visual Studio에서 제공하는 상황에 맞는 메뉴에는 상업용 리팩터링 도구만큼은 아니지만 클래스에서 인터페이스를 추출하는 데 도움이 되는 기능이 있습니다.

이제 Silverlight 응용 프로그램의 핵심은 IDownloader 인터페이스와 통신하므로 패키지 캐싱을 위한 모든 논리는 실제 다운로더 클래스 내부로 가져와야 합니다.

interface IDownloader
{
    void LoadPackage(string xapUrl, string asm, string cls);
    event EventHandler<Samples.XapEventArgs> XapDownloaded;
}

특히 LoadPackage 메서드를 다시 작성하여 격리된 저장소 내에 지정된 XAP 패키지가 있는지 확인하고 없는 경우 인터넷에서 다운로드하는 논리를 추가해야 합니다. 그림 6에는 Downloader 클래스의 코드 중 많은 부분이 나와 있습니다. 메서드는 먼저 내부 캐시에서 XAP 패키지에 대한 스트림을 가져오려고 시도하며 이 시도가 실패하면 호스트 서버에서 패키지를 다운로드합니다. 이에 대한 내용은 다음 칼럼에서 자세히 설명하겠습니다.

그림 6 다운로더 구성 요소에 대한 캐시 지원

public void LoadPackage(string xap, string asm, string cls)
{
    // Cache data within the class
    Initialize(xap, asm, cls, PackageContent.ClassFromAssembly);

    // Have a look in the cache
    Stream xapStream = LookupCacheForPackage();
    if (xapStream == null)
        StartDownload();
    else
    {
        // Process and extract resources
        FindClassFromAssembly(xapStream);
    }
}

protected Stream LookupCacheForPackage()
{
    // Look up the XAP package for the assembly.
    // Assuming the XAP URL is a file name with no HTTP information
    string xapFile = m_data.XapName;

    return DownloadCache.Load(xapFile);
}

protected void StartDownload()
{
    Uri address = new Uri(m_data.XapName, UriKind.RelativeOrAbsolute);
    WebClient client = new WebClient();

    switch (m_data.ActionRequired)
    {
        case PackageContent.ClassFromAssembly:
            client.OpenReadCompleted += 
                new OpenReadCompletedEventHandler(OnCompleted);
            break;
        default:
            return;
    }
    client.OpenReadAsync(address);
}

private void OnCompleted(object sender, OpenReadCompletedEventArgs e)
{
    // Handler registered at the application level?
    if (XapDownloaded == null)
        return;

    if (e.Error != null)
        return;

    // Save to the cache
    DownloadCache.Add(m_data.XapName, e.Result);

    // Process and extract resources
    FindClassFromAssembly(e.Result);
}

private void FindClassFromAssembly(Stream content)
{
    // Load a particular assembly from XAP
    Assembly a = GetAssemblyFromPackage(m_data.AssemblyName, content);

    // Get an instance of the specified user control class
    object page = a.CreateInstance(m_data.ClassName);

    // Fire the event
    XapEventArgs args = new XapEventArgs();
    args.DownloadedContent = page as UserControl;
    XapDownloaded(this, args);
}

Silverlight에서 다운로드는 비동기 프로세스이므로 내부 메서드 StartDownload는 클라이언트에서 패키지가 완전하게 사용 가능하게 되면 "완료됨" 이벤트를 발생시킵니다. 이벤트 처리기는 먼저 XAP 패키지의 내용을 로컬 파일에 저장한 다음 여기에서 리소스를 추출합니다. 샘플 코드에서는 어셈블리만 추출하고 있지만 보다 일반적인 구성 요소에서는 애니메이션을 위한 XAML, 이미지 또는 다른 보조 파일과 같은 다른 리소스 유형을 추출하도록 캐싱 기능을 확장할 수 있습니다.

Silverlightuser 컨트롤을 다운로드하고 현재 XAML 트리에 삽입하는 데는 Downloader 클래스에 있는 LoadPackage 메서드가 사용됩니다. XAP 패키지는 다중 파일 컨테이너이므로 사용자 컨트롤을 포함하는 어셈블리와 클래스 이름을 지정해야 합니다. 그림 6의 코드는 패키지에서 지정된 어셈블리를 추출하고, 이를 현재 AppDomain으로 로드한 다음, 지정된 포함 클래스의 인스턴스를 만듭니다.

어셈블리에 종속성이 있는 경우에는 어떻게 할까요? 그림 6에 있는 코드로는 이러한 시나리오가 해결되지 않습니다. 따라서 LoadPackage에 인수로 전달된 어셈블리에 다른 어셈블리에 대한 종속성(같은 XAP 패키지에 있더라도)이 있는 경우 클래스 내의 실행 흐름이 종속성 어셈블리에 도달하는 순간 예외가 발생합니다. 요점은 패키지의 모든 어셈블리를 메모리에 로드해야 한다는 것입니다. 이를 위해서는 매니페스트 파일에 액세스하고, 배포된 어셈블리에 대해 읽은 다음, 이를 처리해야 합니다. 그림 7에는 메니페스트 파일에 참조된 모든 어셈블리를 메모리에 로드하는 방법이 나와 있습니다.

그림 7 메니페스트의 모든 어셈블리 로드

private Assembly GetAssemblyFromPackage(
     string assemblyName, Stream xapStream)
{
    // Local variables
    StreamResourceInfo resPackage = null;
    StreamResourceInfo resAssembly = null;

    // Initialize
    Uri assemblyUri = new Uri(assemblyName, UriKind.Relative);
    resPackage = new StreamResourceInfo(xapStream, null);
    resAssembly = Application.GetResourceStream(
                              resPackage, assemblyUri);

    // Extract the primary assembly and load into the AppDomain 
    AssemblyPart part = new AssemblyPart();
    Assembly a = part.Load(resAssembly.Stream);

    // Load other assemblies (dependencies) from manifest
    Uri manifestUri = new Uri("AppManifest.xaml", UriKind.Relative);
    Stream manifestStream = Application.GetResourceStream(
        resPackage, manifestUri).Stream; 
    string manifest = new StreamReader(manifestStream).ReadToEnd();

    // Parse the manifest to get the list of referenced assemblies
    List<AssemblyPart> parts = ManifestHelper.GetDeploymentParts(manifest);

    foreach (AssemblyPart ap in parts)  
    {
        // Skip over primary assembly (already processed) 
        if (!ap.Source.ToLower().Equals(assemblyName))
        {
            StreamResourceInfo sri = null;
            sri = Application.GetResourceStream(
                resPackage, new Uri(ap.Source, UriKind.Relative));
            ap.Load(sri.Stream);
        }
    }

    // Close stream and returns
    xapStream.Close();
    return a;
}

매니페스트 파일은 다음과 같은 XML 파일입니다.

<Deployment EntryPointAssembly="More" EntryPointType="More.App" 
            RuntimeVersion="2.0.31005.0">
  <Deployment.Parts>
    <AssemblyPart x:Name="More" Source="More.dll" />
    <AssemblyPart x:Name="TestLib" Source="TestLib.dll" />
  </Deployment.Parts>
</Deployment> 

이 파일을 구문 분석하는 데는 LINQ-to-XML을 사용할 수 있습니다. 소스 코드에는 AssemblyPart 개체의 목록을 반환하는 메서드가 있는 샘플 ManifestHelper 클래스가 포함되어 있습니다(그림 8 참조). Silverlight 2 베타 버전에서는 XamlReader 클래스를 사용하여 메니페스트 파일을 Deployment 개체로 구문 분석할 수 있다는 것을 알아 두십시오.

// This code throws in Silverlight 2 RTM
Deployment deploy = XamlReader.Load(manifest) as Deployment;

그림 8 LINQ-to-XML을 사용하여 메니페스트 구문 분석

public class ManifestHelper
{
   public static List<AssemblyPart> GetDeploymentParts(string manifest)
   {
      XElement deploymentRoot = XDocument.Parse(manifest).Root;
      List<AssemblyPart> parts = 
          (from n in deploymentRoot.Descendants().Elements() 
           select new AssemblyPart() { 
                Source = n.Attribute("Source").Value }
          ).ToList();

          return parts;
   }
}

릴리스 버전에서는 Deployment 개체가 Singleton으로 변환되었기 때문에 동적으로 어셈블리를 로드하는 데는 이를 사용할 수 없게 되었습니다. 따라서 메니페스트의 XML을 수동으로 구문 분석해야 합니다.

만료 정책

지금까지의 구현에서 어셈블리 캐시는 영구적이며 사용자가 패키지를 업데이트하는 방법은 없습니다. 유일한 해결 방법은 시스템 관리자가 응용 프로그램의 격리된 저장소 파일을 찾고 Windows 탐색기를 사용하여 이 파일을 수동으로 삭제하는 것입니다.

다운로드한 후 일정한 시간이 지나면 캐시된 XAP 파일을 폐기하는 간단한 만료 정책을 추가하려면 어떻게 해야 하는지 알아보겠습니다. 만료 정책을 넣을 올바른 위치는 그림 6에 나오는 것처럼 DownloadCache 클래스입니다. XAP 파일을 캐시에 추가할 때 다운로드 시간에 대한 약간의 정보를 저장하고 패키지를 가져오기 위해 캐시에 액세스할 때 패키지가 만료되었는지 확인해야 합니다 그림 9 참조).

그림 9 만료 여부 테스트

public bool IsExpired(string xapFile)
{
    bool expired = true;
    if (m_ItemsIndex.ContainsKey(xapFile))
    {
        DateTime dt = (DateTime)m_ItemsIndex[xapFile];

        // Expires after 1 hour
        expired = dt.AddSeconds(3600) < DateTime.Now;    
        if (expired)
            Remove(xapFile);
    }

    return expired;
}

간단해 보이는 알고리즘이지만 상당히 중요한 한 가지 사실이 문제가 됩니다. Silverlight에는 마지막 업데이트나 생성 시간과 같은 파일 특성에 액세스하는 방법이 없습니다. 즉, 여러분이 직접 시간 정보를 관리해야 합니다. 다른 말로 하면 캐시에 XAP를 추가할 때 일종의 사용자 지정 및 지속성 사전에 패키지가 다운로드된 시점을 추적하는 항목도 만들어야 합니다. 물론 이 정보는 격리된 저장소에 유지해야 합니다. 그림 10에서 이 개념을 볼 수 있습니다.

그림 10 다운로드 세부 사항을 격리된 저장소에 유지

public static Stream Load(string file)
{
    IsolatedStorageFile iso;
    iso = IsolatedStorageFile.GetUserStoreForApplication();

    if (!iso.FileExists(file))
    {
        iso.Dispose();
        return null;
    }

    // Check some expiration policy
    CacheIndex m_Index = new CacheIndex();
    if (!m_Index.IsExpired(file))
        return iso.OpenFile(file, FileMode.Open);

    // Force reload
    iso.Dispose();
    return null;
}

CacheIndex는 Silverlight 네이티브 응용 프로그램 설정 API를 사용하여 XAP 이름과 다운로드 시간의 사전을 격리된 저장소에 유지하는 도우미 클래스입니다. m_ItemIndex 멤버는 그림 11에 나오는 것처럼 CacheIndex 생성자에서 인스턴스화되는 일반 Dictionary 개체입니다.

그림 11 CacheIndex 클래스

public class CacheIndex
{
    private const string XAPCACHENAME = "XapCache";
    private Dictionary<string, object> m_ItemsIndex; 
    public CacheIndex()
    {
      IsolatedStorageSettings iss;
      iss = IsolatedStorageSettings.ApplicationSettings;
      if (iss.Contains(XAPCACHENAME))
         m_ItemsIndex = iss[XAPCACHENAME] as Dictionary<string, object>;
      else
      {
         m_ItemsIndex = new Dictionary<string, object>();
         iss[XAPCACHENAME] = m_ItemsIndex;
         iss.Save();
      }
   }
  ...
}

Silverlight 2의 매우 유용한 기능인 ApplicationSettings는 문자열/개체 사전으로 구성되며 응용 프로그램 로딩 시에 자동으로 저장소에서 읽고, 종료 시에 자동으로 저장소에 저장됩니다. 사전에 추가하는 모든 직렬화 가능 개체는 자동으로 유지됩니다.

사전에 XAPCACHENAME 항목을 만들면 XAP 사전에 콘텐츠를 유지하도록 준비할 수 있습니다. XAP 사전은 그림 12에 나오는 것처럼 다운로드한 각 패키지와 다운로드 시간당 한 개의 항목을 포함합니다. ApplicationSettings API는 응용 프로그램이 종료되기 전에 지속성을 적용하는 Save 메서드도 제공합니다.

그림 12 다운로드 정보 추가

public void Add(string xapFile)
{
    m_ItemsIndex[xapFile] = DateTime.Now;
    IsolatedStorageSettings iss;
    iss = IsolatedStorageSettings.ApplicationSettings;
    iss.Save();                
}

public void Remove(string xapFile)
{
    m_ItemsIndex.Remove(xapFile);
    IsolatedStorageSettings iss;
    iss = IsolatedStorageSettings.ApplicationSettings;
    iss.Save();
}

전체 과정

지금까지 설명한 모든 변경과 세부 사항은 Downloader 클래스라는 공용 클래스 내부에서 이루어집니다. 이 클래스를 여러분의 Silverlight 응용 프로그램에 통합하면 한 번의 작업으로 여러 수준의 캐싱 기능을 사용할 수 있습니다. 다운로드한 사용자 컨트롤을 적절한 코딩을 통해 응용 프로그램 세션 동안 캐시할 수 있습니다. 예를 들어 탭 항목에 표시될 콘텐츠를 다운로드한다면 패키지를 계속 다운로드할 필요 없이 탭 항목을 반복적으로 표시 및 숨길 수 있습니다.

지난 달 칼럼에서와 같이 WebClient 클래스를 통해 다운로드하는 경우 브라우저 엔진을 통한 브라우저 수준 캐싱이 제공됩니다. 다운로드한 XAP 패키지는 사용자가 브라우저 캐시를 삭제할 때까지 사용자 시스템에 유지됩니다. 마지막으로 이 칼럼에서 설명한 Downloader 클래스는 격리된 저장소를 통한 영구적인 캐시를 지원합니다. 이것은 WebClient를 통해 다운로드할 때마다 XAP 패키지가 로컬 저장소에도 저장됩니다.

Downloader 클래스는 또한 올바른 패키지가 있는 경우 저장소에서 XAP 파일을 가져오는 기능도 제공합니다. 이 기능은 여러 응용 프로그램 세션 간에 작동합니다. 패키지를 한 번 다운로드하고 작업한 후에 응용 프로그램을 종료하고 다시 시작하면 저장소에 만료되지 않은 패키지가 있는 경우 패키지가 다시 로드됩니다. 패키지가 만료된 경우에는 어떻게 될까요? 이 경우에 다운로더는 WebClient에 작업을 맡깁니다. 그러나 이 시점에 WebClient는 이전에 브라우저가 캐시한 동일한 패키지 복사본을 반환할 수도 있습니다.

이것은 의도적인 것입니다. 브라우저 수준 캐싱을 우회하려면 지난 달에 설명한 것처럼 원래 HTTP 요청에서 이를 비활성화해야 합니다. 페이지 수준에서 속성을 캐시하거나 페이지와 함께 전달되는 다른 리소스에 영향을 주지 않고 보다 정확하게 만료 정책을 설정할 수 있는 위치인 HTTP 처리기를 통해 패키지를 얻어야 합니다.

몇 가지 결론

XAP 패키지를 캐싱하는 것은 DLL, XAML 애니메이션 또는 멀티미디어 콘텐츠와 같은 개별 리소스를 캐싱하는 것과는 다릅니다. 현재 구현에서는 사용할 때마다 XAP 패키지에서 리소스를 추출합니다. 그러나 Downloader 클래스에서 이러한 측면을 더욱 개선할 수 있습니다. 패키지는 또한 사용자 컨트롤을 제공하지만 사용자가 해당 사용자 인터페이스에 적용할 수 있는 변경 내용을 추적하지는 않습니다. XAML 트리에 대한 변경 내용을 동적으로 추적하는 것은 별도의 기사로 설명해야 할 만큼 심도 있는 내용입니다.

로컬 사용자 시스템의 Silverlight 전용 파일 시스템에 액세스하는 데는 두 가지 방법이 있습니다. 이 칼럼의 모든 코드 조각에서는 IsolatedStorageFile 클래스의 GetUserStoreForApplication을 사용했습니다. 이 메서드는 응용 프로그램별로 격리된 파일 시스템의 섹션에 액세스하기 위한 토큰을 반환하며 이것은 응용 프로그램과 연결된 모든 어셈블리만 동일한 저장소를 사용한다는 의미입니다. 저장소를 선택하고 같은 사이트에서 호스트되는 모든 응용 프로그램에서 공유할 수도 있습니다. 이 경우에는 GetUserStoreForSite 메서드를 통해 토큰을 얻습니다.

Silverlight 응용 프로그램을 마우스 오른쪽 단추로 클릭하여 열 수 있는 Silverlight 구성 대화 상자에서 로컬 저장소를 관리하거나 완전히 비활성화할 수도 있습니다. 이 경우 토큰을 얻으려고 하면 예외가 발생합니다. 디스크 할당량도 도메인별로 로컬 저장소에 적용되며 기본값은 1MB입니다. 영구적 Silverlight 캐시를 계획할 때는 이러한 사항도 기억하십시오.

Dino에게 질문이나 의견이 있으면 cutting@microsoft.com으로 보내시기 바랍니다.

Dino Esposito는 IDesign의 설계자이며 Microsoft .NET: Architecting Applications for the Enterprise(Microsoft Press, 2008)의 공동 저자이기도 합니다. 이탈리아에 거주하고 있는 Dino는 전 세계의 IT 업계 관련 행사에서 많은 활동을 펼치고 있습니다. 문의 사항이 있으면 블로그 weblogs.asp.net/despos를 방문하시기 바랍니다.