콘텐츠로 이동

ASP.NET Core 미들웨어 파이프라인 심화

ASP.NET Core의 미들웨어 파이프라인은 HTTP 요청을 처리하는 컴포넌트 체인입니다. 각 미들웨어는 요청을 처리하고 다음 미들웨어를 호출하거나 파이프라인을 단락(short-circuit)시킵니다.


요청 → MW1 → MW2 → MW3 → 엔드포인트
응답 ← MW1 ← MW2 ← MW3 ←
// 실행 순서 확인
app.Use(async (ctx, next) =>
{
Console.WriteLine("MW1 진입");
await next(ctx);
Console.WriteLine("MW1 반환");
});
app.Use(async (ctx, next) =>
{
Console.WriteLine("MW2 진입");
await next(ctx);
Console.WriteLine("MW2 반환");
});
app.Run(ctx =>
{
Console.WriteLine("엔드포인트");
return ctx.Response.WriteAsync("OK");
});
// 출력 순서:
// MW1 진입 → MW2 진입 → 엔드포인트 → MW2 반환 → MW1 반환

// Use — 다음 미들웨어 호출 가능
app.Use(async (ctx, next) =>
{
// 전처리
await next(ctx); // 다음으로 전달
// 후처리
});
// Run — 파이프라인 종단 (next 없음)
app.Run(async ctx =>
{
await ctx.Response.WriteAsync("종료");
// 이후 미들웨어 실행 안됨
});
// Map — 경로 분기
app.Map("/api", apiApp =>
{
apiApp.Run(ctx => ctx.Response.WriteAsync("API"));
});
// MapWhen — 조건 분기
app.MapWhen(
ctx => ctx.Request.Headers.ContainsKey("X-API-KEY"),
branch => branch.UseMiddleware<ApiKeyMiddleware>());

3. 커스텀 미들웨어 — 규약 기반

섹션 제목: “3. 커스텀 미들웨어 — 규약 기반”
public class RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
await next(context); // 다음 미들웨어
sw.Stop();
logger.LogInformation(
"{Method} {Path} → {Status} ({Elapsed}ms)",
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}
// 등록
app.UseMiddleware<RequestTimingMiddleware>();
// 또는 확장 메서드로
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(
this IApplicationBuilder app)
=> app.UseMiddleware<RequestTimingMiddleware>();
}
app.UseRequestTiming();

// IMiddleware: DI에서 매 요청마다 생성 (Scoped 가능)
public class AuthMiddleware(IAuthService authService) : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var token = context.Request.Headers["Authorization"]
.FirstOrDefault()?.Split(" ").Last();
if (token is null || !await authService.ValidateAsync(token))
{
context.Response.StatusCode = 401;
return; // 단락
}
await next(context);
}
}
// 등록 (DI에 명시적 등록 필요)
builder.Services.AddScoped<AuthMiddleware>();
app.UseMiddleware<AuthMiddleware>();

// 헬스체크 — 이후 미들웨어 건너뜀
app.Use(async (ctx, next) =>
{
if (ctx.Request.Path == "/health")
{
ctx.Response.StatusCode = 200;
await ctx.Response.WriteAsync("OK");
return; // next 호출 안함 → 단락
}
await next(ctx);
});

public class GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
try
{
await next(context);
}
catch (NotFoundException ex)
{
logger.LogWarning(ex, "리소스 없음");
context.Response.StatusCode = 404;
await context.Response.WriteAsJsonAsync(new { error = ex.Message });
}
catch (Exception ex)
{
logger.LogError(ex, "처리되지 않은 예외");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { error = "서버 오류" });
}
}
}
// 파이프라인 맨 앞에 등록
app.UseMiddleware<GlobalExceptionMiddleware>();

app.UseExceptionHandler(); // 1. 예외 처리 (가장 먼저)
app.UseHttpsRedirection(); // 2. HTTPS 리다이렉트
app.UseStaticFiles(); // 3. 정적 파일 (단락 가능)
app.UseRouting(); // 4. 라우팅
app.UseAuthentication(); // 5. 인증
app.UseAuthorization(); // 6. 인가
app.UseRateLimiter(); // 7. 속도 제한
app.MapControllers(); // 8. 엔드포인트

미들웨어 파이프라인은 등록 순서가 곧 실행 순서입니다. 예외 처리는 가장 바깥에, 인증/인가는 라우팅 다음에 배치하세요. IMiddleware를 사용하면 Scoped 서비스를 안전하게 주입할 수 있으며, 단락 패턴으로 불필요한 처리를 조기에 종료해 성능을 높일 수 있습니다.