컨트롤러: 필터

등록일시: 2016-10-31 08:00,  수정일시: 2017-01-13 23:48
조회수: 9,034
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 필터(Filters)가 동작하는 방식과 구성방법을 살펴보고 간단한 예제들과 함께 각각의 필터 유형들에 관해서 살펴봅니다.

ASP.NET Core MVC에서는 필터(Filters)를 이용해서 실행 파이프라인의 특정 단계 전후에 원하는 코드를 실행할 수 있습니다. 필터는 전역으로, 컨트롤러 별로, 액션 별로 각각 구성이 가능합니다.

GitHub에서 샘플 코드 확인 및 다운로드 받기

필터의 동작 방식

각각의 필터 형식은 파이프라인의 서로 다른 단계에 실행되며, 그에 따라 자체적으로 의도된 시나리오들을 갖고 있습니다. 구현할 필터의 형식은 해당 필터로 수행할 작업과 요청 파이프라인에서 필터가 실행될 단계를 고려해서 선택해야 합니다. 필터는 MVC가 실행할 액션을 결정한 뒤에 수행되는, MVC 액션 호출 파이프라인(MVC Action Invocation Pipeline) 과정의 일부로 실행되며, 이 과정은 필터 파이프라인(Filter Pipeline)이라고 부르기도 합니다.

다양한 필터 형식들이 파이프라인의 각기 다른 시점에 실행됩니다. 예를 들어 Authorization 필터 같은 일부 필터는 파이프라인의 특정 단계 이전에 실행되고, 그 이후에는 아무런 작업도 수행하지 않습니다. 반면, 다음 그림에서 볼 수 있는 것처럼 Action 필터 같은 또 다른 필터는 파이프라인 실행상 특정 단계 이전과 이후, 모두에 실행될 수 있습니다.

필터 선택하기

Authorization 필터는 현재 사용자가 지금 처리 중인 요청에 대한 권한을 부여 받았는지 여부를 판단할 때 사용됩니다.

Resource 필터는 권한이 부여된 이후에 요청을 처리하는 가장 첫 번째 필터이자, 필터 파이프라인을 빠져 나갈때 요청에 관여할 수 있는 가장 마지막 필터 중 하나입니다. 이 필터는 캐싱을 구현하거나, 성능상의 목적으로 필터 파이프라인을 신속하게 빠져나가야 할 때 특히 유용합니다.

Action 필터는 개별 액션 메서드에 대한 호출을 감싸고, 액션으로 전달되는 인자나 반환되는 액션 결과를 조정할 수 있습니다.

Exception 필터는 처리되지 않은 예외에 대한 MVC 응용 프로그램의 전역 정책을 적용할 때 사용됩니다.

Result 필터는 개별 액션 결과(Action Results)의 실행을 감싸며, 액션 메서드가 정상적으로 실행된 경우에만 실행됩니다. 뷰 실행이나 포맷터 실행을 감싸는 로직을 구현하기에 적합한 단계입니다.

구현

모든 필터는 서로 다른 두 가지 인터페이스 정의를 통해서 동기 구현과 비동기 구현 양쪽을 모두 지원할 수 있습니다. 동기 구현 혹은 비동기 구현 여부는 수행할 작업의 유형에 따라서 선택해야 하며, 프레임워크의 관점에 따라 서로 전환이 가능합니다.

동기 필터는 두 가지 메서드, 즉 OnStageExecuting 메서드와 OnStageExecuted 메서드를 정의합니다 (Authorization 필터 같은 예외적인 경우도 존재합니다). OnStageExecuting 메서드는 Stage 명에 해당하는 이벤트 파이프라인 단계 이전에 호출되고, OnStageExecuted 메서드는 Stage 명에 해당하는 이벤트 파이프라인 단계 이후에 호출됩니다.

using FiltersSample.Helper; 
using Microsoft.AspNetCore.Mvc.Filters;  

namespace FiltersSample.Filters 
{ 
    public class SampleActionFilter : IActionFilter    
    { 
        public void OnActionExecuting(ActionExecutingContext context)
        {
            // do something before the action executes
        }

        public void OnActionExecuted(ActionExecutedContext context) 
        {    
            // do something after the action executes
        }
    }
}

반면, 비동기 필터는 Stage 명에 해당하는 파이프라인 단계의 실행을 감싸는 단일 OnStageExecutionAsync 메서드 하나만 정의합니다. OnStageExecutionAsync 메서드는 호출되서 대기 중일 때, Stage 명에 해당하는 파이프라인 단계를 실행하는 StageExecutionDelegate 대리자를 제공받습니다.

using System.Threading.Tasks; 
using Microsoft.AspNetCore.Mvc.Filters;  

