❓ 미들웨어(Middleware)
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0
ASP.NET Core의 미들웨어는 모든 HTTP 요청과 응답 파이프라인을 형성하는 일련의 구성 요소입니다.
각 미들웨어 구성 요소는 다음을 수행할 수 있습니다:
- 들어오는 요청을 검사합니다.
- 요청 또는 응답을 수정합니다 (필요한 경우).
- 파이프라인의 다음 미들웨어를 호출하거나, 프로세스를 단락(short-circuit)시키고 자체적으로 응답을 생성합니다.
이러한 파이프라인을 통해 애플리케이션의 로직을 모듈화하고, 인증, 로깅, 오류 처리, 라우팅 등과 같은 기능을 깔끔하고 유지 관리하기 쉬운 방식으로 추가할 수 있습니다.
⛓️ 미들웨어 체인 (요청 파이프라인)
ASP.NET Core 요청 파이프라인은 차례로 호출되는 일련의 요청 대리자로 구성됩니다.
요청 파이프라인을 일련의 연결된 파이프라고 상상해 보세요.
각 미들웨어 조각은 이 파이프라인의 밸브와 같아서, 정보의 흐름을 제어하고 다양한 단계에서 특정 작업을 적용할 수 있습니다.
미들웨어를 등록하는 순서는 매우 중요하며, 등록된 순서대로 실행됩니다.

