빼꼼...

안녕하세요 웹지니입니다! 흐헤헤 -ㅅ-;;;
어제 있었던 북한의 도발 때문에 많이들 불안하셨을 것으로 생각합니다.
저로서는 조금 더 부지런히, 열심히 살아야겠다고 다시 한 번 추스르게 된 계기가 되었어요.
뭐... 이번 포스트를 꼭 그래서 올리는 건 아니구요...-ㅅ-;;

예전에 블로그를 통해 Custom HtmlHelper 메서드를 구현하는 것과 관련된 글을 올린 적이 있었어요. 벌써 1년도 넘게 지난 시점이군요. 당시 HtmlHelper 클래스의 확장 메서드를 구현하면서 최종적으로 태그를 생성하는 과정에서 ASP.NET MVC가 제공하는 TagBuilder 클래스를 사용했었는데요.

오늘 포스트는 요즘 유행(?)하는 Fluent Interface 패턴 형태로 TagBuilder 클래스를 손쉽게 활용할 수 있도록 확장하는 내용을 담아봅니다. Fluent Interface 패턴을 활용하면 매우 읽기 쉬운 코드를 작성할 수 있다는 장점이 있지요. 해서 한 번 만들어 봤습니다. 이름하야 FluentTagBuilder!!



Implementing FluentTagBuilder

아시는 분은 아시겠지만 Fluent Interface의 핵심은 메서드가 자기 자신 혹은 관련된 다른 객체의 인스턴스를 리턴하는 것입니다. 즉, 메서드 호출 시 스스로를 계속 리턴하여 연속적으로 메서드를 호출할 수 있도록 구현하는 것이지요. 실질적으로 HTML 태그를 생성하는 기능은 TagBuilder가 이미 훌륭하게 구현하고 있으니 그냥 맡겨버리고 우리는 이 TagBuilder 클래스의 인스턴스를 계속해서 리턴하는 Wrapper 메서드만을 구현해주면 됩니다. 일단 코드를 좀 살펴볼까요?

public class FluentTagBuilder : TagBuilder
{
	public FluentTagBuilder(string tagName)
		: base(tagName)
	{ }

	public FluentTagBuilder Append(FluentTagBuilder innerTag)
	{
		base.InnerHtml += innerTag.ToString();
		return this;
	}

	public FluentTagBuilder AppendHtml(string html)
	{
		base.InnerHtml += html;
		return this;
	}

	public FluentTagBuilder AppendText(string html) {
		  base.InnerHtml += HttpUtility.HtmlEncode(html);
		  return this;
	}

	public FluentTagBuilder With(FluentTagBuilder tagBuilder)
	{
		base.InnerHtml = tagBuilder.ToString();
		return this;
	}

	public FluentTagBuilder With(string attribute, string attributeValue)
	{
		base.Attributes.Add(attribute, attributeValue);
		return this;
	}

	public FluentTagBuilder With(IDictionary attributes)
	{
		base.MergeAttributes(attributes);
		return this;
	}

	public FluentTagBuilder And(string attribute, string attributeValue)
	{
		base.Attributes.Add(attribute, attributeValue);
		return this;
	}

	public FluentTagBuilder AndIf(bool condition, string attribute, string attributeValue)
	{
		if (condition)
		{
			base.Attributes.Add(attribute, attributeValue);
		}
		return this;
	}
}
예제 코드에서 크게 어렵거나 이해가 안 가는 부분은 없으실거라 생각해요. 그러면 실제로 어떻게 사용하는지를 알아볼까요? 예전 포스트에서 구현했던 이미지를 포함하는 링크를 생성하는 ImageLink 메서드를 FluentTagBuilder 클래스를 이용하면 아래와 같이 구현할 수 있습니다.

public static MvcHtmlString ImageLink(
	this HtmlHelper helper, 
	string imageUrl, 
	string actionName, 
	string controllerName, 
	RouteValueDictionary routeValues, 
	IDictionary imgAttributes, 
	IDictionary htmlAttributes)
{
	UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
	string resolvedImageUrl = urlHelper.Content(imageUrl);

	string url = UrlHelper.GenerateUrl(
		null, actionName, controllerName, routeValues, helper.RouteCollection,
		helper.ViewContext.RequestContext, true
	);
	
	FluentTagBuilder imageTagBuilder = new FluentTagBuilder("img")
		.With(imgAttributes)
		.With("src", resolvedImageUrl);

	return MvcHtmlString.Create(new FluentTagBuilder("a")
		.Append(imageTagBuilder)
		.With("href", url)
		.With(htmlAttributes)
		.ToString()
	);
}
18번 라인부터 27번 라인까지의 코드가 바로 FluentTagBuilder 클래스를 이용하여 태그를 생성하는 부분입니다. 예전 포스트와 비교할 때 코드가 확연히 줄어들었을 뿐 아니라 코드를 이해하기도 더 쉬워진 것 같죠?

아직 어딘가에 살아있음을 보여주기 위한 짧은 포스트는 여기서 마치고 이만 물러갑니다.
멋진 하루 보내시기 바랍니다!
Posted by 웹지니 트랙백 0 : 댓글 6

안녕하세요? 웹지니입니다.

2010년 새해가 시작된지도 벌써 1주일이나 지났네요. 다들 건강하시죠?
전 요즘 감기가 걸려서 개고생을 하고 있습니다. 사실 아프려면 크게 아파야 좀 쉬기도 하고 그럴텐데 지금처럼 콧물만 졸졸 흐르면 괜히 지저분하기만 하고 일하기만 더 힘들고 그러네요.

2010년 새해에도 웹지니는 여전히 ASP.NET을 이용한 크로스 플랫폼 웹 애플리케이션을 개발하고 있습니다.
물론 Primary Target은 Windows 플랫폼이지만 리눅스/유닉스 계열과 Mac OS 등을 탑재한 시스템에도 저희 팀에서 구현한 애플리케이션이 동작해야 한다는 (그것도 별도의 코드 수정이나 재컴파일 없이 -ㅅ-;;) 다소 난해한 요구 사항과 맞닥뜨린 상황입니다.
해서 Windows 이외의 플랫폼에서는 마이크로소프트가 지원하고 노벨(Novell)이 프로젝트를 주도하는 모노(Mono)를 사용하게 된 것이지요.

사실 10년 간 Windows 플랫폼만 사용해 온 웹지니에게 리눅스 플랫폼은 매우 생소한 녀석이었기에 이런 저런 고생을 좀 했습니다.
특히 Windows 플랫폼에서는 그냥 “다음” 버튼만 누르면 간단히 해결되던 소프트웨어의 설치가 가장 난제였었지요.
해서 이번 포스트는 크게 두 가지 목적으로 작성되었습니다.

1. Ubuntu 리눅스에서 소스 컴파일을 통해 모노 프레임워크를 설치하는 방법을 잊어버리지 않기 위해서
2. 혹시 다른 .NET/모노 개발자에게 도움이 될까봐

그럼 시작해 볼까요?

1. Installing MonoDevelop 2.0 on Ubuntu 9.10

일단 이 포스트는 Ubuntu 리눅스 9.10 버전을 기준으로 하며 여러분이 이미 가상 머신 혹은 실제 물리적 머신에 Ubuntu 9.10을 설치한 상태라고 가정합니다. 사실 Ubuntu 9.10에는 Mono 2.4.2.3 버전이 이미 설치되어 있으며 모노 개발을 위한 IDE인 MonoDevelop 2.0이 미리 패키징되어 있어 손쉽게 설치가 가능합니다.

Mono 2.4.2.3과 MonoDevelop 2.0만으로도 얼마든지 리눅스 플랫폼에서 .NET 개발이 가능합니다. 게다가 설치도 참 쉽죠~잉. 리눅스 터미널에서 다음과 같은 커맨드만 실행하면 됩니다.

  1: sudo apt-get install monodevelop monodevelop-database

그런 후 리눅스 사용자 계정의 비밀 번호를 입력해 주면 간단히 MonoDevelop 2.0을 설치할 수 있습니다. 그러나 최신 버전은 Mono 2.6.1과 MonoDevelop 2.2이며 늘 최신 버전에 목말라 하는 웹지니는 미리 패키징 된 개발 도구들 대신 최신 소스를 다운로드하여 직접 컴파일 후 설치하는 과정에 도전해 보기로 한 것이지요.

2. Building and Installing Mono 2.6.1 on Ubuntu 9.10

Mono 2.6.1을 설치는 과정은 제 개인적으로는 절대 쉽지 않았습니다. 우선 Ubuntu에 필요한 라이브러리들이 모두 설치되어 있어야 하기에 어떤 것들을 먼저 설치해야 하는지 알아내는 것도 쉽지 않았지요. 그러면 이제 필요한 라이브러리를 먼저 설치해볼까요? 터미널에서 다음과 같은 커맨드를 실행합니다.

  1: sudo apt-get remove mono-common
  2: 
  3: sudo apt-get install build-essential pkg-config bison gettext libglib2.0-dev libcairo2-dev libungif4-dev libjpeg62-dev libtiff4-dev 
  4: 
  5: wget http://ftp.novell.com/pub/mono/sources/libgdiplus/libgdiplus-2.6.tar.bz2
  6: tar xvjf libgdiplus-2.6.tar.bz2
  7: cd libgdiplus-2.6
  8: ./configure && make && sudo make install

자 먼저 1번 라인의 커맨드를 이용하여 이전 버전의 mono-common 라이브러리를 삭제한 후 3번 라인과 같이 필요한 라이브러리들을 모두 설치합니다. 그런 후 마지막으로 libgdiplus 라이브러리를 설치해야 하는데 이 라이브러리는 Mono 2.6.1에 맞춘 최신 버전이 제공되므로 5번 라인과 같이 FTP를 통해 먼저 다운로드 한 후 6번 라인의 커맨드로 압축을 해제하고 8번 라인과 같이 설치해 줍니다.

여기까지 설치했으면 이제 드디어 Mono 2.6.1을 설치할 차례입니다. 다음의 커맨드를 차례로 실행해 보겠습니다.

  1: wget http://ftp.novell.com/pub/mono/sources/mono/mono-2.6.1.tar.bz2
  2: tar xvjf mono-2.6.1.tar.bz2
  3: cd mono2.6.1
  4: ./configure && make && sudo make install

차례대로 설명을 드리자면 1번 라인의 커맨드로 Mono 2.6.1의 소스 코드를 다운로드 한 후 2번 라인의 커맨드로 압축을 해제하고 4번 라인의 커맨드로 설치를 합니다. 설치 과정은 생각보다 오래 걸립니다 (흡연자라면 담배 한 대 때리고 오심이... 쿨럭). 알 수 없는 내용들이 터미널에 가득 출력이 되고 가끔 .NET 컴파일 경고가 보이기도 하지만 일단 불안해 하지 말고 넘어갑니다. 설치 과정이 모두 완료되면 다음과 같이 시스템에 설치된 모노의 버전을 확인할 수 있습니다.

  1: mono --version
  2: 
  3: Mono JIT compiler version 2.6.1 (tarball Thu Jan 7 16:44:43 KST 2010)
  4: Copyright (C) 2002-2008 Novell, Inc and Contributors. www.mono-project.com
  5: 	TLS:		__thread
  6: 	GC:		Included Boehm (with typed GC and Parallel Mark)
  7: 	SIGSEGV:	altstack
  8: 	Architecture:	x86
  9: 	Disabled:	none
 10: 

1번 라인과 같이 mono 실행 파일에 --version 옵션을 주면 3라인 이후의 정보가 출력됩니다. 3번 라인의 버전이 2.6.1로 표시되면 올바르게 설치가 완료된 것입니다.

3. Building and Installing MonoDevelop 2.2 on Ubuntu 9.10

Mono 2.6.1을 설치했으면 이제 모노 개발을 위한 IDE인 MonoDevelop의 최신 버전인 2.2 버전을 설치해 보겠습니다. 안타깝게도 MonoDevelop 2.2는 Windows 플랫폼을 비롯하여 Mac OS X와 노벨의 OpenSUSE 리눅스에서의 설치를 위한 패키지를 지원하지만 Ubuntu 리눅스를 위한 패키지는 지원하지 않습니다. 해서 번거롭지만 소스 컴파일을 통해 설치해야 하는 것이지요. MonoDevelop을 설치하기 이전에 먼저 GTK# 및 Gnome#이라는 라이브러리를 설치해야 합니다. 터미널에서 다음과 같이 커맨드를 실행합니다.

  1: wget http://ftp.novell.com/pub/mono/sources/gtk-sharp212/gtk-sharp-2.12.9.tar.bz2
  2: wget http://ftp.novell.com/pub/mono/sources/gnome-sharp2/gnome-sharp-2.24.1.tar.bz2
  3: wget http://ftp.novell.com/pub/mono/sources/gnome-desktop-sharp2/gnome-desktop-sharp-2.24.0.tar.bz2
  4: 
  5: tar xvjf gtk-sharp-2.12.9.tar.bz2
  6: tar xvjf gnome-sharp-2.24.1.tar.bz2
  7: tar xvjf gnome-desktop-sharp-2.24.0.tar.bz2
  8: 
  9: cd gtk-sharp-2.12.9
 10: ./configure && make && sudo make install
 11: 
 12: cd ../gnome-sharp-2.24.1
 13: ./configure && make && sudo make install
 14: 
 15: cd ../gnome-desktop-sharp-2.24.0
 16: ./configure && make && sudo make install

먼저 위와 같이 세 개의 GTK#및 Gnome# 라이브러리를 다운로드 한 후 각자 압축을 해제하고 각각의 라이브러리가 압축 해제된 폴더에서 설치를 실행합니다. 설치가 완료되면 이제 다음의 커맨드를 이용하여 MonoDevelop 2.2의 소스 코드를 다운로드 하고 설치합니다.

  1: wget http://ftp.novell.com/pub/mono/sources/monodevelop/monodevelop-2.2.tar.bz2
  2: 
  3: tar xvjf monodevelop-2.2.tar.bz2
  4: 
  5: cd monodevelop-2.2
  6: ./configure && make && sudo make install

위의 커맨드를 실행하고 설치가 완료될 때까지 기다립니다. 설치가 완료되면 아래 그림과 같이 Ubuntu 데스크톱의 [Applications] 메뉴에 [Programming > MonoDevelop] 메뉴가 생성됩니다.

MonoDevelop 메뉴를 클릭하면 아래 그림과 같이 MonoDevelop 2.2가 실행됩니다. 새 프로젝트 대화 상자에서 볼 수 있듯이 ASP.NET MVC 1.0 프로젝트과 Silverlight의 리눅스 버전인 Moonlight 애플리케이션 개발을 위한 템플릿도 지원되네요. 훗~

이상으로 Mono 2.6.1과 MonoDevelop 2.2를 설치해 보았습니다. 사실 Mono 2.6.1은 아직 .NET 프레임워크 3.5의 모든 기능을 구현하고 있지는 않습니다만 .NET 2.0의 클래스 라이브러리는 거의 대부분 구현하고 있어 실제 프로젝트를 진행하기에 큰 무리는 없어 보입니다.

앞으로 .NET/Mono 프레임워크로 크로스 플랫폼 웹 애플리케이션을 구현하면서 알게 되는 유용한 정보들도 함께 공유하도록 할게요.
허접한 설치 경험담은 여기서 끝~

Posted by 웹지니 트랙백 0 : 댓글 0

안녕하세요? 웹지니입니다.
오늘 드디어 Professional ASP.NET MVC 1.0 번역서의 최종 완성된 PDF 문서를 전달받았습니다. 약속대로 제1장을 공개해 드려야죠?

표지 이미지 아래의 링크를 클릭하시면 제1장의 PDF 파일을 다운로드 하실 수 있습니다.
아... 근데 블로그에 온통 책 자랑으로 도배질을 하고 있네요...

다운로드: Professional_ASPNET_MVC_ch1.PDF (12.26 mb)

많은 분들께 도움이 되기를 바랍니다 =)

Posted by 웹지니 트랙백 0 : 댓글 0

안녕하세요? 웹지니입니다.
모두 추석 연휴는 즐겁게 보내셨나요? 저도 오랫만에 식구들과 편안한 시간을 가졌습니다. 열심히 재충전 했으니 이제 또 열심히 달려봐야죠?
추석 연휴 전날 즉 10월 1일부터 재충전 한답시고 인터넷을 뚝 끊었다가 오늘 출근해서 다시 보니 ASP.NET MVC 2.0 Preview 2 버전이 그새 릴리즈 되었더라구요 -ㅅ-;;
이번 Preview 2 버전에서 달라진 점들을 간략히 요약해 볼까요?

1. Client-Side Validation
ASP.NET MVC 2.0 Preview 2에서는 jQuery 기반의 클라이언트 측 유효성 검사 라이브러리가 추가되었습니다.

