[maso:저 자:유경상 ]풍성한 ASP.NET 개발환경의 이해|[ASP.NET]

ASP.NET 정식 버전이 등장한지 1년여가 지났다. ASP.NET은 기존 버전이라 할 수 있는 ASP에 비해 매우 다른 접근 방식을 취하고 있다. 기존 ASP 4.0은 HTML 태그 사이사이에 ASP 코드가 삽입되어 있는 형태로서 CGI나 ISAPI에 비해 개발 생산성을 높여 준다는 장점을 등에 업고 성공했다. 시간이 지나감에 따라 ASP 코드와 HTML이 뒤범벅돼 있는 페이지가 양산되기 시작했고 이는 웹 사이트의 유지 보수와 가독성을 떨어뜨리는 요인으로 작용했다. ASP.NET은 새로운 모델을 제시했다. 소위 웹폼(WebForm) 모델은 웹 페이지를 VB 6.0과 같은 폼 디자인 형태로 개발을 가능하게 해줄 뿐더러 이벤트에 기반한 페이지 제어 모델을 제공한다. 웹폼 모델의 핵심은 ASP.NET의 System.Web.UI.Page 클래스와 다양한 웹 컨트롤이라고 할 수 있다.

웹 컨트롤은 ASP.NET을 처음 배우는 개발자에게 웹 프로그래밍이 쉽다는 느낌을 주지만 어느 정도 경험이 쌓이면 한계로 다가온다. 처음에는 다양한 기능을 제공하는 웹 컨트롤이 도움을 주는 듯 느끼지만 다양한 웹 컨트롤을 사용하다 보면 웹 컨트롤의 제약사항에 빠지기도 하고 무분별한 웹 컨트롤의 사용으로 인해 웹 사이트의 성능이 크게 떨어지는 것을 경험하곤 한다. 웹 컨트롤은 어떻게 사용하는가에 따라 ASP.NET 개발자에게 축복으로 다가오기도 하고 재앙으로 다가오기도 한다는 점을 명심하자.

대표 선수, 데이터그리드
ASP.NET에서 기본적으로 제공하는 웹 컨트롤 중 가장 다양하고 막강한 기능을 가진 컨트롤을 뽑으라면 단연 데이터그리드(DataGrid) 컨트롤을 꼽을 것이다. 데이터 바인딩은 물론이요, 페이징, 정렬, 편집, 추가, 삭제 등의 기능을 갖고 있다. ASP.NET을 처음 접하는 개발자는 데이터그리드의 기능에 놀라며 그 현란함에 감탄하기 마련이다. 그러나 막상 데이터그리드를 실제 사이트에 사용하려고 하면 다양한 문제에 부딪히곤 한다. 데이터그리드의 막강한 기능들은 그 대가가 있다. 첫째로 데이터그리드는 매우 큰 ViewState를 생산한다. 이는 웹 페이지가 생성하는 HTML의 크기가 커짐을 의미하며 이는 곧 웹 페이지의 처리 속도 저하를 가져온다. 그래서 ViewState를 disable시키면, 데이터그리드의 막강한 기능들인 페이징, 정렬 등의 기능은 사용할 수 없게 되어 버린다. 데이터그리드의 편집/추가/삭제 기능은 사실상 많이 사용되지 않기 때문에 차치하더라도 페이징과 정렬을 사용할 수 없게 되면 상당한 양의 코딩을 개발자가 추가로 해야 한다는 부담이 생기곤 한다.

데이터그리드 구조
왜 ViewState를 disable하면 데이터그리드의 기능들이 작동하지 않을까? 이 질문에 대답하기 위해서는 데이터그리드의 구조에 대해 이해할 필요가 있다. 데이터그리드의 구조가 다양한 기능으로 인해 매우 복잡한 것처럼 보이지만 알고 보면 그다지 복잡하다고 볼 수만은 없다. 텍스트 박스나 리스트 박스와 같은 단순 컨트롤의 경우 구조라고 할 것도 없이 간단하지만 데이터그리드는 여러 컨트롤로 구성된 합성 컨트롤(Composite Control)로서 컨트롤이 계층 구조를 이루고 있다. 데이터그리드의 컨트롤 계층 구조를 알아보는 가장 쉬운 방법은 페이지의 트레이스(Trace) 속성을 True로 설정하는 것이다. Trace가 활성화되면 <화면 1>과 같은 결과를 얻을 수 있으며 구조를 파악하는 데 도움이 된다. 데이터그리드의 일반적인 컨트롤 계층 구조는 <그림 1>과 같다.



<화면 1> ASP.NET 트레이스 기능으로 살펴본 데이터그리드 컨트롤 계층 구조




<그림 1>에서 주의할 사항은 이 그림이 컨트롤의 계층 구조를 나타내고 있다는 점이다. 컨트롤이라 함은 System.Web.UI.Control 클래스에서 파생된 웹 컨트롤을 의미하므로 <그림 1>의 구조는 데이터그리드를 구성하는 웹 컨트롤의 구조라는 것이다. 컨트롤이 아닌 클래스의 관점에서 보면 데이터그리드는 <그림 1>에 나타난 웹 컨트롤 클래스 외에도 DataGridColumn, BoundColumn 등의 컬럼 관련 클래스들, 데이터그리드의 외양에 많은 영향을 미치는 스타일 관련 클래스들과 관련이 있다. 컬럼 관련 클래스에 대해서는 조금 후에 언급하기로 하고 컨트롤 계층 구조에 대해 좀더 상세히 살펴보자.
데이터그리드 컨트롤의 자식 컨트롤은 DataGridTable 하나뿐이다(자손이 아닌 자식임에 유의). DataGridTable 컨트롤은 HTML

