캐시된 페이지에 동적 컨텐츠 추가하기
같은 페이지에서 동적 컨텐츠와 캐시된 컨텐츠를 어떻게 함께 사용하는지 배워봅시다. 캐시후 대체(post-cache substitution) 라는 개념을 통해 이미 캐시 처리된 페이지에 광고, 뉴스와 같은 동적 컨텐츠를 삽입할 수 있습니다.
출력 캐시의 장점을 활용한다면, ASP.NET MVC 응용 프로그램의 성능을 깜짝 놀랄정도로 향샹시킬 수 있습니다. 페이지가 매번 호출될 때마다 생성되는 반복적이고 소모적인 과정을 없애고, 한번 생성된 페이지를 서버 메모리에 캐시해 둔 다음 이를 다른 사용자들에게 제공할 수 있기 때문입니다. 캐시된 페이지는 더 이상 서버 자원을 소모하지 않기 때문에 성능 향상에 큰 도움이 됩니다.
그러나, 여기서 문제가 하나 있습니다! 그렇게 캐시된 페이지에 동적 컨텐츠를 출력해야 한다면 과연 가능할까요? 예를 들어, 캐시된 페이지에 배너 광고를 출력해야 한다고 가정해 봅시다. 여러분은 그러한 배너 광고를 캐시하고 싶지는 않을 것입니다. 그렇게 된다면 모든 사용자가 항상 같은 광고만을 보게 될 것이고, 그런 방식으로는 광고비를 받을 수 없을테니까요.
다행히도 쉬운 해결책이 있습니다. ASP.NET 프레임워크는 "캐시후 대체 (post-cache substitution)" 라는 기능을 제공하고 있기에 이를 활용하면 됩니다. 이는 이미 메모리에 캐시된 페이지에도 동적 컨텐츠를 삽입할 수 있게 해주는 기능입니다.
일반적으로, [OutputCache] 어트리뷰트를 사용해서 페이지를 캐시하는데 기본적인 캐시 방식은 서버와 클라이언트(웹 브라우저)에 모두 페이지를 캐시합니다만, "캐시후 대체"를 사용하는 경우에는 서버 단에서만 캐시를 하게 됩니다.
"캐시후 대체" 사용하기
캐시후 대체를 사용하려면 두 단계를 거쳐야 하는데요. 먼저 출력하고 싶은 동적 컨텐츠를 문자열로 반환하는 메서드를 정의한 뒤, HttpResponse.WriteSubstitution() 메서드를 호출하여 동적 컨텐츠를 캐시된 페이지에 삽입하면 됩니다.
캐시된 페이지에 뉴스를 랜덤하게 출력하는 것을 예로 들어봅시다. 목록 1의 클래스는 RenderNews()라는 하나의 메서드를 노출하고 있는데, 이 메서드는 세 개의 뉴스 중에서 무작위로 하나를 반환하고 있습니다.
목록 1 - Models\News.cs
using System;
using System.Collections.Generic;
using System.Web;
namespace MvcApplication1.Models
{
public class News
{
public static string RenderNews(HttpContext context)
{
var news = new List<string>
{
"Gas prices go up!",
"Life discovered on Mars!",
"Moon disappears!"
};
var rnd = new Random();
return news[rnd.Next(news.Count)];
}
}
}
그 다음, 캐시후 대체라는 ASP.NET 프레임워크의 장점을 사용하기 위해 HttpResponse.WriteSubstitution() 메서드를 호출합니다. 이 메서드는 캐시된 페이지의 특정 영역을 동적 컨텐츠로 치환하는 코드를 설정합니다. 목록 2에서 WriteSubstitution() 메서드는 랜덤 뉴스를 출력하기 위해 사용되고 있습니다.
목록2 - Views\Home\Index.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %>
<%@ Import Namespace="MvcApplication1.Models" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Index</title>
</head>
<body>
<div>
<% Response.WriteSubstitution(News.RenderNews); %>
<hr />
The content of this page is output cached.
<%= DateTime.Now %>
</div>
</body>
</html>
RenderNews 메서드가 WriteSubstitution 메서드의 매개변수로 전달되고 있는 부분에 주목하실 필요가 있습니다. RenderNews는 이 시점에서 실행되는 것이 아닙니다 (메서드명 뒤에 괄호가 없습니다). 이는 단지 메서드의 참조를 전달하는 것입니다.
목록 3의 컨트롤러에 의해 반환되는 뷰는 캐시됩니다. Index() 액션의 결과를 60초 동안 캐시하도록 어트리뷰트가 지정되어 있는 것을 보면 알 수 있죠.
목록 3 - Controllers\HomeController.cs
using System.Web.Mvc;
namespace MvcApplication1.Controllers
{
[HandleError]
public class HomeController : Controller
{
[OutputCache(Duration=60, VaryByParam="none")]
public ActionResult Index()
{
return View();
}
}
}
비록 Index 뷰가 캐시되도록 지정되어 있긴 하지만 Index 페이지를 호출할 때마다 랜덤하게 다른 뉴스가 보여집니다. Index 페이지에서 시간은 60초 동안 변하지 않습니다(그림 1 참조). 시간이 변하지 않는다는 사실은 페이지가 캐시되었다는 것을 증명합니다. 하지만, WriteSubstitution() 메서드에 의해 삽입된 컨텐츠 즉 뉴스는 각 호출마다 달라지는 것을 확인할 수 있습니다.
그림 1 - 캐시된 페이지에 동적 컨텐츠 삽입하기