2. Areas
ASP.NET MVC 2.0 Preview 2에서는 Area 지원이 보다 원활해 졌습니다. 이미 Preview 1버전에서도 지원은 가능했지만 자동화되어 있지 않았기에 개발자가 직접 프로젝트 파일을 수정해야 하는 불편이 있었지요.

3. Model Validation Providers
모델 객체를 바인딩할 때 개발자가 원하는 유효성 검사를 수행할 수 있게 되었습니다. 기본적으로는 DataAnnotations API를 이용하고 있네요. 사실 아시는 분은 아시겠지만 DataAnnotations API는 아직 문제가 좀 있지요?

4. Metadata Providers
앞서 모델 객체의 유효성 검사와 연관되는 사항으로 모델 객체에 대한 사용자 정의 메타데이터 객체를 지정할 수 있습니다. 기본적으로 사용되는 메타데이터 제공자 역시 DataAnnotations API를 활용하고 있습니다.

Preview 1에 비해 많이 달라진 부분은 없는 것 같네요. 아마도 안정성의 향상과 Preview 1에서의 불편했던 점을 해소하는 방향으로 개발이 진행된 느낌입니다. 저도 아직 설치를 못해봤으니 빨리 설치해 봐야겠어요. 참, 다운로드는 여기를 클릭하세요!

Posted by 웹지니 트랙백 0 : 댓글 1

안녕하세요? 웹지니입니다.

지난 포스트에서 말씀드렸듯이 제가 번역 중인 Professional ASP.NET MVC 1.0의 번역서 제1장을 공개합니다.

img1

 

이번에 공개되는 부분은 아직 출판사를 통해 편집 및 교정이 완료되지 않은, 그야말로 드래프트한 상태입니다만 ASP.NET MVC 1.0을 학습하시기에 부족함은 없을 것이라 생각합니다.

번역이 완료된 상태의 MS-Word 문서를 그대로 PDF로 만든 것이라 보시기에 다소 불편하실 수도 있어요. 시간이 조금 더 지나 출판사에서 편집이 완료되면 재구성된 문서를 다시 한 번 공개하도록 하겠습니다. 모쪼록 이번에 공개된 문서가 ASP.NET MVC 1.0을 공부하고자 하는 많은 분들께 도움이 되길 바랍니다.

아울러 내용을 살펴보시면서 오탈자나 오역 등을 발견하시면 댓글도 부탁드려요 ^^ 
감사합니다.

다운로드: Professional ASP.NET MVC 1.0 - Chapter 1 NerdDinner.pdf (10.89 mb)

P.S: 제 1장의 베타리딩에 참여해 주신 SQL Server MVP 성대중님과 다음 .NET 개발자 모임 카페의 심재운 님께 감사의 말씀을 전합니다.

Posted by 웹지니 트랙백 0 : 댓글 0

안녕하세요? 웹지니입니다.

오늘은 .NET on OpenSource 시리즈의 두 번째 포스트로 Memcached라는 이름의 분산 메모리 캐싱 시스템을 Ubuntu 리눅스에 설치하고 이를 .NET Application에서 활용하는 방법에 대해 소개해 볼까 합니다. 사실 이번 시리즈를 시작하게 된 계기가 있는데요. 다른 .NET 개발자 분들은 어떨지 모르겠지만 저 개인적으로는 많은 부분을 Microsoft에 의존하고 있었던 것 같아요. 그 사실을 Memcached라는 녀석을 알게 되면서 깨닫게 되었습니다.

현재 Microsoft는 Velocity라는 이름으로 Memcached와 유사한 분산 캐싱 시스템을 구현 중에 있어요. Velocity의 가장 버전은 이 글을 쓰는 시점에서는 CTP3이며 조만간 CTP4가 출시될 예정입니다. 사실 CTP 버전이면 실무에 도입하기에는 다소 무리가 따르죠. 반면 Memcached는 이미 LiveJournal.com을 비롯하여 이미 많은 곳에서 사용되고 있으며 충분히 검증된 시스템입니다.

이런 경우 당연히 후자인 Memcached를 선택하는 것이 타당하겠지요. 문제는 .NET 환경에서 사용이 가능한지 여부입니다. 다행히 Memcached 그 자체는 객체를 메모리 캐시에 넣고 빼는 매우 기본적인 기능에만 충실합니다. 대신 Client API를 위한 프로토콜을 정의하고 Memcached 서버와 클라이언트 사이의 통신은 Client API가 책임지도록 하고 있어요. 따라서 잘 만들어진 .NET용 Client API가 있다면 .NET 환경에서도 얼마든지 Memcached 서버를 활용할 수 있는 셈이지요.

또한 Win32버전의 Memcached 서버도 존재합니다. 그러나 안타깝게도 원래 Memcached를 개발하고 배포하는 Danga Interactive가 제공하는 것은 아니며 Linux용 Memcached가 현재 1.2.8 버전인 것에 비해 Win32용은 1.2.1 버전이 제공되고 있습니다. 해서 저는 최종적으로 Linux 환경에서 Memcached 1.2.8을 설치하고 .NET 환경을 위한 Client API를 사용하여 .NET Application에 분산 캐시를 도입하기로 마음 먹었답니다.

1. Installing libevent on Ubuntu

여러분이 이미 Ubuntu 리눅스가 설치된 VM이나 혹은 물리적 머신을 가지고 있다는 가정하에 Ubuntu 리눅스에 Memcached 서버를 설치해 보도록 하겠습니다. Memcached 서버를 설치하려면 그 전에 libevent라는 라이브러리를 먼저 설치해야 합니다. 우선 libevent 라이브러리의 홈페이지를 방문해 볼까요?

img1

그림에서 보듯이 좌측의 Download 메뉴를 보면 libevent-1.4.11이 현재 가장 최신의 안정 버전임을 알 수 있습니다. 그러면 리눅스 환경에서 이 파일을 다운로드하여 설치를 해야겠지요. 링크를 마우스 오른쪽 버튼으로 클릭해 보면 해당 링크가 가리키는 주소를 알 수 있습니다. 이 주소는 리눅스에서 파일을 다운로드할 때 필요합니다. 리눅스의 터미널을 열고 다음과 같이 명령을 날려봅니다.

  1: $ wget http://www.monkey.org/~provos/libevent-1.4.1-stable.tar.gz\
  2: $ gunzip libevent-1.4.11-stable.tar.gz
  3: $ tar xvf libevent-1.4.11-stable.tar
  4: $ cd libevent-1.4.11-stable/
  5: $ ./configure
  6: $ make
  7: $ sudo make install

1번 라인의 커맨드는 libevent 홈페이지로부터 libevent-1.4.11-stable.tar.gz 파일을 다운로드하는 명령입니다. 다운로드가 완료되면 2번 라인과 같이 gunzip 명령으로 gz 파일의 압축을 해제한 후 다시 3번 라인과 같이 tar 명령을 이용하여 libevent-1.4.11-stable.tar 파일의 압축을 해제합니다.

압축이 해제되면 4번 라인과 같이 압축이 해제된 디렉터리로 이동한 후 5번, 6번, 7번 라인의 명령을 차례대로 실행합니다. 이 세 가지 명령은 리눅스에서 뭔가를 빌드해서 설치하기 위한 기본 단계입니다. 별 다른 오류 없이 설치가 완료되었다면 whereis 명령으로 libevent 라이브러리가 제대로 설치되었는지 확인할 수 있습니다.

  1: $ whereis libevent
  2: libevent: /usr/local/lib/libevent.a /usr/local/lib/libevent.so /usr/local/lib/libevent.la

저의 경우에는 /usr/local/lib 디렉터리에 libevent가 설치된 것을 확인할 수 있었습니다. 그러면 이제 Memcached를 설치해 보겠습니다.

2. Installing Memcached on Ubuntu

Memcached는 이미 리눅스 배포판에 포함되어 있습니다. 이 포스트에서 사용하고 있는 Ubuntu 9.0.4의 경우에도 이미 포함되어 있으며 아래의 커맨드를 통해 확인할 수 있습니다.

  1: apt-cache search memcached 혹은
  2: apt-cache pkgnames | grep memcached

둘 중 하나의 커맨드를 실행해 보면 Memcached와 관련된 패키지들이 주르륵 나타날 것입니다. 그러나 가장 최신 버전의 모듈이 아닌 관계로 웹을 통해 Memcached 1.2.8 버전을 다운로드해서 설치해 보겠습니다. 리눅스의 터미널에서 다음의 커맨드를 차례대로 실행합니다.

  1: $ wget http://memcached.googlecode.com/files/memcached-1.2.8.tar.gz
  2: $ gunzip memcached-1.2.8.tar.gz
  3: $ tar xvf memcached-1.2.8.tar
  4: $ cd memcached-1.2.8/
  5: $ sudo mkdir -p /opt/memcached/
  6: $ ./configure --prefix=/opt/memcached/
  7: $ make
  8: $ sudo make install
  9: $ whereis memcached
 10: /opt/memcached/bin/memcached

우선 1번 라인과 같이 Memcached 1.2.8 버전을 다운로드한 후 2번, 3번 라인과 같이 파일의 압축을 해제합니다. 그런 후 5번 라인과 같이 /opt/memcached/ 라는 이름의 디렉터리를 생성하는데 이는 제가 Memcached를 설치하려고 하는 디렉터리입니다. 이 디렉터리는 6번 라인과 같이 설치 환경을 설정할 때 --prefix 매개 변수를 이용할 수 있습니다. 그런 후 7번, 8번 라인과 같이 make, make install을 차례로 실행하여 Memcached를 설치한 후 9번 라인과 같이 설치를 확인하면 10번 라인처럼 /opt/memcached/bin/디렉터리에 Memcached가 설치됩니다.

3. Configuring and Running Memcached Service

이제 Memcached의 설치를 마쳤으므로 간단한 설정을 거쳐 서비스를 실행해 보겠습니다. 우선 Memcached가 설치된 /opt/memcached/bin/ 디렉터리로 이동하여 Memcached를 실행해 봅니다.

  1: $ cd /opt/memcached/bin/
  2: $ memcached

그러면 아마도 Memcached가 설치되지 않았다는 메시지를 보게 될 것입니다. 아니, 방금 설치했는데 이게 무슨 소리??? 그래서 Memcached가 제대로 설치됐는지를 다음과 같이 확인해 봅니다.

  1: $ ldd /opt/memcached/bin/memcached
  2:   linux-gate.so.1 => (oxb7fa0000)
  3:   libevent-1.4.so.2 => not found
  4:   libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e2e000)
  5:   /lib/ld-linux.so.2 (0xb7fa1000)

ldd 명령을 통해 Memcached가 필요로 하는 Library Depenency를 살펴보니 3번 라인과 같이 libevent 라이브러리를 찾을 수 없다는 문장이 보이네요. 아하 이제보니 앞서 설치한 libevent 라이브러리를 Memcached가 알아보지 못했나봅니다. 해서 /etc/ld.so.conf 파일을 편집하여 libevent 라이브러리가 설치된 폴더를 지정해 주어야 합니다. vi 에디터를 이용해서 ld.so.conf 파일에 다음과 같이 문장을 추가합니다.

  1: include /usr/local/lib/libevent/

그런 후 다음과 같이 ldconfig 명령을 실행하여 방금 수정한 설정 내용이 적용되도록 합니다.

  1: $ sudo ldconfig /etc/ld.so.conf
  2: $ ldd /opt/memcached/bin/memcached
  3:   linux-gate.so.1=> (0xb8034000)
  4:   libevent-1.4.so.2 => /user/local/lib/libevent-1.4.so.2 (0xb800d000)
  5:   libc.so.6 => /lib/tls/i686/libc.so.6 (0x7eaa000)
  6:   libnsl.so.1 => /lib/tls/i686/cmov/libnsl.so.1 (0xb7e90000)
  7:   librt.so.1 => /lib/tls/i686/cmov/librt.so.1 (0xb7e87000)
  8:   libresolv.so.2 => /lib/tls/i686/cmov/libresolv.so.2 (0xb7e71000)
  9:   /lib/ld-linux.so.2 (0xb8035000)
 10:   libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0xb7e58000)

1번 라인과 같이 ldconfig 명령을 실행한 후 2번의 ldd 명령을 다시 날려보면 이번에는 모든 라이브러리들이 제대로 로드되어 있음을 확인할 수 있습니다. 이제 다음과 같이 Memcached를 실행해 봅니다.

  1: $ sudo /opt/memcached/bin/memcached -d -m 2048 -p 11211

위의 명령은 /opt/memcached/bin/ 디렉터리의 memcached 파일을 실행합니다. 이  때 –m 옵션에 의해 메모리는 2GB를 사용하며 포트 번호는 –p 11211에 의해 11211번 포트를 사용하게 됩니다. 참고로 이 포트 번호는 Memcached의 기본 포트 번호입니다. 이 커맨드 역시 /etc/rc.local 파일에 기록해 두면 리눅스가 재시작할 때 자동으로 Memcached 서비스를 시작하도록 구성할 수 있습니다.

  1: /opt/memcached/bin/memcached -d -m 2048 -p 11211 2 > &1 > /dev/null

4. Using Memcached on Microsoft .NET

이제 Memcached 서비스를 설치하고 서비스를 시작하는데 성공했다면 .NET 환경에서 Memcached 서비스를 활용하는 방법에 대해 살펴보겠습니다. 먼저 Memcached 서비스를 사용하기 위한 .NET용 Client API를 구해야 합니다. 이미 CodePlex를 통해 몇 가지 API가 제공되고 있으며 그 중 제가 보기에 enyim.com Memcached Client 프로젝트가 가장 괜찮아 보였습니다.

img2

오른쪽의 Download Now 링크를 클릭하여 enyim.com_memcached_1.2.0.2.zip 파일을 다운로드 한 후 압축을 풀어보면 Enyim.Caching.dll 파일을 발견할 수 있습니다. 그러면 이 어셈블리를 이용하여 Memcached 서비스를 이용하는 간단한 ASP.NET 웹 애플리케이션을 구현해 보겠습니다.

4.1 Create new classic ASP.NET Web Application project

Visual Studio 2008을 실행하고 새로운 ASP.NET Web Application 프로젝트를 선택한 후 아래 그림과 같이 새 프로젝트를 생성합니다.

img3

프로젝트가 생성되면 솔루션 탐색기를 통해 앞서 다운로드 한 Enyim.Caching.dll 파일을 프로젝트로 참조합니다.

img4

그런 후 Web.config 파일을 열고 앞서 다운로드한 파일의 압축이 해제된 폴더에 저장된 Sample.config 파일을 참고하여 Memcached 서비스에 대한 설정을 추가해야 합니다. 이 코드는 다음과 같습니다.

  1: <configuration>
  2:   <configSections>
  3:     <sectionGroup name="enyim.com">
  4:       <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" />
  5:     </sectionGroup>
  6:   </configSections>
  7:   <enyim.com>
  8:     <memcached enabled="true">
  9:       <!-- keyTransformer="" -->
 10:       <servers>
 11:         <add address="192.168.160.11" port="11211" />
 12:       </servers>
 13:       <socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:10:00" deadTimeout="00:02:00" />
 14:     </memcached>
 15:   </enyim.com>
 16: </configuration>
 17: 

여기서 가장 중요한 설정은 바로 8번 라인부터 14번 라인까지의 설정입니다. 8번 라인에서는 Memcached를 사용하도록 enabled 특성에 true를 지정하였으며 11번 라인에서는 Memcached가 서비스되는 서버의 IP와 포트 번호를 지정합니다. <servers> 요소에는 여러 개의 <add> 요소를 지정할 수 있으므로 여러 개의 Memcached 서버를 지정하여 일종의 클러스터링 캐시를 구성할 수도 있습니다.

이제 Memcached 캐시를 이용하는 간단한 예제를 구현해 보겠습니다. 우선 다음과 같이 Account라는 이름의 클래스를 구현합니다.

  1: public class Account
  2: {
  3:   public string UserName { get; set; }
  4:   public string DisplayName { get; set; }
  5:   public int Age { get; set; }
  6: 
  7:   public override string ToString()
  8:   {
  9:     return String.Format(
 10:       "{0}, {1}",
 11:       this.DisplayName,
 12:       this.Age
 13:     );
 14:   }
 15: }

