ASP.NET Core 미들웨어의 기초

등록일시: 2017-06-01 08:00,  수정일시: 2017-10-21 13:03
조회수: 10,214
이 문서는 ASP.NET Core 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 ASP.NET Core 응용 프로그램의 미들웨어에 관해서 알아봅니다.

예제 코드 살펴보기 및 다운로드

미들웨어란

미들웨어는 요청 및 응답을 처리하기 위해서 응용 프로그램의 파이프라인으로 조립되는 소프트웨어입니다. 각 구성 요소는 파이프라인의 다음 구성 요소로 요청을 전달할지 여부를 선택하고, 파이프라인의 다음 구성 요소가 호출되기 이전 및 이후에 특정 동작을 수행할 수 있습니다. 요청 파이프라인 구축에는 요청 대리자(Request Delegates)가 사용되며, 요청 대리자는 각 HTTP 요청을 처리합니다.

요청 대리자는 Startup 클래스의 Configure 메서드에 전달되는 IApplicationBuilder 인스턴스가 제공하는 Run, MapUse 확장 메서드를 이용해서 구성됩니다. 각각의 요청 대리자는 인라인 익명 메서드로 지정될 수도 있고 (이를 인라인 미들웨어라고 합니다), 재사용 가능한 별도의 클래스로 정의될 수도 있습니다. 이런 재사용 가능한 클래스 및 인라인 익명 메서드를 미들웨어(Middleware) 또는 미들웨어 구성 요소(Middleware Components)라고 합니다. 요청 파이프라인의 각 미들웨어 구성 요소는 파이프라인의 다음 구성 요소를 호출하거나, 필요한 경우 적절한 방식으로 호출 체인을 중단하고 빠져나가야 (Short-Circuiting) 합니다.

Migrating HTTP Modules to Middleware 문서에서는 ASP.NET Core와 이전 버전 간의 요청 파이프라인의 차이점을 설명하고, 더 다양한 미들웨어 예제를 살펴봅니다.

IApplicationBuilder를 이용해서 미들웨어 파이프라인 생성하기

ASP.NET Core의 요청 파이프라인은 다음 도표에서 볼 수 있는 것처럼, 순차적으로 다른 요청 대리자를 호출하는 일련의 요청 대리자들로 구성됩니다 (실행 흐름은 검은색 화살표를 따릅니다):

각 대리자는 다음 대리자가 호출되기 전이나 후에 작업을 수행할 수 있습니다. 또한 대리자는 요청을 다음 대리자로 전달하지 않기로 결정할 수도 있는데, 이를 요청 파이프라인을 중단하고 빠져나간다(Short-Circuiting)라고 표현합니다. 불필요한 작업을 수행할 필요가 없는 경우에는 요청 파이프라인을 중단하고 빠져나가는 것이 바람직한 경우가 많습니다. 예를 들어서, 정적 파일 미들웨어는 정적 파일에 대한 요청을 반환한 다음, 파이프라인의 나머지 미들웨어들을 실행하지 않고 그대로 빠져나갑니다. 또한, 예외 처리 대리자는 파이프라인의 초기에 호출되어야 하는데, 그래야만 파이프라인의 이후 단계에서 발생하는 예외를 잡을 수 있기 때문입니다.

가장 간단한 ASP.NET Core 응용 프로그램은 모든 요청을 처리하는 단일 요청 대리자만 설정하는 경우입니다. 이 경우에는 사실상 요청 파이프라인이 존재하지 않는 것으로 봐도 무방합니다. 대신, 모든 HTTP 요청에 대한 응답으로 단일 익명 함수가 호출됩니다.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}

app.Run 대리자는 파이프라인을 그대로 종료합니다.

반면 app.Use 확장 메서드를 사용하면 다수의 요청 대리자를 서로 연결할 수 있습니다. 이때, next 매개 변수는 파이프라인 상의 다음 대리자를 나타냅니다. (next 매개 변수를 호출하지 않음으로써 파이프라인을 중단하고 빠져나갈 수 있다는 점을 기억해두시기 바랍니다.) 일반적으로 다음 예제에서 볼 수 있는 것처럼, 다음 대리자를 호출하는 전후에 필요한 작업을 수행합니다:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // Do work that doesn't write to the Response.
            await next.Invoke();
            // Do logging or other work that doesn't write to the Response.
        });

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from 2nd delegate.");
        });
    }
}
경고

