JailBreak From Controllers and Actions

ASP.NET MVC 2010. 8. 30. 22:38 Posted by 알 수 없는 사용자
  ASP.NET MVC 는 가볍고 확장성이 넓으며 , 응용이 무한한 가능성을 지닌 새로운 솔루션이다. 사람들은 ASP.NET 의 ViewState 의 무게에 좌절했으며 , 스마트폰의 보급으로 인해 예전의 후퇴한 하드웨어 스팩에서는 가벼움만이 대세였다. URL과 html 소스코드는 점차 경량화 되어갔으며 , 그 과정에서 라이트 하며 , 서로간의 결합도가 낮은 어떠한 솔루션을 찾는건 필연적이었다. 그러한 배경에서 등장한 ASP.NET MVC 는 이제 출시된지 2년을 넘긴 닷넷의 주력상품중에 하나가 되었다. 그럼에도 불구하고 MVC 는 여전히 높은 진입 장벽을 가지고 있다. 특히 생산성이 강조되는 인트라넷 시스템에서는 여전히 기존 ASP.NET이 선호되고 있으며 , MVC 는 여러 장점에도 불구하고 선호도의 우선순위가 떨어지고 있는 형국이다. 그러나 이러한 원인들은 사실 선입견에 불과하며 , MVC 를 사용해볼수록 그 놀라운 확장성에 놀라게 된다. 

이러한 장점에도 불구하고 , 이 MVC 는 항상 컨트롤러 - 액션으로 불리는 콤보가 존재해야 웹페이지를 생성할수 있는 단점이 존재한다. 이는 필자의 관점에서는 오히려 asp.net 보다 퇴보한 것으로써 , aspx 페이지가 비하인드 코드 없이 생성 가능 한것에 비해 상당히 큰 제약으로 다가오게 된다. 예를 들어 사용자에게 회사 대표 이사를 소개하는 페이지를 작성한다고 해보자. 이 경우 이 페이지의 컨텐츠는 적어도 대표이사가 변경되지 않는 이상 거의 변하지 않을것이라고 기대할수 있다. 이런경우에 이 페이지에 어떠한 비하인드 코드가 들어가는것은 낭비라고 생각된다. 기존에 asp.net 은 이 부분을 html 혹은 비하인드 코드를 제거한 aspx 페이지를 이용함으로써 컴파일링 없이 손쉽게 페이지가 생성가능했다. 필자는 이부분을 MS 어필하고 싶었으나 , 여러 복잡한 사유로 인해(귀차니즘...) 전달하지 못하였었다. 그렇다면 , MVC를 이용하면서 이러한 퇴보는 받아들여져야 하는것인가? 결론을 말하자면 그렇지 않다. 이번 글에서 어떻게 Controller - Action 콤보를 피해서 자유롭게 페이지를 생성할수 있는지에 대해 설명하도록 하겠다.

 사실 이 요구 사항은 MVC가 처음 세상에 소개되었을때부터 점진적으로 요청되어 왔다고 보인다. 일단 몇가지 알려진 해결방법을 소개 하도록 하겠다.

1. mvc가 최초로 소개되었을때 소개되었던 방법이다. mvc2에서도 여전히 유효하며 , 내 생각엔 가장 널리 쓰이는 방법인것으로 보인다.
 
AvoidController.cs
public class AvoidController : Controller
{
    protected override void HandleUnknownAction(string actionName)
    {
        this.View(actionName).ExecuteResult(this.ControllerContext); 
    }
}



해당 함수가 호출되면 해당 컨트롤러 안에 액션이 존재하지 않더라도 ActionName 으로 이루어진 View 를 실행시켜준다. 아주좋다. 처음 서술했던 대부분의 이슈가 소량의 코드로 해결된다. 그렇지만 여전히 이슈는 남아있다. 이 코드는 보다시피 컨트롤러를 상속받아서 실행된다. 기본적으로 MVC는 다음과 같은 순서로 view 페이지를 탐색한다.

[컨트롤러가 존재할때의 페이지 서치]

[컨트롤러가 존재하지 않을때의 페이지 서치]


 컨트롤러가 존재하지 않는다면 해당 페이지 역시 탐색해낼수 없다. http://www.loveciel.com/Policy/EURA 와 같은 페이지가 생성된다고 가정했을때 , Policy라는 컨트롤러를 반드시 만들어야 한다.

