Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

ASP.NET Core 성능과 확장성 극대화: 실전에서 검증된 전략

ASP.NET Core는 고성능·확장 가능한 웹 애플리케이션 구축을 위해 설계된 현대적인 오픈소스 크로스 플랫폼 프레임워크입니다. 마이크로서비스부터 엔터프라이즈급 API까지, 견고한 아키텍처 덕분에 개발자는 뛰어난 처리량(Throughput), 최소화된 지연 시간(Latency), 효율적인 리소스 활용을 동시에 달성할 수 있습니다.

이 글에서는 ASP.NET Core 애플리케이션의 성능과 확장성을 극대화하기 위한 핵심 전략, 구성 팁, 그리고 바로 적용할 수 있는 코드 예제를 살펴보겠습니다.

🚀 성능과 확장성의 이해

구현에 들어가기 전에 먼저 두 가지 핵심 개념을 명확히 해보겠습니다.

  • 성능(Performance): 단일 요청에 얼마나 빠르게 응답하는가
    (예: 응답 시간을 300ms에서 100ms로 단축)

  • 확장성(Scalability): 증가하는 부하를 얼마나 잘 감당하는가
    (예: 1만 명의 동시 접속자를 장애 없이 처리)

ASP.NET Core는 효율적인 메모리 관리, 비동기 프로그래밍, 의존성 주입(DI), 캐싱, 분산 시스템 기본 지원을 통해 이 두 가지 목표를 모두 실현합니다.

ASP.NET Core 성능과 확장성 극대화: 실전에서 검증된 전략

⚙️ 비동기 프로그래밍 활용

ASP.NET Core 런타임은 비동기 I/O 작업에 최적화되어 있습니다. asyncawait 키워드를 활용하면 스레드가 블로킹되지 않아 더 많은 요청을 동시에 처리할 수 있습니다.

✅ 예제: 비동기 컨트롤러 액션

 
 [ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
 private readonly IProductService _productService;
 public ProductsController(IProductService productService)
 {
 _productService = productService;
 }
 [HttpGet("{id}")]
 public async Task<IActionResult> GetProductById(int id)
 {
 var product = await _productService.GetProductAsync(id);
 if (product == null)
 return NotFound();
 return Ok(product);
 }
}
 

Task<IActionResult>를 반환하면 데이터베이스 조회나 외부 API 호출 같은 I/O 작업을 기다리는 동안 스레드가 차단되지 않습니다. 이는 부하가 집중되는 상황에서 확장성을 크게 향상시킵니다.

🧩 미들웨어 파이프라인 최적화

미들웨어 컴포넌트는 각 요청을 순차적으로 처리합니다. 따라서 미들웨어는 가볍게 유지하고 불필요한 연산을 피하는 것이 중요합니다.

✅ 예제: 경량 커스텀 미들웨어

 
 public class RequestTimingMiddleware
{
 private readonly RequestDelegate _next;
 private readonly ILogger<RequestTimingMiddleware> _logger;
 public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
 {
 _next = next;
 _logger = logger;
 }
 public async Task InvokeAsync(HttpContext context)
 {
 var start = DateTime.UtcNow;
 await _next(context);
 var elapsed = DateTime.UtcNow - start;
 _logger.LogInformation($"Request took {elapsed.TotalMilliseconds} ms");
 }
}
// Program.cs에 등록
app.UseMiddleware<RequestTimingMiddleware>();
 

👉 :
가벼운 미들웨어(라우팅, 압축 등)는 파이프라인 상단에 배치하고, 무거운 미들웨어(인증 등)는 하단에 배치하세요.

⚡ 응답 캐싱 활성화

캐싱을 활용하면 결과를 반복해서 계산하거나 데이터베이스에 매번 접근할 필요가 줄어듭니다. ASP.NET Core는 내장 응답 캐싱 미들웨어(Response Caching Middleware)를 제공합니다.

✅ 예제: 응답 캐싱 활성화

 
 // Program.cs
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
app.MapGet("/time", (HttpContext context) =>
{
 context.Response.GetTypedHeaders().CacheControl =
 new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
 {
 Public = true,
 MaxAge = TimeSpan.FromSeconds(30)
 };
 return DateTime.UtcNow.ToString("T");
});
 

이제 30초 이내의 후속 요청은 캐시에서 즉시 처리되어 성능이 눈에 띄게 향상됩니다.

🧠 EF Core로 데이터 액세스 최적화

데이터베이스 액세스는 대개 애플리케이션의 주요 병목 지점입니다. 다음 기법을 적용해 Entity Framework Core를 효율적으로 사용하세요.

  • 읽기 전용 쿼리에는 AsNoTracking()

  • 반복 실행되는 쿼리에는 컴파일된 쿼리(Compiled Queries)

  • 연결 풀링(Connection Pooling)

✅ 예제: AsNoTracking() 사용

 
 public async Task<IEnumerable<Product>> GetAllProductsAsync()
{
 return await _context.Products
 .AsNoTracking() // 성능 향상
 .ToListAsync();
}
 

동일한 쿼리를 자주 실행한다면 컴파일된 쿼리를 활용해 보세요.

 
 private static readonly Func<AppDbContext, int, Task<Product?>> _getProductById =
 EF.CompileAsyncQuery((AppDbContext context, int id) =>
 context.Products.FirstOrDefault(p => p.Id == id));
public Task<Product?> GetProductAsync(int id) =>
 _getProductById(_context, id);
 

🧰 출력 압축 적용

클라이언트로 응답을 보내기 전에 압축하면 대역폭 사용량이 줄고 전송 속도가 빨라집니다.