namespace FiltersSample.Filters 
{ 
    public class SampleAsyncActionFilter : IAsyncActionFilter 
    { 
        public async Task OnActionExecutionAsync(
            ActionExecutingContext context, 
            ActionExecutionDelegate next) 
        {
             // do something before the action executes
             await next();
             // do something after the action executes
        }
    }
}

노트

동기 및 비동기 버전의 필터 인터페이스를 모두 구현하지 말고 두 인터페이스 중 하나만 구현하십시오. 만약, 필터에서 비동기 작업을 수행해야 한다면 비동기 인터페이스를 구현하십시오. 그렇지 않은 경우에만 동기 인터페이스를 구현하면 됩니다. 프레임워크는 먼저 필터가 비동기 인터페이스를 구현하고 있는지부터 확인한 다음, 만약 그렇다면 비동기 버전을 호출합니다. 반면 그렇지 않다면, 그때 동기 인터페이스의 메서드를 호출합니다. 따라서, 한 클래스에 두 가지 인터페이스를 모두 구현한다면 프레임워크는 비동기 메서드만 호출할 것입니다. 참고로 필터 구현 시 액션의 비동기 여부는 문제가 되지 않으며, 액션과는 독립적으로 비동기 혹은 동기로 구현할 수 있습니다.

필터 범위

필터는 세 가지 다른 수준의 범위로 설정이 가능합니다. 먼저, 특정 액션에 어트리뷰트를 적용해서 개별적으로 필터를 추가할 수 있습니다. 그리고 컨트롤러에 어트리뷰트를 적용해서 컨트롤러 내에 존재하는 모든 액션에 필터를 추가할 수도 있습니다. 마지막으로 모든 MVC 액션에 필터가 적용되도록 전역으로 필터를 등록할 수도 있습니다.

전역 필터는 Startup 파일의 ConfigureServices 메서드에서 MVC를 구성할 때 추가됩니다:

public void ConfigureServices(IServiceCollection services) 
{
    services.AddMvc(options =>
    {
        options.Filters.Add(typeof(SampleActionFilter)); // by type 
        options.Filters.Add(new SampleGlobalActionFilter()); // an instance
    });

    services.AddScoped<AddHeaderFilterWithDi>(); 
}

필터는 형식으로 추가할 수도 있고, 인스턴스로 추가할 수도 있습니다. 필터를 인스턴스로 추가하면 모든 요청에 해당 인스턴스가 사용됩니다. 반면, 형식으로 추가하면 형식 활성화(Type-Activated) 방식으로 처리되어 매번 요청 시마다 인스턴스가 생성되고 DI에 통해서 생성자의 모든 의존성이 채워지게 됩니다. 형식으로 필터를 추가하는 방식은 그 내용적으로 Filters.Add(new TypeFilterAttribute(typeof(MyFilter))) 코드와 동일합니다.

필터 인터페이스를 어트리뷰트(Attributes) 형태로 구현하면 편리한 경우가 많습니다. 필터 어트리뷰트는 컨트롤러와 액션 메서드에 적용할 수 있습니다. 프레임워크에는 서브클래싱이나 사용자 지정에 사용할 수 있는 내장 어트리뷰트-기본 필터들이 기본적으로 포함되어 있습니다. 이를테면, 다음 필터는 ResultFilterAttribute를 상속받은 다음, OnResultExecuting 메서드를 재정의해서 응답에 헤더를 추가합니다.

using Microsoft.AspNetCore.Mvc.Filters;  
                                                
namespace FiltersSample.Filters 
{ 
    public class AddHeaderAttribute : ResultFilterAttribute 
    {
        private readonly string _name;
        private readonly string _value;

        public AddHeaderAttribute(string name, string value)
        {
            _name = name;
            _value = value;
        }  
        
        public override void OnResultExecuting(ResultExecutingContext context) 
        {
            context.HttpContext.Response.Headers.Add(
                _name, new string[] { _value });
            base.OnResultExecuting(context);
        }
    } 
}

위의 예제에서 볼 수 있는 것처럼 어트리뷰트를 사용하면 필터에 인자를 전달할 수 있습니다. 다음과 같이 컨트롤러나 액션 메서드에 이 어트리뷰트를 적용한 다음, 응답에 추가하고자 하는 HTTP 헤더의 이름과 값을 지정하면 됩니다:

[AddHeader("Author", "Steve Smith @ardalis")] 
public class SampleController : Controller 
{
    public IActionResult Index()
    {
        return Content("Examine the headers using developer tools.");     
    } 
}

Index 액션의 결과는 다음과 같으며, 그림의 우측 하단에서 지정한 응답 헤더를 확인할 수 있습니다.

일부 필터 인터페이스는 사용자 지정 구현 시, 기본 클래스로 사용할 수 있는 대응하는 어트리뷰트를 갖고 있습니다.

필터 어트리뷰트:

중단하고 빠져나가기

