강좌로 진행하려고 하는 프로그램은 다음과 같은 아이디어에서 시작합니다.
· 실시간으로 상품 주문 현황이나 통계정보를 웹페이지에서 보여주려고 합니다.
· 해당 정보는 MS SQL에 담겨져 있고 다른 기간계 시스템들에 의해서 정보가 변경되어지고 있습니다.
"이런 상황에서 웹페이지에서 MS SQL에서 생기면 실시간으로 보여지기를 원합니다."
준비
1. 기본 ASP.NET MVC 4.0 웹사이트를 만듭니다.
Visual studio를 시작하고 [File]-[New]-[Project]를 선택하여 프로젝트는 생성합니다.
좌측 트리메뉴 [Installed]-[Templates]-[Visual C#]-[Web]을 선택하고 [ASP.NET MVC 4 Web Application]프로젝트를 만듭니다. 이 부분에서 주의할 점은 이 예제를 만드는 중간에 사용할 sqlDepency관련 library와 호환문제로 인해 .NET Framework 4를 선택해줍니다. (SqlDenpency class: .net framework 2.0에서 새로 추가된 class로 SQL Server 인스턴스와 응용프로그램간의 쿼리 결과에 관한 변경을 전달받기 위한 클레스입니다. 응용프로그램에서 SqlDenpency개체를 생성하고 변경내용을 전달받을 수 있는 이벤트 핸들러 OnChangeEventHandler를 등록해주면 됩니다.) 다음으로 [Select a template]에서 [Internet Application]을 선택하고 [OK]버튼을 클릭하면 ASP.NET MVC 4 Template으로 기본 바탕이 되는 프로젝트가 생성됩니다. 2. jQuery와 Knockout.js 등의 패키지를 업데이트 해줍니다. 기본으로 설치되는 jQuery, knockout.js를 포함한 모든 NuGet Packages 모두 업데이트 해줍니다. 프로젝트에서 우 클릭하여 간편 메뉴에서 [Manage NuGet Package…]를 선택합니다. 우측 패널에서 Updates를 선택하고 중앙 패널 상단에 있는 [Update All]버튼을 클릭합니다. 밀려 있는 업데이트가 진행됩니다. 하지만 이 과정에서 1-2가지 패키지 업데이트에 에러가 발생하는 경우가 있습니다. 이런 경우에는 모두 업데이트가 종료되고 나서 다시 될때 까지 계속 반복해주거나 프로젝트를 닫고 다시 오픈하여 반복해서 업데이트를 진행해보면 되는 경우가 대부분입니다. 3. SignalR 과 Knockout Mapping JS 패키지 설치 SignalR: 실시간 웹을 구현하기 위한 간단하고 편리한 웹 개발 프레임웍입니다. Hub하는 서버의 모듈 단위로 다수의 클라이언트들이 접속하여 해당 Hub가 실시간으로 접속된 클라이언트에 정해진 방식 (javascript function을 호출하는 방식)으로 정보를 Push하므로써 실시간 communication이 가능하도록 해줍니다. Knock Mapping JS: Web page상의 MVVM(Model-View-Viewmodel) 개발 패턴이 가능하도록 ViewModel 바인딩과observing그리고 value변경에 따라 동작하는 function의 추가가 가능하게 하는 프레임윅인 Knockout JS의 sub set으로JSON으로 기술되거나 아니면 일반적인 javascript object라도 일일히 모든 속성들에 관해서 observable하는 것을 간단하게 할 수 있게 하는 라이브러리라고 생각하면 됩니다. 4. entity framework용 sqldependency이용 library 추가 및 참조 현재 문서를 작성하는 시점에 일반적으로 많이 사용하는 Entity Framework으로 만들어진 DbContext와 Scaffolding된 클래스를 사용하고 편리한 LINQ를 사용하기 위해서 현재 SqlDenpency에서 SqlCommand 개체로 변환하고 다시 이벤트 핸들러는 통해 받은 결과를 다시 다루기 위해서 아직은 복잡한 과정을 거쳐하는데 이를 위해 이미 만들어진 라이브러리를 이용하면 편리합니다. 이를 위해 entity framework를 이용해서 SqlDependency를 사용할 수 있는 library를 찾아보았으나 아직은 community version들 밖에 없으니 개인별로 다른 선택을 할 수 있지만 아래의 URL을 이용하여 다운로드 받습니다. http://www.codeproject.com/Articles/496484/SqlDependency-with-Entity-Framework-5-0 다운 받은 파일의 압축을 풉니다. 압축을 푼 폴더의 [EFChangeNotify]라는 폴더를 [Cut]합니다. 작성 중이 솔루션 폴더로 이동합니다. 해당 폴더에 [Paste]합니다. Visual studio로 이동하여 솔루션에서 우클릭하여 간편 메뉴를 열고 [Add]-[Existing Project…]을 선택합니다. 좀전에 이동한 폴더에 EFChangeNotify.csproj을 선택하여 추가합니다. 추가된 EFChangeNotify 프로젝트를 확인합니다. 추가된 프로젝트를 참조하기 위해 MVC Application1프로젝트를 우클릭하여 간편메뉴를 열고 [Add Reference…]를 선택합니다. 좌측 패널의 [Solution]-[Project]를 선택합니다. EFChangeNotify 프로젝트를 선택하고 하단의 [OK]버튼을 클릭합니다. 5. target database를 준비하고 set broker enable 먼저 예제로 사용할 test 데이터 베이스를 생성하고 샘플 테이블을 만들고 샘플 데이터를 입력합니다. Set broker enable은 MS SQL 2005부터 추가된 인스턴스간의 메세지, 큐잉서비스를 제공하는 기능인 Service Broker를 사용하기 위한 것입니다. SqlDepency가 Service Broker를 사용하기 때문에 필요한 합니다. 그리고 아래의 스크립트에서 set enable_borker하는데 너무 오랜 시간이 걸리는 아래의 쿼리를 대체해서 실행합니다. alter database [<dbname>] set enable_broker with rollback immediate; 프로그램 작성하기 1. 서버: 조회용 SignalR Hub작성 먼저 준비된 ToDoList 테이블에 내용을 읽어와서 Hub를 통해 응답하고 웹페이지에서 Knock JS로 binding하는 간단 예제를 만듭니다. 먼저 MVCApplication1프로젝트에 Models폴더에서 우클릭하여 팝업 메뉴에서 [Add]-[ADO.NET Entity Data Model]을 클릭합니다. 사용한 모델의 이름을 입력합니다. [Generate from database]를 선택하고 [Next]버튼을 클릭합니다. [New Connection]을 클릭하여 Data source에서 Microsoft SQL Server (SqlClient)를 확인하고 필요한 Server name, 서버 로그인 정보, 사용한 database를 선택하고 [OK]를 클릭합니다. 암호가 있는 경우에는 [Yes, include the sensitive data in the connection string]한 다음 원하는 entity connection settings에 사용할 이름을 정하고 다음을 클릭합니다. 여기서 입력한 이름이 DbContext이름이 된다는 걸 알고 입력합니다. 사용하기 위하는 todolist table을 선택하고 [Finish]버튼을 클릭하면 text transfer template을 통해서 DbContext와 entity를 추상화한 클래스가 만들어집니다. 아래와 같이 다이어 그램에서 추가된 테이블을 확인합니다. 다음 차례로 Global.asax.cs파일에서 Routetable에 SignalR 기본 Hub route (/SignalR) 위한 매핑을 추가해주기로 합니다. 다음 작업을 진행하기 전에 Entity data model의 Intellisense를 위해 빌드를 합니다. 그리고 프로젝트에 Hubs폴더를 추가해줍니다. Hub class로 사용할 todolistHub.cs를 Class파일로 추가해줍니다. 그리고 아래와 같이 간단한 형태의 모든 todolist를 가져와서 호출한 클라이언트로 내려보내주는 코드를 작성합니다. 2. 클라이언트: SignalR 의 클라이언트 모듈인 SignalR JS를 통해서 SignalR Hub와 통신하고 결과를 받아 올 코드를 작성하고 SignalR으로 받은 결과 JSON object를 Knockout JS를이용하여 List로 바인딩합니다. 이미 생성되어져 있는 /HOME/INDEX 내용을 없애고 아래와 같이 작업을 위한 필요한 JS Library가 추가된 기본 HTML을작성합니다. Knockout JS에서 사용할 ViewModel을 작성합니다. SignalR JS로 SignalR hub에 접속하여 getall을 호출하여 반환받은 tasks를 bind합니다. Knockout JS 템플릿으로 리스트를 만들어 줍니다. [F5]를 눌러 실행합니다. 3. 서버: SignalR Hub의 생성자와 소멸자에 각각 EFChangeNotifier 를 이용하여 테이블의 값이 변경되는 시점에 접속된 모든 클라이언트에서 해당 내용을 전달하는 프로시저를 작성하고 Dispose해주는 코드를 작성합니다. 4. client: SignalR Hub에서 보내주는 신호로 ViewModel의 해당 변경분으로 반영하기 위해ViewModel에 변경을 반영하는 메서드를 추가합니다. SignalR Hub에서 인스턴스화된 개체의 메서드를 호출해줄 수 있는 function을 작성합니다. 실시간 웹사이트에 데이트베이스의 변경이 반영되는 확인하기 아래와 그림과 같이 해당 프로그램을 실행하여 웹페이지를 열어놓고 SQL Management studio에서 해당 테이블을 열고 편집하면서 실시간으로 웹페이지에 반영되는지를 확인합니다. 예제에서 실제 추가하거나 수정한 파일은 아래와 같은 3개의 파일입니다. 1. Global.asax.cs using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace MvcApplication2 { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { //기본 "/SignalR"에 관한 Route 추가 RouteTable.Routes.MapHubs(); AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); AuthConfig.RegisterAuth(); } } } 2. Index.cshtml @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div> <table> <tbody data-bind="foreach: tasklist"> <tr> <td><span data-bind="text: id"></span></td> <td><span data-bind="text: task"></span></td> <td><span data-bind="text: status"></span></td> </tr> </tbody> </table> </div> @* 필요한 Javascript Library들*@ @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/Scripts/jquery.signalR-1.1.3.min.js") @Scripts.Render("~/Scripts/knockout-2.3.0.js") @Scripts.Render("~/Scripts/knockout.mapping-latest.js") @Scripts.Render("~/Signalr/Hubs") <script type="text/javascript" async="async"> // tasklist viewmodel: 시작 var taskListViewModel = function (tasks) { var self = this; self.tasklist = ko.mapping.fromJS([]); ko.mapping.fromJS(tasks, {}, self.tasklist); //taskListViewModel의 update를 위한 메서드 self.updateTasks = function (_tasks) { //변경이 일어난 개체를 하나 하나 루프를 돌립니다. for (var i = 0; i < _tasks.length; i++) { //럭아웃의 유틸을 이용하여 변경된 개체를 찾습니다. var updatedtask = ko.utils.arrayFilter(self.tasklist(), function (value) { return value.id() == _tasks[i].id; })[0]; // 변경된 내용을 꼼꼼이 반영합니다. updatedtask.task(_tasks[i].task); updatedtask.status(_tasks[i].status); } }; }; //tasklist viewmodel: 끝 //todolistHub의 지역복사 var todolistHub = $.connection.todolistHub; //server의 todolistHub의 getall메서드를 호출합니다. $.connection.hub.start().done( function () { todolistHub.server.getall(); } ); var _taskList = null; // 위의 코드에 의해서 호출된 getall 메서드에 의해서 응답이 라운드백될 클라이언트의initList 메서드 todolistHub.client.initList = function (_tasks) { // 서버에서 받은 _tasks라는 개체를 knockout JS에서 사용할 viewmodel로 instance화하고 지역복사합니다. _taskList = new taskListViewModel(_tasks); // html 템플릿에 바인드를 적용합니다. ko.applyBindings(_taskList); }; // SqlDependency를 통해서 서버에서 넘어온 데이터베이스의 변경분 viewmodel에 반영해줄 함수 todolistHub.client.updatedTasks = function (_tasks) { _taskList.updateTasks(_tasks); }; </script> </body> </html> 3. todolistHub.cs using EFChangeNotify; using Microsoft.AspNet.SignalR; using MvcApplication2.Models; using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace MvcApplication2.Hubs { public class todolistHub: Hub { //dispose를 위해서 지역복사할 변수를 선언합니다. EntityChangeNotifier<todolist, testEntities> _notifier = null; //생성자 public todolistHub() { // 테이블의 내용을 모두 가져올 조건으로 entitychangenotifier를 생성합니다. EntityChangeNotifier<todolist, testEntities> _notifier = newEntityChangeNotifier<todolist, testEntities>(t => true); //이벤트 프로지서를 연결합니다. _notifier.Changed += (sender, e) => { //변경된 내용을 모든 클라이언트의 updatedTasks라는 함수를 호출하여 알려줍니다. Clients.All.updatedTasks(e.Results); }; } //소멸자 ~todolistHub() { // dispose합니다. _notifier.Dispose(); } //모든 리스트를 반환하는 함수 public void getall() { //모든 todolist를 가져옵니다. List<todolist> _tasks = new List<todolist>(); using (testEntities _db = new testEntities()) { _tasks = _db.todolists.ToList(); } //호출한 클라이언트의 initList라는 함수에 넘겨줍니다. Clients.Caller.initList(_tasks); } } } 본 내용에서 일일히 다루지 않았지만 아래와 같은 내용은 본 강좌의 내용을 정확하게 이해한다면 어렵지않게 진행이 가능할 것으로 생각합니다. 서버의 추가나 삭제에 관한 것에 대한 웹페이지의 반영은 예제를 조금만 활용하면 구현이 가능합니다. 웹페이지에서 수정/삭제한 것을 DB에 반영하는 건 Knockout.js의 subscribe을 통해서 Signal R을 통해서DB에 전달하기만 하면 반영분은 기 작성된 코드를 통해서 다시 클라이언트에 반영될 것입니다. 감사합니다.
예제에 사용한 library는 개인적으로 비교적 간단한 사용법을 가지고 있는 SqlDependency with Entity Framework 5.0입니다.