응답이 클라이언트로 전송된 이후에는 next.Invoke를 호출하지 마십시오. 응답이 전송되기 시작된 뒤에 HttpResponse를 변경하면 예외가 발생합니다. 가령, 헤더나 상태 코드 등을 설정하면 예외가 발생할 것입니다. next.Invoke를 호출한 뒤에 응답 본문을 작성하는 경우에도:

  • 프로토콜 위반이 발생할 수 있습니다. 예를 들어서, 명시된 content-length보다 긴 내용이 작성될 수 있습니다.
  • 본문 형식이 손상될 수 있습니다. 예를 들어서, CSS 파일에 HTML 바닥글이 작성될 수 있습니다.

HttpResponse.HasStarted는 헤더가 이미 전송됐는지 또는 본문이 이미 작성됐는지 여부를 나타내는 유용한 힌트를 제공해줍니다.

순서

미들웨어 구성 요소가 Configure 메서드에 추가되는 순서에 의해서 미들웨어가 호출되는 순서가 결정됩니다. 요청 시에는 추가된 순서대로 호출되고 응답 시에는 그 역순으로 호출됩니다. 이 순서는 보안, 성능 및 기능에 민감한 영향을 줍니다.

아래 예제의 Configure 메서드는 다음과 같은 미들웨어 구성 요소들을 추가하고 있습니다:

  1. 예외/오류 처리
  2. 정적 파일 서버
  3. 인증
  4. MVC
public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
                                            // thrown in the following middleware.

    app.UseStaticFiles();                   // Return static files and end pipeline.

    app.UseIdentity();                      // Authenticate before you access
                                            // secure resources.

    app.UseMvcWithDefaultRoute();           // Add MVC to the request pipeline.
}

이 코드에서 UseExceptionHandler는 파이프라인에 추가되는 첫번째 미들웨어 구성 요소입니다. 따라서 이후에 호출되는 모든 미들웨어에서 발생하는 모든 예외를 잡을 수 있습니다.

정적 파일 미들웨어는 파이프라인의 초기에 호출되어 정적 파일에 대한 요청을 처리한 다음, 나머지 구성 요소들을 거치지 않고 파이프라인을 중단하고 빠져나갑니다. 정적 파일 미들웨어는 권한 부여 검사를 제공하지 않습니다. wwwroot 디렉터리 하위의 파일들을 비롯한, 이 미들웨어가 제공하는 모든 파일은 공개적으로 사용 가능합니다. 정적 파일의 보안 방법에 대해서는 정적 파일 서비스하기 문서를 참고하시기 바랍니다.

정적 파일 미들웨어에 의해서 처리되지 않은 요청은 인증을 수행하는 Identity 미들웨어로 (app.UseIdentity) 전달됩니다. 그러나 Identity 미들웨어는 인증되지 않은 요청을 중단하고 빠져나가지는 않습니다. Identity가 요청을 인증하기는 하지만, MVC가 특정 컨트롤러와 액션을 선택한 뒤에만 권한 부여가 (또는 거부가) 발생합니다.

다음 예제는 응답 압축 미들웨어 이전에 정적 파일 미들웨어에 의해서 정적 파일에 대한 요청이 처리되는 미들웨어 순서를 보여줍니다. 미들웨어의 순서가 이렇게 구성된 상태에서는 정적 파일이 압축되지 않습니다. 반면 UseMvcWithDefaultRoute의 응답은 압축될 수 있습니다.

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();         // Static files not compressed
                                  // by middleware.
    app.UseResponseCompression();
    app.UseMvcWithDefaultRoute();
}

Run, Map 및 Use 메서드

HTTP 파이프라인은 Run, MapUse 메서드를 이용해서 구성됩니다. Run 메서드는 필요한 작업을 수행한 후 파이프라인을 중단하고 빠져나갑니다 (즉, next 요청 대리자를 호출하지 않습니다). Run이라는 메서드 이름은 규약이며, 일부 미들웨어 구성 요소는 파이프라인의 가장 마지막에서 실행되는 Run[Middleware] 메서드를 노출합니다.

Map* 확장은 파이프라인을 분기하기 위한 규약으로 사용됩니다. Map은 지정한 요청 경로와 일치하는지 여부에 따라서 요청 파이프라인을 분기합니다. 만약 요청 경로가 지정한 경로로 시작하면 해당 분기가 실행됩니다.

