ASP.NET 2.0 리소스-공급자 모델의 확장

2007. 1. 22. 04:28 IT 및 개발/ASP.NET & AJAX

요약: Microsoft ASP.NET 2.0에는 웹 응용 프로그램의 지역화를 위한 여러 가지 향상된 기능이 포함되어 있습니다. 하지만 이러한 훌륭한 기능이 있음에도 불구하고 사이트의 지역화를 마치고 나면 곧 확장성에 대해 염려하게 됩니다. 이 문서에서는 ASP.NET의 확장 기능을 적용하여 엔터프라이즈 지역화 시나리오를 처리하고, 지역화-개발 프로세스를 향상시킬 수 있는 방법을 설명합니다.

이 기사의 코드를 다운로드하십시오.


소개

ASP.NET 2.0에는 웹 응용 프로그램의 지역화를 위한 여러 가지 향상된 기능이 포함되어 있습니다. 이러한 새 기능에 대해서는 필자가 집필한 MSDN 기사인 "ASP.NET 2.0 Localization Features: A Fresh Approach to Localizing Web Applications (영문)"를 참조하십시오.

이러한 새 지역화 기능을 살펴보고 나면 다음과 같은 내용을 알 수 있습니다.

  • Microsoft Visual Studio 2005의 페이지 도구 모음 보기에서 로컬 리소스 생성 메뉴 항목을 사용하면 각 페이지에 대한 리소스를 손쉽게 만들 수 있습니다.
  • 향상된 리소스 편집기와 엄격한 형식의 액세스 방식 덕분에 전역 리소스의 작성과 사용이 훨씬 간편해졌습니다.
  • 선언적인 지역화 식을 사용하여 리소스 항목을 제어 속성 및 콘텐츠 영역에 깔끔하게 매핑할 수 있습니다.
  • 로컬 또는 전역 리소스를 가져와 필요에 따라 ResourceManager를 할당하는 과정을 ResXResourceProviderFactory에서 조정하므로 ResourceManager를 수동으로 인스턴스화할 필요가 없습니다.
  • 브라우저의 culture 기본 설정을 자동으로 감지하고 이 설정을 요청 스레드에 할당하므로 사용자의 culture 기본 설정에 따른 제어가 편리합니다. 익명 사용자인 경우도 마찬가지입니다.

물론 이처럼 훌륭한 기능에도 불구하고 대개 사용자는 더 많은 것을 원합니다. 이러한 기능을 사용하여 사이트를 지역화하고 나면 곧 다음과 같은 다른 사항을 염려하게 됩니다.

  • 별도의 리소스 어셈블리나 데이터베이스 원본과 같은 다른 위치에서 리소스를 가져오려면 어떻게 해야 할까?
  • 일부 로컬 및 전역 리소스를 사용하지만 다른 데이터 원본도 있는 혼합 환경을 어떻게 관리해야 할까?
  • 리소스-공급자 모델, 지역화 식 및 다른 디자이너 통합 기능과 같은 ASP.NET 2.0의 기능을 계속 활용하면서 리소스의 원본을 제어하려면 어떻게 해야 할까?
  • 기존의 지역화 기능과 새롭게 제공되는 확장 가능 옵션을 내 개발 환경과 지역화 프로세스에 맞게 활용하려면 어떻게 해야 할까?

확장성이 중요한 이유가 바로 이 때문입니다. ASP.NET 지역화 기능을 확장하고 개발 환경에 맞게 사용하는 방법은 여러 가지가 있습니다. 이 기사는 ASP.NET의 확장 기능을 적용하여 엔터프라이즈 지역화 시나리오를 처리하고, 지역화-개발 프로세스를 향상시킬 수 있는 방법을 설명할 3회 연재 기사 중 첫 번째 기사입니다.

이 기사에서는 다른 저장소 위치에서 리소스를 가져오기 위한 기능과 페이지 구문 분석, 컴파일 및 런타임 실행 기능을 통합하는 기능에 초점을 맞춥니다. 이를 위해 사용자 지정 리소스 공급자, 사용자 지정 식 작성기 및 기타 지원되는 확장 가능 형식을 조합하여 사용하는 방법을 설명합니다. 두 번째 기사에서는 선택한 리소스 저장소를 Visual Studio 2005의 기본 제공 생산성 기능과 결합하여 개발 프로세스를 향상시키는 방법을 설명하며, 세 번째 기사에서는 클라이언트측 사용자 지정과 같은 기능을 지원할 수 있는 복잡한 리소스 계층을 제어하는 다른 방법에 대해 살펴봅니다.


리소스를 어디에 두어야 할까?

지역화된 리소스를 웹 사이트에 통합하는 일은 언제나 고된 작업입니다. 리소스 생성은 일반적으로 어려운 일이고 번역을 위해 리소스를 구성하는 데도 관리되는 프로세스가 필요하지만, 웹 사이트 리소스의 지역화에 있어 무엇보다도 어려운 것은 무엇이 리소스에 해당하는지, 리소스를 어떻게 할당해야 하는지, 그리고 최상의 성능과 관리 효율을 위해 필요한 것은 무엇인지 파악하는 일입니다.

ASP.NET 2.0을 사용한 리소스 만들기 및 액세스

ASP.NET 2.0은 각 페이지에 대한 로컬 리소스를 만들 수 있는 기능을 제공합니다. 바로 이 기능이 더 효율적인 페이지 디자인과 국제화 프로세스의 바탕이 되었습니다.

  1. 페이지 디자인 - 정적 HTML 및 ASP.NET 서버 컨트롤을 조합하여 적용합니다.
  2. 지역화를 위한 정적 영역 준비 - 정적 영역을 ASP.NET Localize 컨트롤로 래핑합니다.
  3. 적절한 컨트롤 이름 제공 - 모든 서버 컨트롤에 적절한 이름을 제공하여 생성되는 이벤트 처리기와 리소스 키를 알아보기 쉽도록 합니다.
  4. 공유 리소스 만들기 - App_GlobalResources 하위 디렉터리에 공유 리소스를 만듭니다. .resx 파일이 이미 있는 경우는 그대로 사용하고, 그렇지 않으면 여러 페이지 간에 공유할 항목을 저장하기 위해 새 .resx 파일이 생성됩니다.
  5. 공유 리소스 연결 - 해당하는 경우 명시적인 리소스 식을 사용하는 속성을 통해 공유 리소스를 연결합니다. 공유 리소스는 페이지에 대한 로컬 리소스를 만들기 전에 연결하는 것이 좋습니다.
  6. 로컬 리소스 생성 - 페이지 도구 모음 보기에서 로컬 리소스 생성 메뉴 항목을 선택하여 로컬 리소스를 만듭니다.

로컬 리소스를 만든 후에는 페이지에 대한 모든 지역화 가능 속성과 컨트롤이 페이지당 하나씩 생성된 각각의 로컬 리소스 파일로 이동됩니다. 암시적 지역화 식은 페이지 파서가 공용 접두사를 사용하여 컨트롤의 각 리소스 값을 해당 속성에 매핑하도록 합니다. 샘플 코드에 있는 Expressions.aspx 페이지의 다음과 같은 암시적 식을 살펴보겠습니다.

<asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

리소스는 App_LocalResources 디렉터리의 Expressions.aspx.resx 파일에 저장됩니다. 이 Label 컨트롤에 대한 리소스는 "labHelloLocalResource1"이라는 접두사를 공유합니다. 예를 들어 Text 속성은 "labHelloLocalResource1.Text" 키로 저장됩니다.

공용 사용자 인터페이스 영역용 사용자 컨트롤과 마스터 페이지를 사용하여 사용자 인터페이스를 잘 구성하면 각 마스터 페이지, 사용자 컨트롤, 페이지 역시 겹치는 부분 없이 잘 구성됩니다. 이렇게 하면 일반적으로 이전 버전에서는 성가신 작업이었던 각 페이지 부분에서 사용하는 리소스를 구성하는 과정이 손쉽게 진행됩니다. 하지만 여전히 공유 위치에서 리소스를 가져와야 하는 경우가 있는데, 이 경우 다음과 같은 $Resources 식 등의 명시적 리소스 식을 제공하면 됩니다.

<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>

이 경우 리소스의 위치는 App_GlobalResources 디렉터리의 CommonTerms.resx 파일입니다. 이와 같은 명시적 식은 식 편집기(앞서 언급한 MSDN 기사 참조)를 사용하여 간단히 만들 수 있습니다.

암시적 식이나 명시적 식을 사용하면 리소스 공급자를 사용하여 리소스를 가져오는 코드가 생성됩니다. 이러한 선언적 식, 코드 및 리소스 생성 기능의 조합은 이전에는 웹 응용 프로그램에 사용할 수 없었던 생산성 도구를 제공합니다.

리소스 어셈블리 및 ResourceManager

ASP.NET 2.0 응용 프로그램을 컴파일하고 배포하는 방법에는 다음과 같은 여러 가지가 있습니다.

  • 소스를 배포하고 전체 사이트를 JIT 컴파일합니다.
  • 업데이트 가능한 페이지 및 리소스로 사이트를 미리 컴파일합니다.
  • 사이트를 미리 컴파일하여 페이지나 디렉터리별로 어셈블리를 생성합니다.