2.
GhostController.cs
public class GhostController : Controller
{
    public ActionResult ShowPage(String folder , String view)
    {  
        return View("~/views/" + folder + "/" + view + ".aspx");
    }
}



global.asax.cs

routes.MapRoute(
"AnyView" ,
"pages/{folder}/{view}",
new {controller="Ghost" , action="ShowPage"}
);


드디어 우리는 컨트롤러와 Action 콤보에서 해방됬다. 맨앞에 'pages' 라는 페이지 주소를 항상 붙여야 한다는 사실을 제외하면 그렇다는 얘기다. 또한 이것 역시 html 파일은 읽어내지 못한다는 단점이 있다.


Here is my solution

/*
완벽한 분리를 위해서는 조금 많은 준비가 필요하다. 솔직히 말하면 거의 새로운 MVC 템플릿이라고 봐도 무관할것 같다. IoC 를 통해 컨트롤러를 동적으로 감지하고 , MasterPage 를 사용하기위한 몇가지 아이디어를 추가했다. 또한 최근 유행인 IoC 컨테이너를 적극적으로 활용하였다.(이번 예제에서는 Unity 를 사용하였다.)
*/


먼저 mvc가 파일을 찾는 구조에 대해 생각해보도록 하자.
controller/
shared/
위와 같이 이루어져 있다.
내가 설계한 개념도는 위와 같다.

[컨트롤러가 존재할때의 페이지 서치]

[컨트롤러가 존재하지 않을때의 페이지 서치]


이 설계에서는 총 3곳에 위치한 aspx , html 파일을 읽어오도록 디자인 하였다.

1. view 폴더안의 controller 에 대응되는 폴더 안에 위치한 aspx 파일
2. view 폴더안의 controller 에 대응되는 폴더 안에 위치한 html 파일
3. 지정된 폴더안에 path1 ~ 5/파일명1  에 대응되는 파일명
ex) http://www.abc.co.kr/path1/path2/path3/test.html 의 경우 지정된루트폴더\path1\path2\path3\test.html 에 있는 파일을 검색함.


Ghost&Cache Controller 는 페이지로 디자인 되지 않은 부분이기 때문에 , 해당 컨트롤러에 대응되는 폴더가 있으면 되지 않는다. 그렇기 때문에 , 해당 컨트롤러에 대응 되는 페이지는 shared 폴더에 위치시킨다

그후에 모든 임의의 것을 담을 cacheController 를 코딩한다.이 페이지는 다음과 같은 역할을 한다

페이지 파일을 stream 으로 읽음 ㅡ> 읽은 stream 을 ViewData 를 통해 html페이지로 노출시킨다.

이제 페이지 준비는 끝났다. 해당 파일을 읽어올수 있도록 모든 컨트롤러를 상속시킬 대분류 컨트롤러를 만든다

public abstract class IllusionController : Controller
    {
        public  String OriginalControllerName { get; set; }
        private String[] fileType = { "aspx", "html", "htm" };
        protected override void HandleUnknownAction(string actionName)
        {
            Boolean isExist = false;
            String viewFindPath;
            FileInfo info;

            HttpRequestBase request = Request;
            HttpResponseBase response = Response;


            foreach (var element in fileType)
            {
                viewFindPath = Server.MapPath("/") + "Views\\" + OriginalControllerName + "\\" + actionName + "." + element;
                info = new FileInfo(viewFindPath);
                if (info.Exists)
                {
                    isExist = true;
                    if (element.ToLower() == "aspx")
                    {
                        this.View(actionName).ExecuteResult(this.ControllerContext);                      
                    }
                    else
                    {
                        ViewData["CachedView"] = Ciel.Framework.Utility.StreamCache.GetCacheAbsolutePath(viewFindPath);
                        this.View("CacheBase/Result").ExecuteResult(this.ControllerContext);
                    }
                    break;
                }
            }

            if (!isExist)
            {

                String fileName = Request.RawUrl.Replace("/", " ").Trim().Replace(" ", "\\").Split('?')[0];
                if (fileName == String.Empty)
                {
                    fileName = actionName;
                }
                ViewData["CachedView"] = Ciel.Framework.Utility.StreamCache.GetCache(fileName);
                this.View("CacheBase/Result").ExecuteResult(this.ControllerContext);
            }
          
        }

    }


