2012년 3월 15일 목요일

ASP.NET MVC Output Caching with Memcached – Part 1

.NET 4.0 이상 즉 ASP.NET 4.0 이상부터는 Output Cache 에 대한 Provider를 사용자가 설정할 수 있습니다. 제가 회사에서 만든 것은 MongoDB, SharedCache, Memcached 등에 대해 Output Cache 를 저장할 수 있도록 구현했습니다.

개인적으로 Microsoft Server AppFabric 은 별로 좋아하지 않고, 구현하지 않았지만, 그리 어려운 부분은 아닙니다.

이번 글은 일반적으로 가장 많이 사용하고, 속도가 높은 Memcached 를 Cache Repository 로 사용하는 경우에 대해 알아보겠습니다.
 
memcached - a distributed memory object caching system
너무 유명한 제품이라 Memcached 에 대한 소개는 생략하기로 하고, Windows에서 Memcahced를 Windows Service로 사용할 수 있도록 해 주는 방법에 대해 알아봅시다.

우선 Memcached 64-bit for WindowsHow to install Memcached on Windows machine (32-bit) 에서 다운 받아 원하는 위치에 압축을 푸시기만 하면 됩니다.

다음으로 설치 폴더에서 Command Prompt 를 열어

설치폴더/memcached.exe -d install

를 수행하시면, Windows Service로 설치 됩니다.
다음으로는 Memcached 서비스를 실행시킵니다.


설치폴더/memcached.exe -d  start

이제 작업관리자에서 실제 수행 되고 있는지 확인해보시기 바랍니다.

image
이제 실제 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 에서 활용하는 방안에 대해 알아보겠습니다.

댓글 4개:

Jeojy0109 :

완전! 완전! 완전! 감사합니다. ^^

모빌 :

안녕하세요 전 아주 초보 입니다 하지만 조금식 배워가고 있습니다 배성혁님의 포스팅이 아주 잘 되여 있어서 보고있습니다 그러나 한가지 이 포스트에서요 [다음으로 설치 폴더에서 Command Prompt 를 열어] 이문장이 이해가 잘안갑니다 어떻게 열수 있나요 Command Prompt 설치폴더에서요 혹시 시작에서 런이나 또는 cmd를 사용해서 하라는것인가요 궁금합니다 알려주세요 ^^;;

Unknown :

간만에 들어옵니다^^
감사의 인사 고맙게 받겠습니다^^

"DOS Command 창을 열어" 즉 "윈도우키 + R" 을 누르시고, cmd 를 실행하시면 됩니다.

소스에는 여기에 공개되지 않은 Guard 패턴, Utility 함수등이 있을 수 있습니다. 적절히 제거하시고 실행하셔야 합니다.

교육용이 아니라 실제 사용하는 코드라 분리하지 못했네요...

모빌 :

인용[ 여기에 공개되지 않은 Guard 패턴, Utility 함수등이 있을 수 있습니다. 적절히 제거하시고 실행하셔야 합니다.]

위 문장에서 적절히 제거하시고 사용하라는되요 전 그렇게 하지도못하는 사람 입니다 ㅠㅠ 완전 초보라고보시면 됩니다 그래서요 뭘 제것을 해야하는지도 모릅니다 하나식 배워나가도록 하겠습니다 답글 정말 감사 합니다 ^^;;

많이 가르처주세요 ^^;;