태그를 렌더링하는 컨트롤로서 데이터그리드에서만 사용되는 컨트롤이다. 이 컨트롤은 특별한 기능을 하지 않는다. 다만 DataGridItem 컨트롤에 대한 부모 역할을 수행하고
태그를 렌더링할 뿐이다. 데이터그리드를 구성하는 컨트롤 중에서 중요한 역할은 DataGridItem 컨트롤이 수행한다.



<그림1> 데이터그리드 컨트롤 계층 구조


DataGridItem 컨트롤과 TableCell 컨트롤
사실 DataGridItem과 TableCell을 컨트롤로 볼 것인가에 대해 필자는 약간 고민을 했다. 이들은 모두 독립적으로 사용될 수 없으며 항상 다른 컨트롤의 자식으로만 사용되므로 컨트롤보다는 객체 정도로 표현해야 하지 않을까 생각이 들었다. 하지만 이들은 공히 컨트롤 클래스에서 파생되고 웹 컨트롤의 생명 주기를 따르므로 컨트롤의 최소 요건을 모두 갖추었다고 볼 수 있으므로 컨트롤로 분류하기로 했다. 이에 대해 이의 있는 독자는 언제든지 필자에게 메일을 보내기 바란다.

DataGridItem 컨트롤은 TableRow 컨트롤에서 파생된 컨트롤로서 HTML 태그와 대응되는 컨트롤이다. DataGridItem 컨트롤 역시 데이터그리드를 위해서만 사용되며 데이터컨트롤의 헤더, 풋터(footer), 그리고 페이저를 나타내거나 데이터 아이템의 한 레코드와 일대일로 대응되는 개념을 갖는다. 데이터그리드는 AllowPaging 속성(property)에 의해 페이저를 위한 DataGridItem 생성을 결정한다. AllowPaging 속성이 False라면 페이저를 위한 DataGridItem은 생성되지 않는다. 반면 헤더 및 풋터를 위한 DataGridItem은 항상 생성된다. 이는 ShowHeder, ShowFooter 속성과는 무관하다. 데이터그리드의 ShowXXXX 속성이나 Visible 류의 속성은 컨트롤이 HTML 태그들을 생성하는 방법을 결정할 뿐이지 웹폼 내의 컨트롤 생성 여부를 결정하지 않음에 주의해야 한다. 예를 들어 AllowPaging 속성이 True이면 PagerStyle 속성과 관계없이 페이저를 위한 두 개의 DataGridItem 컨트롤이 생성된다.

DataGridItem 컨트롤은 자식 컨트롤로서 TableCell 컨트롤들을 가진다. DataGridItem의 ItemType에 따라 자식 TableCell 컨트롤의 갯수는 달라진다. 페이저는 하나의 TableCell 컨트롤만을 가지며 헤더/풋터, 아이템 등의 경우에는 컬럼의 갯수만큼 TableCell 컨트롤을 가진다. 여기서도 주의할 점은 웹 브라우저에 나타나는 컬럼 수와 TableCell 컨트롤의 갯수는 동일하지 않다는 점이다. 데이터그리드의 Columns 컬렉션이 나타내는 데이터그리드 컬럼들은 Visible 속성을 가지고 있다. 이 속성 역시 TableCell의 생성 여부를 좌우하지 않는다는 점을 명심해야 한다. 따라서 DataGridItem 컨트롤이 갖는 자식 TableCell 컨트롤의 갯수는 Columns 컬렉션이 보유하는 데이터그리드 컬럼 갯수와 AutoGenerateColumn 속성의 값 그리고 데이터소스(DataSource)의 컬럼 갯수와 연관이 있다.

TableCell 컨트롤은 HTML
태그에 대응되는 컨트롤로서 직접 데이터를 표시하거나 다른 컨트롤의 부모 역할을 한다. DataGridItem이 페이저라면 TableCell은 자식 컨트롤로서 다수의 LinkButton 컨트롤과 Literal 컨트롤을 갖으며 DataGridItem이 헤더이고 정렬이 enable되어 있다면 TableCell은 몇 개의 LinkButton 컨트롤을 자식 컨트롤로 보유하게 된다. TableCell이 어떤 자식 컨트롤을 갖는가는 DataGridItem이 어떤 종류인가에 영향을 받지만 해당 컬럼(DataGridColumn)이 어떤 컬럼인가 그리고 현재 DataGridItem이 편집 상태에 있는가에도 영향을 받는다. 대개의 경우 컬럼은 데이터바인딩을 수행하는 단순한 BoundColumn이다.