어떤 방법을 사용하든 최종적으로는 사이트의 각 디렉터리에 대한 리소스 어셈블리가 생성되고, 각 culture별 디렉터리에 해당하는 위성 어셈블리가 생성됩니다. 사이트를 JIT 컴파일하는 경우에도 결과는 동일합니다. 그림 1은 하위 디렉터리 두 개가 있는 사이트를 스페인어로 지역화하기 위해 미리 컴파일한 결과를 보여 줍니다.

사용자 삽입 이미지

그림 1. ASP.NET 웹 사이트의 각 디렉터리에 생성된 리소스 및 위성 어셈블리(더 큰 이미지를 보려면 그림을 클릭하십시오.)

이러한 리소스는 런타임 시에 ResourceManager를 통해 액세스됩니다. ResourceManager는 리소스가 요청될 때 각 리소스 유형(예: Page1.aspx 및 Page2.aspx)에 할당됩니다. 각 리소스 유형에 연결된 리소스 어셈블리는 처음 액세스할 때 ASP.NET 응용 프로그램 도메인에 로드되며 응용 프로그램 도메인이 언로드될 때까지 유지됩니다. 그림 1의 경우 스페인 culture에 해당하는 \SubDir1\Page1.aspx에 처음 액세스하면 \es\App_LocalResources.subdir1.cdcab7d2.resources.dll 어셈블리가 응용 프로그램 도메인에 로드됩니다. 이 어셈블리는 \SubDir1의 모든 페이지에 대한 스페인어 리소스를 포함합니다.

그림 2는 ResourceManager가 각 페이지에 대한 로컬 리소스에 액세스하는 방식을 보여 줍니다. \SubDir1\Page1.aspx가 로드되면 암시적 식에 의한 코드 생성을 통해 ResXResourceProviderFactory가 호출되고, 여기서 LocalResXResourceProvider가 반환됩니다. 이 공급자는 App_LocalResources.subdir1.cdcab7d2 어셈블리의 Page1.aspx 유형에 대한 ResourceManager를 만듭니다. 요청 스레드의 UI culture가 “es”일 경우, \es 디렉터리에 있는 위성 리소스 어셈블리가 응용 프로그램 도메인으로 로드됩니다. UI culture에 맞는 위성 어셈블리가 없는 경우 ResourceManager는 대신 주 리소스 어셈블리를 사용합니다.

사용자 삽입 이미지

그림 2. 주 리소스 어셈블리 또는 지역화된 위성 어셈블리가 응용 프로그램 도메인에 로드되면 ResourceManager가 각 위치의 리소스에 액세스합니다(더 큰 이미지를 보려면 그림을 클릭하십시오.).

리소스 및 위성 어셈블리는 응용 프로그램 도메인에 로드된 상태로 유지됩니다. 또한 각 페이지 또는 공유 리소스 유형의 ResourceManager는 캐시되어 해당 리소스에 대한 이후 요청에 다시 사용됩니다.

이와 같은 방식으로 기본 ASP.NET 리소스-공급자 모델이 리소스에 액세스하게 됩니다. 이제 이 기본 구현에 변경이 필요한 이유를 알아보겠습니다.

다른 위치가 필요한 이유

새로운 ASP.NET 2.0 환경에서 제공하는 기본적인 리소스 생성 및 런타임 액세스 기능은 이전에 비해 훨씬 나은 결과를 만들어냅니다. 그러나 다음과 같은 이유로 인해 다른 리소스 저장 방법을 알아봐야 하는 경우가 종종 있습니다.

  • 다른 저장소에 이미 있는 리소스를 다시 사용
  • 방대한 규모의 정적인 콘텐츠를 실용적으로 저장
  • 관리 효율성 증대

리소스 저장에 널리 사용되는 두 가지 저장소는 외부 리소스 어셈블리와 데이터베이스입니다.

기존 리소스 사용 - 이전 응용 프로그램에서 사용하던 리소스가 있거나 Windows 및 웹 응용 프로그램 간에 공유하는 리소스 어셈블리가 있을 수 있습니다. 일반적으로 ASP.NET 1.1에서 2.0으로 코드를 마이그레이션할 경우 1.1 응용 프로그램에서 .resx 파일을 가져와 2.0 응용 프로그램의 App_GlobalResources 디렉터리에 복사하는 것이 좋습니다. 복사한 .resx는 ASP.NET 2.0 응용 프로그램과 함께 컴파일되며 엄격한 형식의 전역 리소스를 통해 액세스됩니다. 하지만 기존 리소스 어셈블리의 버전을 제어하거나 Windows 및 웹 응용 프로그램에 필요한 리소스 복사본을 하나만 유지하려면 이는 좋은 방법이 아닙니다. 이 경우는 이러한 리소스를 공유 리소스 전용 어셈블리에 저장하는 것이 보다 나은 방법입니다. 따라서 이러한 어셈블리에서 리소스를 가져올 방법이 필요하게 됩니다.

데이터베이스에 리소스 저장 - 웹 응용 프로그램 리소스는 데이터베이스에 저장하는 것이 좋은데, 여기에는 몇 가지 이유가 있습니다. 간단히 생각해도 사이트에 수천 개의 페이지와 리소스 항목이 있는 상태에서 어셈블리 리소스를 사용한다는 것은 이상적인 방법이 아닙니다. 런타임 메모리 사용량도 늘어날 것이고, 응용 프로그램 도메인에 로드되는 어셈블리의 수도 늘어날 것입니다. 결과적으로 규모가 매우 큰 사이트의 경우 데이터베이스 호출 대기 시간이 상당할 것입니다. 이러한 경우 데이터베이스 리소스를 사용하면 중복되는 항목이 줄어들고 복잡한 캐시 옵션을 사용할 수 있으며 보다 많은 양의 콘텐츠를 저장할 수 있으므로 보다 유연하고 관리가 수월한 환경을 만들 수 있습니다. 마지막으로, 데이터베이스에 리소스를 할당하면 번역된 콘텐츠에 보다 복잡한 계층 구조를 지원할 수 있으므로 고객이나 부서에서 지역화된 텍스트의 사용자 지정 버전을 만들 수도 있습니다.

ASP.NET 지역화를 위한 확장성 패턴은 다른 리소스 저장 위치를 지원하여 이러한 결과를 쉽게 달성할 수 있도록 설계되었습니다. 또한 디자인 타임 환경에도 연결할 수 있으므로 다른 저장소에서 리소스를 가져오는 것뿐만 아니라 다른 저장소에서 리소스를 만들 수 있습니다. 이 내용은 필자의 두 번째 기사에서 설명합니다.

외부 리소스 어셈블리나 데이터베이스에 리소스를 저장할 때마다 ASP.NET 2.0의 지역화 기능이 필요할 것입니다. “어디에 있든지” 지역화 식 및 지역화 API를 사용하여 리소스에 액세스할 수 있기 때문입니다. 이러한 기능은 지금부터 이 기사에서 설명할 확장 기능이 있기에 가능합니다.


리소스-공급자 모델

ResourceManager 형식이 런타임 시에 어셈블리에서 리소스를 가져오는 역할을 함을 설명한 바 있습니다. 이 형식은 요청 스레드의 UI culture에 따라 올바른 리소스 집합의 검색을 캡슐화합니다(그림 2 참조). 즉, 요청 스레드의 UI culture가 호출한 사용자의 기본 설정에 대해 올바르게 설정되어 있다면 올바른 위성 어셈블리에서 올바른 리소스를 선택합니다. ASP.NET 2.0 이전에는 각 리소스 유형에 대한 ResourceManager를 직접 인스턴스화하고 그 수명을 관리해야 했습니다. 때문에 각 페이지 요청마다 ResourceManager 인스턴스 작성 또는 액세스를 위한 코드가 필요했으며 리소스 항목에 액세스하기 위해 메서드를 호출해야 했습니다. 선언적인 방식으로 리소스를 페이지 요소에 바인딩하기 위해 사용자 지정 데이터 바인딩 문을 사용하는 방법도 있으나 이 역시 페이지 수준의 데이터 바인딩과 바인딩 변수 할당을 위한 코드가 필요합니다.

ASP.NET 2.0에서는 모든 페이지 또는 사용자 컨트롤에서 지역화 API 함수를 사용하여 리소스를 가져올 수 있습니다. 예를 들어 다음 두 코드에서는 각각 로컬 페이지 리소스와 전역 리소스를 가져옵니다.

this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string;

선언적인 지역화 식을 사용해도 리소스의 페이지 및 컨트롤 속성을 설정할 수 있음을 앞서 설명했습니다. 즉, 다음과 같은 암시적 지역화 식과

<asp:Label ID="labHelloLocal" runat="server" Text="Hello" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

다음과 같은 명시적 지역화 식 모두

<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label>

