Error Handling

개발/jQuery 2010. 2. 14. 17:43

jQuery ASP.NET MVC 액션 메소드에서 Error Handling에 대해서 검색을 해보았습니다.
먼저 jQuery에서는 아래의 사이트처럼 처리하였습니다.

http://www.maheshchari.com/jquery-ajax-error-handling/

$().ready(function(){

      $.ajaxSetup({

            error:function(x,e){

                  if(x.status==0){

                  alert('You are offline!!\n Please Check Your Network.');

                  }else if(x.status==404){

                  alert('Requested URL not found.');

                  }else if(x.status==500){

                  alert('Internel Server Error.');

                  }else if(e=='parsererror'){

                  alert('Error.\nParsing JSON Request failed.');

                  }else if(e=='timeout'){

                  alert('Request Time out.');

                  }else {

                  alert('Unknow Error.\n'+x.responseText);

                  }

            }

      });

});

=> ajaxSetup()이라는 jQuery함수가 있네요. error State에 따라서 if문으로 필터를 해주네요한 가지 주의하실 점은 직접 $.ajax()를 호출해줄 경우에 error처리를 해주면 ajaxSetup에서 설정한 error처리 루틴은 실행되지 않는다는 것입니다.


서버쪽 액션 메소드에서는 아래의 사이트처럼 처리해주었습니다.

http://blog.dantup.com/2009/04/aspnet-mvc-handleerror-attribute-custom.html

 

 

protected override void OnException(ExceptionContext filterContext)

{ 

// Error Handle : ex) Log

 

filterContext.ExceptionHandled = true;

this.View("Error").ExecuteResult(this.ControllerContext);

}

=> Controller OnExeption메소드를 override해서 Exception true로 변경해주고 예외 발생시에 보여줘야 할 페이지와 ControllerContext ExcuteResult()에 매개변수로 넘겨주는 것을 볼 수 있습니다. ErrorHandle Attribute를 사용할 수도 있는데 제가 생각했던대로 동작하지 않았고 OnException메소드에서 모든 예외를 한꺼번에 처리하는 방법이 구조상 심플해보입니다.

 

클라이언트에서는 AjaxSetup로 서버쪽에서는 OnException메소드에서 Error를 처리해줍니다. 그런데 클라이언트에서 Ajax호출시에 서버쪽 액션 메소드에서 예외가 발생하는 경우 서버쪽에서 OnExcption에서 Error 처리해주는 로직이 수행되고나면 클라이언트의 Error의 상태가 변경되는 문제점이 있었습니다. 그렇게 크게 문제가 되지는 않는다는 생각과 일단은 상태가 변경되는 부분에 주석을 걸어주고 Unknow Error쪽의 루틴이 실행되도록 변경하였습니다.

 

 

Posted by resisa
,

이전 포스트에서 jQuery Table Plugin 중에 DataTables을 소개해드렸습니다. 이번 포스트에서는 DataTables를 사용하는 제가 알고 있는 가장 Best Practics한 사용법을 설명드리도록 하겠습니다.

먼저 필요한 기능들을 나열해보도록 하겠습니다.
 - 헤더를 클릭하여 열 정렬이 가능해야 한다.
 - Paging이 가능해야 한다. (1, 2, 3, 4 숫자로 표시되는 Paging)
 - 해당 Page의 보이는 Data만을 조회하여야 한다. (성능이 떨어지면 안된다.)
 - CSS를 통해서 사용자 정의 스타일이 가능해야 한다.

가장 중요한 기능은 Page를 나타내는 테이블을 구현하려고 하다 보니 성능적인 문제가 제일 문제가 되는데 이러한 것은 DataTables의 Server-Side를 호출해주는 방법으로 해결할 수 있습니다. DataTables의 Example을 살펴보면 Server-Side코드들이 php로 작성된 코드들이라 ASP.NET MVC에 맞는 코드를 Forum에서 찾아보았습니다. 여기서 한가지 알아야 할 사항이 있는데 Client에서 Sever쪽 액션 메소드(MVC)를 호출하고 액션 메소드의 결과를 Json타입으로 직렬화하여 리턴해줄 때의 Data형태를 DataTables이 알 수 있는 형태로 리턴을 해주어야 합니다. (물론 일반적인 List<T>타입을 Json타입으로 Serialize해준 후에 Client측에서 DataTables이 원하는 Data형태로 변경해줄 수도 있습니다.)