Account 클래스를 추가했으면 Default.aspx.cs 파일에 다음과 같이 코드를 추가합니다.

  1: protected void Page_Load(object sender, EventArgs e)
  2:     {
  3:       MemcachedClient client = new MemcachedClient();
  4:       List<Account> accounts = client.Get<List<Account>>("myAccounts");
  5:       
  6:       if (accounts == null)
  7:       {
  8:         accounts = CreateSampleData();
  9:         client.Store(StoreMode.Set, "myAccounts", accounts);
 10:         Response.Write("Cached");
 11:         Response.Write("<br>");
 12:       }
 13: 
 14:       foreach (Account account in accounts)
 15:       {
 16:         Response.Write(account.ToString());
 17:         Response.Write("<br>");
 18:       }
 19:     }
 20: 
 21:     private List<Account> CreateSampleData()
 22:     {
 23:       List<Account> accounts = new List<Account>();
 24:       accounts.Add(new Account()
 25:       {
 26:         UserName = "webgenie",
 27:         DisplayName = "웹지니",
 28:         Age = 100
 29:       });
 30: 
 31:       accounts.Add(new Account()
 32:       {
 33:         UserName = "zmeun",
 34:         DisplayName = "천호민",
 35:         Age = 80
 36:       });
 37: 
 38:       accounts.Add(new Account()
 39:       {
 40:         UserName = "sleepy",
 41:         DisplayName = "꽃미남",
 42:         Age = 70
 43:       });
 44: 
 45:       return accounts;
 46:     }

우선 3번 라인의 코드를 보면 MemcachedClient 클래스의 인스턴스를 생성합니다. 이 클래스가 바로 Memcached 서버에 액세스하기 위한 Entry Point 역할을 담당하는 클래스입니다. 4번 라인과 같이 MemcachedClient.Get 메서드를 이용하면 Memcached 서버의 캐시로부터 지정된 키로 저장된 객체를 얻어올 수 있습니다. 만일 객체를 찾을 수 없다면 null이 리턴되므로 이 경우에는 6번 라인의 if 구문을 이용해 새로운 객체를 생성하고 이를 MemcachedClient.Store 메서드를 호출하여 Memcached 서버의 캐시에 객체를 저장합니다.

이제 이 예제 애플리케이션을 실행해 보면 아래 그림과 같이 10번 라인에서 객체를 캐시에 추가할 때 출력한 Cached!라는 문자열이 출력된 채로 데이터가 보여지는 것을 볼 수 있습니다.

img5

F5 키를 눌러 페이지를 새로 고치면 아래 그림과 같이 캐시로부터 데이터를 가져와 보여주게 됩니다.

img6

5. Why need Memcached?

자, 이제 Memcached 서버가 올바르게 동작하는 것을 확인했습니다. 그런데 이 녀석을 어디에 어떻게 써먹을 수 있을까요? ASP.NET도 이미 훌륭한 캐싱 기능을 제공하고 있는데 말이지요.

아시다시피 ASP.NET의 캐싱 기능은 웹 서버의 메모리를 활용합니다. 따라서 너무 많이 사용할 경우 결국은 웹 서버의 메모리 부하로 이어질 수 있다는 단점이 있지요. 그러나 물리적 서버 수에 여유가 있다면 이처럼 Memcached 서버를 구축함으로써 많은 데이터를 캐싱할 수 있어 전반적인 서비스의 성능 향상을 꾀할 수 있습니다.

특히 Memcached Providers 프로젝트 사이트를 보면 이 포스트에서 사용했던 Enyim Memcached Client API를 바탕으로 ASP.NET의 Cache API와 Session 객체를 대체한 Provider 객체가 구현되어 제공되고 있습니다. 따라서 현재 ASP.NET의 Cache나 Session을 사용하고 있다면 이를 Memcached 서버로 옮김으로써 웹 서버의 부하를 줄일 수 있겠지요?

뿐만 아니라 여러 개의 Memcached 서버를 구축하고 Web.config 파일에 서버들의 IP를 추가해주면 캐싱하는 객체들이 여러 Memcached 서버에 분산되어 저장될 뿐 아니라 Memcached 서버에 복제 기능이 추가된 repcached라는 녀석을 이용하면 분산 캐시 클러스터를 구성할 수도 있습니다.

물론 이들 기능들은 Velocity에서도 구현이 되고 있습니다만 분산 캐시가 필요한데 Velocity를 마냥 기다릴 수만 없다면 Memcached가 현재로서는 가장 탁월한 선택이 아닐까 생각됩니다.

요즘들어 느끼는 거지만 세상 참 좋아졌어요 ^^

즐거운 하루 되세요~

[UPDATED: 2009-06-26 13:39]

조금 전 저는 이 포스트와 동일한 방법으로 구성한 Memcached 서버에 간단한 테스트를 진행해 보았습니다. 제가 실행했던 테스트는 미국에 위치한 우리 회사의 DB로부터 100개의 레코드를 가져와 이를 List<T> 타입의 Entity 객체로 변환한 후 GridView 컨트롤에 바인딩 했을 때와 변환된 Entity 객체를 Memcached 캐시에 추가한 후 캐시로부터 가져왔을 때의 성능 비교였습니다. 먼저 그 결과를 보여드리자면 다음과 같습니다.

Try from Database (밀리초) from Cache (밀리초)
1 3862 8
2 3123 7
3 2987 7
4 3136 8
5 3124 7

위의 표에서 알 수 있듯이 성능의 차이가 상당함을 볼 수 있습니다. 물론 DB는 미국에 있고 Memcached 서버는 현재 회사 네트워크 내에 있기 때문에 더 큰 성능 상의 차이가 발생했겠습니다. 해서 회사 내의 네트워크에 존재하는 다른 DB에 대해 동일한 테스트를 수행해 보았습니다. 그 결과는 아래 표와 같습니다.

Try from Database (밀리초) from Cache (밀리초)
1 2176 8
2 2172 7
3 2174 7
4 2182 8
5 2171 7

로컬 DB와의 테스트에서도 상당한 성능 차이가 있군요. 물론 이런 이유로 캐싱을 사용하는 것이겠지만 이 정도라면 Memcached 웹사이트에 적혀있던 "Very Fast"라는 말이 무색하지 않네요. 도움이 되셨기를 바랍니다. ^^

Posted by 웹지니 트랙백 1 : 댓글 0

예제 코드 다운로드: WebApplication1.zip (34.81 kb)

안녕하세요? 웹지니입니다.

여러분, AJAX 많이 사용하시지요? XMLHttpRequest 객체를 이용한 비동기 호출과 자바스크립트를 이용한 HTML 문서의 DOM 객체 조작을 이용하여 페이지를 동적으로 구성하는 AJAX 기법은 이제는 거의 모든 페이지에서 한 두 번은 반드시 사용하게 되는 필수적인 기능이 된 것 같아요.

그런데 사실 AJAX를 이용해서 어떤 기능을 구현할 경우 생각보다 많은 자바스크립트 코드로 인해 곤욕을 치를 때가 있을거에요. 예를 들어 게시판을 AJAX로 구현하는 경우를 생각해 보자면 페이지를 이동할 때마다 각 게시글 목록을 구성하는 DOM 객체들을 제거하고 다시 새로운 DOM 객체들을 만들어 페이지의 어딘가에 쑤셔(?) 넣어야 하죠. 이 과정을 구현하기 위한 자바스크립트 코드가 그닥 짧지 않을 것이라는 점은 직접 구현해 보지 않았더라도 충분히 예상할 수 있을거에요. 게다가 페이징을 위한 UI도 매번 새로 만들어줘야 하고요.

여기에 게시글 목록을 출력하는 과정에서 자바스크립트로는 해결이 안되어 C# 코드의 도움을 받아야 하는 경우 - 예를 들면 글 작성자의 상세 정보를 가져와 UI를 구성해야 한다면 - 라면 훨씬 더 고통스러운 자바스크립트 코딩 작업이 될거에요. 이런 경우라면 차라리 속 편하게 UpdatePanel 컨트롤을 사용하는 게 백번 나은 판단일 수도 있고요. 하지만 UpdatePanel 컨트롤을 사용하게 되면 서버 폼이 반드시 필요하고 또한 ViewState 정보가 AJAX 요청에 포함되어 사실 상 꼭 필요한 데이터만을 요청하고 전달받는다는 AJAX의 기본 철학과는 거리가 먼 이야기가 되고 맙니다.

물론 지금부터 제가 설명할 내용들도 이러한 AJAX의 기본 철학과는 다소 거리감이 느껴집니다. 하지만 UpdatePanel 컨트롤을 사용할 수 없는 환경 또는 사용하기 싫은 경우 어쩌면 좋은 대체 방법이 될 수 있을지도 모른다는 생각이 들어 용감하게 포스팅을 해 봅니다. 그러면 지금부터 웹서비스와 사용자 컨트롤 (*.ascx 파일)을 이용한 페이지 부분 렌더링 예제를 구현해 볼까요?

HttpServerUtility.Execute 메서드

우선 오늘 포스트에서 소개할 내용을 간단하게 한 줄로 요약하자면 

"웹서비스를 이용해서 사용자 컨트롤 (*.ascx) 파일을 렌더링한 결과로 생성된 HTML 코드를 클라이언트로 전달"

하는 방법입니다. 이 때 사용자 컨트롤을 렌더링하는 방법이 문제가 되는데요. 이 문제를 해결해 줄 수 있는 녀석이 바로 우리가 익히 알고 있는 HttpServerUtility.Execute 메서드입니다. HttpServerUtilityServer.Execute 메서드는 과거 ASP에서 다른 페이지를 실행한 결과를 현재 페이지 중간에 끼워넣기 위해 자주 사용되었던 방법이죠. 그렇다면 이 녀석을 어떻게 활용하면 뭔가 이뤄지지 않을까요? MSDN에서 HttpServerUtility.Execute 메서드에 대한 도움말을 찾아보면 이 메서드가 다음과 같이 재정의 되어 있다는 것을 알 수 있습니다.

HttpServerUtility.Execute(string path);
HttpServerUtiltiy.Execute(string path, bool preserveForm);
HttpServerUtility.Execute(string path, TextWriter writer);
HttpServerUtility.Execute(string path, TextWriter writer, bool preserveForm);
HttpServerUtility.Execute(IHttpHandler handler, TextWriter writer, bool preserveForm);


이 Execute 메서드는 void 타입을 리턴하지만 매개 변수에 TextWriter 타입을 사용하는 재정의된 버전들은 이 TextWriter 객체에 지정된 페이지의 실행 결과로 만들어진 HTML 코드를 전달할 수 있습니다. 그런데 가장 마지막 재정의를 보니 첫 번째 매개 변수가 IHttpHandler 인터페이스 타입이네요. 그리고 System.Web.UI.Page 타입은 IHttpHandler 인터페이스를 구현하고 있지요. 그렇다면 여기에 Page 클래스의 인스턴스를 넘겨줄 수 있다는 뜻???

연장 챙겼으면 삽질 고고씽!

앞서 살펴본 HttpServerUtility.Execute 메서드가 바로 이번 포스트의 핵심이라는 것을 눈치채셨을테니 곧바로 예제 애플리케이션의 구현으로 넘어가 보겠습니다. 구현된 예제 코드를 설명해 드리기 앞서 노파심에 덧붙이자면 이 예제는 단순히 활용방법을 소개하기 위한 것일 뿐 실전에서 사용하기에는 다소 무리가 있습니다. 해서 이번 포스트의 내용이 현재 진행 중인 프로젝트에 필요하다고 판단하신다면 예제 코드를 적절히 수정해서 사용하셔야 합니다. 그러면 실질적인 예제의 구현을 시작해 보죠.

우선 Visual Studio 2008에서 새로운 ASP.NET 웹 애플리케이션 프로젝트를 하나 생성하고 아래 그림과 같이 폴더들을 구성합니다.

그림 1: 예제 애플리케이션의 폴더 구조
img1  

그림에서 보듯이 Classes 폴더에는 몇 개의 인터페이스와 클래스가 정의되어 있습니다. 우선 이번 예제에서 Entity 객체로서의 역할을 수행할 Book 클래스의 소스 코드를 살펴볼까요?

코드 1: Book 클래스

  1: using System;
  2: 
  3: namespace WebApplication1.Classes
  4: {
  5: 	public class Book
  6: 	{
  7: 		public string Title { get; set; }
  8: 		public string Author { get; set; }
  9: 	}
 10: }


겁나게 간단한 코드로 구현된 이 클래스는 Title과 Author라는 두 가지 속성을 제공하며 이번 예제에서는 Book 클래스 객체의 컬렉션을 데이터 바인딩을 위한 데이터 원본으로 사용합니다. 자 그럼 다음으로 부분 렌더링에 사용될 사용자 컨트롤을 위한 인터페이스인 IView 인터페이스를 정의해 보겠습니다.

코드 2: IView 인터페이스

  1: using System;
  2: using System.Collections.Generic;
  3: 
  4: namespace WebApplication1.Classes
  5: {
  6: 	public interface IView
  7: 	{
  8: 		Dictionary<String, Object> ViewData { get; }
  9: 	}
 10: }
 11: 

IView 인터페이스는 ViewData 라는 이름의 속성을 제공하며 이 속성은 웹서비스로부터 사용자 컨트롤로 전달될 여러 가지 데이터를 보관하기 위한 용도로 사용됩니다. IView 인터페이스는 사용자 컨트롤의 기반 클래스로 사용될 ViewControl 클래스가 다음과 같이 구현하고 있습니다.

코드 3: ViewControl 클래스

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Web;
  4: using System.Web.UI;
  5: 
  6: namespace WebApplication1.Classes
  7: {
  8: 	public class ViewControl : UserControl, IView
  9: 	{
 10: 		#region IView Members
 11: 
 12: 		public Dictionary<string, object> ViewData
 13: 		{
 14: 			get { return HttpContext.Current.Items["__ViewData"] as Dictionary<String, Object>; }
 15: 		}
 16: 
 17: 		#endregion
 18: 	}
 19: }

ViewControl 클래스는 UserControl 클래스를 상속하며 IView 인터페이스를 구현합니다. IView 인터페이스가 정의한 ViewData 속성은 HttpContext 객체에 담겨져 전달될 예정이므로 14번 라인과 같이 HttpContext 객체로부터 뷰 데이터를 가져와 리턴하는 간단한 코드가 구현되어져 있습니다. 앞으로 부분 렌더링에 사용될 사용자  컨트롤들은 모두 ViewControl 클래스를 상속하여 구현하면 되겠죠?

이제 웹서비스 쪽으로 준비를 해보겠습니다. 우선 IPresenter 라는 이름의 인터페이스를 다음과 같이 정의합니다.

코드 4: IPresenter 인터페이스

  1: using System;
  2: using System.Collections.Generic;
  3: 
  4: namespace WebApplication1.Classes
  5: {
  6: 	public interface IPresenter
  7: 	{
  8: 		Dictionary<String, Object> ViewData { get; }
  9: 
 10: 		string Render(string viewName);
 11: 	}
 12: }
 13: 


이 인터페이스 역시 ViewData라는 속성을 제공하며 앞서 IView 인터페이스와 동일한 Dictionary<String, Object> 타입을 사용합니다. 그리고 Render 라는 메서드를 정의하고 있네요. 이 Render 메서드는 지정된 이름의 사용자 컨트롤을 찾아 컨트롤을 렌더링하고 결과 HTML을 리턴하는 역할을 담당합니다. 이 인터페이스는 AjaxPresenterService 웹서비스가 다음과 같이 구현합니다.

코드 5: AjaxPresenterService 클래스

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Web.Services;
  4: using System.Web;
  5: using System.Web.UI;
  6: using System.Web.Compilation;
  7: using System.Text;
  8: using System.IO;
  9: 
 10: namespace WebApplication1.Classes
 11: {
 12: 	public class AjaxPresenterService : WebService, IPresenter
 13: 	{
 14: 		private Dictionary<String, Object> _viewData = null;
 15: 
 16: 		public AjaxPresenterService() : base()
 17: 		{
 18: 			this._viewData = new Dictionary<string, object>();
 19: 			if (HttpContext.Current != null)
 20: 				HttpContext.Current.Items.Add("__ViewData", this._viewData);
 21: 		}
 22: 
 23: 		#region IPresenter Members
 24: 
 25: 		public Dictionary<string, object> ViewData
 26: 		{
 27: 			get { return this._viewData; }
 28: 		}
 29: 
 30: 		public string Render(string viewName)
 31: 		{
 32: 			string viewPath = String.Format("~/Views/{0}.ascx", viewName);
 33: 			Type type = BuildManager.GetCompiledType(viewPath);
 34: 			ViewControl control = Activator.CreateInstance(type) as ViewControl;
 35: 
 36: 			Page dummyPage = new Page();
 37: 			dummyPage.Controls.Add(control);
 38: 			
 39: 			StringBuilder response = new StringBuilder();
 40: 			StringWriter responseWriter = new StringWriter(response);
 41: 			Server.Execute(dummyPage, responseWriter, true);
 42: 
 43: 			return response.ToString();
 44: 		}
 45: 
 46: 		#endregion
 47: 	}
 48: }


코드가 조금 길어보이지만 그다지 어려운 코드는 없지요? 우선 이 서비스 클래스는 생성자 내에서 ViewData 속성을 구현하기 위한 _viewData 멤버를 초기화하며 HttpContext 객체가 존재하는 경우 생성된 _viewData 멤버를 HttpContext 클래스의 Items 컬렉션에 추가합니다. 중요한 것은 바로 32번 라인부터 시작하는 Render 메서드를 구현하는 코드이겠지요.