GetLocalResourceObject()GetGlobalResourceObject()를 호출하는 코드 생성에 사용할 수 있습니다. 즉, ASP.NET 2.0의 리소스 액세스 방법은 편리한 선언적인 식을 사용할 때도 마찬가지로 결국은 이러한 방법을 사용한다는 것입니다.

리소스-공급자 모델이 필요한 이유가 이것입니다. 이러한 API는 기본 또는 사용자 지정 ResourceProviderFactory를 사용하여 올바른 리소스 항목을 찾고 그 값을 수집합니다. 기본 ResourceProviderFactory는 앞서 설명한 ResXResourceProviderFactory 유형입니다. 이 팩토리는 전역 리소스의 경우 GlobalResXResourceProvider의 인스턴스를, 로컬 페이지 리소스의 경우 LocalResXResourceProvider의 인스턴스를 반환합니다.

결국 이러한 공급자는 ResourceManager에 따라 적절한 위성 어셈블리에서 각 리소스에 액세스합니다. 공급자는 ResourceReader를 사용하여 페이지 구문 분석 단계 동안 페이지 리소스 모음을 수집합니다. 그림 3은 기본적인 리소스-공급자 모델과 연관된 핵심 구성 요소를 보여 줍니다.

사용자 삽입 이미지

그림 3. 기본적인 리소스-공급자 모델을 구성하는 구성 요소: 공급자 팩토리, 로컬 및 전역 리소스 공급자, 리소스 관리자 및 각 리소스 유형에 액세스하기 위한 리소스 판독기
(더 큰 이미지를 보려면 그림을 클릭하십시오.)

이 공급자 모델에는 다음과 같은 몇 가지 장점이 있습니다.

  • ResourceManager의 활성화와 수명을 제어합니다.
  • 지역화 식 및 다른 리소스 API가 공급자를 활용하여 리소스를 검색하므로 단순하고 추상화된 API를 통해 생산성을 높일 수 있습니다.
  • 공급자 모델은 확장 가능하므로 ASP.NET 2.0의 생산성 기능을 활용하면서도 리소스 저장 위치를 변경할 수 있습니다.

이제 사용자 지정 리소스 공급자를 구축하는 방법을 살펴보겠습니다.


데이터베이스 리소스 공급자 구축

사용자 지정 리소스 공급자를 사용하면 App_GlobalResources 또는 App_LocalResources가 아닌 다른 곳의 리소스에 액세스할 수 있습니다. 예를 들어 사용자 지정 리소스 공급자를 사용하면 미리 컴파일된 어셈블리에 배포된 리소스에 액세스하거나 데이터베이스의 콘텐츠에 액세스할 수 있습니다. 이 섹션에서는 데이터베이스 리소스-공급자 모델에 대해 살펴보고, 외부 리소스 어셈블리에 액세스하는 방법은 이 기사의 뒷부분에서 설명합니다.

사용자 지정 리소스 공급자는 ResourceProviderFactory를 포함하며 IResourceProvider 인터페이스를 구현하는 리소스-공급자 유형을 하나 이상 포함합니다. 로컬 또는 전역 리소스에 액세스하기 위한 적절한 IResourceProvider의 인스턴스화는 팩토리를 통해 수행됩니다. 그림 4는 이 기사의 샘플 코드에 구현된 데이터베이스 리소스-공급자 모델을 구성하는 구성 요소를 보여 줍니다.


그림 4. 사용자 지정 데이터베이스 리소스-공급자 모델의 구성 요소 계층 구조

데이터베이스 리소스 항목

먼저 실제 리소스 항목을 저장하는 데이터베이스 테이블의 구조를 검토해 보겠습니다. 샘플에는 CustomResourceProvidersSample이라는 데이터베이스에 StringResources라는 테이블을 만드는 SQL 스크립트가 있습니다. 표 1은 이 테이블에 포함된 필드를 보여 줍니다.

표 1. 리소스 항목이 있는 데이터베이스 테이블

필드 설명
resourceType 각 리소스에 대한 범주입니다. 이는 서로 다른 페이지에 대한 로컬 리소스를 구분하거나 사용자 정의 이름을 통해 전역 리소스 유형을 구분하는 데 사용될 수 있습니다.
cultureCode .NET에서 사용되는 지원 CultureInfo 코드의 culture 코드입니다. ISO 표준에 따릅니다. 누락된 코드에 대해서도 확장할 수 있습니다.
resourceKey 리소스를 가져오는 데 사용되는 리소스 키입니다.
resourceValue 리소스 값입니다. 이 테이블은 최대 4K의 문자열을 지원합니다.

이 샘플의 모든 리소스는 하나의 테이블에 저장되어 있지만 복잡하고 규모가 큰 환경에서는 일반적인 사용 패턴에 맞게 최적화할 수 있도록 여러 테이블에 데이터를 분산하여 저장할 수 있습니다. 이 테이블의 기본 키는 resourceType, cultureCoderesourceKey를 결합한 복합 키입니다. 단일 리소스 값은 대개 기본 키를 사용하여 요청됩니다. 그림 5는 테이블의 내용 중 일부를 보여 줍니다.

사용자 삽입 이미지

그림 5. 샘플의 리소스 항목 중 일부(더 큰 이미지를 보려면 그림을 클릭하십시오.)

페이지 리소스의 resourceType은 응용 프로그램 내에서의 상대 경로를 포함하는 페이지 이름입니다. 예를 들어 Expressions.aspx의 경우는 SubDir1/Expressions.aspx입니다. 이러한 규칙은 다른 하위 디렉터리에 있는 같은 이름의 페이지를 구분하기 위한 것입니다. 기본 리소스-공급자 모델에서 하위 디렉터리마다 다른 로컬 리소스 어셈블리를 사용하는 방식과 유사합니다. 컨트롤 속성에 대한 리소스 키 역시 일반적인 페이지 리소스와 같은 명명 규칙을 따릅니다. 즉 다음과 같은 형식으로 컨트롤 접두사 뒤에 속성 이름이 붙습니다.

[Prefix].[PropertyName]

전역 리소스에는 사용자 지정 resourceType이 있습니다. 샘플 코드에는 Glossary, CommonTermsConfig와 같은 여러 전역 리소스 범주가 있습니다. 이 경우 리소스 키의 이름은 해당 콘텐츠에 맞게 정해집니다.

데이터 액세스 계층인 StringResourcesDALC는 공급자 모델의 사용 패턴을 기반으로 테이블에서 리소스를 가져오는 작업을 추상화합니다.

ResourceProviderFactory 확장

ResourceProviderFactory 유형은 ASP.NET 2.0의 리소스 액세스를 위한 허브에 해당합니다. 즉 요청된 리소스 유형에 따라 전역 또는 로컬 리소스 공급자를 반환하는 기능을 합니다. ResourceProviderFactoryCreateLocalResourceProvider()CreateGlobalResourceProvider()의 두 메서드 구현을 필요로 하는 추상 기본 유형입니다. 사용자 지정 공급자 팩토리를 만들려면 이 기본 유형을 상속하고 이러한 두 메서드의 구현을 제공해야 합니다. 두 메서드는 모두 IResourceProvider 인터페이스를 구현하는 리소스 공급자의 인스턴스를 반환해야 합니다.

기본 ResourceProviderFactory 유형 선언은 목록 1과 같습니다.

목록 1. ResourceProviderFactory 추상 유형

public abstract class ResourceProviderFactory
{
      protected ResourceProviderFactory();
      public abstract IResourceProvider CreateGlobalResourceProvider(string classKey);
      public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath);
}

ResourceProviderFactory는 컴파일을 위한 페이지 구문 분석 단계와 지역화 API 호출을 위한 런타임 시에 리소스 공급자를 제공합니다.

  • 페이지 파서 - 페이지는 컴파일을 위해 먼저 디자인 타임에 구문 분석되며 로컬 및 전역 리소스에 대한 명시적 식은 이 단계에서 유효성이 검사됩니다. 컴파일이 시작되면 모든 식에 대한 코드가 컴파일된 페이지에 생성됩니다. 리소스 공급자는 이 단계에서 파서에 사용됩니다.
  • 런타임 - 런타임에는 컴파일된 페이지의 식이 의미가 없습니다. 컴파일하는 동안 생성된 코드는 지역화 API를 사용하여 로컬 및 전역 리소스에 액세스합니다. 리소스 공급자는 로컬 및 전역 리소스 유형에 대해 생성됩니다.

샘플 코드에서 DBResourceProviderFactory는 두 경로 모두에 대해 DBResourceProvider를 만듭니다. 이는 로컬 및 전역 리소스가 같은 방법으로 액세스되기 때문입니다. DBResourceProviderFactory에 대한 코드는 목록 2와 같습니다.

목록 2. DBResourceProviderFactory는 ResourceProviderFactory의 사용자 지정 구현이며 데이터베이스 리소스를 지원합니다.

using System;
using System.Web.Compilation;
using System.Web;
using System.Globalization;

namespace CustomResourceProviders
{
  public class DBResourceProviderFactory : ResourceProviderFactory
  {

    public override IResourceProvider CreateGlobalResourceProvider
(string classKey)
    {
      return new DBResourceProvider(classKey);
    }