각 델리게이트는 다음 델리게이트를 호출하기 전과 후에 작업을 수행할 수 있습니다.
예외 처리 델리게이트는 파이프라인의 후반 단계에서 발생하는 예외를 Catch할 수 있도록 파이프라인의 초기에 호출되어야 합니다.
가장 간단한 ASP.NET Core 앱은 모든 요청을 처리하는 단일 요청 델리게이트를 설정합니다.
이 경우에는 실제 요청 파이프라인이 포함되지 않습니다.
대신, 모든 HTTP 요청에 대한 응답으로 단일 익명 함수가 호출됩니다.
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.Run(async context => { await context.Response.WriteAsync("Hello world!"); }); app.Run(); // 이 app.Run은 위의 app.Run이 파이프라인을 종료하므로 실행되지 않습니다.
Use
메서드를 사용하면 여러 요청 델리게이트를 체인으로 연결할 수 있습니다.
이때 next
매개변수는 파이프라인의 다음 델리게이트를 나타냅니다.
next
매개변수를 호출하지 않음으로써 파이프라인을 단락(short-circuit)시킬 수 있습니다.
다음 예시에서 보여주듯이, 일반적으로 next
델리게이트를 호출하기 전과 후에 모두 작업을 수행할 수 있습니다.
var builder = WebApplication.CreateBuilder(args); // 웹 애플리케이션 빌더를 생성합니다. var app = builder.Build(); // 빌더를 사용하여 웹 애플리케이션 인스턴스를 생성합니다. // 첫 번째 미들웨어: app.Use()를 사용하여 파이프라인에 추가합니다. app.Use(async (context, next) => { // 응답에 쓸 수 있는 작업을 수행합니다. (예: 헤더 추가, 응답의 시작 부분 작성) // 이 부분의 코드는 다음 미들웨어가 실행되기 전에 실행됩니다. await next.Invoke(); // 다음 미들웨어(이 경우 app.Run)를 호출하여 제어를 넘깁니다. // 응답에 쓰지 않는 로깅 또는 다른 작업을 수행합니다. // 이 부분의 코드는 다음 미들웨어가 실행된 후(응답이 생성된 후) 실행됩니다. }); // 두 번째 미들웨어: app.Run()을 사용하여 파이프라인에 추가합니다. // app.Run()은 파이프라인을 종료하는 미들웨어입니다. app.Run(async context => { // 이 미들웨어는 "Hello from 2nd delegate."를 응답에 작성하고 파이프라인을 종료합니다. await context.Response.WriteAsync("Hello from 2nd delegate."); }); // 이 app.Run()은 위의 app.Run()이 이미 파이프라인을 종료했기 때문에 실행되지 않습니다. app.Run();
✂️ 요청 파이프라인 단락(Short-circuiting)
ASP.NET Core의 미들웨어 파이프라인에서, 특정 델리게이트(미들웨어)가 요청을 다음 델리게이트로 전달하지 않을 때 이를 요청 파이프라인을 단락(short-circuiting)시킨다고 합니다.
파이프라인 단락은 불필요한 작업을 피할 수 있기 때문에 종종 유용합니다.
예를 들어, 정적 파일 미들웨어(Static File Middleware)는 요청된 파일이 정적 파일(이미지, CSS, JavaScript 등)일 경우 해당 요청을 처리하고
파이프라인의 나머지 부분을 단락시킴으로써 종료 미들웨어(terminal middleware) 역할을 할 수 있습니다.
이렇게 되면 정적 파일을 제공한 후에는 인증이나 라우팅과 같은 다른 미들웨어들이 실행될 필요가 없으므로 효율성이 높아집니다.
✅ 단락과 next.Invoke()
이후 코드 실행
파이프라인의 추가적인 처리를 종료시키는 미들웨어보다 앞서 파이프라인에 추가된 미들웨어는 여전히 next.Invoke()
구문 이후의 코드를 처리합니다.
즉, 비록 파이프라인이 단락되어 최종 응답이 더 이상 아래로 전달되지 않더라도, next.Invoke()
를 호출했던 이전 미들웨어들은 응답이 완료된 후 자신의 후처리 로직을 실행할 수 있습니다.
⚠️ 경고: 응답 전송 후 작업 주의
응답이 클라이언트에게 이미 전송되었거나 전송되기 시작한 후에는 next.Invoke()
를 호출하거나 응답에 쓰려고 시도하지 마세요.
HttpResponse
가 시작된 후에는 응답에 변경을 가하려고 하면 예외가 발생합니다.
예를 들어, 응답이 시작된 후 헤더나 상태 코드를 설정하려고 하면 예외가 발생합니다.
next.Invoke()
호출 후 응답 본문에 쓰는 것은 다음과 같은 문제를 야기할 수 있습니다:
- 프로토콜 위반: 명시된
Content-Length
보다 더 많은 내용을 작성하는 것과 같은 프로토콜 위반을 초래할 수 있습니다. - 본문 형식 손상: CSS 파일에 HTML 푸터(footer)를 작성하는 것처럼 본문 형식을 손상시킬 수 있습니다.
HasStarted
속성은 헤더가 전송되었는지 또는 본문이 작성되었는지 여부를 나타내는 유용한 힌트가 될 수 있습니다.
🫠 app.Use
vs. app.Run
이 두 메서드는 파이프라인에 미들웨어를 추가하는 데 기본적이지만, 핵심적인 차이점이 있습니다:
app.Use(async (context, next) => { ... })
- 요청 수정 불가:
마지막 단계이므로 요청을 다음으로 전달하기 전에 수정할 수 없습니다. - 비종료(Non-Terminal) 미들웨어:
이 유형의 미들웨어는 일반적으로 어떤 작업을 수행한 다음,next
델리게이트를 호출하여 파이프라인의 다음 미들웨어로 제어를 전달합니다. - 요청/응답 수정 가능:
요청을 다음으로 전달하기 전에 요청이나 응답을 변경할 수 있습니다. - 예시:
인증, 로깅, 커스텀 헤더 추가 등.
app.Run(async (context) => { ... })
- 종료(Terminal) 미들웨어:
이 미들웨어는next
를 호출하지 않습니다.
파이프라인을 종료하고 자체적으로 응답을 생성합니다. - 최종 응답에 주로 사용:
더 이상 처리가 필요 없는 요청(예: 간단한 메시지 반환)을 처리하는 데 일반적으로 사용됩니다.
// 여러 app.Run 호출의 결과 app.Run(async (HttpContext context) => { await context.Response.WriteAsync("Hello"); }); app.Run(async (HttpContext context) => { await context.Response.WriteAsync("Hello again"); }); app.Run(); // 이 app.Run은 위의 app.Run들이 이미 파이프라인을 종료했기 때문에 절대 실행되지 않습니다.
이 코드에서는 오직 첫 번째 app.Run
미들웨어만 실행됩니다.
“Hello”를 응답에 작성하여 파이프라인을 종료하고, 그 뒤의 app.Run
(이것은 “Hello again”을 작성할 것임)은 실행될 기회를 얻지 못합니다.
// app.Use와 app.Run으로 미들웨어 체인 연결하기 // 미들웨어 1 app.Use(async (context, next) => { await context.Response.WriteAsync("Hello "); // 1. "Hello " 작성 await next(context); // 2. 다음 미들웨어 호출 }); // 미들웨어 2 app.Use(async (context, next) => { await context.Response.WriteAsync("Hello again "); // 3. "Hello again " 작성 await next(context); // 4. 다음 미들웨어 호출 (이 경우 app.Run) }); // 미들웨어 3 (종료 미들웨어) app.Run(async (HttpContext context) => { await context.Response.WriteAsync("Hello again"); // 5. "Hello again" 작성 후 파이프라인 종료 });
이 코드는 미들웨어를 올바르게 연결하는 방법을 보여줍니다.
- 첫 번째
app.Use
는 응답에 “Hello “를 작성하고next
를 호출하여 다음 미들웨어로 제어를 전달합니다. - 두 번째
app.Use
는 “Hello again “을 작성하고 역시next
를 호출합니다. - 마지막
app.Run
(종료 미들웨어)는 “Hello again”을 작성하고 파이프라인을 종료합니다.
❓ ASP.NET Core의 커스텀 미들웨어
ASP.NET Core는 다양한 내장 미들웨어 구성 요소를 제공하지만, 때로는 애플리케이션 고유의 특정 요구 사항을 해결하기 위해 자신만의 커스텀 미들웨어를 만들어야 할 수도 있습니다.
커스텀 미들웨어를 통해 다음을 수행할 수 있습니다:
- 로직 캡슐화:
관련 작업(예: 로깅, 보안 검사, 사용자 정의 헤더)을 재사용 가능한 구성 요소로 묶습니다. - 동작 사용자 정의:
애플리케이션의 요구 사항에 정확히 맞게 요청/응답 파이프라인을 조정합니다. - 코드 구성 개선:
미들웨어 코드를 깔끔하고 유지 관리하기 쉽게 만듭니다.
🔧 커스텀 미들웨어 클래스의 구조
IMiddleware
구현: 이 인터페이스는 단 하나의 메서드 InvokeAsync(HttpContext context, RequestDelegate next)
를 요구합니다.
이 메서드는 미들웨어 로직의 핵심입니다
InvokeAsync
또는 Invoke
메서드
context
:HttpContext
는 요청 및 응답 객체에 대한 접근을 제공합니다.next
:RequestDelegate
는 파이프라인의 다음 미들웨어를 호출할 수 있도록 합니다.
// MyCustomMiddleware.cs namespace MiddlewareExample.CustomMiddleware { public class MyCustomMiddleware : IMiddleware // IMiddleware 인터페이스 구현 { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { await context.Response.WriteAsync("My Custom Middleware - Starts\n"); // 1. 응답 시작 부분에 출력 await next(context); // 2. 다음 미들웨어 호출 await context.Response.WriteAsync("My Custom Middleware - Ends\n"); // 3. 다음 미들웨어 완료 후 응답 끝 부분에 출력 } } // 쉽게 등록하기 위한 확장 메서드 public static class CustomMiddlewareExtension { public static IApplicationBuilder UseMyCustomMiddleware(this IApplicationBuilder app) { return app.UseMiddleware<MyCustomMiddleware>(); } } }
// Program.cs (또는 Startup.cs) using MiddlewareExample.CustomMiddleware; // ... (다른 설정 코드) ... builder.Services.AddTransient<MyCustomMiddleware>(); // 트랜지언트 서비스로 등록 app.Use(async (HttpContext context, RequestDelegate next) => { await context.Response.WriteAsync("From Middleware 1\n"); // 첫 번째 미들웨어: 시작 부분 await next(context); }); app.UseMyCustomMiddleware(); // 확장 메서드를 사용하여 커스텀 미들웨어 추가 app.Run(async (HttpContext context) => { await context.Response.WriteAsync("From Middleware 3\n"); // 세 번째 미들웨어: 파이프라인 종료 });
- 등록:
ASP.NET Core가 필요할 때MyCustomMiddleware
인스턴스를 생성할 수 있도록 이를 트랜지언트 서비스로 등록합니다. - 파이프라인 통합:
app.UseMyCustomMiddleware()
확장 메서드는 커스텀 미들웨어를 파이프라인에 추가합니다. - 실행 순서:
미들웨어 구성 요소는 파이프라인에 추가된 순서대로 실행됩니다. 이 경우 순서는 미들웨어 1,MyCustomMiddleware
, 그리고 미들웨어 3이 됩니다.
🔥 커스텀 컨벤셔널 미들웨어 (Custom Conventional Middleware)
ASP.NET Core 미들웨어에는 두 가지 유형이 있습니다: 컨벤셔널(Conventional)과 팩토리 기반(Factory-based)
예시에서 보여진 컨벤셔널 미들웨어는 HTTP 요청 및 응답 처리를 위한 커스텀 로직을 캡슐화하는 간단하면서도 강력한 방법입니다.
주요 특징
- 클래스 기반:
컨벤셔널 미들웨어는 클래스로 구현됩니다. - 생성자 주입:
의존성(있는 경우)을 생성자를 통해 받습니다. Invoke
메서드:
이 메서드는 각 요청을 처리하는 로직을 포함하는 미들웨어의 핵심입니다.RequestDelegate
:Invoke
메서드는RequestDelegate
매개변수(_next
로 명명)를 받습니다. 이 델리게이트는 파이프라인의 다음 미들웨어를 나타냅니다.- 유연성:
Invoke
메서드 내에서 요청 및 응답 객체를 완벽하게 제어할 수 있습니다.

