Quick Reference
요청마다 new HttpClient() 하지 않습니다. 서로 다른 서버·인증·기본 헤더 설정을 DI에서 관리할 때는 IHttpClientFactory, 수명이 긴 단일 클라이언트를 직접 관리할 때는 SocketsHttpHandler.PooledConnectionLifetime을 함께 고릅니다. 응답 상태를 분기해야 하면 GetAsync, 성공 JSON만 바로 읽으면 GetFromJsonAsync<T>가 맞습니다.
// Program.cs — IHttpClientFactory 등록
builder.Services.AddHttpClient("weather", client =>
{
client.BaseAddress = new Uri("https://api.weather.example.com/");
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
client.Timeout = TimeSpan.FromSeconds(10);
});
// 서비스에서 사용
public class WeatherService(IHttpClientFactory httpFactory)
{
public async Task<WeatherData?> GetAsync(string city, CancellationToken ct)
{
var client = httpFactory.CreateClient("weather");
return await client.GetFromJsonAsync<WeatherData>($"current/{city}", ct);
}
}문법
어떤 HttpClient 사용 형태가 있나
실전에서는 아래 세 가지를 먼저 구분하면 됩니다.
builder.Services.AddHttpClient("weather", client => { ... }); // 기명 클라이언트
builder.Services.AddHttpClient<GitHubService>(client => { ... }); // 타입 지정 클라이언트
var response = await client.GetAsync("users/1", ct); // 직접 응답 처리- 기명 클라이언트: 설정을 이름으로 구분
- 타입 지정 클라이언트: 서비스 타입에 HttpClient를 묶음
- 직접
GetAsync: 상태 코드와 헤더를 세밀하게 봐야 할 때
new HttpClient() 직접 생성이 위험한 이유 — socket exhaustion
HttpClient를 using으로 매번 생성하고 폐기하면 내부 HttpMessageHandler(TCP 연결을 관리하는 소켓 핸들러)도 함께 닫힙니다. 그런데 소켓은 닫히더라도 OS 수준에서 TIME_WAIT 상태로 일정 시간 머물기 때문에, 짧은 시간에 많은 요청을 처리하면 사용 가능한 소켓이 고갈(exhaustion)됩니다. 이 문제는 로컬에서는 잘 나타나지 않다가 운영 환경 부하 테스트나 트래픽 급증 시에 SocketException으로 나타납니다.
// ❌ 매 요청마다 new + Dispose — socket exhaustion 유발
public async Task<string> GetDataAsync(string url)
{
using var client = new HttpClient(); // 소켓 낭비
return await client.GetStringAsync(url);
}
// ⚠ 장수명 클라이언트도 연결 수명 정책이 없으면 DNS 변경을 오래 놓칠 수 있다.
private static readonly HttpClient Client = new(
new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
});
public async Task<string> GetDataAsync(string url)
{
return await Client.GetStringAsync(url);
}IHttpClientFactory — 핸들러 풀링으로 두 문제를 줄이는 대표 패턴
IHttpClientFactory는 HttpMessageHandler를 풀링합니다. CreateClient()는 새 HttpClient 인스턴스를 반환하지만, 내부 핸들러는 풀에서 재사용합니다. 기본 구현의 handler lifetime 기본값은 2분이지만 프로젝트에서 바꿀 수 있으며, 이는 클라이언트 timeout이 아니라 핸들러 재사용 기간입니다. factory로 만든 클라이언트는 짧게 쓰고 Dispose해도 되지만, Singleton에 오래 보관하거나 typed client를 Singleton에 주입하면 handler 교체를 놓쳐 DNS 갱신 이점을 잃을 수 있습니다.
// Program.cs / DI 등록
// 1. 기명 클라이언트 — 이름으로 구분하여 설정
builder.Services.AddHttpClient("github", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");
});
// 2. 타입 지정 클라이언트 — 서비스 타입으로 바인딩 (권장)
builder.Services.AddHttpClient<GitHubService>(client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");
});
// 타입 지정 클라이언트 구현
public class GitHubService(HttpClient client)
{
public async Task<Repo[]?> GetReposAsync(string user, CancellationToken ct)
=> await client.GetFromJsonAsync<Repo[]>($"users/{user}/repos", ct);
}GetFromJsonAsync / PostAsJsonAsync — JSON 통신 실용 패턴
System.Net.Http.Json 패키지(또는 .NET 5+)의 확장 메서드를 사용하면 JSON 직렬화/역직렬화를 수동으로 처리하지 않아도 됩니다. JsonSerializer를 직접 쓰거나 StreamReader로 응답을 읽을 필요가 없습니다.
// GET — JSON 응답을 직접 역직렬화
var user = await client.GetFromJsonAsync<UserDto>($"users/{id}", ct);
// POST — 객체를 JSON으로 직렬화하여 전송
var newUser = new CreateUserRequest { Name = "Mina", Email = "mina@example.com" };
var response = await client.PostAsJsonAsync("users", newUser, ct);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<UserDto>(ct);
// PUT
await client.PutAsJsonAsync($"users/{id}", updateRequest, ct);
// DELETE
var delResponse = await client.DeleteAsync($"users/{id}", ct);
delResponse.EnsureSuccessStatusCode();
// 응답 상태 코드 확인이 필요할 때 — GetAsync 사용
using var res = await client.GetAsync($"users/{id}", ct);
if (res.StatusCode == HttpStatusCode.NotFound) return null;
res.EnsureSuccessStatusCode();
var dto = await res.Content.ReadFromJsonAsync<UserDto>(ct);// ❌ multipart 경계를 HttpClient가 직접 만들도록 두지 않으면 업로드가 깨질 수 있다
using var request = new HttpRequestMessage(HttpMethod.Post, "upload");
request.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data");
// ✅ MultipartFormDataContent가 boundary를 만들게 둔다
using var form = new MultipartFormDataContent();
form.Add(new StringContent("Mina"), "name");
form.Add(new StreamContent(fileStream), "file", "avatar.png");
await client.PostAsync("upload", form, ct);Timeout 설정과 CancellationToken 결합
HttpClient.Timeout은 HttpClient 인스턴스 전체에 적용되는 기본 timeout입니다. 요청별로 다른 timeout이 필요하거나, 외부에서 온 취소 신호와 결합해야 할 때는 CancellationTokenSource를 함께 사용합니다. timeout과 외부 취소는 모두 OperationCanceledException 계열로 보일 수 있으므로, 예외 타입만 보고 원인을 판단하지 말고 각 source 상태를 확인합니다.
public async Task<WeatherData?> GetWeatherAsync(string city, CancellationToken externalCt)
{
// 요청별 timeout: 5초
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
// 외부 취소 신호(예: ASP.NET Core 요청 취소) + 요청 timeout 결합
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
externalCt, timeoutCts.Token);
try
{
return await _client.GetFromJsonAsync<WeatherData>(
$"current/{city}", linkedCts.Token);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
_logger.LogWarning("날씨 API 요청 시간 초과: {City}", city);
return null;
}
catch (OperationCanceledException) when (externalCt.IsCancellationRequested)
{
throw; // 호출자가 요청 취소를 자신의 정책으로 처리한다.
}
}HttpClient 수명과 요청
| 상황 | 적합한 선택 |
|---|---|
| 간단한 GET + JSON 파싱 | GetFromJsonAsync<T> |
| POST + JSON 본문 | PostAsJsonAsync |
| 응답 상태 코드 확인 필요 | GetAsync + EnsureSuccessStatusCode() |
| 전역 BaseAddress / 헤더 설정 | AddHttpClient<T> 타입 지정 클라이언트 |
| 요청별 다른 timeout | CancellationTokenSource + CreateLinkedTokenSource |
| 운영 환경에서 권장되는 기본 선택 | IHttpClientFactory 또는 장수명 HttpClient 전략 검토 |
주의할 점
HttpClient를 using으로 매번 new하는 방식은 운영 환경에서 문제가 됩니다. IHttpClientFactory가 현대 .NET 애플리케이션의 대표적인 해결책이지만, Microsoft 문서 기준으로는 SocketsHttpHandler.PooledConnectionLifetime를 설정한 장수명 HttpClient도 대안이 될 수 있습니다.
// ❌ using으로 매번 생성 — socket exhaustion
using var client = new HttpClient();
var data = await client.GetStringAsync(url);
// ✅ 장수명 클라이언트 — 연결을 교체해 DNS 변화를 다시 조회할 수 있게 한다.
private static readonly HttpClient Shared = new(
new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
});
// ✅ IHttpClientFactory — 핸들러 풀링; 짧게 만든 client를 오래 보관하지 않는다.
public class MyService(IHttpClientFactory factory)
{
public async Task<string> GetAsync(string url, CancellationToken ct)
{
var client = factory.CreateClient();
return await client.GetStringAsync(url, ct);
}
}IHttpClientFactory는 CookieContainer도 handler와 함께 공유할 수 있으므로, 쿠키 격리가 필요한 흐름에는 맞지 않을 수 있습니다. 장수명 클라이언트를 직접 관리한다면 SocketsHttpHandler.PooledConnectionLifetime 같은 연결 수명 전략을 같이 설정합니다. EnsureSuccessStatusCode()는 4xx/5xx 응답 시 HttpRequestException을 던집니다. 404를 예외 없이 처리해야 한다면 response.StatusCode를 먼저 분기하세요.
참고 링크
2 sources