    public override IResourceProvider CreateLocalResourceProvider
(string virtualPath)
    {
      string classKey = virtualPath;
      if (!string.IsNullOrEmpty(virtualPath))
      {
        virtualPath = virtualPath.Remove(0, 1);
        classKey = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1);
      }
      return new DBResourceProvider(classKey);
    }
  }
}

암시적 식 또는 로컬 리소스를 호출하는 명시적 식의 경우 페이지에 대한 공급자를 만들기 위해 GetLocalResourceProvider()가 호출됩니다. 다음은 로컬 리소스를 사용하는 암시적 식과 명시적 식의 예입니다. 이 코드는 코드 샘플의 Expressions.aspx 페이지에 정의되어 있습니다.

<asp:Label ID="labHelloLocal" runat="server" Text="HelloDefault" meta:resourcekey="labHelloLocalResource1" ></asp:Label>
<asp:Label ID="Label1" runat="server" Text="<%$ Resources:labHelloLocalResource1.Text %>" ></asp:Label>

GetLocalResourceProvider()는 단일 매개 변수를 취하며, 이는 응용 프로그램 디렉터리를 포함하는 페이지의 가상 경로입니다. 위 코드의 두 식 모두 이 매개 변수로 "/LocalizedWebSite/Expressions.aspx"를 전달합니다. 그림 5를 보면 로컬 리소스가 응용 프로그램 디렉터리가 아닌 페이지의 상대 경로를 나타내는 resourceType을 사용하여 저장됨을 알 수 있습니다. 따라서 GetLocalResourceProvider()DBResourceProvider의 인스턴스를 만들기 전에 경로에서 응용 프로그램 디렉터리 부분을 잘라냅니다.

전역 리소스를 요청하는 명시적 식의 경우 식에 직접 지정된 리소스 유형이 GetGlobalResourceProvider()로 전달됩니다. 코드 샘플의 Expressions.aspx 페이지에서 다음과 같은 명시적 식을 살펴보겠습니다.

<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>"></asp:Label>

이 경우 리소스 유형이 CommonTerms이므로 GetGlobalResourceProvider()CommonTerms를 매개 변수로 전달하여 호출되며, 이 유형에 대한 DBResourceProvider가 생성됩니다.

지정된 리소스 유형에 대해 DBResourceProvider 인스턴스가 하나만 생성되며 이후 사용을 위해 캐시됩니다. 따라서 팩토리는 공급자 인스턴스가 캐시에 없는 경우에만 호출됩니다. 공급자를 만들고 캐시하는 과정은 리소스 액세스에 사용되는 지역화 API 내에 캡슐화되어 있습니다.

ResourceProviderFactory 구성

구성에 다른 ResourceProviderFactory 유형을 지정하지 않는 한 런타임에는 ResxResourceProviderFactory가 사용됩니다. 웹 구성 파일의 <globalization> 부분에서 resourceProviderFactoryType이라는 특성을 볼 수 있는데, 사용할 ResourceProviderFactory 유형을 이 특성을 통해 지정할 수 있습니다. DBResourceProviderFactory를 구성하려면 다음 설정을 추가합니다.

<system.web>

...other settings

      <globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.DBResourceProviderFactory, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />
</system.web>

참고   제공된 샘플 코드의 DBResourceProviderFactoryCustomResourceProviders 어셈블리 내의 CustomResourceProviders 네임스페이스에 속해 있습니다. 이 어셈블리는 강력한 이름을 가지며 GAC(전역 어셈블리 캐시)에 복사할 수 있습니다.

이제 페이지 구문 분석과 런타임에 리소스 공급자를 만들기 위해 DBResourceProviderFactory가 사용됩니다.

IResourceProvider 구현

리소스-공급자 모델의 핵심은 리소스-공급자 유형입니다. ResourceProviderFactory도 중요하지만 저장 위치에 관계없이 런타임 시에 리소스 항목을 반환하는 역할을 하는 것은 결국 리소스 공급자입니다. 이전 섹션에서 설명했듯이 공급자는 ResourceProviderFactory 구현에 의해 만들어지며 이후 사용을 위해 캐시됩니다. 그림 4의 리소스-공급자 모델을 보면 DBResourceProvider 유형이 로컬 및 전역 리소스 모두에 대해 사용됨을 알 수 있습니다. 이 유형은 데이터베이스에서 리소스를 가져오는 역할을 하지만 이 작업을 처리할 때는 DBResourceReaderStringResourcesDALC 구성 요소를 사용합니다.

리소스 공급자는 목록 3과 같은 IResourceProvider 인터페이스를 구현합니다.

목록 3. IResourceProvider 인터페이스

public interface IResourceProvider
{
      object GetObject(string resourceKey, CultureInfo culture);
      IResourceReader ResourceReader { get; }
}

개별 리소스는 GetObject()를 통해 얻어지며 ResourceReader 속성은 공급자 인스턴스의 리소스 유형을 기준으로 리소스 모음을 반환합니다.

페이지 구문 분석 단계에서 페이지에 대한 모든 로컬 리소스를 가져오기 위해 공급자가 사용되며, 명시적 식의 유효성이 검사되고, 컴파일하는 동안 페이지에 대한 코드가 생성됩니다. 로컬 리소스의 경우 암시적 식에 대한 코드 생성을 위해 리소스 판독기가 사용됩니다. 로컬 및 전역 리소스에 대한 명시적 식은 적절한 공급자에 대한 GetObject() 호출과 함께 개별적으로 유효성이 검사됩니다.

런타임 시에는 파서가 생성한 코드가 페이지 초기화에 따라 로컬 및 전역 리소스를 가져오기 위해 GetObject() 호출을 트리거합니다.

개별 데이터베이스 리소스 가져오기

GetObject()에 대한 DBResourceProvider 구현은 다음과 같습니다.

public object GetObject(string resourceKey, CultureInfo culture)
{

  if (string.IsNullOrEmpty(resourceKey))
  {
    throw new ArgumentNullException("resourceKey");
  }  

  if (culture == null)
  {
    culture = CultureInfo.CurrentUICulture;
  }

  string resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);
}

실제로는 리소스를 가져오기 위한 작업이 StringResourcesDALC 유형으로 위임되어 데이터베이스 쿼리를 처리하게 됩니다(그림 4 참조). 이 구성 요소는 실제 리소스를 찾는 데 필요한 리소스 대체(필요한 리소스가 없을 때 기본 리소스를 대신 사용하는 것) 및 기타 논리를 공급자로부터 분리합니다.

GetResourceByCultureAndKey()는 데이터베이스 연결을 초기화하고 SqlDataReader를 실행하여 값을 가져옵니다. 이 값에는 리소스 대체 논리(이후에 설명함)에 필요한 값도 포함됩니다.

여러 리소스 가져오기

DBResourceProvider는 다음과 같은 ResourceReader 속성의 구현에서 DBResourceReader의 인스턴스를 반환합니다.

public System.Resources.IResourceReader ResourceReader
{
  get
  {
    ListDictionary resourceDictionary = this.m_dalc.GetResourcesByCulture(CultureInfo.InvariantCulture);

    return new DBResourceReader(resourceDictionary);
  }
}

StringResourcesDALC는 특정 유형(InvariantCulture)에 대한 기본 리소스를 수집하는 기능을 합니다. 쿼리 결과로 만들어진 ListDictionary는 열거를 위해 DBResourceReader로 래핑됩니다.

DBResourceReaderIResourceReader를 구현합니다. 이 구현의 핵심 요소는 다음과 같습니다.

public class DBResourceReader : DisposableBaseType, IResourceReader, IEnumerable<KeyValuePair<string, object>>
{
  private ListDictionary m_resourceDictionary;
  public DBResourceReader(ListDictionary resourceDictionary)
  {
    this.m_resourceDictionary = resourceDictionary;
  }

  public IDictionaryEnumerator GetEnumerator()
  {
    return this.m_resourceDictionary.GetEnumerator();
  }

  // 다른 메서드
}
(참고: 프로그래머 주석은 예제 프로그램 파일에는 영문으로 제공되며 기사에는 이해를 돕기 위해 번역문으로 제공됩니다.)

페이지 파서는 판독기의 사전 열거자를 사용하여 암시적 식에 대한 코드를 생성합니다. 판독기가 제공되지 않은 경우나 판독기의 사전이 비어 있는 경우에는 코드가 생성될 수 없습니다. 암시적 식은 명시적이지 않으므로 모든 속성 값에 값이 있어야 하는 것은 아닙니다. 따라서 암시적 식의 경우 값을 설정하기 위한 코드가 생성되지 않으면 페이지에 기본값이 적용되어 표시됩니다.

리소스 대체

리소스 대체는 리소스-공급자 구현의 중요한 부분입니다. 리소스는 요청 스레드에 대한 현재 UI culture를 기반으로 런타임 시에 요청됩니다.

System.Threading.Thread.Current.CurrentUICulture