예제 1에서 보았던 HandleUnknownAction 을 확장 하였다.
처음 foreach 문 까지는 view 폴더 내부의 aspx , html 을 찾기위함이다. 이곳에서 찾아진 파일이 있을경우 , 그에 맞는 처리를 행한다.
여 기서 특이점은 OriginalControllerName 프로퍼티인데 , 컨트롤러를 검색할때 최초에 호출한 컨트롤러를 담기위해 사용한다. 이는 이동한 컨트롤러를 정의할때 판별되므로, 컨트롤러 개체 생성시에 값을 넘겨주어야 한다. 이부분은 이후의 코드에서 다시 설명하도록 하겠다. 이후에 view 폴더에서 파일을 찾지 못했을경우는 , 이후에 정의된 폴더로 이동해서 추가적으로 html 파일을 찾는다. 굳이 이 옵션을 추가한 이유는 , 해당 파일은 디자이너에 의해 코딩된 순수 html 일 가능성이 높기 때문에 , 개발자가 코드를 관리하는 영역과 분리하고 싶었기 때문이다. 추가적으로 이러한 html 파일은 cdn 등으로 별도의 관리가 가능한것도 이유라고 할수 있겠다.

UnityControllerFactory.cs

public override IController CreateController(RequestContext context, string controllerName)
        {
            Type type = GetControllerType(context, controllerName);

            /*
             * 작성자 : 김시원
             * 업데이트 : 10/03/08
             * 내용 : 컨트롤러가 없을때 Cache 컨트롤러로 보낸다.
             */
            if (type == null)
            {
                type = GetControllerType(context, "Cache");
                //throw new InvalidOperationException(string.Format("Could not find a controller with the name {0}", controllerName));
            }
            IUnityContainer container = GetContainer(context);
            Illusion.IllusionController resultController = (Illusion.IllusionController)container.Resolve(type);
            resultController.OriginalControllerName = controllerName;
            return (IController)resultController;
        }


Unity Controller 를 이용해서 컨트롤러 객체를 생성하는 모습이다. 이곳에서는 두가지 작업을 행한다 첫번째는 컨트롤러 개체를 생성해서 생성된 개체가 없을때는 무조건 Cache 컨트롤러를 생성하는것이고 두번째는 처음 호출이 의도되었던 컨트롤러 명을 넘겨주는것이다. 이곳에서 컨트롤러를 넘기면 , 조금전 코드에서 보았던 코드가 올바르게 동작하는것을 확인할수 있을것이다.


global.asax.cs
 
private void RegisterRoutes(RouteCollection routes)
        {
.........
 /*등록되지 않은 엑션은 전부 여기서 처리*/
            routes.MapRoute(
                "CacheRoute1",
                "{action}/{param1}",                           // 5자리 까지 가능
                new { controller = "Cache", action = "Result", id = UrlParameter.Optional }
            );
            routes.MapRoute(
                "CacheRoute2",
                "{action}/{param1}/{param2}",                           // 5자리 까지 가능
                new { controller = "Cache", action = "Result", id = UrlParameter.Optional }
            );
            routes.MapRoute(
                "CacheRoute3",
                "{action}/{param1}/{param2}/{param3}",                           // 5자리 까지 가능
                new { controller = "Cache", action = "Result", id = UrlParameter.Optional }
            );
            routes.MapRoute(
                "CacheRoute4",
                "{action}/{param1}/{param2}/{param3}/{param4}",                           // 5자리 까지 가능
                new { controller = "Cache", action = "Result", id = UrlParameter.Optional }
            );
.......

}

라 우터에서 정의되지 않은 모든 라우팅 요청은 해당 라우터로 들어가게 된다. 모든 경로는 CacheController 의 Result Action 을 호출하도록 되어 있다.이 부분에서 우리는 우리의 컨트롤러를 동작시키고자 싶어할수도 있다. 그렇다면 그 경우 해당 컨트롤러를 하나씩 등록시켜 주면 된다.


Summary

ASP.NET MVC는 아직 완벽하지 않다. MVC는 있는 그대로 쓰기에는 우리가 구현해야 하는 요소가 너무 많은것이 사실이다. 그러나 그 댓가로 개발자는 좀더 가볍고 , SEO 친화적인  결과물을 얻을수 있다. 이에 더해 asp.net 에 있던 개념을 하나라도 더 사용할수 있다면 , 더욱더 많은 사용자가 asp.net mvc 에 도전할것이다. 결론적으로 말하면 asp.net MVC 는 매력이 있으며 도전할 가치가 있다. 이 글이 asp.net mvc 베이스의 개발에 조금이나마 도움이 되었으면 하는 바램이다.