본문 바로가기
개발/ASP.NET

ASP.NET MVC의 개발이 처음인 사람을 위한 조언

by ispie 2019. 1. 28.

직장에서 ASP.NET MVC를 처음 만지면서 개발하는 사람에 대해 그 사람이 만든 코드를 보면서 코드 리뷰와 ASP.NET MVC의 기능에 대해 조언할 기회가 있었기 때문에 그 내용을 간단히 정리해 보았습니다.

 

상사의 지시였던지라 [MSDN을 참고 하세요.] 로 끝낼 수는 없는 상황이었기에ㅎㅎ....

 

 

1. 어플 설정은 배포 환경별로 분리시킨다

개발환경과 스테이징 환경, 운영환경이 각자 같은 키에 값이 다를 경우, 아래와 같이 설정하면 배포 환경별로 각자 맞는 값을 반영시킵니다.

xdt:Transform="Replace" xdt:Locator="Match(key)"

 

【Web.config】

배포 환경별로 다른 값을 설정합니다(아래 샘플 참고)

 

Web.config (예) 로컬PC)

<appSettings>
	<add key="env" value="Local"/>
</appSettings>

 

Web.Debug.config (예) 개발환경)

<appSettings>
	<add key="env" value="Test" xdt:Transform="Replace" xdt:Locator="Match(key)" />
</appSettings>

 

Web.Release.config (예) 운영환경)

<appSettings>
	<add key="env" value="Live" xdt:Transform="Replace" xdt:Locator="Match(key)" />
</appSettings>

 

 

2. Log4net의 설정

Log4net의 설정은 Web.config의 Configuration(Debug/Release) 별로 나눠서 설정 합니다.

 

Properties\AssemblyInfo.cs

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: log4net.Config.XmlConfigurator(Watch = true)]★추가

 

Web.config (예) 개발환경)

<log4net debug="true">
  <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
    <param name="File" value="C:\logs\" />
    <param name="AppendToFile" value="true" />
    <param name="MaxSizeRollBackups" value="10" />
    <param name="RollingStyle" value="date" />
    <param name="StaticLogFileName" value="false" />
    <param name="DatePattern" value='yyyy-MM-dd".log"' />
    <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
    <layout type="log4net.Layout.PatternLayout">
      <param name="ConversionPattern" value="%d [%t] %-5p - %m%n" />
    </layout>
  </appender>
    <root>
      <level value="DEBUG" />
      <appender-ref ref="RollingLogFileAppender" />
    </root>
</log4net>

 

Web.Debug.config/Web.Release.config ※환경에 맞게 설정 합니다.

<log4net debug="true" xdt:Transform="Replace">
  <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
    <param name="File" value="C:\logs\Test\" />
    <param name="AppendToFile" value="true" />
    <param name="MaxSizeRollBackups" value="10" />
    <param name="RollingStyle" value="date" />
    <param name="StaticLogFileName" value="false" />
    <param name="DatePattern" value='yyyy-MM-dd".log"' />
    <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
    <layout type="log4net.Layout.PatternLayout">
      <param name="ConversionPattern" value="%d [%t] %-5p - %m%n" />
    </layout>
  </appender>
    <root>
      <level value="DEBUG" />
      <appender-ref ref="RollingLogFileAppender" />
    </root>
</log4net>

 

 

3. Action 호출 전후의 공통 로직과 공통 인증


MVC에선 ActionFilterAttribute를 계승한 커스텀 Attribute의 내부에서 Action이 호출되는 전후의 타이밍에 공통처리를 행하는 것이 가능합니다.

 

 

3-1. Action 호출 전후의 공통 로직의 참고

 

/Filters/KariiInitAttribute.cs (예시)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Pj.Kari.Filters
{
    // 커스텀 액션 필터의 작성
    // https://msdn.microsoft.com/ja-jp/library/dd381609(v=vs.98).aspx
    public class KariInitAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);
        }
    }
}

 

KariInitAttribute 적용 예시


※Controller/Action 단위로 적용 됩니다.

[KariInit]
public class HonyararaController : BaseController

 

만약 특정의 Controller/Action의 경우엔 필터의 적용을 하고 싶지 않은 경우(Controller단위로 커스텀 Attribute를 적용시킨 경우 등), 아래의 방법으로 현재의 Controller/Action를 판별하여 대상외 처리를 하는 것도 가능합니다.

var currentController = httpContext.Request.RequestContext.RouteData.Values["controller"].ToString();
var currentAction = httpContext.Request.RequestContext.RouteData.Values["action"].ToString();

 

 

3-2. 공통 인증의 참고

 

Action이 호출되기 전에 인증 처리를 시키고 싶을 경우(로그인 유저가 권한을 가지고 있는가의 체크 등)엔 AuthorizeAttribute를 계승한 커스텀 Attribute를 사용하는 것이 바람직 합니다. 적용 예시는 「3-1. Action 호출 전후의 공통 로직의 참고」와 동일합니다.

 

 

AuthorizeAttribute Class

Override Authorize Attribute in ASP.NET MVC

 

 

MyAuthorizeAttribute

public class MyAuthorizeAttribute: AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var authorized = base.AuthorizeCore(httpContext);
        if (!authorized)
        {
            // The user is not authorized => no need to go any further
            return false;
        }

        // We have an authenticated user, let's get his username
        string authenticatedUser = httpContext.User.Identity.Name;

        // and check if he has completed his profile
        if (!this.IsProfileCompleted(authenticatedUser))
        {
            // we store some key into the current HttpContext so that 
            // the HandleUnauthorizedRequest method would know whether it
            // should redirect to the Login or CompleteProfile page
            httpContext.Items["redirectToCompleteProfile"] = true;
            return false;
        }

        return true;
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Items.Contains("redirectToCompleteProfile"))
        {
            var routeValues = new RouteValueDictionary(new
            {
                controller = "someController",
                action = "someAction",
            });
            filterContext.Result = new RedirectToRouteResult(routeValues);
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }

    private bool IsProfileCompleted(string user)
    {
        // You know what to do here => go hit your database to verify if the
        // current user has already completed his profile by checking
        // the corresponding field
        throw new NotImplementedException();
    }
}

 

 