BoundColumn인 경우 TableCell 컨트롤은 편집 상태가 아닌 경우에는 자식 컨트롤을 갖지 않고 TableCell 컨트롤의 Text 속성에 데이터 값이 표현된다. 편집 상태라면 TableCell은 TextBox 컨트롤을 자식 컨트롤로 갖으며 이 컨트롤의 Text 속성에 데이터가 표시된다. 비슷하게 TableCell의 컬럼이 ButtonColumn 타입이라면 LinkButton 컨트롤 혹은 Button 컨트롤이 TableCell의 자식이 되며 데이터 역시 이 자식 컨트롤에 표시된다. 이외에도 TableCell의 컬럼이 TemplateColumn이라면 템플릿이 지정하는 서버 컨트롤이 TableCell의 자식으로 설정되며, 데이터바인딩 역시 템플릿이 지정하는 데이터바인딩 식에 의해 표현된다.

DataGridColumn 클래스
DataGridColumn 클래스는 데이터그리드의 컬럼 정보를 갖는 클래스로서 중요한 역할을 수행한다. 이 클래스는 추상(abstract) 클래스로서 인스턴스를 가질 수 없다. 따라서 실제 데이터그리드의 컬럼은 DataGridColumn에서 파생된 BoundColumn, ButtonColumn, EditCommandColumn, HyperLinkColumn, TemplateColumn 클래스 중 하나이다. 이들 클래스는 모두 웹 컨트롤이 아니다. 즉, System.Web.UI.Control 클래스에서 파생된 것이 아니라는 것이다. 하지만 이들 클래스들은 각 TableCell 컨트롤이 어떤 자식 컨트롤을 가질 것인가를 결정한다.

데이터그리드는 Columns 컬렉션에 컬럼들의 리스트를 보유하게 된다. Columns 컬렉션에 컬럼을 추가하는 것은 디자인 타임이나 런타임에 가능하다. 디자인 타임에 설정되는 컬럼들은 aspx 파일내의 ASP.NET 태그를 통해 수행될 수 있으며, 이 태그들은 Columns 컬렉션에 DataGridColumn에서 파생된 컬럼 객체를 추가하는 코드로 변환된다. AutoGenerateColumn 속성 값이 True라면 데이터바인딩이 수행될 때 데이터소스의 컬럼 정보를 통해 BoundColumn 클래스의 인스턴스가 생성된다. 주의할 점은 AutoGenerateColumn 속성에 의해 생성된 BoundColumn 클래스는 Columns 컬렉션을 통해 액세스할 수 없다는 점이다.

DataGridColumn 클래스의 중요한 메쏘드는 InitializeCell 메쏘드이다. 이 메쏘드는 protected virtual 메쏘드로서 데이터그리드가 DataGridItem의 인스턴스를 만든 후, 데이터그리드에 설정된 각 컬럼(AutoGenerateColumn에 의해 생성된 컬럼에 대해서도)에 대해 호출한다. InitializeCell 메쏘드는 매개변수로 초기화할 TableCell 객체, 컬럼의 인덱스, DataGridItem의 타입을 매개변수로 취하며 이들에 의해 적절히 TableCell 객체를 초기화한다. 이 메쏘드에서 TableCell의 자식 컨트롤을 생성하기도 하며, 보다 중요한 것은 데이터바인딩을 수행할 DataBind 이벤트의 이벤트 핸들러를 설정한다는 점이다.

<리스트 1> BoundColumn.InitiaizeCell 메쏘드
public virtual void InitializeCell(TableCell cell, int columnIndex, ListItemType itemType)
{
Control childControl;
Control bindingControl;
TextBox textBox;

base.InitializeCell(cell, columnIndex, itemType);
childControl = null;
bindingControl = null;
switch (itemType) {
case ListItemType.Item:
case ListItemType.AlternatingItem:
case ListItemType.SelectedItem:
if (this.DataField.Length == 0)
break;
bindingControl = cell;
break;
case ListItemType.EditItem:
if (this.ReadOnly)
goto case ListItemType.SelectedItem;
textBox = new TextBox();
childControl = textBox;
if (this.boundField.Length != 0)
bindingControl = textBox;
break;
}
if (childControl != null)
cell.Controls.Add(childControl);
if (bindingControl != null)
bindingControl.DataBinding += new EventHandler(this, OnDataBindColumn);
}

<리스트 1>은 BoundColumn 클래스의 InitiailzeCell 메쏘드이다. 이 메쏘드를 살펴보면 현재 TableCell이 속한 DataGridItem의 ItemType에 의거하여 TableCell을 초기화하는 작업을 담고 있다. DataGridItem이 EditItem이라면 자식 컨트롤로서 TextBox를 생성한다는 점과 데이터 바인딩 이벤트를 이 클래스의 OnDataBindColum 메쏘드로 설정한다는 점을 유심히 살펴보자. 데이터 바인딩이 일어나면 TableCell의 데이터 바인딩 메쏘드인 DataBind가 호출될 것이며 TableCell의 DataBind 메쏘드는 DataBind 이벤트를 발생하고 자식 컨트롤의 DataBind를 호출될 것이다. 따라서 자식 컨트롤의 존재 여부와 관계없이 데이터 바인딩은 BoundColumn 클래스의 OnDataBindColumn 메쏘드 내부에서 이뤄진다는 말이다.