public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

    private static void HandleMapTest2(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 2");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

다음의 표는 위의 코드를 사용할 경우, http://localhost:1234의 요청 및 응답을 보여줍니다:

요청 응답
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

Map을 사용하면, 각 요청마다 일치하는 경로 세그먼트가 HttpRequest.Path에서 제거되고 HttpRequest.PathBase에 추가됩니다.

MapWhen은 지정한 조건자의 결과에 따라서 요청 파이프라인을 분기합니다. Func<HttpContext, bool> 형식의 조건자를 지정해서 파이프라인의 새로운 분기로 요청을 맵핑할 수 있습니다. 다음 예제에서는 branch라는 쿼리 문자열 변수가 존재하는지 여부를 감지하기 위해서 조건자를 사용합니다:

public class Startup
{
    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranch);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

다음의 표는 위의 코드를 사용할 경우, http://localhost:1234의 요청 및 응답을 보여줍니다:

요청 응답
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=master Branch used = master

Map은 중첩을 지원합니다. 다음 그 사례입니다:

app.Map("/level1", level1App => {
    level1App.Map("/level2a", level2AApp => {
        // "/level1/level2a"
        //...
    });
    level1App.Map("/level2b", level2BApp => {
        // "/level1/level2b"
        //...
    });
});

Map은 한 번에 여러 단계로 구성된 세그먼트도 지원합니다:

app.Map("/level1/level2", HandleMultiSeg);

내장 미들웨어

ASP.NET Core는 기본적으로 다음과 같은 미들웨어 구성 요소를 제공합니다:

미들웨어 설명
인증 인증 기능을 지원합니다.
CORS 원본 간 리소스 공유(CORS)를 구성합니다.
응답 캐시 응답을 캐시하는 기능을 지원합니다.
응답 압축 응답을 압축하는 기능을 지원합니다.
라우팅 요청 라우트를 정의하고 제약합니다.
세션 사용자 세션 관리 기능을 지원합니다.
정적 파일 정적 파일 서비스 및 디렉터리 브라우징 기능을 지원합니다.
URL 재지정 미들웨어 URL을 재작성하고 요청을 재지정하는 기능을 지원합니다.

미들웨어 작성하기

일반적으로 미들웨어는 클래스로 캡슐화되어 확장 메서드와 함께 제공됩니다. 쿼리 문자열에 지정된 정보를 이용해서 현재 요청에 대한 문화권을 설정하는 다음 미들웨어를 살펴보시기 바랍니다:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use((context, next) =>
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            return next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}
노트

이 예제 코드는 미들웨어 구성 요소를 생성하는 방법을 보여주기 위한 용도로 작성된 것입니다. ASP.NET Core의 내장 지역화 기능에 관해서는 Globalization and localization 문서를 참고하시기 바랍니다.

http://localhost:7997/?culture=no와 같이 쿼리 문자열에 문화권을 지정해서 미들웨어를 테스트해 볼 수 있습니다.

다음 코드는 이 미들웨어 대리자를 클래스로 구현한 것입니다:

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestCultureMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            return this._next(context);
        }
    }
}

그리고 다음 확장 메서드는 IApplicationBuilder를 통해서 미들웨어를 노출합니다:

using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}

마지막으로 다음 코드는 Configure 메서드에서 미들웨어를 호출합니다:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestCulture();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });
    }
}

미들웨어는 자신의 의존성을 생성자에 명시함으로써 명시적 의존성 원칙(Explicit Dependencies Principle)을 준수해야 합니다. 기본적으로 미들웨어는 응용 프로그램의 수명(Application Lifetime) 중 단 한 번만 생성됩니다. 요청 내에서 미들웨어와 서비스를 공유해야만 한다면, 아래의 요청별 의존성 절을 참고하시기 바랍니다.

미들웨어 구성 요소는 생성자 매개 변수를 통해서 의존성 주입으로 자신의 의존성을 해결할 수 있습니다. 또한 UseMiddleware<T>를 이용해서 추가적인 매개 변수를 직접 전달받을 수도 있습니다.

요청별 의존성

미들웨어는 매번 요청이 전달될 때가 아닌, 응용 프로그램이 구동되는 시점에 생성되기 때문에, 미들웨어 생성자에 의해서 사용되는 범위(Scoped) 수명 서비스는 각 요청 동안 주입된 다른 의존성 형식과 공유되지 않습니다. 미들웨어와 다른 형식 간에 범위 서비스를 공유해야 한다면, 해당 서비스를 Invoke 메서드의 시그니처에 추가하십시오. 다음 예제와 같이 Invoke 메서드는 의존성 주입으로 만들어진 추가적인 매개 변수를 전달받을 수 있습니다:

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty = 1000;
        await _next(httpContext);
    }
}

추가 자료