필터 메서드에 제공되는 context 매개변수의 Result 속성을 설정하면 언제든지 필터 파이프라인을 중단하고 빠져나갈 수 있습니다. 예를 들어서, 다음 ShortCircuitingResourceFilter는 모든 액션 필터를 비롯한, 파이프라인 상 이후에 실행되는 모든 다른 필터들의 실행을 막습니다.

using System; 
using Microsoft.AspNetCore.Mvc; 
using Microsoft.AspNetCore.Mvc.Filters;  

namespace FiltersSample.Filters 
{     
    public class ShortCircuitingResourceFilterAttribute : Attribute,
            IResourceFilter     
    {
        public void OnResourceExecuting(ResourceExecutingContext context)
        { 
            context.Result = new ContentResult() 
            { 
                Content = "Resource unavailable - header should not be set" 
            }; 
        }

        public void OnResourceExecuted(ResourceExecutedContext context)
        {
        }     
    } 
}

다음 코드에는 SomeResource 액션 메서드에 ShortCircuitingResourceFilter 필터와 AddHeader 필터가 적용되어 있습니다. 그러나 ShortCircuitingResourceFilter가 먼저 실행되기 때문에 파이프라인의 나머지 단계들이 모두 중단되므로, SomeResource 액션을 대상으로는 AddHeader 필터가 실행되지 않습니다. 두 필터를 모두 액션 메서드 수준에 적용하더라도 결과는 마찬가지며, 그 이유는 ShortCircuitingResourceFilter가 먼저 실행되기 때문입니다 (실행 순서 절을 참고하십시오).

[AddHeader("Author", "Steve Smith @ardalis")] 
public class SampleController : Controller 
{ 
    [ShortCircuitingResourceFilter] 
    public IActionResult SomeResource()
    {
         return Content("Successful access to resource - header should be set.");     
    }

필터 구성하기

전역 필터는 Startup.cs 파일에서 구성됩니다. 그리고 의존성이 존재하지 않는 어트리뷰트 기반의 필터는 원하는 필터에 적합한 기본 어트리뷰트 형식을 상속받기만 하면 됩니다. 그러나 DI를 통해서 의존성을 제공받지만 전역 범위로 구성되지는 않는 필터를 생성하기 위해서는 컨트롤러나 액션에 ServiceFilterAttribute 또는 TypeFilterAttribute를 적용해야 합니다.

의존성 주입

일반적으로 어트리뷰트로 구현되고 컨트롤러 클래스나 액션 메서드에 직접 적용되는 필터는 생성자의 의존성을 의존성 주입(DI, Dependency Injection)에서 제공받을 수 없습니다. 어트리뷰트가 적용되는 시점에 생성자 매개변수를 전달받아야 하기 때문입니다. 이는 어트리뷰트 방식의 제약조건으로도 볼 수 있습니다.

그러나 필터가 DI를 통해서 제공받아야만 하는 의존성을 갖고 있는 경우를 대비해서, 이를 지원하는 몇 가지 접근 방식도 제공됩니다. 다음과 같은 어트리뷰트를 사용하면 의존성 주입이 필요한 필터를 클래스나 액션 메서드에 적용할 수 있습니다:

먼저 TypeFilter는 필터의 인스턴스를 생성하고 DI의 서비스를 이용해서 의존성을 해결합니다. 반면 ServiceFilter는 DI를 이용해서 필터의 인스턴스 자체를 가져옵니다. 다음은 ServiceFilter의 사용법을 보여주는 예제입니다:

[ServiceFilter(typeof(AddHeaderFilterWithDi))] 
public IActionResult Index() 
{
    return View(); 
}

그러나 ConfigureServices에서 필터 형식을 등록하지 않고 ServiceFilter를 사용하면 다음과 같은 예외가 던져집니다:

System.InvalidOperationException: No service for type 
'FiltersSample.Filters.AddHeaderFilterWithDI' has been registered. 

이 예외를 피하려면 반드시 ConfigureServices에서 AddHeaderFilterWithDI 형식을 등록해야 합니다:

services.AddScoped<AddHeaderFilterWithDi>(); 

ServiceFilterAttributeIFilterFactory 인터페이스를 구현하고 있으며, 이 인터페이스는 IFilter 인스턴스를 생성하는 CreateInstance라는 단일 메서드를 노출합니다. ServiceFilterAttribute는 지정된 형식을 서비스 컨테이너(DI)로부터 로드하도록 IFilterFactory 인터페이스의 CreateInstance 메서드가 구현되어 있습니다.

TypeFilterAttributeServiceFilterAttribute와 매우 비슷하지만 (그리고 역시 IFilterFactory를 구현하고 있지만), 전달된 형식에 대한 의존성을 직접 DI 컨테이너를 이용해서 해결하지 않습니다. 대신 Microsoft.Extensions.DependencyInjection.ObjectFactory를 이용해서 지정된 형식의 인스턴스를 생성합니다.

바로 이 차이점 때문에 TypeFilterAttribute를 이용해서 참조되는 형식들은 미리 컨테이너에 등록하지 않아도 무방합니다. (그러나 해당 형식 자체의 의존성은 여전히 컨테이너를 통해서 해결됩니다). 또한, TypeFilterAttribute는 선택적으로 해당 형식의 생성자의 인자를 전달 받을 수도 있습니다. 다음 예제는 TypeFilterAttribute를 이용해서 대상 형식에 인자들을 전달하는 방법을 보여줍니다:

[TypeFilter(typeof(AddHeaderAttribute),
    Arguments = new object[] { "Author", "Steve Smith (@ardalis)" })]
public IActionResult Hi(string name)
{
    return Content($"Hi {name}"); 
}

만약, 아무런 인자도 필요없지만 DI를 통해서 해결되어야 하는 생성자 의존성을 갖고 있는 간단한 필터가 필요한 경우에는, 클래스나 메서드에 TypeFilterAttribute를 상속받는 자체적으로 지정한 이름의 어트리뷰트를 적용할 수 있습니다 ([TypeFilter(typeof(FilterType))] 대신). 다음 필터는 이를 구현하는 방법을 보여줍니다:

public class SampleActionFilterAttribute : TypeFilterAttribute 
{
    public SampleActionFilterAttribute() : base(typeof(SampleActionFilterImpl))
    {
    }