32번 라인에서 알 수 있듯이 Render 메서드는 viewName 매개 변수에 전달된 사용자 컨트롤의 이름을 이용하여 ~/Views/viewName.ascx 파일을 찾습니다. 예를 들어 Render("MyView")와 같이 호출하면 ~/Views/MyView.ascx 파일을 찾게 되는 것이지요.

이렇게 사용자 컨트롤의 가상 경로를 알아내게 되면 33번 라인과 같이 BuildManager 클래스의 GetCompiledType 메서드를 호출하여 해당 사용자 컨트롤이 컴파일된 클래스의 타입 정보를 얻어올 수 있으며 이 타입 정보를 Activator 클래스의 CreateInstance 메서드에 전달하면 해당 클래스의 인스턴스를 얻어올 수 있습니다. 이렇게 얻어온 인스턴스를 ViewControl 타입으로 형 변환하면 해당 사용자 컨트롤이 부분 렌더링을 지원하기 위한 사용자 컨트롤인지를 알 수 있게 되는 것이지요.

사실 사용자 컨트롤을 직접 렌더링할 수 있는 방법은 존재하지 않습니다. 즉 사용자 컨트롤을 렌더링하려면 반드시 페이지의 도움을 받아야 한다는 것이지요. 36번, 37번 라인의 코드가 Page 클래스의 인스턴스를 생성하고 Controls 컬렉션에 앞서 얻어온 사용자 컨트롤의 인스턴스를 추가하는 것은 바로 이런 이유입니다. 이제 임시로 생성한 페이지를 렌더링할 준비가  끝났으니 렌더링된 결과를 받아올 TextWriter 타입의 객체를 생성해야하겠지요? 39번 라인과 40번 라인에서는 StringBuilder 클래스와 StringWriter 클래스의 인스턴스를 준비하고 Server.Execute 메서드에 StringWriter 클래스의 인스턴스를 전달하여 최종적으로 dummyPage 객체가 렌더링된 결과를 가져옵니다. 이렇게 렌더링 된 결과를 String 타입으로 리턴하면 이 HTML 코드를 클라이언트 스크립트에서 그대로 활용할 수 있겠지요? 그러면 이와 같이 웹 서비스를 호출할 페이지를 아래와 같이 구현해 보겠습니다.

코드 6: Default.aspx 페이지의 소스 코드

  1: <head runat="server">
  2:     <title>Untitled Page</title>
  3:     <script language="javascript" type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
  4:     <script language="javascript" type="text/javascript">
  5: 		function renderView() {
  6: 			$.ajax(
  7: 			{
  8: 				type: "POST",
  9: 				url: "/Services/MyService.asmx/Hello",
 10: 				contentType: "application/json; charset=utf-8",
 11: 				data: "{viewName: 'MyView'}",
 12: 				dataType: "json",
 13: 				success: function(data) {
 14: 					$("#result").html(data.d);
 15: 				},
 16: 				error: function(data) {
 17: 					alert(data.statusText);
 18: 				}
 19: 			}
 20: 			);
 21: 		}
 22:     </script>
 23: </head>
 24: <body>
 25:     <input type="button" value="GetView" onclick="renderView()" />
 26:     <div id="result">
 27:     </div>
 28: </body>


이 페이지는 jQuery를 이용하여 AJAX 호출을 수행하는 클라이언트 스크립트와 이 스크립트 함수를 호출하는 하나의 버튼을 가지고 있습니다. 6번 라인에서 20번 라인까지의 코드를 보면 알 수 있듯이 jQuery.ajax 함수를 이용하여 MyService.asmx 라는 이름의 서비스가 제공하는 Hello라는 이름의 메서드를 호출합니다. 이 메서드는 앞서 AjaxPresenterService 서비스의 Render 메서드를 호출하여 특정 사용자 컨트롤을 렌더링한 결과를 전달받아 26번 라인의 <DIV> 태그에 삽입하게 됩니다. MyService.asmx 서비스는 다음과 같은 코드로 구현되어 있습니다.

코드 7: MyService.asmx.cs 파일의 소스 코드

  1: [System.Web.Script.Services.ScriptService]
  2: public class MyService : AjaxPresenterService
  3: {
  4: 	[WebMethod]
  5: 	public string Hello(string viewName)
  6: 	{
  7: 		this.ViewData.Add("Books", CreateDataSource());
  8: 		return this.Render(viewName);
  9: 	}
 10: 
 11: 	private List<Book> CreateDataSource()
 12: 	{
 13: 		List<Book> books = new List<Book>(10);
 14: 
 15: 		for (int i = 1; i <= 10; i++)
 16: 		{
 17: 			Book book = new Book()
 18: 			{
 19: 				Title = String.Format("Title {0}", i),
 20: 				Author = String.Format("Author {0}", i)
 21: 			};
 22: 
 23: 			books.Add(book);
 24: 		}
 25: 
 26: 		return books;
 27: 	}
 28: }


MyService 서비스는 앞서 구현한 AjaxPresenterService 서비스 클래스를 상속하며 Hello 메서드는 전달받은 viewName 매개 변수를 Render 메서드에 전달하여 지정된 사용자 컨트롤을 렌더링한 HTML 코드를 리턴합니다. 물론 그 전에 ViewData 속성에 CreateDataSource 메서드가 리턴하는 데이터 원본을 대입하해 주어야겠지요. CreateDataSource 메서드는 Book 클래스의 인스턴스 10개를 구성하는 매우 간단한 코드이지만 실제로는 여러분의 데이터 저장소에서 데이터를 가져오는 기능을 구현해야 합니다. 한 편 앞서 Default.aspx 파일의 스크립트에서 MyService 서비스의 Hello 메서드에 전달할 매개 변수로 MyView라는 값이 전달되었습니다. 이 MyView가 바로 사용자 컨트롤이며 Views 폴더 아래에 다음과 같이 구현됩니다.

코드 8: MyView.ascx 파일의 마크 업

  1: <asp:Repeater ID="repeater1" runat="server">
  2: 	<HeaderTemplate>
  3: 		<ul>
  4: 	</HeaderTemplate>
  5: 	<ItemTemplate>
  6: 		<li><%# Eval("Title") %> by <%# Eval("Author") %></li>
  7: 	</ItemTemplate>
  8: 	<FooterTemplate>
  9: 		</ul>
 10: 	</FooterTemplate>
 11: </asp:Repeater>


그리고 이 사용자 컨트롤의 비하인드 코드는 다음과 같습니다.

코드 9: MyView.ascx.cs 파일의 소스 코드

  1: public partial class MyView : ViewControl
  2: {
  3: 	protected override void OnLoad(EventArgs e)
  4: 	{
  5: 		this.repeater1.DataSource = this.ViewData["Books"];
  6: 		this.repeater1.DataBind();
  7: 	}
  8: }


MyView 사용자 컨트롤은 Repeater 컨트롤에 ViewControl 클래스의 ViewData 속성에 보관된 데이터 원본을 가져와 바인딩하는 것을 볼 수 있습니다. 이 ViewData 속성에 보관된 데이터 원본은 앞서 MyService.Hello 메서드에서 CreateDataSource 메서드를 호출해 얻은 10개의 Book 클래스 객체의 컬렉션인 것이지요. 이것으로 예제의 구현을 모두 마쳤습니다. 예제를 실행하고 버튼을 클릭하면 AJAX 호출로 포스트 백 과정 없이 데이터가 바인딩된 결과가 페이지에 출력되는 것을 볼 수 있습니다.

그림 2: 예제를 실행한 모습

img2

마치며...

사실 이번 포스트에서 소개한 방법은 그다지 완벽하지 않습니다. 우선 서버 폼을 필요로 하는 <asp:Button>과 같은 컨트롤은 사용할 경우 문제가 있습니다. 이 경우 MyView.ascx 파일에 서버 폼을 선언하면 <FORM> 태그가 렌더링 될텐데 만일 사용자 컨트롤을 렌더링하는 페이지가 이미 서버 폼이나 클라이언트 폼을 가지고 있다면 문제가 되겠죠?

그렇지 않다면 AjaxPresenterService.Render 메서드에서 Page 클래스의 인스턴스를 생성할 때 사용자 컨트롤을 Page 객체에 추가하는 것이 아니라 HtmlForm 클래스의 인스턴스를 하나 더 생성하여 사용자 컨트롤을 HtmlForm.Controls 컬렉션에 추가하고 HtmlForm 객체를 다시 Page 객체에 추가하는 방법을 사용할 수 있습니다. 하지만 이 경우에도 페이지가 이미 <FORM>  태그를 가지고 있을 수 있으므로 정규표현식을 이용하거나 기타 다른 방법으로 StringBuilder에 기록된 결과 HTML에서 <FORM> 태그나 ViewState 필드를 제거해 주는 추가적인 절차가 필요합니다.

따라서 반드시 필요한 경우가 아니라면 이번 예제 애플리케이션처럼 서버 폼이 필요없는 UI 요소들을 이용하여 인터페이스를 구현하는 것이 좋겠습니다.

자, 그럼 전 이만 물러갑니다. 다음 포스트에서 다시 만나요!

Posted by 웹지니 트랙백 0 : 댓글 4

안녕하세요? 웹지니입니다.

ASP.NET MVC 프레임워크에 대한 마소 연재가 끝나자마자 ASP.NET MVC Preview 5가 발표되었어요. 자칫 4번의 연재에 걸쳐 4개의 서로 다른 버전의 ASP.NET MVC 프레임워크를 소개하는 웃지못할 상황이 발생할 뻔 했습니다. -ㅅ-;;

ASP.NET MVC Preview 5는 기존의 Preview 4에서 많은 부분들이 수정되었습니다. 물론 개발자들의 편의와 생산성 향상을 위한 조치겠지만 사실 너무 빨리, 또 너무 자주 바뀌는 느낌도 없진 않네요. 그저 많은 개발자들의 피드백을 적극적으로 수용하고 있다고 좋은 쪽으로 생각하고 지금부터 Preview 5에서의 변경 사항에 대해 하나씩 알아가 보도록 하겠습니다.

이번 이야기는 ASP.NET MVC Preview 5에 추가된 Model Binder에 대한 이야기입니다.

ASP.NET MVC의 Model Binder

ASP.NET MVC 프레임워크는 모델 객체의 구현에 대해 개발자에게 상당한 자유를 부여합니다. 즉 개발자가 임의로 엔터티 객체를 구현하여 모델 객체로 활용하거나 혹은 LINQ to SQL이나 NHibernate와 같은 ORM (Object-Relational Mapping) 도구를 활용하여 생성된 객체들도 모델 객체로 활용할 수 있습니다. ORM 도구를 사용할 경우 개발자의 생산성은 눈에 띄게 향상됩니다.

한 가지 아쉬운 점은 입력되는 데이터를 모델 객체로 개발자가 일일이 변환해 주어야 한다는 점입니다. 즉 컨트롤러의 액션 메서드에 전달된 사용자의 입력 값들을 모델 객체의 속성 값으로 일일이 대입해 주어야 한다는 점입니다. 입력 값의 개수가 많으면 많을수록 점점 더 코드를 작성하기가 귀찮아 지겠죠?

그러나 ASP.NET MVC Preview 5의 Model Binder를 이용하면 이와 같은 불편이 최소화됩니다. ASP.NET MVC Preview 5에서 Model Binder는 IModelBinder 인터페이스를 구현하는 클래스들을 가리킵니다. ASP.NET MVC Preview 5는 기본적으로 DefaultModelBinder 클래스와 DateTimeModelBinder 클래스를 제공합니다. 이 두 클래스의 역할은 각각 다음과 같습니다.

DefaultModelBinder 클래스: RouteData 클래스로부터 라우팅 데이터를 가져옵니다. 만약 라우팅 데이터가 존재하지 않는다면 QueryString과 Form 컬렉션을 통해 필요한 값을 가져옵니다.

DateTimeModelBinder 클래스: DefaultModelBinder 클래스를 상속하며 지정된 값을 DateTime 형식으로 변환하는 역할을 담당합니다.

따라서 IModelBinder 인터페이스를 구현하거나 DefaultModelBinder 클래스를 상속하면 개발자가 직접 Model Binder를 구현할 수 있습니다. 그러면 Model Binder를 어떻게 활용할 수 있는지 한 번 살펴볼까요?

모델 객체 구성하기

예제를 구현하기 위해 우선 간단한 모델 객체를 구현해 보겠습니다. 아래 그림은 BookItem이라는 이름의 모델 객체를 구현하는 코드를 보여줍니다.

그림 1: BookItem 모델 객체를 구현한 코드

img1  

그림에서 보듯이 이 모델 객체는 Title, Author, Publisher, Price 등 4가지 속성을 가진 간단한 엔터티 클래스입니다. 그러면 이제 BookItem 클래스를 처리할 페이지를 하나 만들어 볼까요?

Views 폴더에 Product라는 이름의 폴더를 추가한 후 [MVC View Content Page] 템플릿 형식을 이용하여 Add.aspx 라는 이름의 페이지를 하나 추가하고 아래 그림과 같이 코드를 작성합니다.

그림 2: Add.aspx 템플릿 코드

img2  

그림에서 보듯이 BookItem 클래스의 각 속성에 대응하는 4개의 입력 필드를 추가하고 Submit 버튼을 하나 추가하였습니다. 이제 이 뷰를 렌더링할 컨트롤러의 액션 메서드를 구현해 보겠습니다. Controllers 폴더에 ProductController 클래스를 추가하고 아래 그림과 같이 코드를 작성합니다.

그림 3: ProductController 클래스의 소스 코드

img3  

이제 웹프로젝트를 실행하고 /Product/Add URL을 방문해 보면 아래 그림과 같이 페이지가 나타나는 것을 볼 수 있을 것입니다.

그림 4: /Product/Add 페이지를 방문한 모습

 img4

이제 [제품 등록] 버튼을 클릭했을 때 실행될 액션 메서드를 정의해 보겠습니다. ASP.NET MVC Preview 5버전에서는 드디어 컨트롤러의 액션 메서드에 대한 재정의가 가능해졌습니다. 따라서 아래 그림과 같이 컨트롤러에 액션 메서드를 추가할 수 있습니다.

그림 5: 추가된 액션 메서드의 소스 코드

 img5

그림에서 보듯이 ProductController 클래스는 이제 두 개의 Add 메서드를 가지게 됩니다. 여기서 주목할 것은 AcceptVerbs 특성입니다. 이 특성은 Preview 5에 추가된 것으로 HTTP 동사에 따라서 서로 다른 액션 메서드를 호출할 수 있도록 하기 위해 추가된 것입니다. 그림에서 보듯이 위쪽의 Add 메서드는 /Product/Add URL이 GET 방식으로 호출되었을 때 실행되며 아래쪽의 Add 메서드는 POST 방식으로 호출되었을 때 사용됩니다.

또 다른 주목할 점은 두 번째 Add 메서드의 매개 변수가 BookItem이라는 엔터티 클래스를 직접 사용하고 있다는 점입니다. 이 경우 우리가 알아보고자 하는 Model Binder 클래스의 도움을 받아야 액션 메서드의 매개 변수로 전달된 값들을 BookItem 타입으로 변환할 수 있는 것입니다. 따라서 우선은 Model Binder 클래스를 구현해야겠지요? 프로젝트에 새로운 클래스를 추가하고 아래 그림과 같이 코드를 작성합니다.

그림 6: BookModelBinder 클래스의 소스 코드

 img6

BookModelBinder 클래스가 구현하는 IModelBinder 인터페이스는 GetValue라는 하나의 메서드를 정의하고 있습니다. 바로 이 메서드를 오버라이딩하여 전달된 값들을 모델 객체로 변환하는 작업을 수행해야 합니다. 그림에서 보듯이 Request.Form 컬렉션으로부터 필요한 값을 가져와 BookItem 타입으로 변환하는 과정을 구현하고 있습니다.

BookModelBinder 클래스의 구현을 마쳤으면 이제 이 Model Binder 클래스를 등록해야 비로소 사용이 가능합니다. Model Binder 클래스를 등록하는 방법은 2가지가 존재합니다. 우선 아래 그림과 같이 BookItem 클래스에 ModelBinder 특성을 추가하는 것입니다.

그림 7: ModelBinder 특성이 추가된 BookItem 클래스

img7  

또는 Global.asax.cs 파일에 다음과 같이 코드를 이용하여 Model Binder 클래스를 등록할 수 있습니다.

그림 8: Global.asax.cs 파일에서 Model Binder 클래스를 추가하는 코드

img8  

이제 컨트롤러의 두 번째 Add 메서드가 렌더링 할 List 뷰를 Views/Product 폴더에 추가하고 다음과 같이 코드를 작성합니다.