1. http://weblogs.asp.net/zowens/archive/2010/01/19/jquery-datatables-plugin-meets-c.aspx
2. http://datatables.net/forums/comments.php?DiscussionID=932&page=1#Item_0

첫 번째 방법으로 먼저 시도를 해보았는데 사용하는 방법이 쉽지만 'Caveat'부분을 보면 버그가 있다는 경고 메세지가 있고 실제로 시도해보았을 경우에 단순히 하나의 테이블에서 string의 타입만 있을 경우에는 문제가 없었지만 여러 가지 타입이나 테이블 조인의 경우에 문제가 있어 아쉽게 사용하지 못하게 되었습니다.

두 번째 방법을 살짝 Custom하게 고쳐서 사용해보도록 하겠습니다.

ORM은 Entity Framework를 사용하였으면 DB모델은 아래 그림과 같으며 Script파일을 첨부하였습니다.



Client-Side
var logTable;

 

$(function() {

 

    $("#logList tbody").click(function(event) {

        $(logTable.fnSettings().aoData).each(function() {

            $(this.nTr).removeClass('row_selected');

        });

        $(event.target.parentNode).addClass('row_selected');

    });

 

    logTable = $("#logList").dataTable({

        "bAutoWidth": false,

        "iDisplayLength": 10,

        "bLengthChange": false,

        "bInfo": false,

        "sPaginationType": "full_numbers",

        "bProcessing": true,

        "bServerSide": true,

        "aaSorting": [[0, "asc"]],

        "aoColumns": [

                        { "bVisible": true, "sClass": "Center" },

                        null,

                        null

                            ],

        "sAjaxSource": '<%= Url.Action("GetAllChildren", "Home") %>'

    });

});

 

<table id="logList" class="display">

    <thead>

        <tr>

            <th>P_ID</th>

            <th>P_Name</th>

            <th>C_COUNT</th>

        </tr>

    </thead>

    <tbody></tbody>

</table>

=> click()부분은 Table에서 해당 로우를 선택하였을 경우에 해당 로우만 다른 색깔로 표시하는 것이며 클릭하였을 경우에 액션을 취하고 싶을 경우에 사용하시면 됩니다. logTable이라는 변수를 선언해주는 이유는 DataTable의 sAjaxSource부분이 MVC의 액션 메소드를 호출해주는 부분인데 액션 메소드 호출시에 특정한 상황에 따라서 동적으로 변하는 매개변수를 넘길 경우가 있을 수 있습니다. 이럴경우에 logTable이라는 변수를 사용해서 sAjaxSource를 내용을 고치고 Table의 내용도 변경된 내용으로 Refresh하기 위함입니다. 기타 옵션들은 그 단어만 봐도 어떤 것인지 어느 정도는 알 수 있으며 자세한 사항은 DataTables 사이트를 참조하시기 바랍니다. DataTables를 다운 받아보시면 Demo_Table.css파일이 있는데 이 파일에서 스타일을 원하는 형태로 변경하셔서 사용하실 수 있습니다.

Server-Side Helper Class
public class GridParams

{

    public int iDisplayStart { get; set; }

    public int iDisplayLength { get; set; }

    public string sSearch { get; set; }

    public bool bEscapeRegex { get; set; }

    public int iColumns { get; set; }

    public int iSortingCols { get; set; }

    public int iSortCol_0 { get; set; }

    public string sSortDir_0 { get; set; }

    public int sEcho { get; set; }

    public bool bSortable_0 { get; set; }

    public bool bSearchable_0 { get; set; }

}

 

public class FormatedList

{

    public int sEcho { get; set; }

    public int iTotalRecords { get; set; }

    public int iTotalDisplayRecords { get; set; }

    public string[][] aaData { get; set; }

}

 

public static class QuerableExtensions

{

    public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName, bool asc)

    {

        var type = typeof(T);

        string methodName = asc ? "OrderBy" : "OrderByDescending";

        var property = type.GetProperty(propertyName);

        var parameter = Expression.Parameter(type, "p");

        var propertyAccess = Expression.MakeMemberAccess(parameter, property);

        var orderByExp = Expression.Lambda(propertyAccess, parameter);

        MethodCallExpression resultExp = Expression.Call(typeof(Queryable), methodName, new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));

        return source.Provider.CreateQuery<T>(resultExp);

    }

}

 

public static class GridsUtils

{