TableCell 컨트롤이나 그 자식 컨트롤은 데이터소스가 어떤 것인지 그리고 데이터소스 내에서 어떤 컬럼에 바인딩될 것인지 전혀 알지 못하므로 데이터 바인딩을 수행할 수 있는 객체는 BoundColumn 객체가 되는 것은 매우 자연스럽다고 할 수 있다. BoundColumn은 바인딩할 데이터 필드(DataField 속성)의 이름을 알고 있으며 DataGridItem 컨트롤의 DataItem 속성은 바인딩할 데이터의 행(row)을 나타내고 있으므로(DataBind 이벤트의 sender 매개변수를 통해 DataGriItem 컨트롤이 전달될 것이다) 데이터 바인딩을 수행할 수 있는 것이다.

DataGridColumn 클래스와 그 자식 클래스는 TableCell과 밀접한 관계를 가지고 있으며 데이터 바인딩에서도 중요한 역할을 수행함을 알 수 있을 것이다. 필요에 따라 DataGridColumn 클래스를 상속하여 커스텀 컬럼을 만들 수도 있다. 몇몇 필수적인 메쏘드와 프로퍼티를 구현하기만 하면 원하는 HTML을 렌더링하도록 구성이 가능할 것이다. 이에 대한 상세한 내용은 이 글의 범위에서 벗어나므로 생략하기로 한다.

그 외의 클래스들
지금까지 설명한 컨트롤과 클래스들 외에도 데이터그리드와 연관된 클래스들은 데이터그리드 클래스의 베이스 클래스인 BaseDataList 클래스, 데이터그리드의 각종 스타일에 대한 TableItemStyle 클래스, 페이징에 대한 추상화를 제공하는 PagedDataSource 클래스, DataGridItem과 DataGridColumn에 대한 컬렉션을 제공하는 DataGridItemCollection 클래스, DataGridColumnCollection 클래스 등의 클래스가 있다. 이들 클래스들은 데이터그리드가 작동하는데 직간접적으로 관여한다. 특히 BaseDataList 클래스는 데이터리스트 컨트롤과 데이터그리드 컨트롤에 대한 베이스 클래스로서 데이터바인딩을 지원하는 리스트 컨트롤의 공통적인 인터페이스와 구현을 제공한다.

데이터그리드 컨트롤에 대한 분석을 하고자 한다면 시작점은 BaseDataList가 돼야 할 것이다. PagedDataSource 클래스는 데이터그리드의 데이터소스 속성에 설정된 데이터 소스에 대한 페이징 뷰를 제공한다. 예를 들어 데이터소스가 30개의 레코드를 갖고 PageSize가 10인 상태에서 AllowPagaing이 True라면 PagedDataSource는 현재 페이지의 10개 레코드만을 반환하고 AllowPagaing이 False라면 현재 페이지에 관계 없이 전체 30개의 레코드를 반환한다. PagedDataSource 클래스의 이러한 데이터소스 추상화는 데이터그리드 컨트롤이 AllowPaging 속성에 관계없이 단일 코드 플로우(flow)를 갖도록 해준다. 그 외의 클래스는 데이터그리드의 외양을 제어하고 컬렉션을 구현하는 데 이용된다.

데이터그리드 제어 흐름
데이터그리드의 구조를 살펴봤으니 이제 제어의 흐름을 살펴보자. 데이터그리드도 역시 일반적인 웹 컨트롤의 일종이므로 웹 컨트롤의 일반 제어 흐름을 따른다. 웹 컨트롤의 일반적인 제어 흐름과 데이터그리드의 구체적인 작업은 <표 1>과 같다. 데이터그리드의 전체 제어 흐름을 설명하기에는 그 분량이 너무 많으므로 핵심적인 몇몇 사항과 주의해야 할 사항에 대해서만 언급하기로 한다.

<1> 웹 컨트롤의 일반적인 제어 흐름과 데이터그리드의 행동
일반적인 제어 흐름컨트롤 생성 관련 초기화일반적인 수행 내용
InitializeViewState에 기록된 속성 값으로 속성들이 초기화됨.초기화. 속성들은 초기 값 가짐. <asp:DataGrid> 태그의 속성 값들 설정됨.
Load ViewStatePost된 폼 데이터를 처리 IPostBackEventHandler 인터페이스를 구현하는 컨트롤만이 해당됨.IsPostBackTrue인 경우, ViewState로부터 컨트롤 계층 구조를 만듦(<그림 1> 참조). IsPostBackFalse라면 아무런 작업 없음.
Process Post Data하고, 그에 알맞게 속성을 업데이트.N/A Post된 데이터는 모두 자식 컨트롤로 라우팅됨. 데이터그리드는 IPostBackDataHandler 인터페이스를 구현하지 않음
LoadLoad 이벤트 처리.N/A
Raise Change EventPost된 폼 데이터에 의해 변경사항을 이벤트로 알림. (ex) Change 이벤트 류자식 컨트롤의 변경 이벤트가 이 단계에서 발생함.
Raise PostBack Event(Click Event)Post 이벤트 류의 처리. (ex) LinkButtonClick 이벤트 IPostBackDataHandler 인터페이스를 구현하는 컨트롤만이 해당됨.데이터그리드는 IPostBackEventHandler 이벤트를 구현하지 않음. 자식 컨트롤이 이벤트를 버블링(Bubbling). 버블링의 결과로 PageIndexChanged, SortCommand, EditCommand, SelectedIndexChanged 등의 이벤트 발생함.
PreRenderPreRender 이벤트 처리헤더, 풋터, 페이저, 데이터 아이템 등에 대해 스타일 등을 적용.
Save ViewState상태 저장아이템 갯수, 컬럼 정보, 스타일 정보 등을 ViewState에 기록. 표시된 데이터는 데이터그리드가 저장하지 않고 자식 컨트롤에게 위임함.
RenderingHTML 등의 태그 생성HTML 태그 생성
Disposing리소스 해제N/A. 자식 컨트롤에게 위임
UnloadUnload 이벤트N/A