// NameConcatenationMiddleware.cs public class NameConcatenationMiddleware { private readonly RequestDelegate _next; public NameConcatenationMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { if (context.Request.Query.ContainsKey("firstname") && context.Request.Query.ContainsKey("lastname")) { string fullName = $"{context.Request.Query["firstname"]} {context.Request.Query["lastname"]}"; await context.Response.WriteAsync(fullName); return; // 명시적으로 반환하여 다음 미들웨어 호출 방지 } await _next(context); } } // MiddlewareExtensions.cs public static class MiddlewareExtensions { public static IApplicationBuilder UseNameConcatenation(this IApplicationBuilder builder) { return builder.UseMiddleware<NameConcatenationMiddleware>(); } }
🔀 미들웨어 파이프라인의 이상적인 순서
미들웨어의 순서는 애플리케이션의 동작과 효율성, 보안에 큰 영향을 미칩니다.
다음은 일반적으로 권장되는 이상적인 순서입니다

예외/오류 처리(Exception/Error Handling):
- 목적: 파이프라인의 어느 곳에서든 발생하는 예외를 Catch하고 처리합니다.
- 예시:
UseExceptionHandler
(프로덕션용),UseDeveloperExceptionPage
(개발 환경용). - 이유: 예외를 초기에 Catch하여 파이프라인 아래로 전파되어 더 큰 문제를 일으키는 것을 방지합니다.
HTTPS 리디렉션(HTTPS Redirection):
- 목적: 보안을 위해 HTTP 요청을 HTTPS로 리디렉션합니다.
- 예시:
UseHttpsRedirection
. - 이유: 보안을 최우선으로 하여 모든 통신이 암호화되도록 합니다.
정적 파일(Static Files):
- 목적: 이미지, CSS, JavaScript 파일과 같은 정적 파일을 클라이언트에게 직접 제공합니다.
- 예시:
UseStaticFiles
. - 이유: 정적 파일 요청은 빠르게 처리되어야 하며, 불필요하게 파이프라인의 다른 무거운 구성 요소를 거치지 않도록 일찍 처리합니다.
라우팅(Routing):
- 목적: URL을 기반으로 들어오는 요청을 특정 엔드포인트에 매칭합니다.
- 예시:
UseRouting
,UseEndpoints
. - 이유: 라우팅은 애플리케이션의 핵심 로직이 요청을 어떻게 처리할지 결정하는 기반이 됩니다.
CORS (Cross-Origin Resource Sharing):
- 목적: 다른 도메인으로부터의 안전한 교차 출처(cross-origin) 요청을 가능하게 합니다.
- 예시:
UseCors
. - 이유: 인증/인가 전에 위치하여, 사전 요청(preflight request)이 불필요하게 인증/인가 미들웨어를 거치지 않도록 합니다.
인증(Authentication):
- 목적: 사용자 신원을 확인하고 사용자 주체(principal)를 설정합니다.
- 예시:
UseAuthentication
. - 이유: 사용자가 누구인지 확인한 후에 리소스에 대한 접근 권한을 부여할 수 있습니다.
인가(Authorization):
- 목적: 사용자가 특정 리소스에 접근하거나 특정 작업을 수행할 수 있는지 여부를 결정합니다.
- 예시:
UseAuthorization
. - 이유: 인증된 사용자에게만 권한 부여 여부를 검사합니다.
커스텀 미들웨어(Custom Middleware):
- 목적: 로깅, 기능 플래그 등 애플리케이션별 미들웨어 구성 요소를 처리합니다.
- 이유: 애플리케이션별 로직을 적절한 단계에서 파이프라인 내에 배치합니다.
MVC/Razor Pages/Minimal APIs:
- 목적: 실제 애플리케이션의 최종 엔드포인트 처리 로직을 실행합니다.
- 예시:
MapControllers()
,MapRazorPages()
,MapGet()
등.

