본문 바로가기

C#

C# HttpClient에서 다중 요청 병렬 처리 구현

다수의 HTTP API를 호출할 때는 비동기 병렬 처리를 통해 총 소요 시간을 크게 줄일 수 있습니다. 본 글에서는 HttpClient로 안전하고 효율적인 병렬 요청을 구현하는 실전 패턴을 정리합니다.

1. 핵심 요약

HttpClient는 재사용합니다. Task.WhenAll로 병렬 처리하고, 필요한 경우 SemaphoreSlim으로 동시성을 제한합니다. ASP.NET Core에서는 IHttpClientFactory를 활용하여 연결/수명/재시도 정책을 관리합니다.

2. 올바른 HttpClient 재사용과 핸들러 설정

요청마다 HttpClient를 새로 만들면 소켓 고갈과 성능 저하가 발생합니다. SocketsHttpHandler와 함께 HttpClient를 재사용하고, 서버당 연결 수와 자동 압축을 설정합니다.

using System;using System.Collections.Generic;using System.Linq;using System.Net;using System.Net.Http;using System.Text.Json;using System.Threading;using System.Threading.Tasks;public static class ParallelFetcher{    private static readonly HttpClient Http = new HttpClient(new SocketsHttpHandler    {        MaxConnectionsPerServer = 100,        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate    })    {        Timeout = TimeSpan.FromSeconds(30),        DefaultRequestVersion = System.Net.HttpVersion.Version20    };    public static async Task<List<(string Url, JsonElement Data)>> FetchAllAsync(IEnumerable<string> urls, CancellationToken ct)    {        var tasks = urls.Select(async url =>        {            using var req = new HttpRequestMessage(HttpMethod.Get, url);            using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct);            res.EnsureSuccessStatusCode();            await using var stream = await res.Content.ReadAsStreamAsync(ct);            var data = await JsonSerializer.DeserializeAsync<JsonElement>(stream, cancellationToken: ct);            return (Url: url, Data: data);        });        return (await Task.WhenAll(tasks)).ToList();    }}

3. Task.WhenAll로 간단한 병렬 처리

여러 URL을 동시에 호출하고 모두 완료될 때까지 대기합니다. 예외는 WhenAll 시점에 집계되므로 try/catch로 감싸 처리합니다.

public static async Task UseWhenAllAsync(CancellationToken ct){    var urls = new[]    {        "https://api.example.com/a",        "https://api.example.com/b",        "https://api.example.com/c"    };    try    {        var results = await ParallelFetcher.FetchAllAsync(urls, ct);        foreach (var (url, json) in results)        {            Console.WriteLine($"{url} - ok");        }    }    catch (Exception ex)    {        Console.Error.WriteLine($"병렬 요청 실패: {ex.Message}");    }}

4. 동시성 제한: 과부하 방지

대상 서버 또는 네트워크에 부담을 줄 수 있으므로 동시 요청 수를 제한하는 것이 안전합니다. SemaphoreSlim으로 간단히 구현할 수 있습니다.

using System.Collections.Generic;using System.Linq;using System.Threading;using System.Threading.Tasks;public static class LimitedFetcher{    private static readonly HttpClient Http = new HttpClient();    public static async Task<List<string>> FetchWithLimitAsync(IEnumerable<string> urls, int maxDegree, CancellationToken ct)    {        var sem = new SemaphoreSlim(maxDegree);        var tasks = urls.Select(async url =>        {            await sem.WaitAsync(ct);            try            {                return await Http.GetStringAsync(url, ct);            }            finally            {                sem.Release();            }        });        return (await Task.WhenAll(tasks)).ToList();    }}

5. ASP.NET Core에서 IHttpClientFactory + Typed Client

IHttpClientFactory는 핸들러 수명, 연결 풀, 재시도 정책을 중앙에서 관리합니다. Typed Client로 병렬 호출을 캡슐화합니다.

using System;using System.Collections.Generic;using System.Linq;using System.Net;using System.Net.Http;using System.Threading;using System.Threading.Tasks;using Microsoft.Extensions.DependencyInjection;using Polly;using Polly.Extensions.Http;var services = new ServiceCollection();services.AddHttpClient<DataClient>(client =>{    client.Timeout = TimeSpan.FromSeconds(20);    client.DefaultRequestVersion = System.Net.HttpVersion.Version20;})    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler    {        MaxConnectionsPerServer = 50,        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate    })    .AddPolicyHandler(GetRetryPolicy());services.AddTransient<MyService>();var provider = services.BuildServiceProvider();var svc = provider.GetRequiredService<MyService>();await svc.RunAsync(CancellationToken.None);static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() => HttpPolicyExtensions    .HandleTransientHttpError()    .WaitAndRetryAsync(3, i => TimeSpan.FromMilliseconds(200 * Math.Pow(2, i)));public class DataClient{    private readonly HttpClient _http;    public DataClient(HttpClient http) => _http = http;    public async Task<List<HttpResponseMessage>> FetchAllAsync(IEnumerable<string> urls, CancellationToken ct)    {        var tasks = urls.Select(url => _http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct));        return (await Task.WhenAll(tasks)).ToList();    }}public class MyService{    private readonly DataClient _client;    public MyService(DataClient client) => _client = client;    public async Task RunAsync(CancellationToken ct)    {        var urls = new[] { "https://api.example.com/a", "https://api.example.com/b" };        var responses = await _client.FetchAllAsync(urls, ct);        foreach (var res in responses)        {            res.EnsureSuccessStatusCode();            var content = await res.Content.ReadAsStringAsync(ct);            Console.WriteLine($"{res.RequestMessage!.RequestUri} - {content.Length} bytes");            res.Dispose();        }    }}

6. 취소, 타임아웃, 스트리밍 옵션

긴 요청을 빠르게 중단하려면 취소 토큰과 타임아웃을 함께 사용합니다. ResponseHeadersRead로 헤더 수신 즉시 스트리밍을 시작합니다.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));var urls = new[] { "https://api.example.com/a", "https://api.example.com/b" };var results = await ParallelFetcher.FetchAllAsync(urls, cts.Token);

7. 재시도/백오프: Polly 통합

일시적 오류(5xx, 타임아웃)에는 지수 백오프 재시도가 효과적입니다. IHttpClientFactory에 Polly 정책을 추가해 표준화합니다.

using Polly;using Polly.Extensions.Http;static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(){    return HttpPolicyExtensions        .HandleTransientHttpError()        .WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(200 * Math.Pow(2, attempt)));}

8. 성능 팁과 체크리스트

HTTP/2를 사용해 연결을 효율화합니다. 서버당 연결(MaxConnectionsPerServer)을 적절히 조정합니다. 자동 압축을 활성화하고, 큰 응답은 스트리밍으로 처리합니다. JSON은 System.Text.Json으로 비동기 역직렬화합니다. 작업은 async/await로 끝까지 비동기로 유지합니다.

9. 마무리

다중 요청 병렬 처리는 Task.WhenAll, 적절한 동시성 제한, 올바른 HttpClient 재사용, 그리고 IHttpClientFactory+Polly 조합이 핵심입니다. 위 패턴을 적용하면 안정성과 처리량이 함께 향상됩니다.