    private class SampleActionFilterImpl : IActionFilter 
    {
        private readonly ILogger _logger;

        public SampleActionFilterImpl(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
        }
        
        public void OnActionExecuting(ActionExecutingContext context)
        {
            _logger.LogInformation("Business action starting...");
            // perform some business logic work
        }
        
        public void OnActionExecuted(ActionExecutedContext context)
        {
            // perform some business logic work
            _logger.LogInformation("Business action completed.");
        }
    }
}

이 필터는 [TypeFilter][ServiceFilter]를 사용하는 대신, [SampleActionFilter] 구문을 이용해서 클래스나 메서드에 적용할 수 있습니다.

노트

이미 로깅을 지원하기 위한 내장 프레임워크 로깅 기능이 제공되고 있으므로, 전적으로 로깅만을 위해서 필터를 구현하여 사용하는 것은 지양하는 것이 좋습니다. 필터에 로깅 기능을 추가하더라도, MVC 액션이나 그 밖의 프레임워크 관련 이벤트보다는 업무 도메인의 관심사나 필터의 특정 동작에 중점을 둬야합니다.

마지막으로 IFilterFactoryIFilter를 구현합니다. 따라서, IFilterFactory의 인스턴스는 필터 파이프라인 어디에서나 IFilter의 인스턴스로 사용이 가능합니다. 프레임워크는 필터 호출을 준비할 때, 먼저 필터를 IFilterFactory로 형변환할 수 있는지 시도해봅니다. 만약 이 형변환이 성공하면, 실행될 IFilter의 인스턴스를 생성하기 위해서 CreateInstance 메서드가 호출됩니다. 이런 방식은 매우 유연한 설계를 제공해주는데, 응용 프로그램이 시작될 때 필터 파이프라인을 명시적으로 정밀하게 설정할 필요가 없어지기 때문입니다.

필터를 구현할 수 있는 접근방식의 하나로, 자체적인 어트리뷰트 구현에서 IFilterFactory를 구현할 수도 있습니다:

public class AddHeaderWithFactoryAttribute : Attribute, IFilterFactory 
{
    // Implement IFilterFactory 
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) 
    { 
        return new InternalAddHeaderFilter(); 
    }
    private class InternalAddHeaderFilter : IResultFilter
    {
        public void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(
                "Internal", new string[] { "Header Added" });
        } 
            
        public void OnResultExecuted(ResultExecutedContext context)
        {
        }
    }
}

실행 순서

필터는 액션 메서드나 컨트롤러에 적용되거나 (어트리뷰트를 이용해서), 전역 필터 컬렉션에 추가될 수 있습니다. 일반적으로 이런 범위도 실행 순서를 결정하는 한 요인으로 작용합니다. 액션에 가장 가까운 필터의 실행이 가장 먼저 완료되기 때문에, 일반적으로 실행 순서를 명시적으로 지정하지 않고도 동작을 재지정할 수 있습니다. 이를 "러시안 인형" 중첩이라고도 부르는데, 마트료시카 인형처럼 범위가 증가될 때마다 이전 범위를 감싸기 때문입니다.