Program.cs
파일에 미들웨어 구성 요소가 추가되는 순서는 요청 시 미들웨어 구성 요소가 호출되는 순서를 정의하며, 응답 시에는 역순으로 호출됩니다.
이러한 순서는 보안, 성능 및 기능에 매우 중요합니다.
Program.cs
의 다음 강조 표시된 코드는 보안 관련 미들웨어 구성 요소를 일반적으로 권장되는 순서로 추가하는 예시입니다:
using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using WebMiddleware.Data; var builder = WebApplication.CreateBuilder(args); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddRazorPages(); builder.Services.AddControllersWithViews(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseMigrationsEndPoint(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); // app.UseCookiePolicy(); app.UseRouting(); // app.UseRateLimiter(); // app.UseRequestLocalization(); // app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); // app.UseSession(); // app.UseResponseCompression(); // app.UseResponseCaching(); app.MapRazorPages(); app.MapDefaultControllerRoute(); app.Run();
✅ UseWhen()
UseWhen()
은 ASP.NET Core의 IApplicationBuilder
인터페이스에 있는 강력한 확장 메서드입니다.
이는 조건(predicate)에 따라 미들웨어를 요청 파이프라인에 조건부로 추가할 수 있도록 합니다.
즉, 특정 조건이 충족될 때만 특정 미들웨어 구성 요소가 실행되는 동적인 파이프라인을 만들 수 있습니다.
app.UseWhen( context => /* 여기에 조건 */, // HttpContext를 받아 true/false 반환 app => /* 이 분기에서 실행될 미들웨어 구성 */ // 조건이 true일 때 실행될 미들웨어 파이프라인 );
context
:
현재 요청을 나타내는HttpContext
객체입니다.- Predicate (조건):
HttpContext
를 받아들이고 미들웨어 분기가 실행되어야 할 경우true
를, 그렇지 않을 경우false
를 반환하는 함수입니다. - Middleware Configuration (미들웨어 구성):
조건이true
일 때 실행되어야 할 미들웨어 구성 요소를 구성하는 액션입니다.
여기서app.Use()
,app.Run()
, 또는 다른 미들웨어 등록 메서드를 사용합니다.
✨ UseWhen()
작동 방식
- 조건 평가:
요청이 들어오면UseWhen()
메서드는 먼저HttpContext
에 대해 조건 함수를 평가합니다. - 분기(조건이 true일 경우):
조건이true
를 반환하면, 구성 액션에 지정된 미들웨어 분기가 실행됩니다.
요청은 이 분기를 통해 흐르며, 수정되거나 응답을 생성할 수 있습니다. - 메인 파이프라인 재진입:
분기가 실행된 후(또는 조건이false
여서 건너뛰어진 경우), 요청 흐름은 메인 파이프라인으로 다시 진입하여UseWhen()
호출 뒤에 등록된 다음 미들웨어 구성 요소로 계속 진행됩니다.
app.UseWhen( context => context.Request.Query.ContainsKey("username"), // 조건: 쿼리 문자열에 "username"이 있는지 확인 app => { app.Use(async (context, next) => { await context.Response.WriteAsync("Hello from Middleware branch\n"); // 분기 미들웨어: "Hello from Middleware branch" 작성 await next(); // 다음 미들웨어 호출 }); }); app.Run(async context => { await context.Response.WriteAsync("Hello from middleware at main chain"); // 메인 파이프라인 미들웨어 });
- 조건:
context.Request.Query.ContainsKey("username")
조건은 쿼리 문자열에 “username”이라는 매개변수가 포함되어 있는지 확인합니다. - 분기 미들웨어:
“username” 매개변수가 존재하면 분기 미들웨어가 실행됩니다.
이 미들웨어는 응답에 “Hello from Middleware branch”를 작성하고next
를 호출하여 나머지 파이프라인이 계속되도록 합니다. - 메인 파이프라인:
마지막app.Run
미들웨어는 메인 파이프라인의 일부입니다.
이는 응답에 “Hello from middleware at main chain”을 작성합니다.
🦋 UseWhen()
사용 시점
- 조건부 기능:
요청에 따라 특정 기능을 활성화하거나 비활성화합니다 (예: 특정 사용자에게만 로깅, 쿼리 매개변수에 따른 캐싱 규칙 적용). - 동적 파이프라인:
다양한 요청에 맞춰 조정되는 파이프라인을 만듭니다 (예: 특정 경로에 대해 다른 인증 미들웨어). - A/B 테스트:
실험을 위해 사용자 하위 집합을 대체 미들웨어 분기를 통해 라우팅합니다. - 디버깅 및 진단:
개발 환경에서만 진단 미들웨어를 적용합니다.