ASP.NET MVC의 개발이 처음인 사람을 위한 조언
최근 직장에서 ASP.NET MVC를 처음 만지면서 개발하는 사람에 대해 그 사람이 만든 코드를 보면서 코드 리뷰와 ASP.NET MVC의 기능에 대해 조언할 기회가 있었기 때문에 해당 내용을 정리해 보았다.
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 호출 전후의 공통 로직의 참고」와 동일하다.
참고 페이지
[Asp.net-mvc] MVC 4でのAuthorizeAttributeのオーバーライド
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의 집약 예외 처리
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<string>();
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. 시큐리티 대책
하기 기사의 내용과 동일.
以上