범위 외에 IOrderedFilter를 구현해서 필터의 실행 순서를 재정의 할 수도 있습니다. 이 인터페이스는 간단한 int Order 속성만 노출하는데, 필터는 이 속성의 숫자값을 기준으로 오름차순으로 실행됩니다. TypeFilterAttributeServiceFilterAttribute를 비롯한 모든 내장 필터는 IOrderedFilter 인터페이스를 구현하고 있으며, 클래스나 메서드에 어트리뷰트를 적용할 때 필터의 순서를 지정할 수 있습니다. 기본적으로 모든 내장 필터들의 Order 속성은 0으로 설정되어 있으므로, 범위가 타이 브레이커(Tie-Breaker)로 사용되어 결정 요인으로 작용합니다 (Order 속성을 0 이외의 값으로 설정하지 않으면).

또한, Controller 기본 클래스를 상속받는 모든 컨트롤러는 OnActionExecuting 메서드와 OnActionExecuted 메서드를 포함합니다. 이 메서드들은 특정 액션을 대상으로 실행되는 필터들을 감싸며, 가장 먼저 실행되고 가장 마지막으로 실행됩니다. 모든 필터의 Order 속성이 설정되지 않았다고 가정할 경우, 범위에 기반한 실행 순서는 다음과 같습니다:

  1. Controller 클래스의 OnActionExecuting 메서드
  2. 전역 필터의 OnActionExecuting 메서드
  3. 클래스 필터의 OnActionExecuting 메서드
  4. 메서드 필터의 OnActionExecuting 메서드
  5. 메서드 필터의 OnActionExecuted 메서드
  6. 클래스 필터의 OnActionExecuted 메서드
  7. 전역 필터의 OnActionExecuted 메서드
  8. Controller 클래스의 OnActionExecuted 메서드

노트

필터의 실행 순서를 결정할 때, IOrderedFilter의 우선 순위가 범위보다 높습니다. 먼저 Order 속성에 의해서 순서가 결정된 다음, 범위를 이용해서 같은 순위의 필터 순서를 다시 결정합니다. 별도로 설정하지 않으면 Order 속성의 기본값은 0입니다.

범위에 기반한 기본 실행 순서를 변경하려면, 클래스 수준 필터나 메서드 수준 필터의 Order 속성을 명시적으로 설정하면 됩니다. 예를 들어, 다음과 같이 메서드 수준 어트리뷰트에 Order=-1를 추가할 수 있습니다:

[MyFilter(Name = "Method Level Attribute", Order=-1)] 

이 경우, 0보다 작은 값을 지정했기 때문에 이 필터가 전역 필터나 클래스 수준 필터보다 (Order 속성을 설정하지 않았다고 가정할 경우) 먼저 실행됩니다.

새로운 실행 순서는 다음과 같습니다:

  1. Controller 클래스의 OnActionExecuting 메서드
  2. 메서드 필터의 OnActionExecuting 메서드
  3. 전역 필터의 OnActionExecuting 메서드
  4. 클래스 필터의 OnActionExecuting 메서드
  5. 클래스 필터의 OnActionExecuted 메서드
  6. 전역 필터의 OnActionExecuted 메서드
  7. 메서드 필터의 OnActionExecuted 메서드
  8. Controller 클래스의 OnActionExecuted 메서드

노트

Controller 클래스의 메서드들은 항상 다른 필터들 보다 먼저 실행되고 나중에 실행됩니다. 이 메서드들은 IFilter 인스턴스의 형태로 구현되지 않았기 때문에, IFilter의 실행 순서 알고리즘의 영향을 받지 않습니다.

Authorization 필터

Authorization 필터는 액션 메서드에 대한 접근을 제어하는, 필터 파이프라인에서 가장 먼저 실행되는 필터입니다. 다른 대부분의 필터들이 각 단계의 이전과 이후에 실행되는 메서드를 모두 지원하는 것과는 달리, 이 필터는 단계 이전에 실행되는 메서드만 지원합니다. 사용자 지정 Authorization 필터는 직접 권한부여 프레임워크를 개발할 경우에만 구현해야 합니다. 이때 주의해야 할 점은 Authorization 필터 내부에서는 예외를 처리할 주체가 존재하지 않기 때문에 예외를 던지면 안된다는 사실입니다 (Exception 필터는 해당 예외를 처리하지 않습니다). 대신 챌린지(Challenge)를 발급하거나 다른 방법을 찾아야 합니다.

권한부여에 관해서 더 자세히 알아보시기 바랍니다.

Resource 필터

Resource 필터IResourceFilter 인터페이스나 IAsyncResourceFilter 인터페이스를 구현하고, 그 실행으로 필터 파이프라인의 거의 대부분의 영역을 감싸는 필터입니다 (오직 Authorization 필터만 그전에 실행되며, 다른 모든 필터 및 액션의 처리는 Resource 필터의 OnResourceExecuting 메서드와 OnResourceExecuted 메서드 사이에서 수행됩니다). Resource 필터는 요청이 수행하는 대부분의 작업을 그대로 중단하고 신속하게 빠져나가야 할 때 특히 유용합니다. 캐싱은 Resource 필터의 한 가지 활용 사례로, 캐시에 응답이 이미 존재하는 경우 필터가 즉시 결과를 설정하고 액션의 나머지 처리 과정들을 중단하게 됩니다.