✅ 예제: 응답 압축 활성화

 
 // Program.cs
builder.Services.AddResponseCompression(options =>
{
 options.EnableForHttps = true;
 options.MimeTypes = new[] { "text/plain", "application/json" };
});
var app = builder.Build();
app.UseResponseCompression();
 

이후 모든 application/json 응답은 자동으로 GZIP으로 압축됩니다.

🌍 로드 밸런싱을 통한 수평 확장(Scale-Out)

트래픽이 급증하면 성능 튜닝만으로는 한계가 있습니다. 확장성 확보를 위해서는 여러 서버에 부하를 분산해야 합니다.

  • 수평 확장(Horizontal Scaling): 서버 대수 추가

  • 로드 밸런서: NGINX, Azure Front Door, AWS ELB 등

분산 환경에서는 세션 상태캐시를 반드시 외부화해야 합니다(예: Redis).

✅ 예제: 분산 캐시(Redis) 구성

 
 builder.Services.AddStackExchangeRedisCache(options =>
{
 options.Configuration = "localhost:6379";
});
public class CacheService
{
 private readonly IDistributedCache _cache;
 public CacheService(IDistributedCache cache)
 {
 _cache = cache;
 }
 public async Task SetCacheAsync(string key, string value)
 {
 await _cache.SetStringAsync(key, value, new DistributedCacheEntryOptions
 {
 AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
 });
 }
 public Task<string?> GetCacheAsync(string key) => _cache.GetStringAsync(key);
}
 

이렇게 하면 애플리케이션이 상태 비저장(Stateless) 구조가 되어, 로드 밸런싱에 필수적인 조건을 충족하게 됩니다.

🧩 높은 처리량을 위한 Kestrel 및 호스팅 구성

ASP.NET Core의 내장 웹 서버인 Kestrel은 적절히 구성하면 초당 수십만 건의 요청을 처리할 수 있습니다.

✅ 예제: Kestrel 구성 최적화

 
 builder.WebHost.ConfigureKestrel(options =>
{
 options.Limits.MaxConcurrentConnections = 10000;
 options.Limits.MaxConcurrentUpgradedConnections = 1000;
 options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
 

추가 권장 사항:

  • 정적 파일 처리와 TLS 종료를 위해 리버스 프록시 서버(NGINX, IIS 등)를 함께 사용하세요.

  • 자동 확장(Auto-scaling)을 위해 컨테이너 환경(예: Kubernetes)에 배포하세요.

🧮 메모리 및 객체 풀링 활용

잦은 객체 할당과 가비지 컬렉션(GC) 부담을 줄이기 위해 ASP.NET Core는 객체 풀링(Object Pooling)을 지원합니다.

✅ 예제: ArrayPool<T> 사용

 
 using System.Buffers;
public class BufferService
{
 public void ProcessData()
 {
 var pool = ArrayPool<byte>.Shared;
 var buffer = pool.Rent(1024); // 1KB 버퍼 임대
 try
 {
 // 버퍼 사용
 }
 finally
 {
 pool.Return(buffer);
 }
 }
}
 

이 방식은 힙 할당을 최소화하고 GC 압박을 줄여줍니다. 성능에 민감한 애플리케이션에서는 필수적인 기법입니다.

🧱 시작 시간과 메모리 사용량 최소화

  • Program.cs에서 불필요한 서비스 등록은 피하세요.

  • 적절한 경우 AddTransient 대신 AddSingleton을 사용하세요.

  • *.csproj 파일에서 종속성을 정리(Trim)하세요.

✅ 예제: Minimal API 구성

 
 var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IProductService, ProductService>();
var app = builder.Build();
app.MapGet("/products", async (IProductService service) =>
 await service.GetAllProductsAsync());
app.Run();
 

Minimal API는 보일러플레이트 코드를 줄이고 시작 성능을 개선합니다.

📊 모니터링과 벤치마킹

측정하지 않으면 개선할 수 없습니다. 다음 도구를 적극 활용하세요.

  • dotnet-tracedotnet-counters

  • Application Insights

  • BenchmarkDotNet

✅ 예제: BenchmarkDotNet 사용

 
 [MemoryDiagnoser]
public class PerformanceTests
{
 private readonly ProductService _service = new();
 [Benchmark]
 public async Task FetchProducts()
 {
 await _service.GetAllProductsAsync();
 }
}
 

벤치마크를 실행하면 병목 지점과 메모리 비효율을 정확히 파악할 수 있습니다.

🧩 추가 최적화 팁

  • 더 나은 병렬 처리를 위해 HTTP/2 또는 HTTP/3를 활성화하세요.

  • 정적 자산은 CDN을 통해 제공하세요.

  • 데이터베이스와 HTTP 클라이언트에 연결 풀링을 적용하세요.

  • 소켓 고갈(Socket Exhaustion) 방지를 위해 IHttpClientFactory를 사용하세요.

 
 builder.Services.AddHttpClient("MyClient")
 .SetHandlerLifetime(TimeSpan.FromMinutes(5));
 

🏁 결론

ASP.NET Core에서 높은 성능과 확장성을 달성하려면 비동기 설계, 캐싱, 효율적인 데이터 액세스, 그리고 현명한 인프라 선택이 유기적으로 결합되어야 합니다.

미들웨어와 Kestrel 구성 최적화부터 Redis 분산 캐시, 응답 압축 활용까지 이 글에서 소개한 전략들을 적용하면, ASP.NET Core 애플리케이션도 낮은 지연 시간과 높은 안정성으로 대규모 워크로드를 충분히 감당할 수 있습니다.