.NET 4.0 이상 즉 ASP.NET 4.0 이상부터는 Output Cache 에 대한 Provider를 사용자가 설정할 수 있습니다. 제가 회사에서 만든 것은 MongoDB, SharedCache, Memcached 등에 대해 Output Cache 를 저장할 수 있도록 구현했습니다.
개인적으로
Microsoft Server AppFabric 은 별로 좋아하지 않고, 구현하지 않았지만, 그리 어려운 부분은 아닙니다.
이번 글은 일반적으로 가장 많이 사용하고, 속도가 높은
Memcached 를 Cache Repository 로 사용하는 경우에 대해 알아보겠습니다.
너무 유명한 제품이라 Memcached 에 대한 소개는 생략하기로 하고, Windows에서 Memcahced를 Windows Service로 사용할 수 있도록 해 주는 방법에 대해 알아봅시다.
우선
Memcached 64-bit for Windows 나
How to install Memcached on Windows machine (32-bit) 에서 다운 받아 원하는 위치에 압축을 푸시기만 하면 됩니다.
다음으로 설치 폴더에서 Command Prompt 를 열어
설치폴더/memcached.exe -d install
를 수행하시면, Windows Service로 설치 됩니다.
다음으로는 Memcached 서비스를 실행시킵니다.
설치폴더/memcached.exe -d start
이제 작업관리자에서 실제 수행 되고 있는지 확인해보시기 바랍니다.
이제 실제 Memcached 캐시 서버에 접속하여 캐시를 저장/로드를 수행할 Client를 제작해야 합니다. 이를 위해 Memcached Client Driver 인 Enym.Caching 을 사용합니다.
Enym.Caching 을 사용하여, 제작한 클래스는 다음과 같습니다.
-------------------------------------------------------------------
using System;using Enyim.Caching;using Enyim.Caching.Memcached;using NSoft.NFramework.Json;
namespace NSoft.NFramework.Caching.Memcached{
/// <summary>
/// Memcached 캐시 서버를 저장소로 사용하는 Cache Repository 입니다.
/// 참고: https://github.com/enyim/EnyimMemcached/wiki/MemcachedClient-Usage
/// </summary>
public class MemcachedRepository : AbstractCacheRepository
{
#region << logger >>
private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
private static readonly bool IsDebugEnabled = log.IsDebugEnabled;
#endregion
public MemcachedRepository() : base() {}
public MemcachedRepository(ISerializer serializer) : base(serializer) {}
private MemcachedClient _client;
public MemcachedClient Client
{
get { return _client ?? (_client = new MemcachedClient()); }
set { _client = value; }
}
/// <summary>
/// 캐시에 저장된 항목을 반환합니다.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public override object Get(string key)
{
key.ShouldNotBeWhiteSpace("key");
if(IsDebugEnabled)
log.Debug("캐시에서 키[{0}]에 해당하는 값을 조회합니다.", key);
var item = Client.Get(key);
var isSerialized = (Serializer != null) && (item is CacheItem);
if(isSerialized == false)
return item;
var cacheItem = (CacheItem) item;
if(Serializer is IJsonSerializer)
return ((IJsonSerializer) Serializer).Deserialize(cacheItem.ItemData, cacheItem.ItemType);
return Serializer.Deserialize(cacheItem.ItemData);
}
/// <summary>
/// 캐시에 항목을 저장합니다.
/// </summary>
/// <param name="key"></param>
/// <param name="item"></param>
/// <param name="validFor"></param>
public override void Set(string key, object item, TimeSpan validFor = default(TimeSpan))
{
key.ShouldNotBeWhiteSpace("key");
if(IsDebugEnabled)
log.Debug("캐시에 값을 저장합니다. key=[{0}], item=[{1}], Expiry=[{2}]", key, item, validFor);
// BUG: MemcachedClient는 유효기간을 지정하면 저장이 되지 않습니다.
//
if(Serializer != null)
Client.Store(StoreMode.Set, key, CreateCacheItem(this, item));
else
Client.Store(StoreMode.Set, key, item);
}
/// <summary>
/// 캐시에서 항목을 제거합니다.
/// </summary>
/// <param name="key"></param>
public override void Remove(string key)
{
key.ShouldNotBeWhiteSpace("key");
if(IsDebugEnabled)
log.Debug("캐시에 저장된 항목을 삭제합니다... key=[{0}]", key);
Client.Remove(key);
}
/// <summary>
/// 캐시의 모든 항목을 삭제합니다.
/// </summary>
public override void Clear()
{
if(IsDebugEnabled)
log.Debug("캐시에 저장된 모든 항목을 삭제합니다...");
Client.FlushAll();
}
private static CacheItem CreateCacheItem(MemcachedRepository repository, object item)
{
return
new CacheItem
{
ItemType = item.GetType(),
ItemData = repository.Serializer.Serialize(item)
};
}
/// <summary>
/// 캐시로 저장되는 정보
/// </summary>
[Serializable]
private class CacheItem
{
public Type ItemType { get; set; }
public byte[] ItemData { get; set; }
}
}
}
-------------------------------------------------------------------
CacheItem class는 캐시에 저장된 내용이 Serializable 이 아닌 경우, 예외를 일으키기 때문에 래핑하는 역할을 수해합니다.
이제 실제로 Memcached 를 사용하는 OutputCacheProvider를 제작해봅시다.
-------------------------------------------------------------------
/// <summary>
/// ASP.NET 웹 Page의 Output Cache를 Memcached 서버에 저장해주는 OutputCacheProvider입니다./// </summary>
/// <summary>
/// .NET 4.0 이상에서 ASP.NET 웹 Page의 Output Cache를 Memcached 서버에 저장해주는 OutputCacheProvider입니다./// 참고:/// http://www.4guysfromrolla.com/articles/061610-1.aspx/// http://weblogs.asp.net/gunnarpeipman/archive/2009/11/19/asp-net-4-0-writing-custom-output-cache-providers.aspx/// http://weblogs.asp.net/scottgu/archive/2010/01/27/extensible-output-caching-with-asp-net-4-vs-2010-and-net-4-0-series.aspx/// </summary>
/// <example>
/// <code>
/// <system.web>
/// <compilation debug="true" targetFramework="4.0"/>
/// <caching>
/// <outputCache defaultProvider="MemcachedOutputCacheProvider">
/// <providers>
/// <add name="MemcachedOutputCacheProvider"
/// type="NSoft.NFramework.Caching.Memcached.Web.MemcachedOutputCacheProvider, NSoft.NFramework.Memcached"/>
/// </providers>
/// </outputCache>
/// </caching>
/// </system.web>
/// </code>
/// </example>public class MemcachedOutputCacheProvider : AbstractOutputCacheProvider{
#region << logger >>
private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
private static readonly bool IsDebugEnabled = log.IsDebugEnabled;
#endregion
public MemcachedOutputCacheProvider() : base(() => new MemcachedRepository()) {}
}
-------------------------------------------------------------------
아니 뭐야? 뭐가 이리 간단해? 네… 아시다시피 Output Cache 를 Cache에 저장/로드 하는 것은 어떤 캐시 시스템을 사용해도 똑같습니다. 그래서 AbstractOutputCacheProvider 에게 ICacheRepository 를 제공하여, 확장을 손쉽게 할 수 있는 구조를 가지게 되었습니다.
그럼 핵심적인 AbstractOutputCacheProvider 소스를 보시면
-------------------------------------------------------------------
/// <summary>
/// .NET 4.0 이상에서 ASP.NET Page의 OutputCache를 <see cref="CacheRepository"/>를 통해 저장/로드됩니다./// 참고:/// http://www.4guysfromrolla.com/articles/061610-1.aspx/// http://weblogs.asp.net/gunnarpeipman/archive/2009/11/19/asp-net-4-0-writing-custom-output-cache-providers.aspx/// http://weblogs.asp.net/scottgu/archive/2010/01/27/extensible-output-caching-with-asp-net-4-vs-2010-and-net-4-0-series.aspx/// </summary>public abstract class AbstractOutputCacheProvider : OutputCacheProvider, IOutputCacheProvider{
#region << logger >>
private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
private static readonly bool IsDebugEnabled = log.IsDebugEnabled;
#endregion
private static readonly Func<ICacheRepository> _defaultCacheRepositoryFactory =
() =>
{
ICacheRepository repository = null;
try
{
if(IoC.IsInitialized)
repository = IoC.Resolve<ICacheRepository>();
}
catch(Exception ex)
{
if(log.IsWarnEnabled)
log.WarnException("IoC로부터 ICacheRepository를 Resolve하는데 실패했습니다.", ex);
}
if(repository == null)
repository = new SysCacheRepository();
return repository;
};
protected AbstractOutputCacheProvider() : this(_defaultCacheRepositoryFactory) {}
protected AbstractOutputCacheProvider(Func<ICacheRepository> cacheRepositoryFactory)
{
cacheRepositoryFactory.ShouldNotBeNull("cacheRepositoryFactory");
CacheRepository = cacheRepositoryFactory();
}
/// <summary>
/// 실제 캐시 저장소에 데이타를 저장/조회하는 API를 제공하는 Repository입니다.
/// </summary>
public ICacheRepository CacheRepository { get; protected set; }
/// <summary>
/// 출력 캐시에서 지정된 항목에 대한 참조를 반환합니다.
/// </summary>
/// <returns>
/// 캐시에서 지정된 항목을 식별하는 <paramref name="key"/> 값이거나 캐시에 지정된 항목이 없는 경우 null입니다.
/// </returns>
/// <param name="key">출력 캐시에서 캐시된 항목에 대한 고유 식별자입니다. </param>
public override object Get(string key)
{
if(IsDebugEnabled)
log.Debug("ASP.NET Page OutputCache를 로드합니다... key=[{0}]", key);
return CacheRepository.Get(key);
}
/// <summary>
/// 지정된 항목을 출력 캐시에 삽입합니다.
/// </summary>
/// <returns>
/// 지정된 공급자에 대한 참조입니다.
/// </returns>
/// <param name="key"><paramref name="entry"/>에 대한 고유 식별자입니다.</param><param name="entry">출력 캐시에 추가할 내용입니다.</param>
/// <param name="utcExpiry">캐시된 항목이 만료되는 날짜와 시간입니다.</param>
public override object Add(string key, object entry, DateTime utcExpiry)
{
if(IsDebugEnabled)
log.Debug("ASP.NET Page OutputCache를 캐시에 추가합니다. key=[{0}], utcExpiry=[{1}]", key, utcExpiry);
CacheRepository.Set(key, entry, utcExpiry.Subtract(DateTime.UtcNow));
return entry;
}
/// <summary>
/// 지정된 항목을 출력 캐시에 삽입하고 이미 캐시되어 있는 경우 해당 항목을 덮어씁니다.
/// </summary>
/// <param name="key"><paramref name="entry"/>에 대한 고유 식별자입니다.</param><param name="entry">출력 캐시에 추가할 내용입니다.</param>
/// <param name="utcExpiry">캐시된 <paramref name="entry"/>가 만료되는 날짜와 시간입니다.</param>
public override void Set(string key, object entry, DateTime utcExpiry)
{
if(IsDebugEnabled)
log.Debug("ASP.NET Page OutputCache를 캐시에 설정합니다. key=[{0}], utcExpiry=[{1}]", key, utcExpiry);
CacheRepository.Set(key, entry, utcExpiry.Subtract(DateTime.UtcNow));
}
/// <summary>
/// 출력 캐시에서 지정된 항목을 제거합니다.
/// </summary>
/// <param name="key">출력 캐시에서 제거할 항목에 대한 고유 식별자입니다. </param>
public override void Remove(string key)
{
if(IsDebugEnabled)
log.Debug("ASP.NET Page OutputCache를 삭제합니다. key=[{0}]", key);
CacheRepository.Remove(key);
}
}
-------------------------------------------------------------------
AbstractOutputCacheProvider 를 상속받아, SharedCacheRepository, MongoCacheRepository, RedisCacheRepository 등을 제공하게 되면 각각의 Cache Server 별로 OutputCacheProvider를 손쉽게 만들 수 있습니다. 이렇게 ICacheRepository로 나눈 것은 OutputCache 뿐 아니라 ViewState 를 저장하는 PageStatePersister, 세션 상태를 저장하는 SessionStateStoreProvider 를 손쉽게 구현할 수 있기 때문입니다.
마지막으로, OutputCacheProvider 를 적용하기 위해서는 web.config 에 다음과 같이 지정해 주시면 됩니다.
우선 Memcached 서버에 대한 설정을 지정해 줍니다.
-------------------------------------------------------------------
<configSections>
<!-- Memcached -->
<sectionGroup name="enyim.com">
<section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching"/>
</sectionGroup>
</configSections>
<!-- Memcached -->
<!-- 참고: https://github.com/enyim/EnyimMemcached/wiki/MemcachedClient-Configuration -->
<enyim.com>
<memcached protocol="Binary">
<servers>
<add address="127.0.0.1" port="11211"/>
</servers>
<socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00"/>
</memcached>
</enyim.com>
-------------------------------------------------------------------
마지막으로 OutputCache 에 대한 Provider를 지정합니다.
-------------------------------------------------------------------
<system.web>
<caching>
<outputCache defaultProvider="MemcachedOutputCacheProvider">
<providers>
<add name="MemcachedOutputCacheProvider" type="NSoft.NFramework.Caching.Memcached.Web.MemcachedOutputCacheProvider, NSoft.NFramework.Caching.Memcached"/>
</providers>
</outputCache>
</caching>
-------------------------------------------------------------------
자 좀 복잡해 보이지만, .NET 4.0 에서 제공하는 이러한 확장성을 제대로 활용하면, 좀 더 유연하고, 견고한 제품을 만들 수 있을거라 생각됩니다.
특히 CMS 등 컨텐츠의 Read 가 월등히 많은 경우에 더욱 효과가 있고, 메모리의 제한이 있다면, MongoDB 등을 활용하는 것도 좋은 방안이 될 수 있습니다.
아니면 gzip으로 압축하여 캐시에 저장하고, Browser가 gzip을 지원하는 경우 gzip 된 컨텐츠를 아무런 처리없이 그대로 응답할 수 있으므로, 서버 리소스 절약 및 성능 향상에 기여할 수 있습니다.
다음으로는 실제 ASP.NET MVC 에서 활용하는 방안에 대해 알아보겠습니다.