컨트롤 계층 구조 생성
<그림 1>과 같은 컨트롤 계층 구조는 언제 생성되는 것일까? 데이터그리드의 컨트롤 계층 구조를 생성하기 위해서는 몇 가지 정보가 필요하다. 첫째로 데이터 아이템의 갯수가 필요하다. 헤더, 풋터, 페이저 등을 제외한 순수한 데이터를 위한 DataGridItem의 갯수가 컨트롤 계층 구조에서 필수 조건이 된다. 두 번째로 필요한 것은 컬럼의 갯수 및 각 컬럼의 타입(BoundColumn, ButtonColumn 등)이며 세 번째는 페이징 관련 정보(AllowPaging, PageSize 등의 속성)이다. 이들 정보가 있으면 컨트롤 계층 구조를 만들 수 있다.

먼저 데이터 아이템 갯수에 대해 생각해보자. 아니 생각해 볼 것도 말 것도 없이 데이터 아이템 갯수는 데이터 바인딩이 이뤄질 때 알아낼 수 있다. 데이터 바인딩을 수행하기 위해서는 DataSource 속성이 설정돼야 하고 데이터소스 속성으로부터 데이터 갯수를 확인할 수 있다. 나머지 컬럼 정보와 페이징 관련 정보는 데이터그리드 컨트롤이 생성될 때 알 수 있다. 이들은 디폴트 값도 가지고 있으므로 아무런 문제도 되지 않는다. 게다가 태그에 명시된 여러 특성들은 모두 데이터그리드 클래스의 속성 값으로 설정되는 C#(VB.NET) 코드로 변환된다. 즉 이러한 초기화는 Init 과정에서 이미 처리된 상태이다. 따라서 데이터 아이템의 갯수만 알면 컨트롤 계층 구조를 만들 수 있다.

일반적으로 데이터그리드로 작업할 때 데이터소스에 데이터 셋 혹은 데이터 테이블을 설정하고 곧바로 DataBind 메쏘드를 호출하여 바인딩을 수행한다. 따라서 DataBind 메쏘드가 호출된 후에 자식 컨트롤의 계층 구조가 만들어 진다. 구체적으로 컨트롤 계층 구조를 생성하는 데이터그리드 클래스의 메쏘드는 CreateControlHierarchy 메쏘드이다. 대개의 경우 CreateChildControl() 메쏘드에서 자식 컨트롤을 생성하는 것이 일반적이지만 데이터그리드나 데이터리스트 컨트롤 공히 CreateChildControl 메쏘드는 별반 일을 수행하지 않고 CreateControlHierarchy 메쏘드를 호출하며 CreateControlHierarchy 메쏘드에서 실제 작업들을 수행하도록 되어 있다.

자, 그렇다면 꼭 데이터 바인딩이 수행돼야만 컨트롤의 계층 구조가 생성되는 것인가? 그렇지는 않다. 우리가 데이터그리드의 EnableViewState를 true로 설정해 놓고 LinkButton 등을 이용하여 포스트 백(Post back)을 발생하더라도 데이터그리드는 컨트롤 계층 구조를 유지한다. 데이터소스 속성이 설정되거나 DataBind 메쏘드가 호출되지 않더라도 말이다. 이렇게 데이터그리드가 상태를 유지하는 것은 데이터그리드가 데이터 아이템의 갯수를 ViewState에 기록해 두기 때문이다. 데이터그리드가 ViewState를 로드하는 과정에서 컨트롤의 계층 구조가 생성되며, 이때 ViewState에서 데이터 아이템의 갯수를 취하며 나머지 컬럼 정보/페이징 정보들은 앞서 언급한 대로 Init 과정에서 이미 초기화가 완료됐다. 따라서 포스트 백이 발생하는 경우에는 LoadViewState 메쏘드의 처리 과정에서 컨트롤의 계층 구조는 생성되는 것이다.

그렇다면 ViewState가 disable되어 있다면 어떻게 되는가? 데이터 바인딩 과정이야 ViewSate와 별반 관계가 없으니 그렇다 치고, 포스트 백이 발생하는 경우라면 데이터 아이템의 갯수를 알아낼 수 없으므로 당연히 컨트롤 계층 구조 역시 생성되지 않는다. 컨트롤 계층 구조가 생성되지 않는다는 말은 <그림 1>에서 나타나는 DataGridTable 컨트롤도, DataGridItem 컨트롤도, TableCell 컨트롤도 생성되지 않는다. 물론 부모 컨트롤이 없으니 페이징을 위한 링크 버튼이나 헤더를 위한 DataGridItem 역시 생성되지 않음은 물론이다. 왜 ViewState를 disable하면 페이징이 수행되지 않는가에 대한 약간의 힌트를 여기서 얻을 수 있을 것이다. 그러나 여전히 궁금증은 남는다. 자식 컨트롤이 생성되지 않음과 페이징이나 정렬이 되지 않는 것과 무슨 관계일까? 페이징이나 정렬은 이벤트에 의존한다. 즉 데이터그리드는 페이징/정렬을 수행할 수 있도록 이벤트를 생성하여 페이지에게 기회를 줄 뿐이다. 다음부터 설명할 데이터그리드의 이벤트 처리 방식에서 그 해답을 찾아보자.