요청 스레드의 현재 culture가 "es-EC" 또는 "es-ES"와 같은 특정 culture인 경우 리소스 공급자는 해당 culture에 대한 리소스가 존재하는지 확인해야 합니다. 그러나 중립 culture인 "es"만 지정된 리소스도 있을 수 있습니다. 중립 culture는 상위 culture이며 특정 항목을 찾을 수 없는 경우 상위 culture가 다음으로 확인됩니다. 값이 발견되면 응용 프로그램에 기본 culture가 대신 사용됩니다. 이 예의 경우 기본 culture는 "en"입니다.

리소스 대체는 데이터 액세스 구성 요소인 StringResourcesDALC에 캡슐화됩니다. 리소스를 가져오기 위한 호출이 만들어지면 GetResourceByCultureAndKey()가 호출됩니다. 이 함수는 데이터베이스 연결을 열고 리소스 대체를 수행하는 재귀 함수를 호출한 다음 데이터베이스 연결을 닫습니다. GetResourceByCultureAndKey() 구현은 다음과 같습니다.

public string GetResourceByCultureAndKey(CultureInfo culture, string resourceKey)
{
  string resourceValue = string.Empty;

  try
  {
    if (culture == null || culture.Name.Length == 0)
    {
      culture = new CultureInfo(this.m_defaultResourceCulture);
    }

    this.m_connection.Open();
    resourceValue = this.GetResourceByCultureAndKeyInternal
(culture, resourceKey);
  }
  finally
  {
    this.m_connection.Close();
  }
  return resourceValue;
}

재귀 함수인 GetResourceByCultureAndKeyInternal()은 먼저 지정한 culture에 해당하는 리소스를 찾습니다. 찾는 리소스가 없으면 상위 culture가 검색되고 쿼리가 다시 시도됩니다. 이 시도가 실패하면 리소스 항목을 찾기 위한 마지막 시도를 통해 기본 culture가 사용됩니다. 기본 culture에 대한 리소스 항목이 없으면 이 샘플의 경우 중대한 예외가 발생한 것으로 간주됩니다. GetResourceByCultureAndKeyInternal()의 코드는 다음과 같습니다.

private string GetResourceByCultureAndKeyInternal
(CultureInfo culture, string resourceKey)
{

  StringCollection resources = new StringCollection();
  string resourceValue = null;

  this.m_cmdGetResourceByCultureAndKey.Parameters["cultureCode"].Value
= culture.Name;
               
  this.m_cmdGetResourceByCultureAndKey.Parameters["resourceKey"].Value
= resourceKey;

  using (SqlDataReader reader = this.m_cmdGetResourceByCultureAndKey.ExecuteReader())
  {
    while (reader.Read())
    {
      resources.Add(reader.GetString(reader.GetOrdinal("resourceValue")));
    }
  }

  if (resources.Count == 0)
  {
    if (culture.Name == this.m_defaultResourceCulture)
    {
      throw new InvalidOperationException(String.Format(
Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_DefaultResourceNotFound, resourceKey));
    }

    culture = culture.Parent;
    if (culture.Name.Length == 0)
    {
      culture = new CultureInfo(this.m_defaultResourceCulture);
    }
    resourceValue = this.GetResourceByCultureAndKeyInternal(culture, resourceKey);
  }
  else if (resources.Count == 1)
  {
    resourceValue = resources[0];
  }
  else
  {
    throw new DataException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_DuplicateResourceFound, resourceKey));
  }

  return resourceValue;
}

리소스 대체를 저장 프로시저나 SQL CLR 구성 요소에 캡슐화할 수도 있습니다. 대체 규칙은 데이터베이스 디자인과 연관된 면이 있고, 비즈니스 계층에는 그다지 중요하지 않기 때문입니다.

리소스 캐싱

기본 공급자 모델을 사용할 경우, 리소스 어셈블리에서 리소스를 가져올 때 어셈블리를 한 번 로드하여 응용 프로그램 도메인에 캐시합니다. 데이터베이스 리소스의 경우 모든 리소스 요청 때마다 데이터베이스에 연결하지 않도록 하기 위해서 자체적으로 캐싱 메커니즘을 구현해야 합니다. DBResourceProvider가 이 작업을 처리합니다.

공급자에 대한 GetObject() 구현이 어떻게 구성되는지 앞서 설명했으며 이 때는 캐싱을 사용하지 않았습니다. 데이터베이스의 리소스는 다음과 같이 데이터 액세스 계층으로의 호출을 통해 가져오게 됩니다.

resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);

각 리소스 유형마다 하나의 공급자 인스턴스가 존재하며, 반복 사용을 위해 캐시된다는 점을 유의하십시오. 공급자 내에서 각 culture 요청을 위해 사전 내에 리소스 항목을 캐시하면 이 사전 항목은 공급자와 함께 메모리 내에 캐시됩니다. 개체를 가져오기 위한 코드는 먼저 사전 캐시에서 값을 찾고, 없을 경우 데이터베이스에서 개체를 가져온 다음 캐시 항목을 만듭니다. 결과는 다음과 같습니다.

string resourceValue = null;
Dictionary<string, string> resCacheByCulture = null;
if (m_resourceCache.ContainsKey(culture.Name))
{
  resCacheByCulture = m_resourceCache[culture.Name];
  if (resCacheByCulture.ContainsKey(resourceKey))
  {
    resourceValue = resCacheByCulture[resourceKey];
  }
}

if (resourceValue == null)
{
  resourceValue = m_dalc.GetResourceByCultureAndKey(culture, resourceKey);

  lock(this)
  {
    if (resCacheByCulture == null)
    {
      resCacheByCulture = new Dictionary<string, string>();
      m_resourceCache.Add(culture.Name, resCacheByCulture);
    }
  resCacheByCulture.Add(resourceKey, resourceValue);
  }
}

return resourceValue;

캐싱은 데이터베이스에 리소스를 저장할 때 성능을 향상시키기 위해 필요합니다. 이 예에서 값은 응용 프로그램 도메인이 해제될 때까지 캐시되며, 이는 응용 프로그램을 다시 시작하지 않는 한 런타임 시에 데이터베이스 리소스의 동적 업데이트가 반영되지 않는다는 의미입니다. 이러한 동적 업데이트를 허용하려면 데이터베이스 캐시에 종속되도록 리소스를 캐시하기 위한 추가 작업이 필요합니다.

스레드 보호

웹 응용 프로그램에서 고려해야 할 또 하나의 문제는 스레드 보호입니다. 그림 4의 데이터베이스 공급자 모델에 참여하는 구성 요소는 .NET 동기화 기술을 사용하여 스레드가 보호되도록 설계되었습니다.

특정 리소스 유형에 대한 DBResourceProvider 또는 StringResourcesDALC의 인스턴스는 여러 스레드에 의해 호출될 수 있습니다. 간단한 예로 같은 페이지에 대한 두 요청에서 이 인스턴스를 호출할 수 있습니다. StringResourcesDALC 구성 요소에서는 데이터베이스에서 데이터를 가져오는 공용 메서드가 해당 유형에 대해 연결을 열고, 쿼리 매개 변수 값을 설정하고, SqlDataReader를 실행하는 인스턴스 변수를 수정합니다. 이러한 함수에서 스레드를 보호하기 위해 MethodImplAttribute가 적용되었습니다.

[MethodImpl(MethodImplOptions.Synchronized)]

이 특성은 메서드 호출이 이루어지는 동안 StringResourcesDALC 개체를 잠그고 다른 호출자를 차단합니다. 리소스가 캐시되면 성능 향상을 위해 데이터 액세스 구성 요소는 호출되지 않습니다.

DBResourceProvider에서는 사전 캐시에 대한 변경을 일반적인 lock 문을 통해 차단합니다.

lock(this)
   { ... }

이 캐싱 코드에 대한 보다 자세한 내용은 이전 섹션에서 설명했습니다. lock 문은 코드 블록이 실행되는 동안 전체 개체와 해당 멤버를 잠급니다. 즉 한 번에 한 스레드만 캐시에 값을 추가할 수 있습니다.

사용자 지정 리소스 공급자 모델 사용

ASP.NET 2.0 이전에는 각 리소스 유형에 대한 ResourceManager를 직접 인스턴스화하고 그 수명을 관리하기 위해 코드를 작성해야 했습니다. 하지만 ASP.NET 2.0에서는 지역화 API를 사용하여 프로그래밍하는 경우 리소스-공급자 모델이 이 작업을 대신 처리하며, 요청 시 리소스 공급자를 만들고 캐시합니다. 따라서 다음과 같은 방법으로 리소스에 액세스할 때는 ASP.NET 2.0 기술을 사용해야 합니다.

  • 페이지-개체 메서드
  • HttpContext 메서드
  • 지역화 식

마스터 페이지, 웹 페이지 및 사용자 컨트롤은 공용 기본 유형인 TemplateControl을 공유합니다. 앞에서도 언급했지만 이 기본 유형은 리소스 액세스 방식에 있어 GetLocalResourceObject()GetGlobalResourceObject()라는 두 개의 오버로드된 작업을 제공합니다. 이 작업에서는 캐시된 리소스 공급자를 사용하여 공급자의 GetObject() 구현을 통해 리소스를 가져옵니다. 공급자가 아직 캐시되지 않은 경우 ResourceProviderFactoryCreateLocalResourceProvider() 또는 CreateGlobalResourceProvider()를 사용하여 공급자를 만듭니다. 이 방식의 장점은 리소스 값을 가져오기 위한 페이지 코드를 손쉽게 작성할 수 있다는 점입니다.