4. 예외의 집약

ASP.NET MVC에서는 예외를 집약시켜서 처리하는 것이 가능합니다. 또한 유저의 동향을 매번 로그로 출력하는 것도 가능합니다.

 

ASP.NET MVCの集約例外処理

 

 

아래는 샘플 내용입니다.

 

Global.asax.cs

protected void Application_Start()
{
	AreaRegistration.RegisterAllAreas();
	FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);//active★
	RouteConfig.RegisterRoutes(RouteTable.Routes);
}

// Application_Error를 사용할 경우엔 아래 내용을 코멘트 아웃
//protected void Application_Error(object sender, EventArgs e)
//{
//    var exception = Server.GetLastError();
//    if (exception == null)
//    {
//        return;
//    }
//    Log.Err(exception.Message, exception);
//}

 

App_Start/FilterConfig.cs

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
	//filters.Add(new HandleErrorAttribute());//MVC Default는 코멘트 아웃

	filters.Add(new LogAttribute()); //유저의 움직임을 로그출력
	filters.Add(new ExceptionHandleAttribute()); //집약 예외 처리
}

 

Filters/ExceptionHandleAttribute.cs ※새로 파일 추가

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace Pj.Kari.Filters
{
    public class ExceptionHandleAttribute : HandleErrorAttribute
    {
        readonly log4net.ILog logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public override void OnException(ExceptionContext exceptionContext)
        {
            var controllerName = exceptionContext.RouteData.Values["controller"].ToString();
            var actionName = exceptionContext.RouteData.Values["action"].ToString();
            var exceptionMsg = exceptionContext.Exception.Message;
            var exMessage = string.Format("{0}", exceptionMsg);

            // 리퍼런스 에러
            if (exceptionContext.Exception.GetType() == typeof(NullReferenceException))
            {
                // 
            }

            // 인증 에러
            if (exceptionContext.Exception.GetType() == typeof(HttpAntiForgeryException))
            {
                exceptionContext.ExceptionHandled = true;
                exceptionContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary(
                        new
                        {
                            controller = "Home",
                            action = "Index"
                        })
                );
                return;
            }

            if (exceptionContext.ExceptionHandled)
            {
                return;
            }

            // Ajax리퀘스트 에러
            if (exceptionContext.HttpContext.Request.IsAjaxRequest())
            {
                var text = string.Empty;
                text = HttpUtility.JavaScriptStringEncode(exceptionMsg);

                exceptionContext.Result = new JavaScriptResult()
                {
                    // js를 return시키는 것이 가능
                    //Script = "alert('에러입니다.');"
                };
            }
            else
            {
                exceptionContext.Result = new ViewResult()
                {
                    ViewName = "../Error/Error", // ★Ajax리퀘스트 에러시에 표시하고 싶은 공통 에러 페이지
                    ViewData = new ViewDataDictionary
                    {
                        Model = new HandleErrorInfo(exceptionContext.Exception, controllerName, actionName)
                    }
                };
            }

            var errorMsgList = new List();
            var innerExection = exceptionContext.Exception;
            while (innerExection != null)
            {
                errorMsgList.Add(innerExection.Message);
                innerExection = innerExection.InnerException;
            }

            logger.Fatal(string.Format("{1}{0}{2}", Environment.NewLine, exMessage, string.Join(Environment.NewLine, errorMsgList)), exceptionContext.Exception);
            exceptionContext.ExceptionHandled = true;
            exceptionContext.HttpContext.Response.StatusCode = 500;
            HttpContext.Current.Response.TrySkipIisCustomErrors = true;
        }
    }
}

 

Filters/LogAttribute.cs ※새로 파일 추가

using log4net;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Mvc;

namespace Pj.Kari.Filters
{
    public class LogAttribute : ActionFilterAttribute
    {
        private ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        private Stopwatch stopwatch = new Stopwatch();

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            stopwatch.Reset();
            stopwatch.Start();

            // 유저 정보를 취득하여 현재 있는 유저가 어떤 조작을 하고 있는지 로그로 기록하는 것이 가능
            log.Debug(string.Format("▼Start...{0}.{1}",
                // 유저ID
                // 유저 이름
                filterContext.RouteData.Values["controller"],
                filterContext.RouteData.Values["action"]));
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            //filterContext.Exception

            // 리스폰스 헤더에 캐시 무효화를 추가
            var response = filterContext.HttpContext.Response;
            response.Cache.SetCacheability(HttpCacheability.NoCache);

            base.OnActionExecuted(filterContext);
        }

        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            base.OnResultExecuting(filterContext);
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            // 유저 정보를 취득하여 현재 있는 유저가 어떤 조작을 하고 있는지 로그로 기록하는 것이 가능
            stopwatch.Stop();
            log.Debug(string.Format("▲End.....{0}.{1}..........took {2}ms",
                // 유저ID
                // 유저 이름
                filterContext.RouteData.Values["controller"],
                filterContext.RouteData.Values["action"],
                stopwatch.ElapsedMilliseconds));
        }
    }
}

 

 

5. 시큐리티 대책

 

ASP.NET MVC : 어플 단위에서 설정 가능한 security 대책 방법

 

 

이상 입니다.

댓글