이벤트 처리
일반적으로 웹 컨트롤의 이벤트 발생은 포스트 백과 관계가 있다. TextBox 컨트롤의 Changed 이벤트는 포스트 백된 텍스트 박스의 값이 이전 값과 비교하여 변경된 경우 발생하며, LinkButton 컨트롤은__doPostBack 자바 스크립트의 수행 결과로서 포스트 백이 발생한 경우에 Click 이벤트를 발생시킨다. 이렇게 포스트 백과 관련된 이벤트를 처리하기 위해 대개의 웹 컨트롤은 IPostBackDataHandler 인터페이스나 IPostBackEventHandler 인터페이스를 구현한다. TextBox 컨트롤과 같이 포스트 백된 데이터를 속성 값으로 할당하는 류의 ‘처리’를 하는 컨트롤들(ListBox 컨트롤, DropDownList 컨트롤, CheckBox 컨트롤, RadioButtonList 컨트롤 등)은 IPostBackDataHandler 인터페이스를 구현한다. 한편 클릭 류의 행동(behavior)을 보이는 컨트롤들(Button 컨트롤, LinkButton 컨트롤, ImageButton 컨트롤, Calendar 컨트롤)은 IPostBackEventHandler 인터페이스를 구현한다. 이들 인터페이스의 멤버나 사용방법 등은 MSDN을 참조하자.

데이터그리드 역시 포스트 백과 연관이 있다. 데이터그리드의 아이템이 Edit 중일 때는 사용자의 입력이 포스트 백되며, 페이징을 위해 링크 버튼을 클릭하면 역시 클릭 류의 이벤트가 발생한다. 하지만 데이터그리드 컨트롤은 IPostBackDataHandler 인터페이스도 IPostBackEventHandler 인터페이스도 구현하지 않는다. 그렇다면 페이징을 위한 링크가 클릭될 때 어떻게 데이터그리드의 PageIndexChanged 이벤트가 발생하는 것일까?

데이터그리드의 이벤트들은 대부분 자식 컨트롤에게 위임되어 있다. 즉 이벤트를 발생하는 주체는 데이터그리드가 아니라 데이터그리드의 자식 컨트롤인 것이다. 그렇다면 어떻게 이벤트 핸들러는 데이터그리드의 속성일까? 이는 이벤트 버블링이라 부르는 이벤트 전파 기법을 통해 이뤄진다. 이벤트 버블링은 자식 컨트롤이 이벤트를 부모 컨트롤로 전파하는 기법이다. 자식 컨트롤은 이벤트에 대해 RaiseBubbleEvent 메쏘드를 호출하고 부모 컨트롤은 OnBubbleEvent 메쏘드를 오버라이드한다. 자식 컨트롤이 RaiseBubbleEvent를 호출하면 부모 컨트롤의 OnBubbleEvent를 호출하도록 되어 있다. 만약 부모 컨트롤의 OnBubbleEvent 메쏘드가 true를 반환하면 버블링은 중지되며 false를 반환하면 다시 상위 컨트롤로 이벤트는 계속 전파된다. OnBubbleEvent는 실제로 모든 웹 컨트롤의 베이스 클래스인 System.Web.UI.Control 클래스의 가상 메쏘드로서 특별히 이 메쏘드를 오버라이드하지 않으면 false를 반환한다(<리스트 2>). 이벤트 버블링을 통해 자식 컨트롤의 이벤트는 상위로 전파될 수 있는 것이다.

<리스트 2> 컨트롤 클래스의 RiaseBubbleEvent와 OnBubbleEvent 메쏘드
public class Control : IComponent, IDisposable, IParserAccessor,
IDataBindingsAccessor
{
protected void RaiseBubbleEvent(object source, EventArgs args)
{
Control parent = this.Parent;
while (parent != null) {
if (parent.OnBubbleEvent(source, args))
return;
parent = parent.Parent;
}
}

protected virtual bool OnBubbleEvent(object source, EventArgs args)
{
return false;
}
}

데이터그리드의 PageIndexChanged, SelectedIndexChanged, SortCommand, EditCommand, UpdateCommand, CancelCommand, DeleteCommand 이벤트는 모두 이 이벤트 버블링을 통해 발생된다. 실제 이벤트를 수신하는, 즉 IPostBackEventHandler를 구현하는 클래스는 LinkButton 혹은 Button 클래스다. 이들은 Command 이벤트에 대해 이벤트를 버블링하며 데이터그리드 클래스의 OnBubbleEvent 메쏘드는 버블링된 이벤트를 검사하여 적절한 이벤트로 디스패치한다.
페이징에 사용되는 링크 버튼을 예로 들어 보자. 페이저 DataGridItem은 하나의 TableCell 컨트롤을 생성하며 이 컨트롤에 대한 초기화는 DataGrid 클래스의 InitializePager 메쏘드에서 수행된다. 이 메쏘드에서는 다음과 같은 코드로서 LinkButton을 생성하고 TableCell 컨트롤에 추가한다.