출처 : http://i1.asp.net/asp.net/images/mvc/19/CS/clip_image002_3.jpg
헬퍼 메서드 안에서 캐시후 대체 사용하기
사용자 도우미 메서드 안으로WriteSubstitution()에 대한 호출을 은닉화하는 것은 "캐시후 대체"를 유용하게 사용하는 좀 더 쉬운 방법입니다. 목록 4는 이러한 방식을 보여주고 있습니다.
목록 4 - AdHelper.cs
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
namespace MvcApplication1.Helpers
{
public static class AdHelper
{
public static void RenderBanner(this HtmlHelper helper)
{
var context = helper.ViewContext.HttpContext;
context.Response.WriteSubstitution(RenderBannerInternal);
}
private static string RenderBannerInternal(HttpContext context)
{
var ads = new List<string>
{
"/ads/banner1.gif",
"/ads/banner2.gif",
"/ads/banner3.gif"
};
var rnd = new Random();
var ad = ads[rnd.Next(ads.Count)];
return String.Format("<img src='{0}' />", ad);
}
}
}
목록 4는RenderBanner(), RenderBannerInternal() 메서드를 노출하는 정적 클래스를 정의하고 있는데요. 그 중 RenderBanner() 메서드가 실제 도우미 메서드입니다. 이 메서드는ASP.NET MVC HtmlHelper 클래스를 확장하는 메서드이기에, 뷰에서 일반적인 헬퍼 메서드를 사용하는 것과 같이 Html.RenderBanner() 처럼 호출할 수 있습니다.
RenderBanner() 메서드는 RenderBannerInternal() 메서드의 참조를 WriteSubstitution() 메서드에 전달하고 있으며, 결과적으로는 HttpResponse.WriteSubstitution() 를 호출합니다.
RenderBannerInternal() 메서드는 private 메서드이므로 헬퍼 메서드로서 노출되지 않습니다. 이 메서드는 세 개의 배너 광고 이미지 중 랜덤하게 하나를 반환하기 위해서 작성된 것입니다.
목록 5는 RenderBanner() 메서드를 Index 뷰에서 어떻게 사용하는지 보여주고 있습니다. MvcApplication1.Helpers 네임스페이스를 포함하기 위해 <%@ Import %> 지시문이 추가된 것에 유의하십시오. 부주의하여 이 네임스페이스를 포함시키지 않는다면 RenderBanner() 메서드는 Html 속성에서 메서드로 나타나지 않을 겁니다.
목록 5 - Views\Home\Index.aspx (RenderBanner() 메서드 사용)
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %>
<%@ Import Namespace="MvcApplication1.Models" %>
<%@ Import Namespace="MvcApplication1.Helpers" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Index</title>
</head>
<body>
<div>
<% Response.WriteSubstitution(News.RenderNews); %>
<hr />
<% Html.RenderBanner(); %>
<hr />
The content of this page is output cached.
<%= DateTime.Now %>
</div>
</body>
</html>
목록 5의 뷰에 의해 렌더링되는 페이지는 각 호출마다 다른 배너 광고를 출력합니다 (그림 2 참조). 이 페이지는 캐시되지만 배너 광고는RenderBanner() 라는 헬퍼 메서드에 의해 동적으로 삽입됩니다.
그림 2 - 랜덤 배너 광고를 보여주는 Index 뷰

출처 : http://i1.asp.net/asp.net/images/mvc/19/CS/clip_image004_3.jpg
요약
이번 자습서에서는 캐시된 페이지에 어떻게 동적으로 컨텐츠를 업데이트 하는지 살펴보았습니다. 캐시된 페이지에 동적 컨텐츠를 삽입하기 위해 사용하는 HttpResponse.WriteSubstitution() 메서드에 대해 알아보았고, 또한 뷰에서 간편하게 사용하기 위해 헬퍼 메서드로 은닉화하는 것까지 구현하였습니다.
가능하다면 모든 곳에서 캐시의 장점을 활용하십시오. 그러면, 응용 프로그램의 성능을 놀랄만큼 향상시킬 수 있습니다. 심지어는, 이번 자습서에서 설명한대로 동적 컨텐츠가 필요한 상황에서도 여러분은 캐시의 장점을 누릴 수 있습니다.