    public delegate string[] GetStringDelegate<T>(T obj);

 

    public static string[][] GetStrings<T>(this IQueryable<T> source, GetStringDelegate<T> del)

    {

        string[][] retArray = new string[source.Count()][];

        int i = 0;

        foreach (var s in source)

        {

            retArray.SetValue(del(s), i);

            i++;

        }

        return retArray;

    }

}

=> GridParams클래스는 열을 클릭하거나 페이지를 클릭하거나 조회 상자에서 조회하는 내용 등의 정보를 Server쪽에서 받기 쉽게 정의해놓은 클래스입니다. FormatList클래스는 위의에 말했듯이 DataTable이 인식할 수 있는 정보를 받기 위해 정의해놓은 클래스입니다. 참고적으로 iTotalDisplayRecord는 Page에 나타나는 숫자를 의미합니다. QuerableExtensions클래스는 정렬 시에 사용하기 위해서 확장해놓은 클래스인데 해당 클래스의 프로퍼티와 asc, desc인지 여부를 Expression으로 구현해놓았습니다. 그런데 헤더 정렬시에 꼭 프로퍼티 이름만으로 정렬을 하는 것이 아니라 자식 프로퍼티의 개수로도 소트할 수 있기 때문에 꼭 사용하지는 않습니다. GridsUtils클래스는 IQeuryable<T>형태로 반환된 결과물을 string[][]의 형태로 만들어주기 위한 Helper 클래스입니다.

Server-Side 액션 메소드
public ActionResult GetAllParent(GridParams gridParams)

{

    using (TestEntities Context = new TestEntities())

    {

        //data

        var query = Context.Parent.Include("Children").AsQueryable();

 

        int total = query.Count();

 

        //search whatever property you like

        if (!string.IsNullOrEmpty(gridParams.sSearch))

        {

            query = from x in query

                    where x.P_Name.Contains(gridParams.sSearch)

                    select x;

        }

 

        //sort

        if (gridParams.iSortingCols > 0)

        {

            var propertyName = "P_ID";

 

            switch (gridParams.iSortCol_0)

            {

                case 0: propertyName = "P_ID"; break;

                case 1: propertyName = "P_Name"; break;

            }

 

            query = query.OrderBy(propertyName, (gridParams.sSortDir_0 == "asc"));

        }

 

        //display

        int totaldisp = query.Count();

        if (gridParams.iDisplayLength > 0)

            query = query.Skip(gridParams.iDisplayStart).Take(gridParams.iDisplayLength);

 

        return Json(new FormatedList

        {

            sEcho = gridParams.sEcho,

            iTotalRecords = total,

            iTotalDisplayRecords = totaldisp,

            aaData = query.GetStrings(t => new string[]

            {

                t.P_ID.ToString(),

                t.P_Name,

                t.Children.Count.ToString()

            })

        });

    }

}

=> 여기서 제일 중요한 부분은 AsQueryable()로 IQueryable<T>로 리턴해주는 부분입니다. 필요한 기능목록에서 Sort와 Page시에 사용되는 매개변수(GridParams)와 함께 실질적인 쿼리만을 만들어주며 Json타입으로 Serialize해주는 부분에서 실질적인 쿼리가 DB에 질의 됩니다. 물론 그 전에 Count를 알기 위한 쿼리는 Count()를 호출해줄 때마다 질의됩니다.

그런데 아마 Page, Sort에 대해서 궁금증이 생기실 수 있습니다. 과연 얼마나 효율적으로 데이터를 가져올까요? 전체 데이터를 가져와서 asc, desc로 정렬한 이후에 가져오는 걸까요? 정답은 row_number()라는 SQL함수에 있습니다. (이전에 row_number() 대한 포스트가 있는데 참고하시기 바랍니다.)
SQL Server Profiler로 질의된 쿼리들을 살펴보도록 하겠습니다.

SELECT

[Project2].[P_ID] AS [P_ID],

[Project2].[P_Name] AS [P_Name],

[Project2].[C1] AS [C1],

[Project2].[C3] AS [C2],

[Project2].[C2] AS [C3],

[Project2].[C_ID] AS [C_ID],

[Project2].[C_Name] AS [C_Name],

[Project2].[P_ID1] AS [P_ID1]