// cell 변수는 TableCell 컨트롤을 나타내고, pageNumber는 버튼의 페이지 번호 문자열이다.
btn = new DataGridLinkButton();
btn.Text = pageNumber;
btn.CommandName = "Page";
btn.CommandArgument = pageNumber;
btn.CausesValidation = false;
cell.Controls.Add(btn);

만약 사용자가 LinkButton을 클릭하면 연결된 Click 이벤트 핸들러가 없으므로 아무런 작업도 하지 않을 것이다. 다만 이벤트 버블링에 의해 이벤트는 데이터그리드 클래스의 OnBubbleEvent 메쏘드가 호출될 것이며 이 OnBubbleEvent 메쏘드는 CommandName 속성, CommandArgument 속성을 이용하여 어떠한 이벤트인가를 판별해 내고 이벤트를 발생시킬 것이다.

데이터그리드 진단
이제 데이터그리드를 진단하기 위해 필요한 사항들을 모두 알아봤다. 완벽하지는 않지만 말이다. 모든 것을 다 알려주면 독자들도 재미없어 할 것 같아서 듬성듬성 틈을 두었다. 이제 앞서 언급했던 데이터그리드의 문제점을 진단해 보자.

많은 웹 컨트롤
데이터그리드의 구조는 간략하고 강력하게 구조화되어 있다. 즉 많은 일을 자식 컨트롤에게 위임함으로써 상당히 구조적인 방법으로 기능들을 구현하고 있다. 소위 말하는 객체 지향적인 방법을 충실히 따라 설계됐고 구현됐다고 볼 수 있는 것이다. 하지만 이 때문에 문제가 발생한다. 데이터그리드가 생성하는 웹 컨트롤의 숫자는 매우 많다. 데이터 아이템의 수가 20(PageSize = 20 정도로 생각하면 되겠다)이고 컬럼이 10개라고 했을 때 생성되는 컨트롤의 갯수는 200개가 훌쩍 넘어 버린다.

왜 웹 컨트롤의 갯수가 많으면 문제가 되는가? 앞서 <표 1>에서 나타냈듯 웹 컨트롤은 일련의 제어 흐름을 가진다. 모든 컨트롤에 대해 OnInit, LoadViewState, OnLoad, OnPreRender, SaveViewState, Render 메쏘드가 호출된다. 비록 아무런 일도 하지 않더라도 컨트롤 클래스가 이들에 대한 기본 구현을 제공하므로 일정 프로세싱 파워를 소비하게 된다. 가랑비에 옷 젖는다는 말이 있듯 이렇게 많은 컨트롤을 자식 컨트롤로 가지고 있는 데이터그리드는 오버헤드를 갖지 않을 수 없다.

거대한 ViewState
데이터그리드의 ViewState는 크다. 데이터 아이템이 10여 개가 넘고 컬럼이 10여 개 정도 되면 10KB가 넘는 ViewState를 생성하기도 한다. 심지어는 HTML 태그들 보다 ViewState가 더 큰 페이지도 있을 수 있다. 이렇게 ViewState가 큰 이유는 데이터그리드가 데이터 아이템의 갯수, 컬럼 정보 등 필수적인 데이터를 ViewState에 기록할 뿐만 아니라, HeaderStyle, FooterStyle 등 각종 스타일까지도 ViewState에 기록한다. 더욱 좋지 못한 것은 자식 컨트롤까지 ViewState에 자신의 상태정보를 기록한다는 것이다. 실제로 데이터그리드는 자식 컨트롤의 상태 정보에 관여하지 않는다. 어떤 셀의 데이터가 포스트 백 사이에 유지되고 있다면 그것은 TableCell이 스스로 ViewState에 자신의 상태를 기록한 것이다. 따라서 ViewState의 크기는 눈덩이처럼 불어나게 된다. ViewState를 웹 페이지에 렌더링하기 위해서는 일련의 직렬화(serialization)화 작업을 수행해야 하며, 이를 다시 Base64 인코딩을 수행해야 한다. 게다가 ViewState가 임의로 변경되었는가를 확인하기 위해 해시 작업 역시 수행된다. ViewState가 크면 클수록 처리에 필요한 CPU 파워는 증가하기 마련이다.

비효율적인 작업
앞서 데이터그리드의 컨트롤 계층 구조가 언제 생성되는가에 대해 논의했다. 데이터 바인딩이 수행될 때와 LoadViewState에서 컨트롤 계층 구조가 만들어진다고 했을 것이다. 데이터그리드의 페이징 시나리오를 살펴보자. 최초로 페이지가 열릴 때 우리는 대개 다음과 같이 데이터그리드에 바인딩을 수행하고 PageIndexChanged 이벤트 핸들러를 구현할 것이다.

private void Page_Load()
{
if (!Page.IsPostBack) {
DataGrid1.DataSource = GetData();
DataGrid1.DataBind();
}
}
h
private void DataGrid1_PageIndexChanged(object sender,
DataGridPageChangedEventArgs e)
{
DataGrid1.CurrentPageIndex = e.NewPageIndex;
DataGrid1.DataSource = GetData();
DataGrid1.DataBind();
}

