❓ 미들웨어(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 테스트:
실험을 위해 사용자 하위 집합을 대체 미들웨어 분기를 통해 라우팅합니다. - 디버깅 및 진단:
개발 환경에서만 진단 미들웨어를 적용합니다.