this.labHelloLocal.Text = this.GetLocalResourceObject("labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = this.GetGlobalResourceObject("CommonTerms", "Hello") as string;

사실 암시적 식과 명시적 식을 사용한 페이지를 컴파일할 경우 이와 매우 흡사한 코드가 생성됩니다.

HttpContext 유형에서 정적 메서드를 사용하여 로컬 및 전역 리소스에 액세스할 수도 있습니다. 이 방법은 페이지에 속하지 않는 코드를 작성할 때 유용합니다.

this.labHelloLocal.Text = HttpContext.GetLocalResourceObject("/RuntimeCode.aspx", "labHelloLocalResource1.Text") as string;

this.labHelloGlobal.Text = HttpContext.GetGlobalResourceObject("CommonTerms", "Hello") as string;

실제로는 지역화 식을 사용하는 것이 리소스에 액세스하는 데 훨씬 편리합니다. 지역화 식은 지역화 API를 통해 리소스에 액세스하기 위한 코드를 자동으로 생성하는 선언적인 모델을 제공합니다. 결국 모든 경로의 끝은 지역화 API와 구성된 ResourceProviderFactory로 연결됩니다.

데이터베이스 리소스: 장점과 단점

리소스를 데이터베이스에 저장하면 다음과 같은 여러 이점이 있습니다.

  • 호출 코드에 영향을 주지 않고도 리소스에 복잡한 계층적 요구 사항을 적용할 수 있습니다. 예를 들어 문자열을 번역할 수 있도록 허용하면서도 고객이나 부서에서 기본 문자열을 사용자 정의하도록 허용할 수 있습니다.
  • 유연한 캐싱과 메모리 사용을 통한 콘텐츠 구성 덕분에 방대한 분량의 HTML 콘텐츠를 보다 쉽게 관리할 수 있습니다. 기본적으로 위성 리소스는 포함된 모든 콘텐츠와 함께 응용 프로그램 도메인에 로드되지만 데이터베이스 리소스는 보다 정교한 알고리즘을 통해 캐시되고 해제됩니다.
  • 정보를 데이터베이스라는 통일된 장소에 저장할 수 있습니다. .resx 파일을 여러 개 사용하는 것에 비해 전체적인 관리 효율이 증대됩니다. 또한 번역 작업자와 작업하는 방식도 간단해집니다.

데이터베이스 저장소에는 몇 가지 단점도 있습니다.

  • 이 방식에는 더 많은 고민과 계획이 수반됩니다. 리소스를 테이블로 어떻게 구성할 것인가? 한 테이블에 모두 넣어야 할까? 범주별로 나누어야 할까? 이후에 여러 테이블로 분산하기 위해 단일 저장 프로시저나 SQL CLR 구성 요소로 액세스해야 할까?
  • 데이터베이스에 Visual Studio 2005의 생산성 기능을 통합하려면 추가 작업이 필요합니다. 즉, 로컬 리소스 생성을 사용하여 리소스를 자동으로 생성하거나 식 대화 상자에서 데이터베이스 정보를 볼 수 없습니다. 이 정도 수준의 통합을 위해서는 개발자를 위한 디자인 타임 환경 통합을 위한 사용자 지정 구성 요소를 구축해야 합니다. 이에 대해서는 다음 기사에서 설명하겠습니다.

데이터베이스 리소스에 생산성 기술을 통합하기 위해 추가 작업이 필요하기는 하지만, 이런 단점보다는 장점이 더 크다고 볼 수도 있습니다. 구조와 리소스 구성을 계획하는 일이 기본 리소스-할당 구조를 사용하더라도 어차피 해야 하는 일이라면 더욱 그럴 것입니다.


외부 어셈블리의 리소스에 액세스

리소스-공급자 모델을 사용하여 미리 컴파일된 외부 어셈블리의 리소스에 액세스할 수도 있습니다. 이렇게 하면 웹 및 Windows 응용 프로그램 간에 공용 리소스를 공유할 수 있으므로 버전 관리와 개발을 위한 통일된 지점을 제공할 수 있습니다. 이 섹션에서는 지금까지 설명한 개념을 외부 리소스 어셈블리 유형에 적용하는 방법에 대해 설명합니다.

그림 6은 외부 리소스-공급자 모델을 구성하는 구성 요소를 보여 줍니다.

사용자 삽입 이미지

그림 6. 외부 리소스-공급자 모델에 대한 구성 요소 계층 구조(더 큰 이미지를 보려면 그림을 클릭하십시오.)

이 구현에 대해 다음과 같은 몇 가지 사실을 알 수 있습니다.

  • 전역 리소스만 지원합니다. ASP.NET 2.0에서 기본적으로 제공되는 페이지 리소스 모델을 대체한다는 것은 적절하지 않습니다. 전역 리소스인 경우에만 외부 리소스 어셈블리에서 가져오게 됩니다.
  • ExternalResourceProviderFactory에서 LocalResXResourceProvider에 액세스할 수 없습니다. 이는 내부 유형이므로 코드에서 만들 수 없습니다. ExternalResourceProviderFactory로 기본 공급자를 대체하면 전역 리소스만 지원하게 됩니다. 다른 방법도 있으나 이후 섹션에서 설명하겠습니다.
  • 리소스에 액세스하는 데 ResourceManager가 사용됩니다. 기본 ResourceManager가 이미 어셈블리로부터 리소스를 액세스할 수 있는 방법을 제공하므로 외부 리소스를 액세스하기 위해 이 기능을 대체할 필요가 없습니다.

이제 이 구현의 주요 사항에 대해 알아보겠습니다.

같은 지역화 식, 다른 사용 사례

리소스 공급자는 지역화 식과 지역화 API로 인해 호출되며 외부 리소스에 액세스하기 위해서는 명시적 식이 사용됩니다. 이러한 식은 전역 리소스에 액세스하는 데 사용되는 것과 비슷하지만 약간의 차이가 있습니다. 특히 리소스 유형과 더불어 어셈블리 이름이 지정되어야 한다는 점이 다릅니다. 기본 공급자는 전역 리소스 어셈블리를 찾는 방법을 알고 있지만 이 외부 리소스 공급자는 같은 결과를 얻기 위해 어셈블리 이름을 필요로 합니다.

기본 공급자 모델(명시적 전역 리소스)의 경우 $Resources 식에 대한 구문은 다음과 같습니다.

<%$ Resources: [resourceType], [resourceKey] %>

구문을 다음과 같이 변경하여 ExternalResourceProviderFactory를 구성하면 외부 리소스에 액세스하는 데 같은 식을 사용할 수 있습니다.

<%$ Resources: [assemblyName]|[resourceType], [resourceKey] %>

예를 들어 전역 리소스 유형이 "CommonTerms"인 CommonResources.dll 어셈블리의 리소스에 액세스하려면 다음과 같은 명시적 식을 사용할 수 있습니다.

<asp:Label ID="labGlobalResource" runat="server" Text="<%$ Resources:CommonResources|CommonTerms, Hello %>" ></asp:Label>

그 결과로 페이지가 컴파일되면 다음과 같은 코드가 생성됩니다.

labGlobalResource.Text = this.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello");

이는 올바른 정보를 제공하면 외부 리소스-공급자 모델에서 기존 식과 지역화 API의 코드를 활용할 수 있음을 의미합니다. 리소스 유형에서 어셈블리 이름을 분리하기 위해 최종적으로 정보를 구문 분석하는 것은 ExternalResourceProvider입니다.

ExternalResourceProviderFactory

DBResourceProviderFactory와 마찬가지로 ExternalResourceProviderFactoryResourceProviderFactory를 상속하고 CreateGlobalResourceProvider()CreateLocalResourceProvider()를 다시 정의합니다. 목록 4는 완전한 구현을 보여 줍니다.

목록 4. ExternalResourceProviderFactory의 구현

public class ExternalResourceProviderFactory : ResourceProviderFactory
{

  public override IResourceProvider CreateGlobalResourceProvider
(string classKey)
  {
    return new GlobalExternalResourceProvider(classKey);
  }

  public override IResourceProvider CreateLocalResourceProvider
(string virtualPath)
  {
    throw new NotSupportedException(String.Format
(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Provider_LocalResourcesNotSupported, "ExternalResourceProviderFactory"));
  }
}

CreateGlobalResourceProvider()는 제공된 클래스 키를 사용하여 GlobalExternalResourceProvider 유형을 인스턴스화합니다. 이 공급자에 대한 클래스 키는 어셈블리 이름과 리소스 유형을 포함해야 한다는 점을 유념하십시오. CreateLocalResourceProvider()를 호출하면 NotSupportedException이 발생하는데, 이는 외부 어셈블리에 로컬 리소스를 저장하지 않기 때문입니다. 즉, 페이지에서 로컬 식을 사용하면 구문 분석 예외가 발생합니다. 따라서 로컬 리소스를 계속 지원하려는 경우에는 ExternalResourceProvider를 사용하는 것은 적절한 시나리오가 아닙니다. 이 문제를 해결하는 방법은 사용자 지정 지역화 식을 사용하는 것으로, 자세한 내용은 이후에 설명합니다.

기존 식과 지역화 API에 ExternalResourceProviderFactory를 연결하려면 Web.config의 <globalization> 부분을 수정해야 합니다.

<globalization uiCulture="auto" culture="auto" resourceProviderFactoryType="CustomResourceProviders.ExternalResourceProviderFactory, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1" />

이렇게 하면 기본 공급자 모델이 외부 리소스-공급자 모델로 바뀌게 됩니다. 공급자가 작동하는 방식을 살펴보겠습니다.

GlobalExternalResourceProvider

GlobalExternalResourceProviderIResourceProvider를 구현합니다. 이 공급자는 GlobalResXResourceProvider와 매우 유사하지만 기존 위성 어셈블리에서 전역 리소스를 가져오며, 리소스가 저장된 특정 어셈블리 이름을 알아야 한다는 점이 다릅니다.

GlobalExternalResourceProvider의 생성자는 파이프 기호("|")로 구분된 어셈블리 이름과 리소스 유형을 취하며 이 정보는 다음과 같이 구문 분석됩니다.

public GlobalExternalResourceProvider(string classKey)
{
  if (classKey.IndexOf('|') > 0)
  {
    string[] textArray = classKey.Split('|');
    this.m_assemblyName = textArray[0];
    this.m_classKey = textArray[1];
  }
  else
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Provider_InvalidConstructor, classKey));

}