앞 코드의 문제점은 이렇다. 페이징을 위해 LinkButton이 클릭되면 포스트 백이 발생하고 포스트 백의 LoadViewState 과정 중에 컨트롤 계층 구조가 생성된다. 이 결과 수백 개의 웹 컨트롤이 생성된다. 그리고 PageIndexChanged 이벤트가 수행됨에 따라 데이터 바인딩이 수행되고 그 결과 다시 수백 개의 웹 컨트롤이 생성될 것이다. LoadViewState의 단계에서 만들었던 컨트롤은 모두 쓸데없는 것들이 되어 버린 것이다. 이와 같이 데이터그리드는 다소 비효율적인 작업을 수행하곤 한다.

ViewState의 의존도
데이터그리드는 많은 정보를 ViewState에 기록한다. 데이터그리드의 주요 속성들(AllowPaging, AllowSorting 등)은 물론이요 각종 스타일 정보 등을 ViewState에 기록한다. 솔직히 이들 속성은 웹 페이지 내에서 상수인 경우가 대부분이므로 굳이 ViewState에 기록할 필요는 없다.

데이터그리드의 ViewState 의존도 중에서 가장 좋지 못한 것은 ViewState가 없으면 포스트 백이 발생했을 때 자식 컨트롤의 계층 구조를 전혀 만들지 않는다는 점이다. 자식 컨트롤들이 만들어 지지 않으면 이벤트가 자식 컨트롤로 전달되지 않으며 자식 컨트롤이 이벤트를 받지 못하면 이벤트 버블링도 발생하지 않음은 너무도 자명하다. 그 결과 페이징, 정렬 등의 이벤트 역시 발생하지 않게 되어 버린다. 쉽게 페이징, 정렬을 사용할 수 있기 때문에 사용하는 데이터그리드이지만 이를 위해 희생해야 할 요소가 점차로 무겁게만 느껴지는 것은 바로 이 때문일 것이다.

데이터그리드 성능 테스트
지금까지 데이터그리드의 문제점을 진단해 보았다. 말로만 하지 말고 실제로 테스트를 통해 앞서 언급한 문제들이 어떻게 작용하는가 살펴보자. 테스트는 두 개의 600MHz CPU와 512MB의 메모리를 갖춘 서버급 컴퓨터에서 닷넷 프레임워크 1.1을 사용했고 테스트에 사용된 도구는 비주얼 스튜디오 닷넷 엔터프라이즈 아키텍처 버전에 포함된 Application Center Test(ACT)를 사용했다. 데이터베이스에 대한 조회가 병목되지 않도록 데이터는 미리 읽어 캐시해 두었으며 페이지에서 캐시된 데이터를 바인딩했다. 웹 페이지는 8개의 데이터 레코드를 표시하며 페이징과 정렬 기능을 사용했으며 다른 효과를 최소화하기 위해 매우 간단히 작성했다(<화면 2>).



<화면 2> 테스트에 사용된 웹 페이지




첫 번째 테스트는 ViewState를 enable시켜 놓고 테스트했으며 두 번째 테스트는 ViewState를 disable시켜 놓았다. 테스트 결과는 <표 2>와 같다. 서버가 초당 처리하는 페이지 갯수는 ViewState를 disable했을 때 약 70% 정도 더 많이 처리했으며 응답 속도는 80% 정도 더 빨랐다. 이로써 데이터그리드 컨트롤의 ViewState가 enable되어 있을 때의 문제점이 명확해졌다. 비록 데이터베이스를 액세스하는 부분이 병목되어 이러한 ViewState의 오버헤드가 잘 나타나지 않기도 하지만 말이다. 그렇다고 무조건 ViewState를 disable할 수도 없다. ViewState가 disable되면 페이징, 정렬 등의 기능을 사용할 수 없기 때문이다.

<표 2> ViewState 활성화에 따른 성능 측정 결과
구분ViewState EnabledViewState Disabled
Content Length7832바이트3554 바이트
Request Per Second95.69RPS161.94RPS
Response Time84.81msec46.69msec



데이터그리드의 확장
이번 연재에서는 웹 컨트롤의 대표 주자(?) 격인 데이터그리드 컨트롤의 구조와 작동 방식 등을 살펴봤다. 그리고 데이터그리드의 어떤 부분이 문제인가를 살펴보고 관련된 테스트도 수행해 보았다. 이제 남은 것은 지금까지 살펴본 내용을 바탕으로 문제점을 해결하는 것이다.

다음 연재에서는 ViewState를 disable한 상태에서도 페이징과 정렬 등의 기능을 사용할 수 있도록 데이터그리드 컨트롤을 확장해 볼 것이다. 이 컨트롤은 DataGridEx라 부를 것이며, 기존 웹 컨트롤을 바탕으로 이를 확장하는 일반적인 방법을 먼저 살펴본 후에 DataGridEx를 구현해 볼 것이다. 이번 연재에서 살펴봤던 문제점을 제거하기 위한 방법을 살펴보고 이 방법에 대한 장단점 역시 살펴볼 것이다. 미리 예습을 해보고 싶은 독자가 있다면 참고자료에 나타낸 MSDN 라이브러리의 항목이나 관련 서적을 살펴보길 바란다. 다음 연재를 기대하는 독자가 있기를 바라며...

정리 | 박은정 whoami@korea.cnet.com

Posted by 퓨전마법사
,