이미 앞에서 살펴본 ShortCircuitingResourceFilterAttribute도 Resource 필터의 한 가지 사례입니다. 다음은 ContentResult 액션 결과만 대상으로 동작하는 상당히 단편적인 캐시 구현을 보여줍니다 (운영 환경에서는 사용하지 마십시오):

public class NaiveCacheResourceFilterAttribute : Attribute, 
    IResourceFilter 
{
    private static readonly Dictionary<string, object> _cache
                = new Dictionary<string, object>();
    private string _cacheKey;
    
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        _cacheKey = context.HttpContext.Request.Path.ToString();
        if (_cache.ContainsKey(_cacheKey))
        {
            var cachedValue = _cache[_cacheKey] as string;
            if (cachedValue != null)    
            { 
                context.Result = new ContentResult() 
                { Content = cachedValue };
            }
        }     
    }      
           
    public void OnResourceExecuted(ResourceExecutedContext context)     
    {
        if (!String.IsNullOrEmpty(_cacheKey) &&    
            !_cache.ContainsKey(_cacheKey))
        { 
            var result = context.Result as ContentResult;    
            if (result != null)    
            { 
                _cache.Add(_cacheKey, result.Content); 
            }
        }     
    } 
}

먼저 이 필터의 OnResourceExecuting 메서드에서는 정적 사전 캐시에 이미 결과가 존재할 경우, contextResult 속성을 설정함으로써 액션을 중지하고 빠져나가면서 캐시된 결과를 반환합니다. 그리고 OnResourceExecuted 메서드에서는 현재 요청의 키가 아직 사용되고 있지 않을 경우, 이후 요청에서 사용하기 위해 현재 Result를 캐시에 저장합니다.

이 필터는 다음과 같이 클래스나 메서드에 적용할 수 있습니다:

[TypeFilter(typeof(NaiveCacheResourceFilterAttribute))] 
public class CachedController : Controller 
{     
    public IActionResult Index()     
    { 
        return Content("This content was generated at " + DateTime.Now);     } 
}

Action 필터

Action 필터IActionFilter 인터페이스나 IAsyncActionFilter 인터페이스를 구현하고, 그 실행으로 액션 메서드의 실행을 감싸는 필터입니다. Action 필터는 모델 바인딩의 결과를 확인해야 하거나 컨트롤러 또는 액션 메서드에 대한 입력을 수정해야 하는 로직을 구현하기에 적합합니다. Action 필터는 액션 메서드의 결과를 살펴보고 직접적으로 수정할 수도 있습니다.

OnActionExecuting 메서드는 액션 메서드가 실행되기 직전에 호출되며, ActionExecutingContext.ActionArguments를 변경해서 액션에 전달되는 입력을 조작하거나 ActionExecutingContext.Controller를 통해서 컨트롤러를 조작할 수 있습니다. OnActionExecuting 메서드에서도 ActionExecutingContext.Result를 설정해서 액션 메서드와 뒤이은 일련의 필터들의 실행을 중단하고 빠져나갈 수 있습니다. OnActionExecuting 메서드에서 예외를 던져도 액션 메서드와 뒤이은 일련의 필터들의 실행을 중단할 수는 있지만, 이 경우에는 성공적인 결과가 아닌 실패로 간주되어 처리됩니다.

OnActionExecuted 메서드는 액션 메서드가 실행된 이후에 호출되며, ActionExecutedContext.Result 속성을 통해서 액션의 결과를 살펴보고 조작할 수 있습니다. 다른 필터에 의해서 액션의 실행이 중단되어 빠져나갈 경우, ActionExecutedContext.Canceled가 true로 설정됩니다. 액션이나 액션에 뒤이은 Action 필터에서 예외를 던지는 경우에는 ActionExecutedContext.Exception이 null이 아닌 값으로 설정됩니다. ActionExecutedContext.Exception를 null로 설정하면 예외가 '처리된 것으로 간주'되어, 결과적으로 ActionExectedContext.Result는 마치 액션 메서드로부터 정상적으로 반환된 것처럼 실행됩니다.

IAsyncActionFilter 인터페이스에서는 OnActionExecuting 메서드와 OnActionExecuted 메서드에서 처리가능한 모든 작업을 OnActionExecutionAsync 메서드에서 수행합니다. ActionExecutionDelegateawait next() 메서드를 호출하면 모든 뒤이은 일련의 액션 필터들과 액션 메서드가 실행되고, ActionExecutedContext가 반환됩니다. OnActionExecutionAsync 메서드 내부에서 필터 파이프라인의 실행을 중단하고 빠져나가려면, 임의의 결과 인스턴스를 ActionExecutingContext.Result에 할당하고 ActionExectionDelegate를 호출하지 않으면 됩니다.

