이번에는 UI 프레임워크에 대해서 알아보도록 하겠다. 여기서 말하는 UI 프레임워크란 Spring.NET이 지원하고 있는 MVC 패턴을 말한다. 패턴을 공부하다보면 주로 제일 먼저 나오는 패턴중의 하나이다. 달봉이도 자바쪽 프로그래밍에 대해서는 잘 모르지만 이야기를 들어보면 자바쪽 웹 프로그램쪽에서는 MVC 패턴에 기반한 프로그래밍이 예전부터 이뤄지고 있다고 한다. 그래서 많은 개발자가 처음 프로그래밍을 배우면서부터 자연스럽게 이 패턴에 익숙해진다는 것이다.
우선 많은 사람들이 MVC 패턴에 대해서 들어봤겠지만 한번 더 간단히 정리해보고 가자. 상세히는 하지 않겠다. 왜? 말빨을 지원해줄만한 지식이 딸린다. 일단 많이 본 그림을 다시 보자.
패턴 공부를 시작한 사람들이라면 많이 봤을 그림이다. 그렇지만 좀 시간만 지나면 다시 까먹는다. 그림이 뭘 말하는지 까먹고 또 까먹고. 다시 볼때마다 네모와 화살표만 보인다-_-;; 달봉이도 그랬다. 현실적으로 이 패턴을 활용할 기회가 없었기 때문이다.
이 패턴에 대한 좀 더 아카데믹한 설명은 다른 전문 패턴 설명 문서를 참조하기를 바란다. 달봉이의 추측성 설명보다는 그쪽이 더 바람직할 것이다. 다만 여기서는 이 패턴이 ASP.NET으로 어떻게 구현될 수 있는지 HOW-TO 위주로 알아본다. 다음 그림은 이 패턴의 각 요소에 대응되는 ASP.NET 웹 애플리케이션의 요소이다.
아직 다는 이해가 되지 않지만 익숙한 aspx, aspx.cs를 보니 쪼옴 숨이 트일 것이다. "애플리케이션 도메인 객체"라는 새로운 요소가 나타나 있다. MVC 패턴의 Model에 해당하는 것이 "애플리케이션 도메인 객체"로 되어 있다. 기존의 .NET 애플리케이션에서는 데이터 액세스 레이어, 비즈니스 레이어 그리고 UI 레이어간에 데이터를 전달할때 흔히 Dataset 객체를 사용하는 경우가 흔했다. 이런 구조에 익숙한 사람이라면 "애플리케이션 도메인 객체"라는 용어를 들어볼 기회가 별로 없었을 듯 싶다.
사용자 정보를 관리하는 페이지를 예로 해서 MVC 패턴을 좀 더 이해해보도록 하자. MVC 패턴에 맞게 구성하면 다음과 유사하게 될 것이다. "사용자 정보"라는 사용자 정의 객체가 필요한데, 이것이 Model 객체가 된다. 이 Model 객체는 사용자에 대한 정보를 가지고 있게 된다. 이 "사용자 정보" 객체는 사용자에 대한 정보( 사원번호, 이름, 현재 부서 등 )를 간직한다. 이 객체는 그림에서처럼 Controller인 Page 객체로부터 상태 변경 요청을 받기도 하고 View로 부터 상태 조회 요청을 받기도 한다.
aspx는 특정 시점의 사용자 정보 즉 실제 Model 객체의 상태를 보여주는 View 를 제공한다.
Controller인 Page 객체는 사용자가 View를 통해 입력한 Model 객체에 대한 정보를 HTTP로 받아서 해석한다. 그런 다음 Model 객체로 전달해서 상태 변경을 요청한다. 또는 Model 객체에서 변경된 상태를 View에 보내서 반영을 요청하기도 한다.
Controller는 도메인 객체의 현재 상태를 조회해서 UI의 컨트롤에 출력을 요청하기도 한다. 그러나 상태를 UI에 출력하기 위해서는 그림에서처럼 때로는 View에서 직접 도메인 객체에 접근해서 그 상태롤 요청하는 경우도 있을 수 있다. 뒤의 샘플 코드에서 보겠지만 미리 살짝 언급하면 사용자가 입력한 정보를 Model 객체에 반영하고 Model 객체의 상태를 UI에 출력하는 방법으로 데이터 바인딩 기술이 사용될 수 있다.
참조를 나타내는 화살표 방향을 이해해 두는 것도 중요할 듯 싶다. 달봉이가 이해한 대로 그렸지만 용어나 그림이 정확한지는 달봉이도 잘 모르겠다.
아직 이 구조가 몸에 착 달라붙지는 않을 것이다. 샘플 코드를 보자. 다음에 보여주는 코드를 통해서는 MVC 패턴이 실제로 어떻게 구현되는지 그 구조 이해에 중점을 두도록 한다. 화살표가 제대로 구현되고 있는지 확인해 보도록 한다. 이 샘플 코드는 Spring.NET의 소스 코드와 함께 제공되는 샘플 프로젝트중에서 SpringAir.Web.2005를 참조하고 있다.
다음 샘플 코드에서 보여줄 페이지에서는 사용자가 비행기 예약을 하고 취소할 수 있는 페이지이다. 이 페이지를 위한 Model 객체가 어떻게 정의되어 있는지 먼저 보자.
▶ Model - Trip 객체
namespace SpringAir.Domain
{
[Serializable]
public class Trip
{
#region Fields
private TripMode mode = TripMode.RoundTrip;
private TripPoint startingFrom = new TripPoint();
private TripPoint returningFrom = new TripPoint();
#endregion
#region Constructor (s) / Destructor
public Trip()
{
}
public Trip(TripMode mode, TripPoint startingFrom, TripPoint returningFrom)
{
this.mode = mode;
this.startingFrom = startingFrom;
this.returningFrom = returningFrom;
}
#endregion
#region Properties
public TripMode Mode
{
get { return this.mode; }
set { this.mode = value; }
}
public TripPoint StartingFrom
{
get { return this.startingFrom; }
set { this.startingFrom = value; }
}
public TripPoint ReturningFrom
{
get { return this.returningFrom; }
set { this.returningFrom = value; }
}
#endregion
/// <summary>
/// Returns a <see cref="System.String"/> representation of this
/// <see cref="SpringAir.Domain.Trip"/>.
/// </summary>
/// <returns>
/// A <see cref="System.String"/> representation of this
/// <see cref="SpringAir.Domain.Trip"/>.
/// </returns>
public override string ToString()
{
StringBuilder buffer = new StringBuilder();
buffer
.Append(Mode).Append(", from ")
.Append(StartingFrom).Append(" to ")
.Append(ReturningFrom);
return buffer.ToString();
}
}
}
코드를 보면 알겠지만, Trip 클래스는 출발지와 반환지를 표현하기 위해서 TripPoint 타입을 사용해서 StartingFrom, ReturningFrom 속성으로 노출하고 있다. 그리고 그 여행이 편도인지 왕복인지를 나타내기 위해서 TripMode 타입의 Mode 속성을 노출하고 있다.
aspx.cs에서 DataSet 객체를 구성해서 바로 비즈니스 레이어 객체를 호출해서 넘기는 방식의 코딩에 익숙한 대부분의 ASP.NET 웹 애플리케이션 개발자들에게는 이런 Model 객체가 익숙하지 않을 것이다. 그러나 MVC 패턴에서는 조회나 수정에서 이런 Model 객체를 사용하게 된다. 따라서 비즈니스 설계 또한 필요하다면 이 Model 객체를 도출할 수 있도록 수정될 필요도 있을 것이다.
이제 특정 시점에서의 이 Model 객체의 정보(상태)를 출력하는 UI를 보도록 하자.
▶ View - TripForm.aspx
<asp:Content ID="body" ContentPlaceHolderID="body" runat="server">
<div style="text-align: center">
<h4>
<asp:Label ID="caption" runat="server"></asp:Label>
</h4>
<spring:ValidationSummary ID="validationSummary" runat="server" />
<table>
<tr class="formLabel">
<td> </td>
<td colspan="3">
<spring:RadioButtonGroup ID="tripMode" runat="server">
<asp:RadioButton ID="OneWay" onclick="showReturnCalendar(false);" runat="server" />
<asp:RadioButton ID="RoundTrip" onclick="showReturnCalendar(true);" runat="server" />
</spring:RadioButtonGroup>
</td>
</tr>
<tr>
<td class="formLabel" align="right">
<asp:Label ID="leavingFrom" runat="server" />
</td>
<td nowrap="nowrap">
<asp:DropDownList ID="leavingFromAirportCode" runat="server" />
<spring:ValidationError id="departureAirportErrors" runat="server" />
</td>
<td class="formLabel" align="right">
<asp:Label ID="goingTo" runat="server" />
</td>
<td nowrap="nowrap">
<asp:DropDownList ID="goingToAirportCode" runat="server" />
<spring:ValidationError id="destinationAirportErrors" runat="server" />
</td>
</tr>
<tr>
<td class="formLabel" align="right">
<asp:Label ID="leavingOn" runat="server" />
</td>
<td nowrap="nowrap">
<spring:Calendar ID="leavingFromDate" runat="server" Width="75px" AllowEditing="true" Skin="system" />
<spring:ValidationError id="departureDateErrors" runat="server" />
</td>
<td class="formLabel" align="right">
<asp:Label ID="returningOn" runat="server" />
</td>
<td nowrap="nowrap">
<div id="returningOnCalendar">
<spring:Calendar ID="returningOnDate" runat="server" Width="75px" AllowEditing="true" Skin="system" />
<spring:ValidationError id="returnDateErrors" runat="server" />
</div>
</td>
</tr>
<tr>
<td class="buttonBar" colspan="4">
<br/>
<asp:Button ID="findFlights" OnClick="SearchForFlights" runat="server"/>
</td>
</tr>
</table>
텍스트가 없는 몇개의 레이블 컨트롤이 있다. 텍스트값은 런타임시에 할당된다. 이것은 애플리케이션의 지역화와 관계된 것으로서 지금의 MVC 패턴 설명에서는 별로 중요하지 않은 부분이다. 중요한 것은 UI에 입력 컨트롤들이 있다는 것이다 . 라디오 버튼 그룹 컨트롤인 tripMode, 드롭 다운 리스트 컨트롤인 leavingFromAirportCode, goingToAirportCode 그리고 달력 컨트롤인 departureDate, returnDate 등이 배치되어 있다. 어떤 것은 ASP.NET에서 제공하는 표준 컨트롤이고 어떤 것은 Spring에서 제공하는 커스터마이징 컨트롤이다. 이 컨트롤들은 Model 객체의 상태값을 출력하게 될 것이다. 그 출력은 MVC의 Controller가 담당한다고 했다. ASP.NET에서는 코드 비하인드 페이지의 Page 객체가 담당하게 된다.
이제 이 Model 객체의 상태값을 UI 컨트롤에 출력하는 Page 객체를 보도록 하자.
▶ Controller 객체 - TripForm 페이지 객체
public partial class TripForm : Page
{
#region Fields
private const string DisplaySuggestedFlights = "displaySuggestedFlights";
private IBookingAgent bookingAgent;
private IAirportDao airportDao;
private Trip trip;
#endregion
#region Properties
/// Biz 레이어의 객체로서 Spring IoC 컨테이너에 의해서 페이지 객체에 injected된다.
public IBookingAgent BookingAgent
{
set { bookingAgent = value; }
}
/// Dao 레이어의 객체로서 Spring IoC 컨테이너에 의해서 페이지 객체에 injected된다.
public IAirportDao AirportDao
{
set { airportDao = value; }
}
/// 이 도메인 객체의 상태는 정의한 바인딩 규칙에 따라 UI의 컨트롤이 제공하는 값으로 채워진다.
public Trip Trip
{
get { return trip; }
set { trip = value; }
}
#endregion
#region Model Management and Data Binding Methods
//--> 베이스 페이지의 Init 이벤트에서 포스트백이 아닌 경우 호출된다.
protected override void InitializeModel()
{
trip = new Trip();
trip.Mode = TripMode.RoundTrip;
trip.StartingFrom.Date = DateTime.Today;
trip.ReturningFrom.Date = DateTime.Today.AddDays(1);
}
//--> 베이스 페이지의 Init 이벤트에서 포스트백인 경우 호출된다.
protected override void LoadModel(object savedModel)
{
trip = (Trip)savedModel;
}
// --> 베이스 페이지의 PreRender 이벤트에서 호출된다.
protected override object SaveModel()
{
return trip;
}
//--> 베이스 페이지의 Init 이벤트에서 포스트백인 경우 호출된다.
//--> InitializeModel()보다 먼저 호출된다.
protected override void InitializeDataBindings()
{
BindingManager.AddBinding("tripMode.Value", "Trip.Mode");
BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode");
BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode");
BindingManager.AddBinding("leavingFromDate.SelectedDate", "Trip.StartingFrom.Date");
BindingManager.AddBinding("returningOnDate.SelectedDate", "Trip.ReturningFrom.Date");
}
#endregion
#region Page Lifecycle Methods
protected override void OnInitializeControls(EventArgs e)
{
if (!IsPostBack)
{
BindAirportDropdowns();
}
}
/// 페이지가 로딩되면서, 출발지, 도착지를 나타내는 드롭다운 컨트롤이 채워진다.
private void BindAirportDropdowns()
{
ArrayList airportList = new ArrayList();
airportList.Add(new Airport(0, string.Empty, string.Empty, "-- " + GetMessage("selectAirport") + " --"));
airportList.AddRange(airportDao.GetAllAirports());
leavingFromAirportCode.DataSource = airportList;
leavingFromAirportCode.DataTextField = "Description";
leavingFromAirportCode.DataValueField = "Code";
leavingFromAirportCode.DataBind();
goingToAirportCode.DataSource = airportList;
goingToAirportCode.DataTextField = "Description";
goingToAirportCode.DataValueField = "Code";
goingToAirportCode.DataBind();
}
#endregion
#region Controller Methods
protected void SearchForFlights(object sender, EventArgs e)
{
if (Validate(trip, tripValidator))
{
FlightSuggestions suggestions = this.bookingAgent.SuggestFlights(Trip);
if (suggestions.HasOutboundFlights)
{
Session[Constants.SuggestedFlightsKey] = suggestions;
SetResult(DisplaySuggestedFlights);
}
}
}
#endregion
}
이 코드에서 어떤 일이 일어나는지 차례대로 정리해보자.
1. 페이지가 처음 로딩될때( IsPostback == false )부터 보자. InitializeModel 메소드는 페이지가 처음 로딩될때만 호출된다. 그 메소드에서는 Trip객체가 생성되고 기본 속성값으로 상태가 세팅된다. 페이지가 렌더링되기직전 즉 베이스 페이지의 PreRender 이벤트에서 SaveModel 메소드가 호출되는데 이때 Trip 객체가 베이스 클래스로 반환되어 HTTP 세션에 캐싱된다.
2. 그런 다음 페이지가 포스트백될때 LoadModel 메소드가 호출되는데 이때 베이스 클래스에서는 앞에서 HTTP 세션에 저장한 Trip 객체을 복원해서 LoadModel의 인자로 넘겨준다.
샘플 페이지에서는 Model이 Trip 객체하나로 구성되어 있지만 실제로는 여러개의 Model 객체로 구성된 딕션너리가 SaveModel 메소드에서 저장되고 LoadModel 메소드에서 복원될 것이다.
3. InitailizeDatabindings 메소드에서는 View의 컨트롤과 Model 객체의 속성들간의 바인딩 규칙을 지정하고 있다. 이 메소드는 페이지가 처음 호출될때 호출되어 바인딩 규칙을 구성하여 캐싱하고 이후부터는 캐싱된 결과를 이용한다. 바인딩 규칙을 추가하기 위해서 BindingManager 속성의 AddBinding 메소드를 사용하고 있는데, 넘겨지는 인자들이 모두 문자열로 되어 있다. 이 문자열들은 컨트롤의 속성, 적절한 Model 객체의 속성으로 파싱된다. 이때 Spring.NET의 Expression Language를 사용하게 된다. 그 파싱 규칙도 이해해 둘 필요가 있을 것이다. 이 규칙을 이해하면 폼위의 컨트롤과 Model 객체외에도 바인딩의 대상을 넓혀 좀 더 유용하게 응용할 수 있을 것이다.
4. 폼 위의 버튼에 대한 핸들러로 SearchForFlights 메소드가 정의되어 있다. 이 메소드를 보면 View의 컨트롤에 대한 참조가 전혀 없다. 이 메소드에서는 injected된 서비스 레이어의 BookingAgent 객체와 Model 객체 trip만을 사용하고 있다. 이곳에서 만약 서비스 레이어의 객체를 호출한 결과를 이용해서 Model 객체 trip의 상태를 변경하면 자동으로 View의 컨트롤의 상태 출력도 변경되어 있을 것이다.
샘플 페이지에서 MVC 패턴을 구현해서 controller 객체 즉 TripForm에서 View쪽의 컨트롤에 대한 참조를 없애는 것이었다. 즉 View와 Controller를 디커플링시키는 것이다. 비즈니스 로직이 포함될 수 있는 Controller쪽에서는 View측의 어떠한 컨트롤에 대한 참조도 없기때문에 좀 더 자유롭게 Controller쪽에 있을 지도 모르는 비즈니스 관련 코드를 좀 더 자유롭게 수정할 수 있게 된다. 이런 MVC 패턴 구현이 가능하게 된 것은 Spring.NET의 웹 프레임워크에서 제공하는 바인딩 기술때문이라는 것을 마지막으로 지적하고 싶다.
Controller 역할을 다시 생각해보자. 사용자로부터 입력된 정보를 Model 객체에 반영하고 Model 객체의 변경된 상태를 View에 출력하도록 요청한다고 했다. TriplForm 객체는 이런 일을 앞에서 말한 바인딩 기술로 구현하고 있다. 즉 View와 Model의 상태 동기화를 바인딩 기술을 이용하고 있다.
앞의 TripForm 객체가 상속받고 있는 Page는 ASP.NET에서 제공하는 표준 객체가 아니다. Spring.Web.UI에 포함된 객체로서 Spring에서 표준 페이지 객체를 상속해서 확장한 객체이다. 이 객체에서는 BindingManager라는 속성을 노출시키고 있는데 이 속성이 반환하는 객체가 "바인딩 객체"로서 View와 Model 객체간의 바인딩을 관리한다. 이 바인딩 관리자는 컨트롤과 Model 객체의 속성간의 바인딩 규칙을 개발자로부터 입력받아야 한다. 앞의 코드중에서 바인딩 규칙을 제공하는 부분을 다시 보면 다음과 같다. 페이지가 로딩되면서 다음과 같은 코드가 실행되어(처음 로딩될때. 포스트백시에는 실행되지 않는다) 바인딩 규칙을 바인드 관리자에게 알려준다.
protected override void InitializeDataBindings()
{
BindingManager.AddBinding("tripMode.Value", "Trip.Mode");
BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode");
BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode");
BindingManager.AddBinding("leavingFromDate.SelectedDate", "Trip.StartingFrom.Date");
BindingManager.AddBinding("returningOnDate.SelectedDate", "Trip.ReturningFrom.Date");
}
바인딩 관리자 및 바인딩에 대한 자세한 내용은 다음에 기회대는 대로 알아보도록 하겠다.
MVC 패턴에서 Controller가 하는 역할 즉 사용자가 입력한 정보를 Model객체에 반영하고 Model객체의 변경된 상태를 View에 반영하는 역할을 Spring.NET이 제공하는 페이지의 바인딩 관리자가 담당하고 있는 것이다. 바인딩 관리자는 View와 Model에 있는 객체를 직접 참조하는 대신에 Expression Evaluation 프레임워크( 레퍼런스 문서 11장)를 통해서 양쪽의 속성을 연결하는 것이다. Spring.NET의 Expression Evaluation 프레임워크가 MVC 패턴 구현에 핵심 역할을 하고 있다는 것을 마지막으로 지적하고 싶다. 앞에서도 말했듯이 이 Expression Evaluation 프레임워크를 좀 더 이해하는 것이 필요할 것으로 보인다. 또한 바인딩의 유효성 검사를 위해서 Validation 프레임워크( 레퍼런스 문서 12장)를 사용하고 있는데 이 또한 공부거리로 보인다.
▶ Spring.NET의 MVC 패턴 지원
View와 Controller는 모두 Model에 의존하고 있지만, Model은 View와 Controller 어떤 것도 참조하지 않고 있다는 것을 알아차리는 것이 중요하다고 보여진다. View, Controller와 Model의 분리는 UI 상관없이 Model의 테스트가 자유롭게 이뤄질 수 있다는 것이다. 또한 View와 Controller의 분리 또한 중요한 이점중의 하나이다.
이 두 이점은 모두, MVC 패턴을 이용하면 비즈니스 로직에서 UI를 분리할 수 있다는 장점을 제공할 수 있다는 것이다.
[ 추가 내용 ]
Spring.NET이 MVC 패턴을 지원하는 것은 결국 이렇게 UI와 비즈니스 로직을 분리할 수 있는 기반을 제공하고 있다는 것을 말한다.
ASP.NET 팀에서는 현재 ASP.NET MVC 프레임워크를 제작했다. 그리고 그 프레임워크에 대한 소스 코드도 제공되고 있다. 코드플렉스 사이트에서 ASP.NET MVC Preview 2 소스를 받아볼 수 있다. 코드는 Visual Studio 2008용 솔루션 파일로 묶여져 있다. 앞으로는 ASP.NET에서도 MVC 개발 패턴에 대한 적극적 지원이 있지 않겠냐는 생각이다. 해서, 이즈음해서는 애플리케이션 개발자라면 MVC 패턴의 개념 정도는 이해하고 있을 필요가 있겠다 하겠다.
'IT 살이 > 04. 기술 - 프로그래밍' 카테고리의 다른 글
개발 프레임워크 만들기 대장정 33 - Spring.NET의 Result Mapping (0) | 2009.04.24 |
---|---|
개발 프레임워크 만들기 대장정 31 - Spring.NET의 데이터 액세스 III (0) | 2009.04.24 |
개발 프레임워크 만들기 대장정 30 - Spring.NET의 데이터 액세스 II (0) | 2009.04.24 |