FROM ( SELECT

       [Limit1].[P_ID] AS [P_ID],

       [Limit1].[P_Name] AS [P_Name],

       [Limit1].[C1] AS [C1],

       [Extent2].[C_ID] AS [C_ID],

       [Extent2].[C_Name] AS [C_Name],

       [Extent2].[P_ID] AS [P_ID1],

       CASE WHEN ([Extent2].[C_ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2],

       CASE WHEN ([Extent2].[C_ID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C3]

       FROM   (SELECT TOP (10) [Project1].[P_ID] AS [P_ID], [Project1].[P_Name] AS [P_Name], [Project1].[C1] AS [C1]

             FROM ( SELECT [Project1].[P_ID] AS [P_ID], [Project1].[P_Name] AS [P_Name], [Project1].[C1] AS [C1], row_number() OVER (ORDER BY [Project1].[P_ID] ASC) AS [row_number]

                    FROM ( SELECT

                           [Extent1].[P_ID] AS [P_ID],

                           [Extent1].[P_Name] AS [P_Name],

                           1 AS [C1]

                           FROM [dbo].[Parent] AS [Extent1]

                    )  AS [Project1]

             )  AS [Project1]

             WHERE [Project1].[row_number] > 0

             ORDER BY [Project1].[P_ID] ASC ) AS [Limit1]

       LEFT OUTER JOIN [dbo].[Children] AS [Extent2] ON [Limit1].[P_ID] = [Extent2].[P_ID]

)  AS [Project2]

ORDER BY [Project2].[P_ID] ASC, [Project2].[C3] ASC

=> 빨간색으로 표시된 부분을 보면 row_number()함수를 사용하였고 select절에는 TOP(10)과 where절에 row_number > 0이 눈에 들어옵니다. Page시에 사용하는 Skip()과 Take()가 쿼리로 변경되는 부분이 바로 이 부분입니다. Sort시에는 row_number()함수안에서 사용하는 Order By절과 제일 마지막에 보이는 Order By절에 의해서 Data가 정렬됩니다.
예를 들어 일반적인 게시판을 생각해보시면 보여지는 글의 건수를 10건이라고 하고 첫 번째 페이지를 보여준다면 쿼리는 TOP(10), row_number > 0이라는 조건절이 만들어져 질의됩니다. 만약에 세 번째 페이지를 보여준다면 TOP(10), row_number > 20이 조건절로 되는 것입니다. 마찬가지로 Order By는 해당 열을 처음 클릭하면 asc 두 번째로 클릭하면 desc으로 조건절이 만들어져 질의되는 것입니다. 실질적으로 row_number()가 어떻게 만들어져 있는지는 알 수 없지만 최소한 화면에 보이는 Data를 빠르게 가져온다는 사실만은 분명합니다. 또한 P_ID가 아닌 P_Name으로 정렬을 해보면 속도가 P_ID보다 느린 것을 알 수 있는데 이것은 row_number()가 인덱스와 상관이 있다는 것을 간접적으로 알 수 있습니다.
Posted by resisa
,
 

1) PostSharp Aspect Lifetime

·         PostSharp 핵심은 바로 Post-Compiler입니다. Post-Compiler 컴파일이 이후의 어셈블리를 수정하는 것을 의미합니다.

 

http://www.codeproject.com/KB/cs/ps-custom-attributes-2.aspx


·         PostSharp에서 Aspect 컴파일 타임에 생성(PostSharp Laos 의해서) 됩니다. 또한 런타임에 수정된 어셈블리를 로드하여 사용하는 복잡한(?!) Aspect Lifetime 가지고 있습니다. 이러한 과정을 자세히 알아보겠습니다.

 

·          컴파일 타임 (숫자는 실질적인 시간의 순서를 나타냅니다.)

1. Aspect 인스턴스화 : 해당 Aspect 적용될 모든 클래스(Target) Aspect 적용할 있는 코드를 만듭니다.

2. 컴파일 타임 초기화 : Aspect 인스턴스 안에 타입, 필드, 메소드를 있도록 초기화 해줍니다. 여기서 초기화 하는 타입, 필드, 메소드는 런타임에서도 사용할 있습니다.

3. 직렬화 : 새롭게 만들어진 어셈블리 리소스에 저장시키기 위해 직렬화해줍니다.

 

·          런타임

4. 1. 역직렬화 : Aspect 처음으로 사용하는 시점에 어셈블리 리소스에 저장된 정보들을 역직렬화합니다.

4. 2. 런타임 초기화 : 타입, 필드, 메소드의 속성을 초기화합니다. 여기서는 역직렬화된 필드를 사용하여 역직렬화된 타입, 필드, 메소드를 저장합니다. (다시 말해서 런타임에서는 직렬화된 정보를 역직렬화 하기 때문에 초기화 메서드에서 역직렬화된 필드를 사용하여야 컴파일 타임에 직렬화되었던 정보를 사용할 있는 것입니다.)

5. 인터셉트 메소드 호출 : Aspect 종류에 따라서 Aspect 실행되어야 시점을 인터셉트해서 호출합니다.

 

 

2) Aspect Kind