Exception 필터

Exception 필터IExceptionFilter 인터페이스나 IAsyncExceptionFilter 인터페이스를 구현합니다.

Exception 필터는 컨트롤러 생성이나 모델 바인딩 중에 발생한 예외를 비롯한 처리되지 않은 예외들을 처리합니다. 이 필터는 파이프라인에서 예외가 발생하는 경우에만 호출됩니다. 이 필터를 활용하면 응용 프로그램 내부에서 공통 오류 처리 정책을 구현하는 단일 장소를 제공할 수 있습니다. 프레임워크에서는 필요한 경우 서브클래싱 할 수 있는 추상 ExceptionFilterAttribute가 제공됩니다. Exception 필터는 MVC 액션에서 발생한 예외를 처리하기에 적합한 장소지만, 오류 처리 미들웨어보다는 유연하지 않습니다. 일반적인 경우에는 미들웨어를 사용하고 선택된 MVC 액션별로 개별적 오류 처리를 수행해야 하는 경우에만 필터를 사용하는 것이 좋습니다.

액션 별로 개별적 오류 처리가 필요한 한 가지 사례는 응용 프로그램에서 API 끝점과 뷰/HTML을 반환하는 액션을 모두 노출하고 있는 경우입니다. 이런 경우, API 끝점에서는 오류 정보를 JSON 형태로 반환해야 하는 반면, 뷰 기반의 액션에서는 HTML 오류 페이지를 반환해야 합니다.

다른 필터들과는 달리 Exception 필터는 두 가지 이벤트를 제공하지 않으며 (단계 이전과 이후에 대한), OnException 메서드만 (또는 OnExceptionAsync 메서드만) 구현합니다. OnException 메서드에 매개변수로 제공되는 ExceptionContext 형식에는 발생한 예외가 담겨있는 Exception 속성이 존재합니다. context.ExceptionHandled 속성을 true로 설정하면, 예외가 처리된 것으로 간주되어 마치 예외가 발생하지 않았던 것처럼 요청이 처리됩니다 (일반적으로 200 OK 상태가 반환됩니다). 다음 필터는 사용자 지정 개발자 오류 뷰를 이용해서, 응용 프로그램을 개발하는 동안 발생하는 예외의 세부 정보를 출력합니다:

using Microsoft.AspNetCore.Hosting; 
using Microsoft.AspNetCore.Mvc; 
using Microsoft.AspNetCore.Mvc.Filters; 
using Microsoft.AspNetCore.Mvc.ModelBinding; 
using Microsoft.AspNetCore.Mvc.ViewFeatures;  

namespace FiltersSample.Filters 
{
    public class CustomExceptionFilterAttribute : ExceptionFilterAttribute     
    {
        private readonly IHostingEnvironment _hostingEnvironment;
        private readonly IModelMetadataProvider _modelMetadataProvider; 
        
        public CustomExceptionFilterAttribute(    
            IHostingEnvironment hostingEnvironment,    
            IModelMetadataProvider modelMetadataProvider)
        {    
            _hostingEnvironment = hostingEnvironment;    
            _modelMetadataProvider = modelMetadataProvider;
        } 
        
        public override void OnException(ExceptionContext context)
        {    
            if (!_hostingEnvironment.IsDevelopment())    
            {        
                // do nothing        
                return;    
            }    
            var result = new ViewResult {ViewName = "CustomError"};
            result.ViewData = new ViewDataDictionary(_modelMetadataProvider,context.ModelState);    
            result.ViewData.Add("Exception", context.Exception);    
            // TODO: Pass additional detailed data via ViewData 
            context.ExceptionHandled = true; // mark exception as handled
            context.Result = result; 
        }  
    } 
}

Result 필터

Result 필터IResultFilter 인터페이스나 IAsyncResultFilter 인터페이스를 구현하고, 그 실행으로 액션 결과의 실행을 감싸는 필터입니다. Result 필터는 액션이나 액션 필터가 액션 결과를 생성하는 성공한 결과를 대상으로만 실행됩니다. Exception 필터가 예외를 처리하는 경우에는, Exception 필터에서 Exception = null로 설정하지 않는한 Result 필터는 실행되지 않습니다.

노트

실행되는 결과의 유형은 해당 액션에 따라서 좌우됩니다. 뷰를 반환하는 MVC 액션은 실행되는 ViewResult의 일부분으로 모든 Razor 처리 과정이 포함됩니다. 반면 API 메서드의 경우에는 특정 직렬화 과정이 결과 실행의 일부분으로 수행됩니다. 액션 결과(Action Results)에 관해서 더 자세히 알아보시기 바랍니다.