생성자에 전달된 classKey 매개 변수의 형식이 잘못되었을 경우 ArgumentException이 발생하며, 페이지 구문 파서는 명시적 식에 대한 오류를 보고합니다. 지역화 API에 대해 직접 작성된 코드는 런타임 시에 실패하게 됩니다.

공급자 인스턴스는 각 고유 어셈블리와 리소스 유형 조합마다 생성됩니다. 유효성 검사를 위해 페이지를 구문 분석하는 동안, 또는 런타임 시에 리소스가 요청되면 다음과 같이 GetObject()가 호출됩니다.

public object GetObject(string resourceKey, System.Globalization.CultureInfo culture)
{
  this.EnsureResourceManager();
  if (culture == null)
  {
    culture = CultureInfo.CurrentUICulture;
  }
  return this.m_resourceManager.GetObject(resourceKey, culture);
}

내부적으로 GlobalExternalResourceProviderResourceManager 유형의 기존 기능에 의존하여 리소스를 얻고 리소스 대체를 처리합니다. 따라서 중요한 것은 올바른 어셈블리에 대한 ResourceManager를 만드는 것입니다. EnsureResourceManager()가 처음으로 호출되면 리소스 어셈블리를 로드하고 이 어셈블리 내에 지정된 유형의 ResourceManager 인스턴스를 만듭니다. 리소스 유형을 포함하지 않는 어셈블리를 지정하면 예외가 발생합니다. 어셈블리를 로드하고 ResourceManager를 만드는 코드는 다음과 같습니다.

Assembly asm = Assembly.Load(this.m_assemblyName);
ResourceManager rm = new ResourceManager(String.Format(CultureInfo.InvariantCulture, "{0}.{1}", this.m_assemblyName, this.m_classKey), asm);
this.m_resourceManager = rm;

ExternalResourceProvider를 사용하면 웹 응용 프로그램의 \bin 디렉터리에 배포된 모든 어셈블리 및 GAC(전역 어셈블리 캐시)에서 리소스를 가져올 수 있습니다.

로컬 리소스는 지원되지 않기 때문에 공급자는 ResourceReader 속성에 대해서는 NotSupportedException을 반환합니다. 따라서 암시적 지역화 식은 구문 분석되지 않습니다.


사용자 지정 지역화 식 지원

모든 리소스가 다른 위치에 저장되어 있고, App_LocalResources 및 App_GlobalResources에 있는 리소스를 사용할 계획이 없다면 사용자 지정 공급자가 매우 좋은 선택이 됩니다. 로컬 및 전역 리소스에 대한 표준 구현(기본 공급자)을 지원하면서도 다른 원본에서 일부 리소스를 가져올 수 있는 방법(사용자 지정 공급자)이 필요하다면 어떻게 해야 할까요? 사용자 지정 리소스 공급자를 대상으로 하는 사용자 지정 식을 구현하면 됩니다.

ResourceExpressionBuilder의 작동 방식

식은 컴파일에 앞서 페이지 구문 분석 단계에 개입하는 식 작성기에 의해 처리됩니다. 식은 <%$ %>으로 구분된 모든 내용을 포함할 수 있으며, 여기에는 응용 프로그램 설정, 연결 문자열 및 지역화 식이 포함됩니다. 이러한 식의 구문은 다음과 같습니다.

<%$ [prefix]: [declaration] %>

이미 설명했듯이 지역화 식은 접두사 "Resources"를 사용합니다. 페이지 파서는 ResourceExpressionBuilder 유형을 사용하여 이러한 식을 처리하는데, 이는 ResourceExpressionBuilder가 <expressionBuilders> 구성의 런타임 기본값에 대해 접두사 "Resources"에 매핑되기 때문입니다.

<expressionBuilders>
<add expressionPrefix="Resources" type="System.Web.Compilation.ResourceExpressionBuilder, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
</expressionBuilders>

컴파일되는 페이지에서는 다음과 같은 작업이 이루어집니다.

  • 페이지가 구문 분석되면 식 작성기의 ParseExpression 작업이 호출되어 식의 구문을 검사합니다. 식이 잘못된 경우(예: 지정한 리소스가 없는 경우) 구문 분석 오류가 생성됩니다.
  • 구문 분석이 성공하면 식 작성기의 GetCodeExpression 작업이 호출되어 식에 대한 코드 생성을 요청합니다. 이 단계가 바로 식 작성기가 페이지 초기화를 위한 코드를 만드는 시점입니다. 생성된 코드는 페이지의 컴파일된 IL에 삽입됩니다.

페이지가 컴파일되지 않도록 설정된 경우 식은 조금 다른 방식으로 처리됩니다. 페이지 컴파일은 각 페이지에 대해 해제할 수 있습니다.

<%@ Page Language="C#" CompilationMode="Never" %>

또는 Web.config 파일을 사용하여 모든 페이지를 컴파일하지 않도록 설정할 수도 있습니다.

<pages compilationMode="Never" />

이 경우 페이지가 요청되어 구문 분석되는 동안 식 작성기의 SupportsEvaluate 속성을 검사하여 컴파일 없이 페이지를 처리할 수 있는지 확인하게 됩니다. 페이지에 대한 코드는 생성되지 않습니다.

런타임 시에 SupportsEvaluate를 다시 한 번 확인하고 EvaluateExpression을 호출하여 각 지역화 식에 대한 값을 가져옵니다.

ResourceExpressionBuilderExpressionBuilder에서 파생됩니다. ExpressionBuilder는 추상 및 가상 메서드를 제공하는 공용 기본 유형으로, 페이지 구문 분석, 코드 생성 및 식 평가를 지원하기 위해 ResourceExpressionBuilder에 의해 구현됩니다. 따라서 사용자 지정 지역화 식을 지원하려면 ExpressionBuilder를 확장하고 자체적인 구현을 제공해야 합니다.

ExpressionBuilder 확장

사용자 지정 지역화 식을 지원하려면 사용자 지정 식 작성기를 구현해야 합니다. ResourceExpressionBuilder와 마찬가지로, 이를 위해서는 ExpressionBuilder를 확장하고 컴파일되지 않은 페이지에 대한 페이지 구문 분석, 코드 생성 및 식 평가를 위한 사용자 지정 구현을 제공해야 합니다.

먼저 이 샘플에 있는 사용자 지정 식 작성기의 용도를 살펴보고, 구현을 위한 구문을 알아보겠습니다. 목표는 <%$ Resources %>에 대한 기본 구현은 그대로 둔 상태에서 외부 어셈블리에서 리소스를 가져오는 기능만 추가하는 것입니다. 이를 위해 리소스 공급자를 완전히 대체하는 방법 대신 새 식을 만들어 처리하는 방법을 사용할 것입니다. 여기에는 새로운 식 접두사, 사용자 지정 ExpressionBuilder, 그리고 이 접두사를 사용자 지정 ExpressionBuilder와 연결할 방법이 필요합니다.

이 예에서는 새 접두사를 "ExternalResource"로 사용합니다. 새 식에 필요한 구문은 다음과 같습니다.

<%$ ExternalResource: [assemblyName]|[resourceType], [resourceKey] %>

이 식은 앞서 설명한 GlobalExternalResourceProvider를 사용하여 지정한 어셈블리에서 리소스를 가져옵니다. 새로운 이 식을 지원하려면 사용자 지정 유형인 ExternalResourceExpressionBuilder를 만들어야 합니다. 표 2는 다시 정의된 각 ExpressionBuilder 메서드에서 제공해야 하는 기능을 요약한 것입니다.