종류

내용

인터셉트 메소드

비고

On Method Boundary

메소드의 안에서 일어날 수 있는 시작점과 종료점 등과 같은 인터셉트 메소드를 제공해줌으로써 해당 시점을 인터셉트해 줄 수 있습니다. Trace 트랜잭션를 사용할 경우에 유용합니다.

OnEntry, OnSuccess, OnException, OnExit

 

On Exception

예외가 발생하는 경우에 처리해야 할 내용을 Aspect 만들 경우에 사용합니다.

OnException

 

On Field Access

모든 필드에 대한 인터셉트를 제공해줍니다.

OnGetValue, SetValue

 

On Method Invocation

On Method Boundary와 비슷합니다. 차이점은 다른 어셈블리에 있는 메소드도 인터셉트 할 수 있습니다.

OnInvocation

 

Composition

많은 Sub-Aspect 복합적으로 사용할 경우에 유용합니다.

클래스(Type)에 인터페이스를 구현해줍니다.

GetPublicInterface

CreateImplementationObject

 

Compound

많은 Sub Aspect를 복합적으로 사용할 경우에 유용합니다.

ProvideAspects

 

Implement Method

abstract or extern 메소드를 인터셉트 해줍니다.

OnExecution

 

Custom Attribute Injector

타켓에 속성  어트리뷰트를  추가할 경우에 사용합니다.

 

 

 

3) PostSharp + Enterprise Library(이하 EL)

·         EL에서도 AOP의 기능을 하는 Policy Injection Application Block(이하 PIAB)이 있습니다. PIAB는 런타임 위빙방식으로 설정파일에 의해서 Policy(정책)에 의해서 다른 Application Block를 사용합니다. PIAB MarshalByRefObject를 상속받는 개체를 요구하거나 인터페이스를 상속받는 개체를 요구합니다. (실제로 PIAB 예제를 사용해봤을 경우에는 MarshalByRefObject를 상속받지 않고서도 실행이 잘되었는데 좀 더 확인을 해봐야 할 것 같습니다.)
또한 정책에 의해서 다른 AB를 주입해주면서 Proxy, Factory 개체들을 생성하는데 바로 PostSharp4EntLib에서는 이러한 Proxy Factory를 제거하는 것을 목적으로 CodePlex에서 EL의 확장팩 정도의 개념으로 오픈소스로 공개되어 있습니다.

·         PostSharp4EntLib에서는 PostSharp의 컴파일 타임에 정책에 해당하는 객체들을 만들어놓고 런타임에 Proxy Factory 없이 사용하는 방식이라고 할 수 있습니다. 참고자료 2번에 링크로 가보시면 사용하는 방법이 3가지 나옵니다. 두 가지 아쉬운 점은 3가지 방법중에 마지막 방법이 제대로 동작하지 않는 것과 모든 방식이 정책 부분은 컴파일 타임에 만들어지지만 실질적으로 사용되는 AB의 설정은 App.config에서 불러옵니다. 예를 들어서 정책으로 Logging AB를 사용하면 정책은 컴파일 타임에 만들어지지만 Logging AB에 대한 정보는 App.config에 있어야 한다는 것입니다. 그래서 이 부분을 수정하여 현재는 Logging AB에 대해서만 App.config가 아닌 컴파일 타임에 정책이 만들어지면서 .config의 정보를 어셈블리 리소스에 저장하고 런타임에 불러와 사용하는 방식으로 변경해서 사용하고 있습니다.

4) 참고자료

1. PostSharp Doc : http://doc.postsharp.org/

2. PostSharp4EntLib : http://www.codeplex.com/entlibcontrib/Wiki/View.aspx?title=PostSharp4EntLib&referringTitle=Home

3. PIAB : http://msdn.microsoft.com/en-us/library/cc511729.aspx

 

Posted by resisa
,