Result 필터는 직접적으로 뷰의 실행이나 포맷터의 실행을 감싸야 하는 로직을 구현하기에 적합합니다. 응답을 생성하는 역할을 수행하는 액션 결과를 Result 필터에서 대체하거나 수정할 수 있습니다.

OnResultExecuting 메서드는 액션 결과가 실행되기 직전에 호출되며, ResultExecutingContext.Result를 이용해서 액션 결과를 조작할 수 있습니다. OnResultExecuting 메서드에서도 ResultExecutingContext.Canceltrue로 설정해서 액션 메서드와 뒤이은 Result 필터들의 실행을 중단하고 빠져나갈 수 있습니다. 그러나 이런 경우에는 MVC가 응답을 생성하지 않기 때문에, 중단으로 인한 빈 응답이 생성되는 것을 피하기 위해서는 일반적으로 응답 개체를 직접 작성해야 합니다. OnResultExecuting 메서드에서 예외를 던져도 액션 결과와 뒤이은 필터들의 실행을 중단할 수는 있지만, 이 경우에는 성공적인 결과가 아닌 실패로 간주되어 처리됩니다.

OnResultExecuted 메서드는 액션 결과가 실행된 이후에 호출됩니다. 이 시점까지 예외가 던져지지 않았다면, 응답이 클라이언트로 전송된 것이나 다름없는 상태이므로 더 이상 수정이 불가능합니다. 다른 필터에 의해서 액션 결과의 실행이 중단되어 빠져나갈 경우, ResultExecutedContext.Canceled가 true로 설정됩니다. 액션 결과나 뒤이은 Result 필터에서 예외를 던진 경우에는 ResultExecutedContext.Exception이 null이 아닌 값으로 설정됩니다. ResultExecutedContext.Exception을 null로 설정하면 예외가 '처리된 것으로 간주'되어 파이프라인의 이후 단계에서 MVC에 의해 예외가 다시 던져지지 않게 됩니다. Result 필터에서 예외를 처리하는 경우에는 응답에 데이터를 기록하기에 적합한 상태인지 여부를 고려해야 합니다. 만약, 액션 결과가 실행되는 도중에 예외를 던졌지만 이미 헤더가 클라이언트로 전송된 경우에는, 다시 실패 코드를 전송할 수 있는 신뢰할 수 있는 메커니즘이 존재하지 않습니다.

IAsyncResultFilter 인터페이스에서는 OnResultExecuting 메서드와 OnResultExecuted 메서드에서 처리가능한 모든 작업을 OnResultExecutionAsync 메서드에서 수행합니다. ResultExecutionDelegateawait next() 메서드를 호출하면 모든 뒤이은 일련의 Result 필터들과 액션 결과가 실행되고, ResultExecutedContext가 반환됩니다. OnResultExecutionAsync 메서드 내부에서 필터 파이프라인의 실행을 중단하고 빠져나가려면, ResultExecutingContext.Cancel를 true로 설정하고 ResultExectionDelegate를 호출하지 않으면 됩니다.

Result 필터를 구현하려면 ResultFilterAttribute를 재지정하면 됩니다. 이미 본문에서는 AddHeaderAttribute 클래스를 통해서 Result 필터의 사례를 살펴봤습니다.

응답에 헤더를 추가해야 한다면 액션 결과가 실행되기 전에 작업을 수행해야 합니다. 그렇지 않으면, 이미 클라이언트로 응답이 전송되었을 것이므로 응답을 수정하기에는 너무 늦은 시점이 될 것입니다. 결론적으로 Result 필터에서는 OnResultExecuted 메서드보다는 OnResultExecuting 메서드에서 헤더를 추가해야 합니다.

필터 vs. 미들웨어

일반적으로 필터는 업무나 응용 프로그램의 횡단(Cross-Cutting) 관심사를 처리하기 위해서 사용됩니다. 이는 미들웨어의 사용 사례와 겹치는 경우가 많습니다. 필터는 미들웨어와 매우 유사한 기능을 제공해주지만, 동작의 범위를 지정할 수 있고, 뷰 처리 직전이나 모델 바인딩 이후 같은 응용 프로그램 내부의 적절한 위치에 삽입이 가능합니다. 또한 필터는 MVC의 일부로서 컨텍스트와 구조에 접근할 수 있습니다. 가령, 미들웨어는 오류를 발생시킨 특정 요청에 대한 모델의 유효성을 감지해서 적절히 대응하기가 어렵지만, 필터를 이용하면 아주 간단하게 처리할 수 있습니다.

필터에 대해 더 자세히 살펴보려면 예제를 다운로드 받아서 수정하거나 테스트해보시기 바랍니다.