ASP.NET Core 미들웨어 파이프라인 심화
ASP.NET Core의 미들웨어 파이프라인은 HTTP 요청을 처리하는 컴포넌트 체인입니다. 각 미들웨어는 요청을 처리하고 다음 미들웨어를 호출하거나 파이프라인을 단락(short-circuit)시킵니다.
1. 파이프라인 실행 순서
섹션 제목: “1. 파이프라인 실행 순서”요청 → 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 반환2. Use / Run / Map 비교
섹션 제목: “2. Use / Run / Map 비교”// 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();4. IMiddleware — DI 완전 통합
섹션 제목: “4. IMiddleware — DI 완전 통합”// 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>();5. 단락(Short-circuit) 패턴
섹션 제목: “5. 단락(Short-circuit) 패턴”// 헬스체크 — 이후 미들웨어 건너뜀app.Use(async (ctx, next) =>{ if (ctx.Request.Path == "/health") { ctx.Response.StatusCode = 200; await ctx.Response.WriteAsync("OK"); return; // next 호출 안함 → 단락 } await next(ctx);});6. 예외 처리 미들웨어
섹션 제목: “6. 예외 처리 미들웨어”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>();7. 미들웨어 등록 권장 순서
섹션 제목: “7. 미들웨어 등록 권장 순서”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. 엔드포인트8. UseWhen — 조건부 분기 (파이프라인 합류)
섹션 제목: “8. UseWhen — 조건부 분기 (파이프라인 합류)”// MapWhen은 분기 후 합류하지 않음// UseWhen은 조건 분기 후 메인 파이프라인으로 복귀app.UseWhen( ctx => ctx.Request.Path.StartsWithSegments("/api"), apiApp => { apiApp.UseMiddleware<ApiRateLimiterMiddleware>(); // 이후 메인 파이프라인 계속 실행 });
app.UseRouting();app.MapControllers();9. 요청 본문 버퍼링
섹션 제목: “9. 요청 본문 버퍼링”// 요청 본문은 기본적으로 한 번만 읽을 수 있음// 미들웨어에서 여러 번 읽으려면 버퍼링 활성화app.Use(async (ctx, next) =>{ ctx.Request.EnableBuffering(); // 스트림 재사용 가능
using var reader = new StreamReader( ctx.Request.Body, leaveOpen: true); var body = await reader.ReadToEndAsync();
ctx.Request.Body.Position = 0; // 위치 초기화
Console.WriteLine($"요청 본문: {body[..Math.Min(100, body.Length)]}"); await next(ctx);});10. 응답 캐싱 미들웨어
섹션 제목: “10. 응답 캐싱 미들웨어”// 빌트인 응답 캐싱builder.Services.AddResponseCaching();builder.Services.AddOutputCache(options =>{ options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromSeconds(60))); options.AddPolicy("products", policy => policy.Expire(TimeSpan.FromMinutes(5)) .SetVaryByQuery("category"));});
app.UseOutputCache(); // .NET 7+
// 엔드포인트에 적용app.MapGet("/products", GetProducts) .CacheOutput("products");미들웨어 파이프라인은 등록 순서가 곧 실행 순서입니다. 예외 처리는 가장 바깥에, 인증/인가는 라우팅 다음에 배치하세요. IMiddleware를 사용하면 Scoped 서비스를 안전하게 주입할 수 있으며, 단락 패턴으로 불필요한 처리를 조기에 종료해 성능을 높일 수 있습니다. UseWhen으로 조건부 분기 후 메인 파이프라인으로 복귀하고, EnableBuffering()으로 요청 본문을 여러 번 읽을 수 있게 하세요.