그림 9: List.aspx 뷰 템플릿 파일의 소스 코드

 img9

웹프로젝트를 컴파일한 후 그림 4의 Product/Add 페이지에서 각 항목에 값을 입력하고 [제품 등록] 버튼을 클릭하면 POST 방식으로 호출되는 두 번째 Add 메서드가 호출되어 아래 그림과 같이 List 뷰를 렌더링하게 됩니다.

그림 10: List뷰가 렌더링 된 모습

img10  

지금까지의 코드를 통해 살펴보았듯이 Model Binder 클래스를 이용하면 컨트롤러의 액션 메서드에서 매번 엔터티 클래스를 생성하는 코드를 작성하는 대신 Model Binder 클래스에서 필요한 작업을 처리할 수 있어 개발 생산성을 높일 수 있게 됩니다.

입력 값에 대한 예외 처리

그런데 그림 4의 화면에서 한 두 가지 항목을 입력하지 않고 버튼을 클릭하면 해당 값이 입력되지 않은 결과를 보게 됩니다. 더군다나 가격의 경우 BookItem 클래스의 Price 속성이 int 타입이기 때문에 아무런 값도 입력하지 않으면 아래 그림과 같이 런타임 에러를 보게 됩니다.

그림 11: 사용자 입력에 대한 유효성 검사를 수행하지 않아 발생하는 런타임 오류

img11  

따라서 BookModelBinder 클래스에서 해당 값들에 대한 유효성 검사를 수행해 주어야 합니다. 앞서 작성한 BookModelBinder.GetValue 메서드를 다음과 같이 수정해 보겠습니다.

그림 12. 수정된 GetValue 메서드

img12  

그림에서 보듯이 GetValue 메서드의 네 번째 매개 변수인 ModelStateDictionary 객체에 AddModelError 메서드를 이용하여 모델 객체의 생성 시 발생한 오류에 대한 정보를 추가할 수 있습니다. 이 ModelStateDictionary 객체는 컨트롤러에서 ViewData.ModelState 속성을 통해 액세스가 가능합니다. 그림 5에서 보듯이 ModelStateDictionary 클래스는 IsValid 속성을 통해 오류 정보가 있는지 없는지를 판단하며 오류가 존재하는 경우 이 속성은 false를 리턴하게 됩니다.

따라서 그림 5의 두 번째 Add 메서드에서는 이 속성 값을 통해 모델 객체의 생성 과정에서 오류가 있었는지를 검사하고 오류가 있었다면 Add 뷰를 다시 렌더링하고 그렇지 않으면 List 뷰를 렌더링하도록 구현 되어졌습니다. 그러면 프로젝트를 다시 빌드하고 /Product/Add 페이지에서 아무런 값도 입력하지 않은 채로 버튼을 클릭해 볼까요? 아마도 아래 그림과 같은 화면을 보시게 될 것입니다.

그림 13: 유효성 검사 코드가 동작한 모습

img13  

앞서와 달리 다시 Add 뷰가 렌더링 된 것을 확인할 수 있습니다. 이 때 각 입력 항목들이 붉은 색으로 변경된 것이 눈에 띄네요. 이는 ModelState 속성에 오류 정보가 존재하는 경우 AddModelError 메서드의 첫 번째 인자로 전달된 것과 동일한 name 특성을 가진 <INPUT>태그에 오류가 발생하였음을 표시하기 위한 CSS 스타일 클래스가 자동으로 지정되기 때문입니다.

렌더링 된 페이지의 소스를 살펴보면 <INPUT>태그에 아래 그림과 같이 "input-validation-error"라는 이름의 CSS 클래스가 지정된 것을 보실 수 있습니다.

그림 14: 렌더링 된 HTML 소스

img14  

그런데 오류 메시지는 전혀 출력이 되지 않는군요. 오류 메시지는 HtmlHelper.ValidationMessage 메서드를 통해 출력해야 하며 따라서 Add.aspx 페이지의 소스가 다음과 같이 변경되어야 합니다.

그림 15: 수정된 Add.aspx 페이지의 소스 코드

img15  

이제 페이지를 다시 실행해 보면 아래 그림과 같이 오류 메시지가 나타나는 것을 볼 수 있을 것입니다.

그림 16: 오류 메시지가 출력된 모습

img16  

이상으로 ASP.NET MVC Preview 5에 새롭게 추가된 Model Binder 클래스 및 관련된 몇 가지 기능에 대해 살펴보았습니다. 궁금하신 내용은 언제든 게시판을 이용해 주세요.

내일부터 추석 연휴로군요. 모두 즐거운 마음으로 뜻깊은 명절 보내시길 바랍니다.

감사합니다.

Posted by 웹지니 트랙백 0 : 댓글 0

이 강좌는 2008년 6월부터 2008년 9월까지 월간 마이크로소프트웨어에 기고했던
ASP.NET MVC 연재의 원고를 그대로 옮긴 것입니다.

그렇기에 평소의 제 문체와 달리 경어를 사용하고 있지 않음을 양해해 주시기 바랍니다.

ASP.NET MVC와 춤을 - ASP.NET MVC Preview 4

월간 마소에 처음 진행하는 연재인데 고생 깨나 하는 것 같다. 첫 연재를 ASP.NET MVC Preview 2 버전으로 시작하자마자 Preview 3가 발표되어 사람 당황스럽게 하더니 마지막 연재를 남겨두고 Preview 4가 등장해 버렸다.

그나마 다행인 것은 원래 이번 연재 순서는 ASP.NET MVC 프레임워크에 대한 못다한 이야기를 풀어보고자 하였는데 Preview 4가 등장하면서 필자가 문제삼고자 했던 부분이 일정 부분 해소되었다는 점이다. 그래서 이번 호에서는 Preview 4에서 달라진 점을 위주로 살펴보도록 하겠다.

연재 순서
ASP.NET MVC 프레임워크 둘러보기
ASP.NET MVC의 URL 라우팅 엔진, 그리고 모델 구성하기
ASP.NET MVC의 뷰와 컨트롤러 구성하기
ASP.NET MVC Preview 4

ASP.NET MVC Preview 4의 새로운 기능들