표 2. 다시 정의된 각 메서드에서 제공하는 기능 요약

메서드 설명
EvaluateExpression 컴파일되지 않은 페이지에서 ExternalResource 식에 대한 리소스 값을 반환합니다.
GetCodeExpression ExternalResource 식에 대해 만들어지는 코드를 반환합니다. 이 코드는 사용자 지정 리소스 공급자인 GlobalExternalResourceProvider를 호출합니다.
ParseExpression 식에 대한 리소스에 액세스를 시도하여 ExternalResource 식의 유효성을 검사합니다. 리소스를 찾을 수 없는 경우 페이지 구문 분석은 실패합니다.
SupportsEvaluate 속성 컴파일되지 않은 페이지의 평가가 지원되는지 나타냅니다. 이 구현에서는 true입니다.

ExternalResourceExpressionBuilder를 사용하면 다음과 같은 사용자 지정 지역화 식을 선언할 수 있습니다.

<asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>

식은 컴파일되기 전에 디자인 타임에 구문 분석됨을 기억하십시오. ParseExpression은 리소스 식이 정확하며 요청한 리소스가 실제로 존재하는지 확인하기 위해 페이지를 구문 분석하는 동안 호출됩니다. 이 구현은 다음 코드와 같습니다.

public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context)
{
  if (string.IsNullOrEmpty(expression))
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture,Properties.Resources.Expression_TooFewParameters, expression));
  }

  ExternalResourceExpressionFields fields = null;
  string classKey = null;
  string resourceKey = null;
           
  string[] expParams = expression.Split(new char[] { ',' });
  if (expParams.Length > 2)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Expression_TooManyParameters, expression));
  }
  if (expParams.Length == 1)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.Expression_TooFewParameters, expression));
  }
  else
  {
    classKey = expParams[0].Trim();
    resourceKey = expParams[1].Trim();
  }

  fields = new ExternalResourceExpressionFields(classKey, resourceKey);

             
  ExternalResourceExpressionBuilder.EnsureResourceProviderFactory();
  IResourceProvider rp = ExternalResourceExpressionBuilder.
s_resourceProviderFactory.CreateGlobalResourceProvider(fields.ClassKey);
           
  object res = rp.GetObject(fields.ResourceKey, CultureInfo.InvariantCulture);
  if (res == null)
  {
    throw new ArgumentException(String.Format(Thread.CurrentThread.CurrentUICulture, Properties.Resources.RM_ResourceNotFound, fields.ResourceKey));
  }
  return fields;
}

코드의 대부분은 식의 유효성을 검사하기 위한 것이지만 가장 중요한 부분은 GlobalExternalResourceProvider를 만드는 부분과 리소스를 가져오기 위해 GetObject()를 호출하는 부분입니다.

페이지가 컴파일되면 페이지의 구문이 분석되고 코드가 생성됩니다. 이 시점에서 식 작성기의 GetCodeExpression 구현이 호출됩니다. 이 작업은 런타임 시에 리소스 값을 가져오기 위해 필요한 코드를 반환하며, 구현 내용은 다음과 같습니다.

public override System.CodeDom.CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
  ExternalResourceExpressionFields fields = parsedData as ExternalResourceExpressionFields;

CodeMethodInvokeExpression exp = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(typeof(ExternalResourceExpressionBuilder)), "GetGlobalResourceObject", new CodePrimitiveExpression(fields.ClassKey), new CodePrimitiveExpression(fields.ResourceKey));

return exp;
}

GetCodeExpression 호출에 의해 생성된 결과를 포함하는 코드는 다음과 같습니다.

labExternalResource.Text = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello") as string;

생성된 코드가 ExternalResourceExpressionBuilder에 의해 구현된 정적 메서드에 의존하고 있음을 알 수 있습니다. GetGlobalResourceObjectGlobalExternalResourceProvider를 인스턴스화하고 리소스 항목을 가져오는 도우미 메서드입니다. 컴파일된 페이지의 경우 이 코드는 런타임 시에 외부 리소스에서 값을 가져옵니다.

컴파일되지 않은 페이지의 경우는 EvaluateExpression 호출을 통해 런타임에 식이 평가됩니다. ExternalResourceExpressionBuilderEvaluateExpression을 다시 정의하여 구현하고, GlobalExternalResourceProvider를 사용하여 적합한 리소스를 가져옵니다.

public override object EvaluateExpression(object target, BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
  ExternalResourceExpressionFields fields = parsedData as ExternalResourceExpressionFields;

  ExternalResourceExpressionBuilder.EnsureResourceProviderFactory();
  IResourceProvider provider = ExternalResourceExpressionBuilder.
s_resourceProviderFactory.CreateGlobalResourceProvider(fields.ClassKey);

  return provider.GetObject(fields.ResourceKey, null);
}

사용자 지정 식 작성기를 구성하고 나면 외부 어셈블리에서 리소스를 가져오기 위한 선언적 문을 자유롭게 사용할 수 있으며, App_LocalResources 또는 App_GlobalResources에서 값을 가져오기 위한 기본 지역화 식도 그대로 사용할 수 있습니다.

ExpressionBuilder 구성

사용자 지정 식 작성기를 구성하려면 이를 Web.config의 <expressionBuilders> 부분에 추가해야 합니다. 이 예에서는 다음 구성을 사용하여 ExternalResourceExpressionBuilder를 "ExternalResources" 접두사에 연결했습니다.

<expressionBuilders>
  <add expressionPrefix="ExternalResources" type="CustomResourceProviders.ExternalResourceExpressionBuilder, CustomResourceProviders, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f201d8942d9dbbb1"/>
</expressionBuilders>

이제 "ExternalResources" 접두사를 사용하는 모든 리소스 식은 이전 단원에서 설명한 ExternalResourceExpressionBuilder 구현에 따라 구문 분석 또는 평가됩니다.

로컬, 전역 및 외부 리소스에 액세스

목록 5는 기본 원본 및 사용자 지정 원본에서 리소스를 가져오기 위한 세 가지 지역화 식(암시적, 명시적, 사용자 지정 명시적)의 적용을 보여 줍니다.

목록 5. 암시적, 명시적, 사용자 지정 명시적 방식의 식을 한 페이지에 사용

<asp:Label ID="labHelloLocal" runat="server" Text="HelloDefault" meta:resourcekey="labHelloLocalResource1" ></asp:Label>

<asp:Label ID="Label1" runat="server" Text="<%$ Resources:labHelloLocalResource1.Text %>" ></asp:Label>

<asp:Label ID="labHelloGlobal" runat="server" Text="<%$ Resources:CommonTerms, Hello %>" ></asp:Label>

<asp:Label ID="labExternalResource" runat="server" Text="<%$ ExternalResources:CommonResources|CommonTerms, Hello %>" meta:localize="false" ></asp:Label>

리소스 공급자를 대체하지 않고 사용자 지정 지역화 식을 사용하면 각 페이지에 대한 로컬 리소스, 웹 사이트와 함께 컴파일되는 전역 리소스뿐만 아니라 외부 리소스(또는 다른 원본)의 맞춤형 리소스까지 사용할 수 있는 유연성을 확보할 수 있습니다. ExternalResourceExpressionBuilder를 사용하면 앞서 설명한 정적 도우미 메서드인 GetGlobalResourceObject()를 사용하여 외부 리소스에 직접 액세스할 수 있습니다.

string s = ExternalResourceExpressionBuilder.GetGlobalResourceObject("CommonResources|CommonTerms", "Hello") as string;

이 기술을 사용하면 기본 리소스 공급자를 자체 공급자로 대체할 필요가 없습니다. 대신 사용자 지정 지역화 식에서 생성되는 코드를 사용하여 필요에 따라 외부 어셈블리의 리소스를 가져올 수 있습니다.


결론

이 기사에서는 데이터베이스나 외부 리소스 어셈블리의 리소스에 액세스하기 위한 사용자 지정 리소스-공급자 모델을 만드는 방법을 배웠습니다. 또한 기본 공급자 모델에 다른 리소스 저장소를 통합하기 위해 사용자 지정 지역화 식을 만드는 방법도 살펴봤습니다. ASP.NET 2.0의 확장 기능을 사용하면 리소스 할당 및 검색을 위한 사용하기 쉬운 대체 방식을 만들 수 있습니다. 이러한 기능의 훌륭한 점은 선언적인 지역화 식을 사용하여 자연스럽게 ASP.NET 2.0 프로그래밍 모델과 연계할 수 있다는 점입니다.

이어지는 다음 기사에서는 전체 그림의 나머지 반에 해당하는 내용을 살펴볼 것입니다. 즉 리소스 식을 만들고 적절한 저장소 위치에 리소스를 생성하기 위해 디자인 타임 환경을 제어하는 방법을 배우게 됩니다.


추가 리소스

ㆍMichele의 블로그: http://www.dasblonde.net/ (영문)
IDesign Inc (영문)

출처 : 한국 마이크로소프트 MSDN (2007년 1월)