ASP.NET MVC Preview 4는 마이크로소프트의 오픈소스 프로젝트 서비스인 CodePlex (http://www.codeplex.com/aspnet)를 통해 공개되었으며 MVC 프레임워크뿐만 아니라 ASP.NET AJAX 4.0 등 ASP.NET 4.0 버전에 포함될 것으로 보이는 여러 기능들에 대한 프로젝트가 동시에 진행되고 있음을 확인할 수 있다. 아래 화면 1과 같이 CodePlex의 [Release] 탭을 클릭하면 필요한 설치 파일을 다운로드 할 수 있다.

 img1

화면 1: CodePlex의 ASP.NET MVC 프로젝트 다운로드 페이지

ASP.NET MVC Preview 3 및 그 이전 버전들은 MVC 프레임워크의 완성도를 높이는 방향으로 개발이 진행되어 왔다. 덕분에 이전 버전들은 보안이나 성능 상에 약간의 문제를 노출해 왔으며 역시나 실무에 적용하기에는 다소 무리가 있다는 느낌을 지울 수 없었다.

그러나 Preview 4 버전에서는 이전 버전들이 가지고 있던 보안 상의 이슈를 해결하거나 기타 많은 개발자들에 의해 제공된 피드백을 수렴하기 위한 방향으로 개발이 진행된 느낌을 주고 있다. 이는 프레임워크 자체의 완성도는 일정 수준에 도달했으며 정식 버전의 출시가 머지 않았음을 예감할 수 있는 대목이기도 하다. 지금부터 Preview 4 버전에서의 변화들을 차례로 살펴보도록 하자.

라우팅 규칙에서 네임스페이스 사용

이전 버전의 MVC 프레임워크는 컨트롤러 클래스를 탐색하기 위해 현재 어셈블리에 구현된 모든 타입을 검사했었다. 이는 모든 컨트롤러 클래스가 ASP.NET MVC 웹 애플리케이션의 Controllers 폴더 내에 구현되어 있다는 것을 전제한 것으로 대규모 프로젝트에서 컨트롤러들을 별도의 어셈블리로 구현한 경우에는 어셈블리가 로드되지 않은 경우 컨트롤러 객체를 찾지 못하는 문제가 있었다. 이런 문제를 해결하기 위해 ControllerBuilder 클래스에 컨트롤러 객체를 탐색할 네임스페이스를 지정할 수 있게 되었다. 아래 Global.asax.cs 파일의 예제 코드를 살펴보자.

코드 1: ControllerBuilder 클래스의 DefaultNamespaces 컬렉션에 네임스페이스를 추가하는 예제

protected void Application_Start() {
ControllerBuilder.Current.DefaultNamespaces.Add(“MyWebApps.Weblog.Controllers”);
ControllerBuilder.Current.DefaultNamespaces.Add(“YourApps.Blog.Controllers”);
}

위의 코드는 ControllerBuilder 클래스가 컨트롤러 객체를 탐색할 때 MyWebApps.Weblog.Controllers 네임스페이스와 YourApps.Blog.Controlls 네임스페이스에 구현된 클래스들을 함께 탐색할 수 있도록 하며 이 때 어셈블리가 이 두 네임스페이스가 구현된 어셈블리가 로드되지 않은 경우 해당 어셈블리를 우선 로드한 후 컨트롤러 객체를 탐색하게 된다.

이렇게 추가된 네임스페이스에 대한 참조는 기본적으로 등록된 모든 라우팅 규칙에 대해 이루어지며 다음의 코드를 사용하면 특정 형태의 라우팅 규칙에 대해서만 필요한 어셈블리를 로드할 수 있도록 지정할 수 있다.

코드 2: 라우팅 규칙에 대한 개별적인 네임스페이스 참조를 추가하는 예제

var dataTokens = new RouteValueDictionary();
dataTokens.Add(“namespaces”, new HashSet<string>(
new string[] {
  “MyWebApps.Weblog.Controllers”,
  “YourApps.Blog.Controllers”
}));

routes.Add(
new Route(“{controller}/{action}/{id}”, new MvcRouteHandler()) {
  Defaults = new RouteValueDictionary(
   new { controller = “Home”, action = “index”, id = (string)null}
  ),
  DataTokens = dataTokens
});

아쉽게도 Preview 4 버전에서는 MapRoute 확장 메서드를 이용하여 라우팅 규칙에 대한 네임스페이스 참조를 지정할 수 없으며 RouteCollection.Add 메서드를 통해서만 이와 같은 작업을 수행할 수 있다.

ASP.NET AJAX 지원

ASP.NET MVC Preview 3에서 제공되었던 AjaxHelper 클래스는 ViewContext 속성 외에 어떠한 메서드도 제공하지 않았으며 심지어 확장 메서드도 정의되어 있지 않았다. 그러나 Preview 4에서는 두 가지 확장 메서드를 통해 AJAX 호출을 지원해주고 있다.

우선 ActionLink 메서드는 AJAX 스타일로 특정 컨트롤러의 액션 메서드를 호출해 주는 <A> 태그를 렌더링 해주며 Form 메서드는 역시 AJAX 스타일로 폼을 서버로 전송하기 위한 <FORM> 태그를 렌더링 해준다. 이들 메서드를 사용하기 위해서는 Content 폴더에 새로 추가된 MicrosoftAjax.js파일과 MicrosoftMvcAjax.js 파일을 여러분이 직접 템플릿 페이지에 추가해야 하는 불편이 있다.

그러나 향후 버전에서는 이와 같은 작업도 자동화 할 수 있는 방법이 제공될 것이라 믿는다. 그러면 ASP.NET MVC 웹 애플리케이션 프로젝트를 이용하여 생성한 기본 프로젝트를 이용하여 간략한 예제를 작성해 보자. 우선 Views\Index.aspx 페이지에 아래의 예제 코드를 작성한다.

코드 3: AjaxHelper.ActionLink 메서드를 이용한 AJAX 지원

<script language="javascript" src="/Content/MicrosoftAjax.debug.js"></script>
<script language="javascript" src="/Content/MicrosoftMvcAjax.debug.js"></script>

<span id="dateTime"></span><br />
<% = this.Ajax.ActionLink("Get DateTime", "GetDateTime", new AjaxOptions {UpdateTargetId = "dateTime"}) %>

AJAX 지원을 추가하기 위해서는 코드 3과 같이 두 개의 스크립트 파일을 로드해야 한다. 세 번째 라인의 <SPAN> 태그는 비동기 업데이트를 통해 콘텐츠가 렌더링 될 요소이며 ID 특성에 “dateTime”이라는 값이 지정되어 있다.

다음으로 마지막 라인은 ActionLink 메서드를 이용하여 GetDateTime 액션 메서드를 AJAX 스타일로 호출하는 <A>태그를 렌더링하는 코드이다. 이 때 AjaxOptions 클래스의 UpdateTargetId 속성에 앞서 비동기 업데이트의 대상이 될 <SPAN>태그의 ID 특성 값인 “dateTime” 값을 지정함으로써 GetDateTime 액션 메서드의 호출 결과가 해당 <SPAN>태그에 렌더링 되도록 지정할 수 있다.

예제의 ActionLink 메서드 호출에는 컨트롤러를 지정하지 않았으므로 현재 뷰 파일을 렌더링하는 컨트롤러 객체의 GetDateTime 액션 메서드를 호출하게 된다. 만일 다른 컨트롤러의 액션 메서드를 호출하고자 한다면 ActionLink 메서드의 다른 재정의 버전을 사용하면 된다. HomeController 컨트롤러 클래스에 다음과 같이 GetDateTime 액션 메서드를 구현해 보자.

코드 4: HomeController.GetDateTime 액션 메서드

public ActionResult GetDateTime() {
return Content(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}

이 액션 메서드는 단순히 현재 시간을 리턴하는 역할만 담당하며 별도의 뷰를 렌더링할 필요가 없으므로 Content 메서드를 호출하여 현재 시간을 문자열로 리턴하도록 하였다. 예제를 실행한 결과는 다음과 같다. 화면 2의 [GetDateTime] 링크를 클릭해 보면 현재 시간이 비동기 방식으로 업데이트 되는 것을 확인할 수 있다.

img2  

화면 2: AjaxHelper.ActionLink 메서드 호출을 통해 생성된 <A>태그와 AJAX 호출이 동작한 모습

이 예제를 AjaxHelper.Form 메서드를 이용하는 방식으로 변경하고자 한다면 Index.aspx 페이지의 소스를 아래와 같이 변경하면 된다.

코드 5: AjaxHelper.Form 메서드를 이용한 AJAX 호출의 사용

<script language="javascript" src="/Content/MicrosoftAjax.debug.js"></script>
<script language="javascript" src="/Content/MicrosoftMvcAjax.debug.js"></script>

<span id="dateTime"></span><br />
<% using (this.Ajax.Form("GetDateTime", new AjaxOptions { UpdateTargetId = "dateTime"})) { %>
<input type="submit" value="Get DateTime" />
<% } %>

위의 코드에서 보듯이 AjaxHelper.Form 메서드는 HtmlHelper.Form 메서드와 마찬가지로 <FORM> 태그 내부의 요소들을 정의하기 위해 using 구문을 이용하여 둘러싸 주어야 한다. Form 메서드의 매개 변수는 ActionLink 메서드와 동일하게 구성하면 된다.

캐싱 지원을 위한 OutputCache 액션 필터

ASP.NET의 가장 큰 장점 중 하나는 프레임워크 수준에서 캐싱 기능을 제공한다는 것이었다. 이 캐싱 기능은 나름대로 쓸만한 것이어서 적절하게 활용하면 웹 애플리케이션의 성능을 상당 부분 향상시킬 수 있었다.

ASP.NET MVC Preview 4에서는 OutputCache 액션 필터를 통해 ASP.NET의 캐싱 기능을 컨트롤러 객체에 추가할 수 있게 되었다. 그러면 앞서 작성했던 HomeController.GetDateTime 액션 메서드에 다음과 같이 OutputCache 액션 필터를 적용하여 캐싱 기능을 추가해 보도록 하자.

코드 6: OutputCache 액션 필터를 이용한 캐싱 기능 활성화

[OutputCache(Duration=10)]
public ActionResult GetDateTime() {
return Content(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}

위의 코드는 GetDateTime 액션 메서드의 실행 결과를 10초 간 캐싱하도록 지정하고 있다. 이제 예제 애플리케이션을 다시 실행하고 [Get DateTime] 링크를 계속 클릭해 보면 출력되는 날짜 및 시간이 10초 간격으로 갱신되는 것을 볼 수 있을 것이다.

예외 처리를 위한 HandleError 예외 필터

웹 애플리케이션의 실행 도중 예외가 발생하면 웹 애플리케이션 디자인을 일관되게 유지하면서 사용자에게 보다 친절하게 보일 수 있는 사용자 정의 오류 페이지를 보여주는 것이 더 좋다는 것에 대해서는 아무도 이견을 달지 않을 것이다.

그럼에도 불구하고 이전 버전의 ASP.NET MVC 웹 애플리케이션은 예외가 발생했을 때 ASP.NET 오류 페이지를 보여주었으며 사용자 정의 오류 페이지를 보여주려면 모든 액션 메서드를 try ~ catch 구문으로 감싸고 catch 구문 내에서 오류 페이지를 위한 뷰를 렌더링하는 일종의 편법을 사용하곤 했다.

Preview 4 버전에서는 이런 문제를 해결하기 위해 IExceptionFilter 인터페이스를 통해 예외 필터라는 새로운 종류의 필터를 제공하며 이 인터페이스를 구현하는 HandleError 예외 필터를 제공한다. 이 필터를 이용하면 사용자 정의 오류 페이지를 손쉽게 보여줄 수 있으며 기본적으로 생성된 프로젝트에서 HomeController 컨트롤러 클래스의 소스 파일을 열어보면 HomeController 클래스가 다음과 같이 선언되어 있음을 볼 수 있을 것이다.

코드 7: HomeController 클래스

[HandleError]
public class HomeController : Controller {
...
}

코드에서 보듯이 HandleError 예외 필터는 클래스 또는 메서드 단위로 지정하게 되며 이 예외 필터를 컨트롤러 클래스에 적용하면 구현된 모든 액션 메서드에 대해 예외 처리가 동작한다. 물론 메서드마다 개별적으로 지정하여 특정 메서드만 예외 처리가 적용되도록 구현할 수도 있다.

HandleError 예외 필터가 지정된 액션 메서드들은 실행 도중 예외가 발생하면 지정된 뷰를 렌더링하며 렌더링할 뷰가 지정되지 않은 경우에는 “Error”라는 이름의 뷰를 렌더링하게 된다. HomeController 클래스의 Index 메서드를 다음과 같이 수정하고 웹 애플리케이션을 실행해 보면 HandleError 예외 필터가 어떻게 동작하는지 알 수 있을 것이다.

코드 8: HomeController 클래스의 Index 메서드

public ActionResult Index() {
throw new ApplicationException("애플리케이션 오류입니다.");
}

코드에서 보듯이 이 메서드는 강제로 ApplicationException 예외를 발생시킨다. 웹 애플리케이션을 실행해 보면 아래 화면 3과 같이 예제 웹 애플리케이션과 동일한 UI가 적용된 오류 페이지를 볼 수 있을 것이다.

img3  

화면 3: HandleError 예외 필터가 동작한 모습

기존의 ASP.NET MVC 프레임워크에서 예외가 어떻게 처리되었는지 궁금하다면 앞서 코드 5에서 [HandleError] 특성 선언 부분의 코드를 제거하고 웹 애플리케이션을 다시 실행해 보면 된다. 또한 예외가 발생한 경우 Error 뷰가 아닌 다른 뷰를 렌더링하고자 한다면 HandleError 특성의 생성자를 다음과 같이 수정하면 된다.

코드 9: 별도의 뷰를 지정하는 HandleError 특성

[HandleError(View=”MyError”)]
public ActionResult GetDateTime() {
… 생략 …
}

인증 필터를 이용한 사용자 인증 지원

이전 버전의 ASP.NET MVC 프레임워크를 사용해 보면서 필자가 느꼈던 가장 큰 불편은 특정 액션 메서드를 호출할 때 호출자가 해당 액션 메서드를 호출할 권한이 있는지 여부를 판단할 수 있는 방법이 프레임워크 수준에서 제공되지 않는다는 점이었다. 그리고 사실 이 부분은 Preview 4 버전이 출시되지 않았다면 이번 호 연재에서 가장 큰 비중을 차지하는 내용이 되었을 것이다.

다행스럽게도 Preview 4 버전에서는 인증 필터를 이용하여 액션 메서드를 호출하는 대상의 인증 및 권한 관리가 가능해졌으며 기본적으로 생성되는 프로젝트에는 ASP.NET의 Membership Provider 모델을 기초로 한 AccountController라는 이름의 컨트롤러를 통해 인증 필터를 사용하는 방법을 소개하고 있다.

================================================================
참고
AccountController 컨트롤러가 ASP.NET의 Membership Provider 모델을 기초로 하고 있으며 로그인 페이지나 계정 등록 페이지의 모습이 마치 Login 컨트롤이나 CreateUserWizard 컨트롤을 사용한 것과 거의 유사하여 자칫 Login 컨트롤과 같이 MembershipProvider를 위한 데이터베이스를 자동으로 생성해 줄 것이라 생각하면 곤란하다.

알다시피 ASP.NET MVC 프레임워크는 서버 컨트롤의 사용을 권장하지 않으므로 이 페이지들은 기존의 Login 컨트롤들을 사용한 것이 아니다. 따라서 데이터베이스도 자동으로 생성되지 않으므로 aspnet_regsql.exe 도구를 이용하여 미리 관련된 테이블과 저장 프로시저를 생성한 후 Web.config의 <connectionStrings> 섹션에서 ApplicationServices 항목의 연결 문자열을 적당히 수정해 주어야 한다.

또한 Visual Studio 2008의 [프로젝트 > ASP.NET 구성] 메뉴를 통해 적어도 하나의 사용자와 역할을 생성한 후에야 비로소 Authorize 인증 필터를 원활히 테스트 할 수 있다. Aspnet_regsql.exe 도구와 ASP.NET 구성 도구에 대한 자세한 내용은 MSDN 라이브러리를 참고하기 바란다.
================================================================

ASP.NET MVC Preview 4 버전에서 인증 필터는 IAuthorizationFilter 인터페이스를 통해 구현된다. 즉, FilterAttribute 클래스를 상속하면서 IAuthorizationFilter 인터페이스를 구현하는 클래스들이 바로 인증 필터로서의 역할을 수행하게 되는 것이며 ASP.NET MVC Preview 4에서는 Authorize 인증 필터가 기본적으로 제공된다. AccontController 컨트롤러 클래스의 소스 코드를 열어보면 아래 코드 10과 같이 Authorize 인증 필터가 적용된 ChangePassword 액션 메서드를 볼 수 있다.

코드 10: Authorize 인증 필터가 적용된 ChangePassword 액션 메서드

[Authorize]
public ActionResult ChangePassword(string currentPassword, ...) {
... 생략 …
}

프로젝트를 실행하고 Account 컨트롤러의 ChangePassword 액션 메서드를 호출하는 URL을 브라우저를 통해 탐색해 보면 아래 화면 4와 같이 로그인 페이지로 이동하는 것을 볼 수 있다.

img4  

화면 4: Authorize 인증 필터가 동작한 모습

Authorize 인증 필터는 기본적으로 ASP.NET의 HttpContext.User 속성을 통해 현재 사용자가 인증된 사용자인지 여부를 판단한다. 만일 인증된 사용자이며 AuthroizeAttribute클래스의 Users 속성에 사용자의 이름이 지정된 경우 인증 티켓의 사용자 이름과 Users 속성에 지정된 이름을 비교하여 동일한 이름을 가진 사용자인 경우에 인증된 사용자로 처리한다.

그러므로 Users 속성을 사용하지 않는 코드 10의 경우에는 인증 티켓이 존재하는 경우라면 모두 인증된 사용자로 취급한다. 만일 Windows 모드나 Forms 모드를 통해 인증된 사용자 중 특정 사용자만 호출할 수 있는 액션 메서드를 정의하고 싶다면 다음과 같은 코드를 사용하면 된다.

코드 11: Authorize 인증 필터를 이용하여 사용자 인증

// Windows 인증을 사용하는 경우
[Authorize(Users=“Computer\\Administrator”)]
[Authorize(Users=”Computer\\Administrator,Computer\\webgenie”)]

//Forms 인증을 사용하는 경우
[Authorize(Users=”admin”)]
[Authorize(Users=”admin,webgenie”)]

Authorize 인증 필터는 Roles 속성을 통해 개별 사용자 계정이 아닌 그룹을 통해 권한 관리를 수행하기도 한다. 물론 Users 속성과 Roles 속성을 함께 사용하여 개별 사용자 또는 그룹에 대해 인증을 수행할 수도 있다.

코드 12: Authorize 인증 필터를 이용한 역할 기반 보안

// Windows 인증을 사용하는 경우
[Authorize(Roles=“Computer\\Administrators”)]
[Authorize(Roles=”Computer\\Administrators,Computer\\PowerUsers”)]

//Forms 인증을 사용하는 경우
[Authorize(Roles=”Admins”)]
[Authorize(Roles=”Admins,Writers”)]

앞서 설명한 HandleError 필터와 Authorize 필터는 기존의 액션 필터와는 약간 다르다. 이들은 모두 FilterAttribute 클래스를 상속하고 있으며 IExceptionFilter 인터페이스와 IAuthorizationFilter 인터페이스를 각각 구현하고 있다. 따라서 독자 여러분도 이들 클래스와 인터페이스를 이용하면 예외가 발생했을 때 로그를 쌓거나 혹은 다른 방식으로 사용자 인증을 수행하는 예외 필터와 인증 필터를 구현할 수 있을 것이다.

우선 IExceptionFilter 인터페이스는 OnExeception이라는 이름의 메서드를 정의하고 있다. 이 메서드는 ControllerActionInvoker 클래스가 컨트롤러 클래스의 액션 메서드를 호출하는 도중 예외가 발생했을 때 InvokeExceptionFilters 메서드에 의해 호출된다. 따라서 여러분이 구현하는 사용자 정의 예외 필터 클래스는 OnException 메서드 내에서 필요한 예외 처리를 수행하면 된다.

마찬가지로 IAuthorizationFilter 인터페이스는 OnAuthorize라는 이름의 메서드를 정의하고 있다. 이 메서드 역시 ControllerActionInvoker 클래스의 InvokeAuthorizeFilters 메서드에 의해 호출되며 이 과정은 컨트롤러의 액션 메서드가 호출되기 전에 먼저 수행된다. 따라서 사용자 정의 인증 필터 클래스는 OnAuthorize 메서드를 통해 필요한 인증 절차를 수행하면 된다.

그러면 IExceptionFilter 인터페이스를 구현하는 간단한 로깅 예외 필터를 구현해 보도록 하자. 우선 프로젝트에 FileLogAttribute라는 이름의 클래스를 생성하고 다음과 같이 클래스를 선언하자.

코드 13: FileLogAttribute 클래스의 선언

using System.IO;
using System.Web.Mvc;

public class FileLogAttribute : FilterAttribute, IExceptionFilter {
public void OnException(ExceptionContext context) {
}
}

클래스를 선언했으면 OnException 메서드에 다음과 같이 코드를 작성해 보자.

코드 14: OnException 메서드를 구현하는 코드

string fileName = String.Format("{0}.log", DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"));

using (FileStream file = File.Create(Path.Combine(HttpRuntime.CodegenDir, fileName))) {
using (StreamWriter writer = new StreamWriter(file)) {
  writer.WriteLine(filterContext.Exception.InnerException.Message);
  writer.WriteLine(filterContext.Exception.InnerException.StackTrace);
}
}

코드에서 알 수 있듯이 이 메서드는 현재 날짜와 시간을 이용하여 *.log 파일을 생성하고 이 파일에 예외 정보를 기록한 후 파일을 ASP.NET의 코드 제너레이션 폴더에 저장하는 역할을 담당한다. 코드 제너레이션 폴더를 선택한 이유는 별다른 추가 작업 없이도 ASP.NET 웹 애플리케이션이 파일을 쓸 수 있는 유일한 공간이기 때문이다. FileLogAttribute 클래스의 구현을 마쳤으면 HomeController 클래스의 Index 메서드를 다음과 같이 수정해 보자.

코드 15: HomeController.Index 메서드

[FileLog]
public ActionResult Index() {
throw new ApplicationException(“애플리케이션 예외가 발생했습니다.”);
}

이제 예제를 실행해 보면 앞서 화면 3과 같이 HandleError 예외 필터에 의한 사용자 정의 오류 페이지가 보여질 것이다. 그러나 ASP.NET의 코드 제너레이션 폴더를 Windows 탐색기로 탐색해 보면 20080813_232245_926.log과 같은 형식의 로그 파일이 생성된 것을 볼 수 있으며 이 파일을 메모장으로 열어보면 화면 4와 같이 예외 정보가 기록되어 있음을 볼 수 있을 것이다.

img5  

화면 5: 생성된 로그 파일

ViewData와 TempData

ASP.NET MVC 프레임워크는 컨트롤러와 뷰 사이의 데이터 교환을 위해 ViewDataDictionary 클래스를 사용하며 이 클래스는 ViewPage 클래스와 Controller 클래스에 각각 ViewData라는 이름의 속성으로 제공된다. Controller 클래스의 ViewData 속성에 보관된 데이터는 컨트롤러가 지정된 뷰를 렌더링할 때 ViewPage 클래스의 ViewData 속성으로 옮겨져 결과적으로 뷰 템플릿에서 컨트롤러가 전달한 뷰 데이터에 액세스가 가능해진다.

이와 같은 동작에서 알 수 있듯이 ViewData 속성에 보관된 값들은 각각의 요청에 대해 개별적으로 동작한다. 즉 ViewData 속성은 어떤 하나의 요청을 처리하는 컨트롤러와 뷰 사이에 데이터를 교환할 때 사용할 수 있지만 요청과 요청 사이의 데이터 교환에는 사용할 수 없다.

그렇다면 현재 컨트롤러가 생성한 데이터를 다른 요청과 공유해야 할 일이 과연 있을까? 물론 있다. 앞서 살펴보았듯이 컨트롤러 클래스는 지정된 다른 액션 메서드를 호출하는 RedirectToAction 메서드와 특정 라우팅 규칙과 일치하는 요청을 수행하는 RedirectToRoute 메서드 등을 제공하기 때문이다. 이를 위해 ASP.NET MVC 프레임워크는 Controller, ViewPage, ViewUserControl, ViewMasterPage 등의 클래스에 TempData라는 속성을 제공한다.

TempData 속성은 TempDataDictionary 클래스로 구현되어 있으며 ASP.NET MVC 프레임워크는 이 임시 데이터를 처리하기 위해 ITempDataProvider라는 인터페이스를 구현하고 있다. 현재 ASP.NET MVC Preview 4 버전에서는 SessionStateTempDataProvider 클래스가 구현되어 있으며 이 클래스는 세션 객체를 통해 여러 요청 간에 데이터를 공유할 수 있도록 지원하고 있다.

또한 기본적으로 TempDataProvider 객체는 ITempDataProvider 인터페이스를 구현함으로써 정의할 수 있기 때문에 이 인터페이스를 구현하는 클래스를 여러분이 직접 구현한다면 세션이 아닌 다른 저장소에 임시 데이터를 공유하는 기능을 웹 애플리케이션에 추가할 수도 있다.

이상으로 총 4회에 걸쳐 ASP.NET MVC 프레임워크에 대해 살펴보았다. 연재가 진행되는 동안 무려 두 번이나 새로운 버전이 출시되는 해프닝 덕분에 글을 쓰는 필자는 물론 글을 읽는 독자도 상당히 혼란스러웠으리라 예상한다. 당초 계획과는 조금 다른 방향으로 연재가 마무리 되어 아쉬움도 적지 않으나 필자의 웹사이트를 통해 더 많은 독자들과 ASP.NET MVC 프레임워크에 대한 정보를 지속적으로 교환할 수 있게 되기를 기대하며 연재를 마친다.

Posted by 웹지니 트랙백 0 : 댓글 0

이 강좌는 2008년 6월부터 2008년 9월까지 월간 마이크로소프트웨어에 기고했던
ASP.NET MVC 연재의 원고를 그대로 옮긴 것입니다.

그렇기에 평소의 제 문체와 달리 경어를 사용하고 있지 않음을 양해해 주시기 바랍니다.

ASP.NET MVC와 춤을 - ASP.NET MVC의 뷰와 컨트롤러 구성하기


MVC 패턴의 핵심은 인바운드 요청을 처리할 적절한 컨트롤러 선택하여 정의된 비즈니스 로직을 실행하는 것이라 할 수 있다. ASP.NET MVC 프레임워크에서 컨트롤러의 선택은 지난 호에서 살펴 본 라우팅 엔진에 의해 처리되며 라우팅 엔진에 의해 선택된 컨트롤러는 데이터 모델을 이용하여 필요한 비즈니스 로직을 실행하고 그에 따른 결과를 뷰를 통해 사용자에게 보여주는 역할을 수행한다. 이번 호에서는 컨트롤러와 뷰의 구현에 대해 알아보도록 하자. 

연재 순서
ASP.NET MVC 프레임워크 둘러보기
ASP.NET MVC의 URL 라우팅 엔진, 그리고 모델 구성하기
ASP.NET MVC의 뷰와 컨트롤러 구성하기
ASP.NET MVC에 대한 못다한 이야기들

ASP.NET MVC 프레임워크에 적용된 Front Controller 패턴에서 Front Controller는 두 가지 컴포넌트로 구성된다. 첫 번째 컴포넌트는 요청을 분석하여 어떤 비즈니스 로직을 수행해야 하는지를 판단하는 핸들러이며 두 번째 컴포넌트는 핸들러에 의해 지정된 비즈니스 로직을 수행하는 컨트롤러이다. ASP.NET MVC 프레임워크에서 핸들러는 라우팅 엔진에 의해 구현되어 있으므로 우리는 실제로 비즈니스 로직을 수행하는 컨트롤러 컴포넌트만 구현하면 된다.

ASP.NET MVC의 컨트롤러

ASP.NET MVC 프레임워크는 컨트롤러의 기본 동작을 정의한 Controller 기반 클래스를 제공한다. 따라서 컨트롤러의 역할을 수행할 모든 클래스는 이 Controller 기반 클래스를 상속해야 하며 라우팅 엔진은 Controller 클래스를 상속한 클래스들 중 HTTP 요청에 관련된 컨트롤러의 이름을 가지고 있는 컨트롤러 클래스를 선택하게 된다. 그렇다면 컨트롤러의 내부적인 구현에 대해 간략히 살펴보도록 하자.

컨트롤러의 내부 구현

Controller 클래스에 정의된 메서드 중 가장 중요한 역할을 담당하는 메서드는 컨트롤러 클래스의 액션 메서드를 실행하는 Execute 메서드이다. 이 메서드는 IController 인터페이스에 정의되어 Controller 클래스가 구현하고 있으며 메서드의 시그너처는 다음과 같다.

protected internal virtual void Execute(ControllerContext controllerContext);

이 메서드는 ASP.NET MVC 프레임워크 내에서 HTTP 요청을 처리하기 위해 구현한 MvcHandler 클래스에 의해 호출된다. MvcHandler 클래스는 요청 URL을 분석하여 라우팅 규칙에 의해 지정된 컨트롤러의 이름을 알아낸 뒤 ControllerFactory 클래스를 이용하여 해당 컨트롤러의 인스턴스를 생성하고 이 컨트롤러 인스턴스의 Execute 메서드를 호출한다. Execute 메서드는 컨트롤러가 노출한 public 메서드 중 지정된 이름의 액션 메서드가 존재하면 이 메서드를 호출하게 된다.

Execute 메서드의 시그너처에서 알 수 있듯이 이 메서드는 virtual 메서드이므로 상속 클래스에서 재정의가 가능하다. 따라서 이 메서드가 매개 변수로 사용하는 ControllerContext 클래스에 대해서도 알아둘 필요가 있겠다. ControllerContext 클래스는 RequestContext 클래스를 상속하는 클래스이며 현재 실행 중인 액션 메서드를 정의하고 있는 컨트롤러와 그 컨트롤러를 요청한 HTTP 요청, 그리고 라우팅 데이터에 대한 액세스를 제공하는 역할을 담당한다. ControllerContext 클래스는 다음과 같은 속성을 제공한다.

속성

설명
Controller IController 타입을 사용하며 현재 동작 중인 컨트롤러 클래스에 대한 참조를 리턴한다.
HttpContext RequestContext 클래스에서 상속되었으며 현재 HTTP 요청에 대한 액세스를 제공하는 HttpContextBase 클래스에 대한 참조를 리턴한다.
RouteData 라우팅 엔진에 의해 분석된 라우트 데이터를 취급하는 RouteData 클래스에 대한 참조를 리턴한다. RouteData 클래스는 System.Web.Routing 어셈블리에 구현되어 있다.

HttpContext 속성과 RouteData 속성은 Controller 클래스 내에도 구현되어 있으므로 언제든지 활용할 수 있다. 뿐만 아니라 Page 클래스와 마찬가지로 Session, Request, Response 등 HTTP 요청을 처리하는데 필요한 속성들은 모두 제공하고 있다.

컨트롤러 클래스를 구현할 때 주의할 점은 모든 컨트롤러 클래스의 이름은 반드시 Controller라는 접미사를 가져야 한다는 것이다. 만일 Controller 접미사를 붙이지 않으면 라우팅 엔진이 컨트롤러를 올바르게 선택하지 못한다.

ASP.NET MVC Preview 3에서 변경된 사항들

컨트롤러의 내부적인 구현 방법에 대해 이해했다면 이제 컨트롤러를 실제로 구현해 볼 차례이다. 그러나 그에 앞서 Preview 3 버전에서 컨트롤러에 어떤 변화가 있었는지를 먼저 살펴보는 것이 순서일 것이다. 기존의 Preview 2 버전에서는 컨트롤러 클래스에 정의된 void 타입을 리턴하는 public 메서드가 액션 메서드로 사용되었다.

이와 같은 조건은 Preview 3 버전에서도 동일하다. 그러나 Preview 3 버전은 이와 별도로 ActionResult 추상 클래스를 정의하고 있으며 액션 메서드는 자신의 실행 결과를 알리기 위해 이 ActionResult 클래스의 인스턴스를 리턴할 수 있게 되었다. ActionResult 추상 클래스를 상속하여 구현된 클래스와 그 역할은 다음 표에 나타나 있다.

타입

설명
ContentResult HTTP 응답 스트림에 지정된 텍스트를 출력한다.
EmptyResult 아무런 작업도 수행하지 않으며 액션 메서드가 null을 리턴하는 경우 반드시 이 타입을 리턴 타입으로 사용해야 한다.
JsonResult 지정된 ViewData 객체를 JSON 문자열로 직렬화한다.
RedirectResult 지정된 URL HTTP 리다이렉션 동작을 수행한다.
RedirectToRouteResult 라우팅 엔진을 이용하여 지정된 경로로 리다이렉션을 수행한다.
ViewResult 현재 액션 메서드와 관련된 뷰를 응답 스트림에 렌더링한다.

또한 컨트롤러 클래스가 위의 ActionResult 타입을 정확히 리턴할 수 있도록 지원하기 위해 아래 표2에 나열한 메서드들이 Controller 클래스에 추가되었다.

메서드

관련된 ActionResult 타입 설명
Content ContentResult HTTP 응답 스트림에 지정된 텍스트를 출력하고 ContentResult 객체를 리턴한다.
Json JsonResult JsonResult 객체를 리턴한다.
Redirect RedirectResult 지정된 URL로 리다이렉션을 수행하고 RedirectResult 객체를 리턴한다.
RedirectToAction RedirectToRouteResult 지정된 컨트롤러의 액션 메서드를 호출하고 RedirectToRouteResult 객체를 리턴한다.
RedirectToRoute RedirectToRouteResult 지정된 라우팅 URL로 리다이렉션을 수행하고 RedirectToRouteResult 객체를 리턴한다.
View ViewResult ViewResult 객체를 리턴한다.

만일 액션 메서드가 null을 리턴하거나 리턴 타입이 void로 선언된다면 액션 메서드는 묵시적으로 EmptyResult 객체를 리턴하게 된다. 또한 ActionResult 타입이 아닌 다른 객체를 리턴한다면 해당 객체의 ToString(CultureInfo.InvariantCulture) 메서드를 실행한 결과를 ContentResult 클래스로 래핑하여 리턴하게 된다.

마지막으로 한 가지 주의할 점은 View 메서드를 제외하고는 어떤 메서드도 뷰를 렌더링하지 않는다는 점이다. 예를 들어 Content 메서드의 경우 액션 메서드에 해당하는 뷰를 렌더링하면서 Content 메서드에 지정된 문자열을 추가로 출력하는 것이라고 오해할 수 있다. 그러나 실제로는 뷰의 렌더링은 생략한 채 지정된 문자열만 응답 스트림에 출력한다.

그러면 이제 코드를 비교해 보도록 하자. 기존의 Preview 2 버전에서 컨트롤러의 액션 메서드는 다음과 유사한 형태로 구현했었다.

코드 1: Preview 2 버전에서의 액션 메서드 예제

public void Hello() {
RenderView(“Hello”, “안녕하세요? 웹지니입니다.”);
}

이 코드는 Preview 2 버전의 Controller 클래스에 정의된 RenderView 메서드를 이용하여 Hello 뷰를 렌더링하면서 “안녕하세요? 웹지니입니다.”라는 문자열을 뷰에 전달하도록 구현된 액션 메서드이다. 이제 이와 같은 메서드를 Preview 3 버전에서는 다음과 같이 구현해야 한다.

코드 2: Preview 3 버전에서의 액션 메서드 예제
public ActionResult Hello() {
ViewData[“SayHello”] = “안녕하세요? 웹지니입니다.”;
return View();
}

이 예제를 다음과 같이 구현하지 않는 것에 대해 의아해 하는 독자가 있을지도 모르겠다.

코드 3: 의도를 충분히 반영하지 못한 액션 메서드 예제
public ActionResult Hello() {
return Content(“안녕하세요? 웹지니입니다.”);
}

앞서 설명했지만 View 메서드를 제외한 나머지 메서드는 액션 메서드와 관련된 뷰를 렌더링하지 않는다. 그러나 Preview 2 버전의 RenderView 메서드는 뷰를 렌더링하면서 뷰에 추가 데이터를 전달하는 용도로 사용된 것이므로 RenderView 메서드와 동일한 동작을 수행하려면 View 메서드를 호출하되 뷰에 추가로 전달할 값들은 View 메서드를 호출하기 전에 ViewData 속성에 미리 대입해 두어야 한다.

BookStore 예제를 위한 컨트롤러 클래스 구현하기

그러면 지난 호에서 일부 구현했던 BookStore 예제를 위한 컨트롤러 클래스를 구현해 보도록 하자. 이 예제는 지난 호에 구현했던 BookStore 데이터베이스와 이 데이터베이스를 토대로 LINK to SQL 클래스로 구현한 BookStore 데이터 모델 클래스를 그대로 활용한다. 우리가 구현하고자 하는 예제 애플리케이션은 데이터베이스에 저장된 도서 목록을 보여주는 기능과 해당 도서의 상세 정보를 보여주는 기능, 마지막으로 검색 시 검색된 도서의 목록을 보여주는 기능 등 세 가지 주요 기능을 제공한다.

라우팅 규칙 설정하기

가장 먼저 해야 할 일은 예제 애플리케이션을 위한 라우팅 규칙을 설정하는 작업이다. Global.asax.cs 파일을 열고 다음과 같이 라우팅 규칙을 설정하는 코드를 작성해 보자.

코드 4: 라우팅 규칙

routes.MapRoute(
"Default",
"Books/{action}",
new { controller = "Books", action = "List" }
);

코드 4에서 정의한 라우팅 규칙은 ASP.NET MVC Web Application 프로젝트가 기본적으로 설정해 둔 라우팅 규칙과는 조금 다르다. 우선 이 라우팅 규칙은 “Books” 컨트롤러만을 사용하도록 설정되어 있으며 action 메서드는 URL을 파싱하여 얻어낸다. 기본 라우팅 규칙에서 사용하던 {id} 부분은 생략된 상태이다.

다음으로 Default.aspx.cs 파일을 다음과 같이 수정하여 예제 애플리케이션을 실행했을 때 도서 목록을 보여주는 뷰가 기본적으로 나타나도록 한다.

코드 5: Default.aspx.cs 파일에 기본 페이지 경로를 지정하는 코드

public void Page_Load(object sender, System.EventArgs e) {
Response.Redirect("~/Books/List");
}

원래 ASP.NET MVC Web Application 프로젝트가 기본적으로 사용하는 경로는 ~/Home이었으나 예제의 실행을 위해 코드 5와 같이 코드를 수정하였다.

컨트롤러 클래스의 구현

이제 컨트롤러 클래스를 구현해 볼 차례이다. Visual Studio 2008의 솔루션 탐색기에서 [Controller] 폴더를 마우스 오른쪽 버튼으로 클릭하고 [추가 > 새 항목] 메뉴를 클릭하면 아래 화면 1과 같이 [새 항목 추가] 대화 상자가 나타난다.

img1  

화면 1: 새 항목 추가 대화 상자

화면 1의 대화 상자에서 [MVC Controller Class] 템플릿을 선택하고 이름을 [BooksController.cs]로 지정한 후 [확인] 버튼을 클릭하여 새로운 컨트롤러 클래스를 프로젝트에 추가한다. 컨트롤러 클래스를 새로 추가하면 Index 라는 이름의 액션 메서드를 가진 컨트롤러의 기본 뼈대가 제공된다. 이 코드를 모두 삭제하고 우리가 사용할 BookStoreDataContext 클래스에 조금 더 쉽게 액세스 하기 위한 멤버 변수를 가지도록 다음과 같이 뼈대 코드를 작성해 보자.

코드 6: BooksController 클래스의 뼈대 코드

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers {
public class BooksController : Controller {
  private BookStoreDataContext dataContext =
   new BookStoreDataContext();
}
}

코드 6에서 굵게 표시된 코드가 우리가 추가해야 할 코드이다. 우선 BookStoreDataContext 클래스를 액세스하기 위해서는 MvcApplication1.Models 네임스페이스에 대한 참조가 필요하며 이 클래스의 인스턴스를 private 멤버로 추가하여 모든 액션 메서드가 쉽게 이 인스턴스에 접근할 수 있도록 구현하였다.

이제 컨트롤러에 액션 메서드를 추가해 보도록 하자. 조금 전 추가한 BooksController 컨트롤러는 List, Detail, Search 등 세 개의 액션 메서드를 제공할 것이다. 우선 도서 목록을 가져오는 List 액션 메서드를 구현하는 코드는 아래와 같다.

코드 7: List 액션 메서드

public ActionResult List() {
ViewData["TotalItems"] = this.dataContext.Books.Count();
ViewData["Data"] = this.dataContext.Books.ToList<Books>();

return View("List");
}

코드 7에서 보듯이 이 메서드는 List라는 이름의 뷰를 렌더링한다. 물론 그 전에 BookStoreDataContext 클래스의 Books 컬렉션에 저장된 도서 목록을 List<Books> 타입으로 변환하여 컨트롤러의 ViewData 속성에 “Data”라는 이름으로 추가하는 것을 볼 수 있다. ViewData 속성은 컨트롤러에서 뷰로 전달할 데이터를 보관하기 위한 속성이며 ViewDataDictionary 타입을 사용한다.

다음으로 구현할 메서드는 나열된 도서 중 하나를 선택했을 때 해당 도서의 상세 정보를 보여주기 위한 뷰를 렌더링하는 Detail 액션 메서드이다. 이 메서드를 구현하는 코드는 다음과 같다.

코드 8: Detail 액션 메서드

public ActionResult Detail(int id) {
ViewData["Data"] = this.dataContext.Books.Where(
  c => c.BookID == id
).ToList<Books>();

return View("Detail");
}

이 메서드는 int형의 id라는 매개 변수를 필요로 하며 이 값은 뷰에서 전달될 것이다. 특정 도서가 선택된 경우 보여질 뷰는 선택된 도서 하나에 대한 데이터만 보여주게 되므로 BookStoreDataContext.Books 컬렉션의 Where 메서드를 이용하여 매개 변수로 전달된 ID를 가지는 도서 하나만을 검색한 후 이것을 List<Books> 타입으로 변환하여 ViewData에 추가한 후 Detail 뷰를 렌더링한다.

마지막으로 검색에 사용될 액션 메서드인 Search 메서드는 다음과 같이 구현한다.

코드 9: Search 액션 메서드

public ActionResult Search(string keyword) {
List<Books> searchResult = this.dataContext.Books.Where(
  c => c.BookTitle.Contains(keyword)
).ToList<Books>();

ViewData["TotalItems"] = this.dataContext.Books.Count();
ViewData["SearchedItems"] = searchResult.Count;
ViewData["Data"] = searchResult;

return View("Search");
}

Search 메서드는 우리가 구현하는 액션 메서드 중 가장 높은 난이도(?)를 자랑한다. 우선 이 메서드는 keyword라는 이름의 매개 변수를 사용하며 이 매개 변수에는 사용자 검색을 위해 입력한 도서의 제목이 전달된다.

이 값은 BookStoreDataContext.Books 컬렉션의 Where 메서드에 전달되어 도서의 제목을 저장하는 속성인 BookTitle 속성 값과의 비교를 위해 사용된다. 여기서 BookTitle.Contains 메서드는 SQL의 LIKE ‘%keyword%’와 동일한 검색을 수행하기 위해 사용되었다. 만일 LIKE ‘keyword%’나 LIKE ‘%keyword’ 검색과 동일한 결과를 보여주고 싶다면 StartsWith나 EndsWith와 같은 메서드를 사용하면 되겠다.

검색 결과를 얻어온 후에는 TotalItems와 SearchedItems 항목에 각각 전체 데이터의 개수와 검색된 데이터의 개수를 저장하고 검색 결과를 Data 항목에 저장한 후 Search 뷰를 렌더링하는 것으로 이 메서드의 구현을 마무리한다

이상으로 컨트롤러 클래스의 구현과 컨트롤러에서 뷰로의 데이터 전달에 대해 알아보았다. 이제는 컨트롤러의 액션 메서드가 렌더링 할 뷰를 구현할 차례이다.

BookStore 예제 애플리케이션을 위한 뷰 구현하기

지금까지 구현했던 List, Detail 그리고 Search 액션 메서드는 각각 동일한 이름의 뷰를 렌더링한다. 따라서 우리는 프로젝트에 List.aspx, Detail.aspx 그리고 Search.aspx 등 세 개의 뷰 파일을 추가해야 한다. 우선 [Views] 폴더에 [Books]라는 이름의 새 폴더를 추가하고 아래 화면 2와 같이 [새 항목 추가] 대화 상자에서 [MVC View Content Page] 템플릿을 이용하여 List.aspx라는 이름의 뷰 템플릿 파일을 추가한다.

img2  

화면 2: List.aspx 뷰 템플릿 페이지를 추가하는 모습

List 뷰의 역할은 전체 도서의 목록과 현재 저장된 도서의 개수를 표시하는 텍스트를 출력하는 것이다. List.aspx 페이지의 마크업은 아래 코드 10과 같이 구현된다.

코드 10: List.aspx 페이지의 마크업 소스
<div class="info">
총 <% = ViewData["TotalItems"] %>개의 도서가 준비되어 있습니다.
</div>
<asp:Repeater ID="booksRepeater" runat="server">
<HeaderTemplate>
  <table border="0" cellpadding="5" cellspacing="0">
</HeaderTemplate>
<ItemTemplate>
   <tr>
    <td>
     <%# Html.Image("~/Content/BooksImages/" + Eval("BookImageUrl").ToString(), new { width = 72 })%>
    </td>
    <td>
     <b><%# Html.ActionLink(Eval("BookTitle").ToString(), "Detail", new { id = (int)Eval("BookID") }) %></b>
     <br /><br />
     저자: <%# Eval("Author") %><br />
     출판사: <%# Eval("Publisher") %><br />
     출간일: <%# Convert.ToDateTime(Eval("DatePublished")).ToString("yyyy년 MM월 dd일") %><br />
     가격: <%# Convert.ToInt32(Eval("Price")).ToString("n0") %>원
    </td>
   </tr>
</ItemTemplate>
<FooterTemplate>
  </table>
</FooterTemplate>
</asp:Repeater>

지난 6월호에서 소개했던 것과 같이 ASP.NET MVC 프레임워크를 사용하더라도 Repeater와 같은 기존의 바인딩 컨트롤들은 여전히 사용이 가능하며 필자는 이 방법을 더 선호하는 편이다. 페이지 상단의 <DIV> 태그에서 사용한 ViewData[“TotalItems”] 값은 BooksController의 List 액션 메서드에서 추가된 데이터이며 Repeater 컨트롤은 ViewData[“Data”] 값을 사용한다. 이 값을 Repeater 컨트롤에 바인딩 하기 위해서는 List.aspx.cs 파일에 다음과 같은 코드를 추가해야 한다.

코드 11: List 뷰 페이지에 컨트롤러에서 전달된 데이터를 바인딩하는 코드

protected override void OnLoad(EventArgs e) {
base.OnLoad(e);

this.booksRepeater.DataSource = this.ViewData["Data"];
this.booksRepeater.DataBind();
}

코드 11에서 보다시피 OnLoad 메서드에서는 ViewData[“Data”] 항목에 저장된 List<Books> 타입을 Repeater 컨트롤의 DataSource 속성에 대입하고 DataBind() 메서드를 호출한다. 이 뷰가 렌더링된 결과는 아래 화면 3과 같다.

img3  

화면 3: List 뷰가 렌더링 된 모습

Repeater 컨트롤 내에서는 몇 가지 새로운 메서드가 사용되었는데 바로 Html.Image 메서드와 Html.ActionLink 메서드이다. Html 속성은 여러 가지 HTML 요소들을 렌더링하기 위해 지원되는 HtmlHelper 클래스를 사용한다. 우선 Image 메서드는 <IMG>태그를 렌더링하기 위한 메서드이며 메서드의 시그너처는 다음과 같다.

string Image(string imgRelativeUrl);
string Image(string imgRelativeUrl, object htmlAttributes);
string Image(string imgRelativeUrl, string alt);
string Image(string imgRelativeUrl, string alt, object htmlAttributes);

메서드의 시그너처에서 알 수 있듯이 imgRelativeUrl 매개 변수는 이미지의 상대 경로를 의미하며 alt는 이미지의 대체 문자열을 의미한다. 마지막으로 htmlAttributes는 <IMG>태그에 추가로 적용될 HTML 특성과 그 값을 새로운 객체 형태로 지정하면 된다. 예제의 Html.Image 메서드는 두 번째 재정의 형식의 메서드를 호출한 것이다.

ActionLink 메서드는 <A> 태그를 렌더링하며 지정된 액션 메서드를 호출하는 URL을 href 특성에 지정한다. 이 메서드의 시그너처는 다음과 같다.

string ActionLink(string linkText, string actionName);
string ActionLink(System.Linq.Expressions.Expression<Action<T>> action, string linkText);
string ActionLink(string linkText, string actionName, object values);
string ActionLink(string linkText, string actionName, string controllerName);
string ActionLink(string linkText, string actionName, RouteValueDictionary valuesDictionary);
string ActionLink(System.Linq.Expressions.Expression<Action<T>> action, string linkText, object htmlAttributes);
string ActionLink(string linkText, string actionName, string controllerName, RoutValueDictionary valuesDictionary);

예제의 Repeater 컨트롤에서는 세 번째 재정의 형식의 메서드가 사용되었으며 HtmlHelper 클래스는 이 두 가지 메서드 외에도 HTML 요소를 렌더링 하기 위한 다음과 같은 메서드들을 제공한다.

메서드

설명
ActionLink 지정된 액션 메서드를 호출하는 <A>태그를 렌더링한다.
Button <BUTTON> 태그를 렌더링한다.
CheckBox <INPUT type=”checkbox”> 태그를 렌더링한다.
DropDownList 드롭다운 리스트를 표시하는 <SELECT> 태그를 렌더링한다.
Form <FORM> 태그를 렌더링한다.
Hidden <INPUT type=”hidden”> 태그를 렌더링한다.
ListBox 리스트 박스를 표시하는 <SELECT> 태그를 렌더링한다.
MailTo mailto: 프로토콜을 사용하는 <A>태그를 렌더링한다.
NavigationButton <INPUT type=”button”>태그를 렌더링하며 클릭하면 지정된 URL로 리다이렉션된다.
Password <INPUT type=”password”> 태그를 렌더링한다.
RadioButton <INPUT type=”radio”> 태그를 렌더링한다.
RadioButtonList 라디오 버튼의 그룹을 렌더링한다.
RenderUserControl 지정된 사용자 컨트롤 (*.ascx)을 렌더링 한다.
RouteLink Global.asax.cs 파일에서 라우팅 엔진에 추가한 라우팅 규칙의 이름을 이용하여 해당 규칙에 적합한 URL을 가진 <A>태그를 렌더링한다.
SubmitButton <INPUT type=”submit”> 태그를 렌더링한다.
SubmitImage <INPUT type=”image”> 태그를 렌더링한다.
TextArea <TEXTAERA> 태그를 렌더링한다.
TextBox <INPUT type=”text”> 태그를 렌더링한다.

다음으로 화면 3의 도서 목록에서 제목을 클릭했을 때 나타날 Detail 뷰를 위한 템플릿 파일을 구현해 보자. 이 파일의 마크업 코드는 다음과 같다.

코드 12: Detail 뷰의 마크업 코드

<div class="info" style="margin-bottom:10px;">
<% = this.Data.BookTitle %>
</div>
<div style="margin-bottom:10px;"><% = Html.ActionLink("목록으로 이동", "List") %></div>
<table border="0" cellpadding="5" cellspacing="0">
<tr>
  <td rowspan="5"><% = Html.Image("~/Content/BooksImages/" + this.Data.BookImageUrl) %></td>
  <td>제목:</td>
  <td><% = this.Data.BookTitle %></td>
</tr>
<tr>
  <td>저자:</td>
  <td><% = this.Data.Author %></td>
</tr>
<tr>
  <td>출판사:</td>
  <td><% = this.Data.Publisher %></td>
</tr>
<tr>
  <td>출간일:</td>
  <td><% = this.Data.DatePublished.ToString("yyyy년 MM월 dd일")%></td>
</tr>
<tr>
  <td>가격:</td>
  <td><% = this.Data.Price.ToString("n0") %> 원</td>
</tr>
</table>

이 뷰는 선택된 하나의 도서 정보만을 렌더링한다. 따라서 데이터 바인딩은 필요치 않으며 Data라는 이름의 속성을 정의하여 이 속성을 통해 필요한 데이터에 액세스하고 있다. 이 Data 속성은 Detail.aspx.cs 파일에 다음과 같이 구현된다.

코드 13: Detail.aspx.cs 파일의 소스

private MvcApplication1.Models.Books data;

protected override void OnLoad(EventArgs e) {
base.OnLoad(e);

List<MvcApplication1.Models.Books> viewData =
  ViewData["Data"] as List<MvcApplication1.Models.Books>;
this.data = viewData[0];
}

public MvcApplication1.Models.Books Data {
get { return this.data; }
}

OnLoad 이벤트 핸들러에서 알 수 있듯이 ViewData[“Data”] 속성이 리턴하는 값을 List<MvcApplication1.Models.Books> 타입으로 변환하고 이 리스트의 첫 번째 아이템을 Data 속성을 위한 멤버 변수에 대입하여 뷰에서 활용할 수 있도록 하였다. 이 뷰가 렌더링 된 모습은 아래 화면 4와 같다.

 img4

화면 4: Detail 뷰가 렌더링 된 모습

마지막으로 검색을 시도했을 때 검색된 결과만이 출력되는 Search 뷰를 구현해 보자. Search 뷰의 마크업 코드는 List뷰의 마크업 코드와 상당부분 유사하다.

코드 14: Search 뷰의 마크업 코드
<div class="info">
총 <% = ViewData["TotalItems"] %>개의 도서 중 <% = ViewData["SearchedItems"] %>개가 검색되었습니다.
</div>
<asp:Repeater ID="booksRepeater" runat="server">
<HeaderTemplate>
  <table border="0" cellpadding="5" cellspacing="0">
</HeaderTemplate>
<ItemTemplate>
   <tr>
    <td>
     <%# Html.Image("~/Content/BooksImages/" + Eval("BookImageUrl").ToString(), new { width = 72 })%>
    </td>
    <td>
     <b><%# Html.ActionLink(Eval("BookTitle").ToString(), "Detail", new { id = (int)Eval("BookID") }) %></b>
     <br /><br />
     출판사: <%# Eval("Publisher") %><br />
     출간일: <%# Convert.ToDateTime(Eval("DatePublished")).ToString("yyyy년 MM월 dd일") %><br />
    </td>
   </tr>
</ItemTemplate>
<FooterTemplate>
  </table>
</FooterTemplate>
</asp:Repeater>

Repeater 컨트롤의 마크업 코드는 List 뷰와 동일하며 위쪽의 <DIV>태그에서는 ViewData[“Totalitems”] 값과 ViewData[“SearchedItems”] 값을 이용하여 문자열을 생성해 내는 것만 다르다. 물론 Search.aspx.cs 파일의 소스도 아래에서 보듯이 List 뷰의 코드와 동일하다.

코드 15: Search.aspx.cs 파일의 소스 코드

protected override void OnLoad(EventArgs e) {
base.OnLoad(e);

this.booksRepeater.DataSource = this.ViewData["data"];
this.booksRepeater.DataBind();
}

Search 뷰가 렌더링 모습은 아래 화면 5와 같다.

 img5

화면 5: Search 뷰가 렌더링 된 모습

화면을 통해 실행된 예제의 모습을 통해 짐작했겠지만 이 예제의 UI는 ASP.NET MVC Web Application 프로젝트가 제공하는 마스터 페이지와 CSS 스타일을 그대로 이용하였다. 다만 마스터 페이지의 소스 코드가 조금 바뀌었는데 이는 기본 제공되던 탭을 없애고 검색 UI를 추가하기 위한 것이다. 마지막으로 살펴볼 마스터 페이지의 소스 코드는 다음과 같다.

코드 16: Site.master 페이지의 마크업 코드

<div id="header">
    <p id="logo">
        <a href="">BookStore Application</a>
    </p>
    <ul id="menu">
        <li>
     <% using (Html.Form("Books", "Search")) { %>
  Search: <% = Html.TextBox("keyword") %>
  <% = Html.SubmitButton("btnSearch", "Search") %>
            <% } %>
        </li>
    </ul>
</div>

예제 코드에서 보듯이 마스터 페이지는 Html.Form 메서드와 Html.TextBox 메서드, 그리고 Html.SubmitButton 메서드를 이용하여 검색 UI를 구현하고 있다. 한 가지 주의할 점은 Html.Form 메서드의 경우 <FORM> 태그 내에 나타나야 하는 요소들을 표현하기 위해 예제에서와 같이 using 구문을 사용해 주어야 한다는 점이다.

Action Filter

ASP.NET MVC 프레임워크의 컨트롤러는 ActionFilter 클래스를 이용하여 액션 메서드가 호출되기 전과 호출된 이후에 추가 작업을 수행할 수 있는 방법을 제공한다. 이를 위해 Controller 클래스는 IActionFilter 인터페이스를 구현하고 있으며 이 인터페이스는 다음과 같은 메서드를 정의하고 있다.

void OnActionExcuted(ActionExecutedContext filterContext);
void OnActionExecuting(ActionExecutingContext filterContext);
void OnResultExecuted(ResultExecutingContext filterContext);
void OnResultExecuting(ResultExecutingContext filterContext);

OnActionExecuting 이벤트와 OnActionExecuted 이벤트는 각각 액션 메서드가 호출되기 전과 호출된 이후에 발생하며 OnResultingExecuting 이벤트와 OnResultExecuted 이벤트는 액션 메서드가 리턴하는 ActionResult 타입의 객체들이 결과를 리턴하기 전과 후에 각각 발생한다.

이 ActionFilterAttribute 특성 클래스를 상속하면 사용자 정의 액션 필터를 구현할 수 있다. 이달의 디스켓으로 제공되는 예제 애플리케이션에는 gzip이나 deflate 알고리즘을 활용하여 HTTP 응답 스트림을 압축하는 사용자 정의 액션 필터가 CompressActionFilterAttribute.cs 파일에 구현되어 있다. 이 액션 필터는 다음과 같이 컨트롤러의 액션 메서드에 지정할 수 있다

코드 10: 액션 메서드에 액션 필터 특성을 적용한 코드

[CompressActionFilter]
public ActionResult List() { ...}

아래의 화면 6과 7은 방금 구현한 CompressActionFilter 액션 필터가 적용되지 않은 경우와 적용된 경우의 List 액션 메서드의 실행 결과를 HttpWatch 도구로 살펴본 모습이다.

img6  

화면 6: 액션 필터가 적용되기 전의 HTTP 응답

img7  

화면 7: 액션 필터가 적용된 후의 HTTP 응답

화면 6과 화면 7을 비교해 보면 우선 화면 7의 HTTP 응답 헤더에는 Content-Encoding 헤더의 값이 gzip으로 설정되어 있음을 볼 수 있으며 전체 응답 스트림의 크기를 의미하는 Content-Length 헤더의 값 역시 4,541바이트와 1,585바이트로 현격한 차이가 있음을 알 수 있다.

액션 필터를 잘 활용한다면 이렇게 콘텐츠를 압축하는 것 외에도 액션 메서드의 실행에 대한 로그를 기록하거나 사용자의 동선을 추적하는 등 다양한 용도로 활용할 수 있을 것이다.

이상으로 ASP.NET MVC 프레임워크를 기반으로 컨트롤러와 뷰를 구현하여 애플리케이션을 제작해가는 과정에 대해 설명하였다. 아무래도 가장 비중이 큰 두 부분을 설명하다 보니 지면이 조금 늘어난 듯한 느낌이 없지 않으나 그럼에도 불구하고 아직 다 풀어놓지 못한 이야기들이 많이 남아 있다. 다음 호에서는 이 미공개(?) 이야기들을 끝으로 ASP.NET MVC 프레임워크에 대한 연재를 마무리 하고자 한다.

Posted by 웹지니 트랙백 0 : 댓글 4