HttpClient를 using으로 감싸면 안 됩니다 ── C# 업무 앱의 HTTP 통신 실무(생성 패턴·타임아웃·재시도)
· 업데이트: · Go Komura · CSharp, .NET, HttpClient, IHttpClientFactory, 네트워크, async/await, Windows 개발, 기술 상담
수정 이력(9건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 기사 맨 앞에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 연결 풀이 핸들러 쪽에 있음을 보여주는 그림과, 소켓 고갈과 DNS 변경이라는 두 가지 실패가 어떻게 이어지는지를 보여주는 그림을 추가했습니다. 아울러 4장의 핸들러 풀 그림을 텍스트에서 도식으로 바꾸고, 그림 번호를 기사 안에서 일련으로 맞췄습니다. 본문 설명은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
- ephemeral 포트 고갈 설명과 원인 분리를 고쳤습니다. 동적 포트 범위의 16384개를 「한 대에서 동시에 열 수 있는 연결의 상한」이라고 적었으나, TCP는 연결을 4-tuple로 식별하므로 같은 포트 번호가 목적지가 다른 연결에 실리는 것은 사양상 가능합니다. 머신 전체의 TIME_WAIT 총수를 16384와 비교하는 기준표도, 다른 목적지로의 정상적인 연결까지 세어 오진하는 형태였으므로, Microsoft의 원인 분리 절차(TIME_WAIT가 많다는 것 자체는 증거가 되지 않는다는 점, 아웃바운드 연결의 일제 실패·이벤트 ID 4227/4231·단일 PID로의 편중으로 확인할 것)에 맞춰 프로세스 단위·목적지 단위로 묶는 형태로 바꿨습니다. `SocketException`이 나왔다고 해서 고갈은 아니라는 점도 명시합니다.
- .NET Framework용 대안 코드 예에서 연결 수명을 설정하는 메서드를 정의해 두고 호출하지 않았기 때문에, 첫 요청보다 앞에 반드시 지나가도록 고쳤습니다. 호출되지 않으면 연결이 무기한 유지되어 대책이 먹지 않습니다.
- 소켓이 고갈되는 구조를 ephemeral 포트 개수와 TIME-WAIT부터 순서대로 설명하도록 바꿨습니다. 아울러 DI와 Generic Host 용어 설명, .NET Framework에 남는 경우의 대안, netstat 결과 판정 기준, 핸들러 풀 구조도를 추가했습니다.
- 본문의 관련 기사 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 것을 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.21635404)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「HttpClient를 using으로 감싸면 안 됩니다 ── C# 업무 앱의 HTTP 통신 실무(생성 패턴·타임아웃·재시도)」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/csharp-httpclient-practical-guide/
- DOI(등록된 아카이브)
- 10.5281/zenodo.21635404
- DOI(마지막 등록 버전)
- 10.5281/zenodo.21635405
「오후가 되면 외부 API 연결이 SocketException으로 실패하기 시작한다」「연결 대상을 바꿨는데도 앱이 이전 서버로 계속 붙는다」──C#의 HttpClient는 GetAsync만 호출하면 쉽지만, 인스턴스를 어떻게 생성·유지할지를 잘못 잡으면 이런 「당장은 되는데, 운영에 올라간 뒤에 깨지는」 장애를 남기게 됩니다.
이 기사에서는 Windows 업무 앱이 외부 Web API나 사내 서비스를 호출하는 장면을 전제로, HttpClient의 올바른 생성 패턴, 타임아웃 설계, 재시도, 오류 처리, 그리고 Windows 환경 고유의 함정까지 실무에서 판단이 어려운 순서로 정리합니다.
1. 먼저 결론(판단표)
HttpClient를 어떻게 둘지는 앱 구성에 따라 답이 달라집니다. 먼저 판단표로 정리합니다.
표에 나오는 약어만 먼저 풀어 둡니다. DI(Dependency Injection: 의존성 주입) 은 클래스가 필요한 부품을 스스로 new하지 않고 밖에서 넘겨받는 설계 기법이며, .NET에서는 Microsoft.Extensions.DependencyInjection이 그 그릇입니다. Generic Host 는 그 DI 컨테이너에 더해 설정·로그·상주 처리의 시작과 종료까지 한꺼번에 맡는 .NET의 범용 호스트로, 「Generic Host란 무엇인가」와 「Generic Host + BackgroundService를 데스크톱 앱에서 쓰기」에서 다룹니다. IHttpClientFactory 는 그 DI 위에 올라가는 HttpClient 공급자입니다(4장). DI를 쓰지 않는 앱에서도 읽을 수 있도록, DI 없는 수단(static + PooledConnectionLifetime)부터 순서대로 설명합니다.
| 앱 형태 | 권장 패턴 | 이유 |
|---|---|---|
| 수 초~수 분 만에 끝나는 콘솔 도구(.NET) | static/싱글톤 HttpClient 1개 |
수명이 짧은 프로세스라면 DNS 변경 문제는 사실상 무시해도 됩니다 |
| 장시간 상주하는 앱·Windows 서비스(.NET, DI 없음) | static HttpClient + SocketsHttpHandler.PooledConnectionLifetime 설정 |
소켓 고갈과 DNS 변경 문제 모두를 해결할 수 있습니다 |
| Generic Host / DI를 쓰는 앱(.NET) | IHttpClientFactory(AddHttpClient) |
핸들러 풀과 교체를 팩토리에 맡길 수 있습니다. 명명된/형식화된 클라이언트로 연결 대상별 설정을 분리할 수 있습니다 |
| .NET Framework 앱 | Microsoft.Extensions.Http 패키지로 IHttpClientFactory 도입 |
.NET Framework에서는 직접 생성할 때 포트 고갈이 일어나기 쉽고, 공식에서도 팩토리 이용을 권합니다1 |
| 연결 대상마다 프록시·Cookie·인증서 설정이 다르다 | 설정마다 HttpClient를 나눈다(재사용하지 않는다) |
핸들러의 연결 설정은 첫 요청을 보낸 뒤에는 바꿀 수 없습니다2 |
그다음, 결론을 먼저 적습니다.
- 요청마다
new HttpClient()해서using으로 폐기하면 안 됩니다.HttpClient는 내부에 연결 풀을 가지며, 재사용하는 전제로 설계되어 있습니다. 매번 생성·폐기하면 부하가 높을 때 사용 가능한 소켓을 다 써 버려SocketException이 발생합니다2. - 그렇다고
static으로 두고 끝도 아닙니다.HttpClient는 DNS를 연결 생성 때만 해석하므로, 연결 대상 IP 주소가 바뀌어도 이전 연결을 계속 씁니다.SocketsHttpHandler.PooledConnectionLifetime으로 연결 수명을 제한하는 것이 공식적으로 권장되는 해결책입니다1. - Generic Host나 DI를 쓰고 있다면
IHttpClientFactory에 맡깁니다. 팩토리가 핸들러를 풀링하고, 기본 2분마다 교체해 소켓 고갈과 DNS 변경 모두에 대응합니다3. - 타임아웃 기본값은 100초입니다. 업무 앱 체감으로는 「무한히 굳는」 것과 크게 다르지 않은 길이이므로, 연결 대상마다 요건을 정해 명시적으로 설정하세요.
- 재시도는 직접 만들지 말고
Microsoft.Extensions.Http.Resilience의 표준 핸들러부터 시작하세요. 재시도·서킷 브레이커·타임아웃을 실적 있는 기본값 세트로 넣을 수 있어, 직접 짠 재시도 루프에서 흔한 「실패한 POST를 무조건 재전송해 이중 등록」 같은 사고를 설계 단계에서 피할 수 있습니다4.
참고로 이 기사의 코드 예는 모두 설명에 필요한 부분만 뽑아 낸 조각입니다. logger는 Microsoft.Extensions.Logging.ILogger, ct는 호출 측에서 넘긴 CancellationToken, url과 client는 각각 연결 대상 URL과 HttpClient 인스턴스를 가리키며, 모두 주변 클래스·메서드에서 준비되어 있다는 전제로 적었습니다. 그대로 Main에 붙여도 동작하지 않습니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 19건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 왜 「using으로 그때그때 생성」이 안 되는가 ── 소켓 고갈
HttpClient가 IDisposable을 구현하고 있어서, 다음 같은 코드는 얼핏 올바르게 보입니다.
// 안티패턴: 요청마다 생성해서 폐기한다
public async Task<string> GetDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
문제는 Dispose해도 OS 수준에서는 소켓이 바로 해제되지 않는다는 점입니다. TCP 사양상, 닫은 쪽 소켓은 TIME_WAIT 상태로 한동안 남습니다. 호출 빈도가 낮을 때는 아무 일도 없지만, 부하가 오르면 미해제 소켓이 쌓이다가 어느 순간 갑자기 SocketException으로 연결할 수 없게 됩니다2.
다만 「소켓이 남는다」만으로는 왜 고갈까지 가는지 와닿지 않습니다. 내부를 한 단계 더 보겠습니다. 핵심은 연결마다 소비되는 자기 쪽 포트 번호가 유한하다는 점입니다.
- 클라이언트가 서버로 TCP 연결을 열 때, 목적지 포트(HTTPS라면 443)는 정해져 있지만, 자기 쪽 포트 번호는 OS가 빈 것에서 자동 할당합니다. 이를 ephemeral 포트(ephemeral port, Windows 용어로는 동적 포트)라고 합니다.
- Windows 기본 동적 포트 범위는 49152~65535, 즉 16384개입니다5. 아웃바운드 연결을 열 때 OS가 할당하는 풀이며, 트랜스포트(TCP/UDP)별·IPv4/IPv6별로 설정됩니다5. 「한 대당 동시 16384 연결까지」라는 상한이 아닙니다. TCP는 연결을 「자기 쪽 주소와 포트」「상대 쪽 주소와 포트」의 4-tuple로 구별하므로6, 같은 포트 번호가 목적지가 다른 여러 연결에 실리는 것은 사양상 가능합니다. 실제로 먹히는 것은 총수 자체보다 한 프로세스가 얼마나 빠른 속도로 풀을 깎는가입니다.
- 연결을 닫아도 먼저 닫은 쪽 포트는 바로 재사용할 수 없습니다. 늦게 도착한 이전 패킷이, 같은 포트 조합으로 열린 새 연결에 섞여 드는 것을 막기 위해 TCP는 TIME-WAIT 상태를 일정 시간(2×MSL: Maximum Segment Lifetime의 2배) 유지합니다6.
- 결과적으로 「요청마다 연결을 열고 닫는」 설계는 요청 1건당 포트를 1개, 수십 초에서 수 분간 사용 완료로 붙잡아 두는 일이 됩니다. 초당 100요청이면 초당 100개 속도로 16384개 칸을 깎는 셈이라, 수 분이면 바닥이 납니다.
연결 풀을 가진 것은 HttpClient 자체가 아니라 그 안의 핸들러입니다. 인스턴스를 다시 만들면 풀까지 다시 만들어진다는 점이 고갈이 시작되는 지점입니다.
flowchart TB
B1["요청마다 new 해서 using으로 폐기한다"] --> B2["핸들러까지 다시 만들어진다<br/>연결 풀은 매번 비어 있다"]
B2 --> B3["매번 TCP 연결을 연다<br/>닫아도 포트는 TIME-WAIT로 남는다"] --> B4["포트를 계속 깎는다 = 소켓 고갈"]
G1["static HttpClient를 하나 재사용한다"] --> G2["핸들러가 연결 풀을 계속 유지한다"]
G1b["팩토리에서 클라이언트를 그때그때 받는다<br/>(재사용하는 것은 클라이언트가 아니라 핸들러·4장)"] --> G2
G2 --> G3["이미 수립된 연결을 재사용한다"] --> G4["포트를 새로 소비하지 않는다"]
그림 1: 재사용되는 것은 핸들러가 품은 연결 풀입니다. HttpClient를 다시 만들면 풀도 함께 버려집니다
풀을 다 쓰면 다음 연결 요구는 할당할 포트가 없어 실패합니다. CPU도 메모리도 여유가 있는데 연결만 안 된다는, 얼핏 알 수 없는 증상이 되는 것은 고갈되는 것이 포트 번호라는 다른 자원이기 때문입니다. 다만 SocketException이 나왔다고 해서 고갈은 아닙니다. 상대 서버 중지, 경로 차단, DNS, TLS 실패도 같은 예외 형으로 올라옵니다. 원인 분리는 다음 항의 절차로 합니다. Microsoft 공식 가이드라인도, 연결을 닫아도 TCP 포트는 즉시 해제되지 않으며 요청 빈도가 높으면 OS의 사용 가능 포트 수 상한에 달할 수 있다고 명시합니다1.
이 장애가 까다로운 것은 개발 중·테스트 중에는 거의 재현되지 않는다는 점입니다. 초에 몇 번의 수동 조작으로는 칸을 다 깎지 못하기 때문이며, 운영의 바쁜 시간대에만 떨어지거나 월말 배치 때만 떨어지는 형태로 나타납니다. 조사할 때는 현상이 난 시점에 다음 명령으로 TIME_WAIT 소켓 수와 자기 환경의 포트 범위를 확인합니다.
# TIME_WAIT 상태의 소켓 수를 센다
(netstat -ano | Select-String "TIME_WAIT").Count
# 자기 환경의 동적 포트 범위를 확인한다(기본은 시작 49152·16384개)
netsh int ipv4 show dynamicport tcp
여기서 머신 전체의 TIME_WAIT 총수를 범위의 16384와 비교하면 안 됩니다. 그 머신이 많은 목적지로 붙어 있으면 총수는 커지지만, 그것은 다른 목적지로의 정상적인 연결을 세는 것일 뿐, 이쪽 앱이 고갈에 가깝다는 뜻이 아닙니다. Microsoft의 원인 분리 절차에도 TIME_WAIT가 대량으로 있다는 것 자체는 고갈의 증거가 되지 않는다(앞으로 고갈될 수 있음을 보일 뿐이다)고 명시되어 있습니다7.
봐야 할 것은 프로세스 단위·목적지 단위 내역과 늘어나는 방식입니다.
# 프로세스와 상태로 묶는다. 한 PID에 TIME_WAIT가 치우쳐 있으면 그 프로세스가 원인
Get-NetTCPConnection | Group-Object State, OwningProcess |
Sort-Object Count -Descending | Select-Object -First 10 Count, Name
# 의심되는 목적지만 로컬 주소와 상대 쪽 엔드포인트로 묶는다
Get-NetTCPConnection -State TimeWait |
Group-Object LocalAddress, RemoteAddress, RemotePort |
Sort-Object Count -Descending | Select-Object -First 10 Count, Name
그다음 정말로 고갈인지는 증상으로 확인합니다. Microsoft가 든 확인 절차는 다음 3점입니다7.
| 확인할 것 | 보는 법 |
|---|---|
| 아웃바운드 연결이 거의 다 실패하는가 | 공유 접근, 다른 서버로의 RDP, telnet을 시도합니다. 한 목적지만 실패하면 고갈이 아닙니다 |
| 이벤트 로그 | 시스템 로그의 이벤트 ID 4227 / 4231 |
| 특정 PID로의 편중 | netstat -anob로 한 PID에 TIME_WAIT가 몰려 있는가 |
절대 개수보다 「늘어나는 방식」이 더 확실한 증거입니다. 앱을 재시작한 직후부터 우상향으로 계속 늘고, 부하를 멈춰도 수 분간 줄지 않으면 생성 패턴이 원인이라고 거의 단정할 수 있습니다. 반대로 부하에 따라 늘었다 줄었다 하며 상한에서 멈추면 연결은 재사용되고 있습니다. 참고로 TIME_WAIT 소켓은 프로세스에서 떨어진 상태라 PID 열에서 호출 측 앱을 특정하지 못할 때가 있습니다. 그때는 외부 주소 열(연결 대상)로 좁히세요. Windows 10 / Windows Server 2016 이후라면 TIME_WAIT를 빠져 BOUND가 된 포트도 netstat -anobq나 Get-NetTCPConnection으로 볼 수 있습니다7.
덧붙여 IHttpClientFactory를 통해 받은 HttpClient는 이 이야기의 대상이 아닙니다. 팩토리가 만든 클라이언트는 Dispose해도 핸들러(연결 풀의 실체)는 폐기되지 않으므로 using으로 감싸도 안전합니다3.
3. static으로 두면 끝이 아닙니다 ── DNS 변경 문제
소켓 고갈 대책으로 HttpClient를 static으로 두는 것은 올바른 방향이지만, 그것만으로는 다른 문제가 남습니다. HttpClient는 DNS를 연결 생성 때만 해석하며, DNS 레코드의 TTL도 참조하지 않습니다2. 연결이 풀 안에서 살아 있는 한, 연결 대상 IP 주소가 바뀌어도 이전 IP로 계속 붙습니다.
「페일오버로 DNS를 바꿨는데 재시작할 때까지 이전 서버를 계속 봤다」는 장애는 이 구조가 원인입니다. 공식 가이드라인이 권하는 해결책은 SocketsHttpHandler.PooledConnectionLifetime으로 연결 수명을 제한하는 것입니다1.
// .NET (Core) / .NET 5+ 권장 패턴:
// 연결을 일정 주기로 다시 만들게 해 DNS 변경을 따라간다
private static readonly HttpClient SharedClient = new(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});
수명이 끝난 연결은 다음 요청 때 다시 만들어지고, 그 타이밍에 DNS가 다시 해석됩니다. 값은 DNS 변경을 얼마나 빨리 따라가고 싶은지로 정합니다. 공식 문서 예에서는 2분이 쓰이지만, 연결 대상이 거의 바뀌지 않는 사내 시스템이라면 더 길어도 문제 없습니다1.
참고로 SocketsHttpHandler는 .NET Core 2.1 이후 구현이며, .NET Framework에서는 쓸 수 없습니다. .NET Framework라면 다음 장의 IHttpClientFactory를 쓰세요1.
여기서 대상 환경을 한곳에 정리해 둡니다. 이 기사에 나오는 수단 중 무엇이 자기 환경에서 되는지는 이 표로 확인하세요.
| 수단 | .NET Framework 4.8 | .NET Core 2.1+ / .NET 5+ |
|---|---|---|
HttpClient 본체 |
쓸 수 있음(내부 구현은 HttpWebRequest / ServicePoint) |
쓸 수 있음(내부 구현은 SocketsHttpHandler) |
SocketsHttpHandler.PooledConnectionLifetime |
쓸 수 없음 | 쓸 수 있음. DNS 추종의 첫 후보1 |
IHttpClientFactory |
Microsoft.Extensions.Http 패키지로 쓸 수 있음. 공식 권장1 |
쓸 수 있음 |
ServicePointManager.DefaultConnectionLimit |
쓸 수 있음(기본 2라는 작기에 주의)8 | HttpClient에는 먹지 않음. 상한은 MaxConnectionsPerServer로 지정합니다 |
ServicePoint.ConnectionLeaseTimeout |
쓸 수 있음(아래) | 실질적으로 의미가 없습니다9 |
.NET Framework에서의 첫 선택은 공식 가이드라인대로 Microsoft.Extensions.Http 패키지로 IHttpClientFactory를 도입하는 것입니다1. 그래도 「기존 큰 WinForms 앱에 먼저 DI부터 넣을 여유는 없다」는 현장도 많을 것입니다. 그때의 절충안이 HttpClient는 하나를 공유한 뒤, 연결 수명을 ServicePoint.ConnectionLeaseTimeout으로 제한하는 방법입니다.
using System;
using System.Net;
using System.Net.Http;
// .NET Framework 4.8 절충안:
// HttpClient는 공유하고, 연결 수명은 ServicePoint 쪽에서 제한해 DNS 변경을 따라가게 한다
private static readonly Uri ApiBase = new Uri("https://order.example.co.jp/");
private static readonly HttpClient SharedClient = CreateSharedClient();
private static HttpClient CreateSharedClient()
{
// 첫 요청보다 앞에 반드시 지나가게 한다. 여기를 호출하지 않으면
// ConnectionLeaseTimeout 은 기본 -1(연결을 무기한 유지) 그대로여서,
// DNS 변경을 따라가지 않는다=이 절의 대책이 전혀 먹지 않는 상태가 된다
ServicePoint sp = ServicePointManager.FindServicePoint(ApiBase);
sp.ConnectionLeaseTimeout = (int)TimeSpan.FromMinutes(2).TotalMilliseconds; // 단위는 밀리초
return new HttpClient { BaseAddress = ApiBase };
}
ConnectionLeaseTimeout은 「지정 시간이 지난 뒤, 요청 1건을 처리한 시점에 그 연결을 닫는다」는 설정이며, 로드 밸런서 전환처럼 연결을 주기적으로 다시 열고 싶을 때를 위해 마련된 것입니다9. 닫힌 연결은 다음 요청에서 다시 만들어지고, 그때 DNS가 다시 해석됩니다. 발상은 PooledConnectionLifetime과 완전히 같고, .NET Framework 쪽 대응하는 기능이라고 보면 됩니다.
다만 ServicePoint를 포함한 WebRequest 계열 API는 신규 개발에서 쓰지 말라고 공식에 명시되어 있습니다9. 이것은 어디까지나 .NET Framework 그대로 연명할 때의 조치이며, .NET으로 옮길 때는 PooledConnectionLifetime이나 IHttpClientFactory로 바꾸세요. 마이그레이션 때의 관점은 「.NET Framework→.NET 마이그레이션하기 전 체크리스트」에 정리해 두었습니다.
여기까지의 두 실패는 모두 「연결을 언제 다시 만들까」를 정하지 않은 데서 옵니다. 2장과 3장의 관계를 한 장으로 그리면 다음과 같습니다.
flowchart TB
Q["HttpClient와 연결 풀을 어떻게 둘까"]
Q -->|"요청마다 new 해서 using으로 폐기"| A["연결 풀이 매번 버려진다<br/>→ ephemeral 포트를 계속 깎는다<br/>= 소켓 고갈(2장)"]
Q -->|"하나를 static으로 두고 계속 유지하기만 한다"| B["연결이 풀에 계속 남는다<br/>→ DNS는 연결 생성 때만 해석된다<br/>= 전환 후에도 이전 IP로 붙는다(3장)"]
Q -->|"연결 풀을 공유한 뒤, 연결에 수명을 둔다"| C["일정 주기로 연결을 다시 만든다<br/>= 고갈과 DNS 변경 모두에 대응할 수 있다"]
C --> C1["환경별 수단은 1장 판단표와 같다<br/>·.NET 5+ : static + PooledConnectionLifetime<br/>·DI / Generic Host : IHttpClientFactory<br/> (클라이언트는 그때그때 받고, 공유되는 것은 핸들러·4장)<br/>·.NET Framework : IHttpClientFactory,<br/> 어려우면 ConnectionLeaseTimeout"]
그림 2: 「재사용하지 않기」와 「재사용만 하고 끝」은 둘 다 실패합니다. 정답은 연결 풀을 재사용한 뒤, 연결에 수명을 두는 것입니다. 무엇을 공유하는지는 수단마다 다릅니다(팩토리라면 클라이언트가 아니라 핸들러)
4. DI를 쓴다면 IHttpClientFactory
Generic Host나 DI 컨테이너를 쓰는 앱에서는 IHttpClientFactory(AddHttpClient)가 첫 후보입니다. Generic Host 자체의 설명은 「Generic Host란 무엇인가」를, 데스크톱 앱 도입은 「Generic Host + BackgroundService를 데스크톱 앱에서 쓰기」를 참조하세요.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// 명명된 클라이언트: 연결 대상마다 설정을 분리한다
builder.Services.AddHttpClient("OrderApi", client =>
{
client.BaseAddress = new Uri("https://order.example.co.jp/");
client.Timeout = TimeSpan.FromSeconds(10);
});
팩토리가 무엇을 하는지는 그림으로 보는 것이 빠릅니다. HttpClient와, 연결 풀을 가진 핸들러가 별개라는 점이 이 구조의 전부입니다.
flowchart TB
C1["CreateClient 1회째<br/>(일회용 클라이언트)"]
C2["CreateClient 2회째<br/>(일회용 클라이언트)"]
C3["교체 후의 CreateClient<br/>(일회용 클라이언트)"]
H1["핸들러 1<br/>연결 풀의 실체는 여기"]
P1["연결 풀<br/>TCP 연결을 재사용하고, 소켓을 버리지 않는다"]
H2["핸들러 2<br/>새로 만들어진다"]
P2["새 연결 풀<br/>여기서 DNS가 다시 해석된다"]
C1 --> H1
C2 --> H1
H1 --> P1
H1 -->|"기본 2분에 수명 만료. 신규 발급을 멈추고,<br/>사용 중인 처리가 끝나는 대로 폐기된다"| H2
C3 --> H2
H2 --> P2
그림 3: IHttpClientFactory의 핸들러 풀과 주기적 교체
이 그림에서 기사 맨 앞에 적은 두 결론을 읽을 수 있습니다. 「일회용은 안 됨」인 이유는, 직접 생성한 new HttpClient()에서는 상단(클라이언트)과 하단(핸들러·연결 풀)이 1대 1로 같이 버려지고, 버릴 때마다 소켓까지 버리기 때문입니다. 「static도 어중간」인 이유는, static으로 두면 하단이 고정되어 폐기되지 않아 소켓은 지키지만, 핸들러 교체(=DNS 재해석)를 가리키는 화살표가 한 번도 일어나지 않기 때문입니다. 팩토리는 상단을 일회용으로 둔 채, 하단만 공유하고, 게다가 주기적으로 교체해 두 조건을 동시에 충족합니다.
팩토리 동작에서 짚어 둘 점은 세 가지입니다.
- 핸들러는 풀링되며, 기본으로 2분마다 교체됩니다.
CreateClient마다 새HttpClient가 돌아오지만, 뒤쪽 핸들러(연결 풀)는 공유되므로 소켓 고갈은 일어나지 않고, 주기적 교체로 DNS 변경도 따라갑니다3. - 팩토리가 만든
HttpClient는 짧게 쓰는 것이 전제입니다. 받은 인스턴스를 싱글톤 필드에 붙잡아 두면 핸들러 교체에 참여하지 못해 DNS 변경을 따라가지 않습니다. 형식화된 클라이언트를 싱글톤 서비스에 주입하는 구성도 같은 이유로 피하세요3. - Cookie에 의존하는 앱에서는 주의가 필요합니다. 핸들러가 풀링된 결과
CookieContainer가 의도치 않게 공유됩니다. Cookie를 쓰는 경우 팩토리를 피하거나, Cookie 처리를 끄고 직접 헤더를 붙이는 설계가 공식에 안내되어 있습니다1.
인증 있는 API(Microsoft Entra ID로 보호된 API 등)를 호출할 때의 토큰 취득 쪽은 「WinForms/WPF 앱에 Entra ID 인증을 넣기」에서 다루므로 그쪽을 참조하세요.
5. 타임아웃 설계 ── 기본 100초는 업무 앱에는 너무 깁니다
HttpClient.Timeout의 기본값은 100초입니다10. 화면 조작의 연장으로 API를 호출하는 업무 앱에서 100초를 기다리게 하는 것은 「굳었다」와 같으므로, 연결 대상마다 명시적으로 설정합니다.
// 클라이언트 전체의 기본 타임아웃
client.Timeout = TimeSpan.FromSeconds(10);
// 특정 요청만 짧게/길게 하고 싶을 때는 CancellationTokenSource를 쓴다
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
HttpResponseMessage response = await client.GetAsync(url, cts.Token);
설계상 주의점은 다음과 같습니다.
- 타임아웃 때 예외는
TaskCanceledException입니다. .NET 5 이후,HttpClient.Timeout에서 온 타임아웃에서는 내부 예외에TimeoutException이 들어갑니다10. 다만 위와 같이 직접 만든CancellationTokenSource에 의한 타임아웃에서는 내부 예외가 붙지 않습니다. 타임아웃과 사용자 원인 취소의 구분은 내부 예외가 아니라 「호출 측에서 넘긴 토큰이 이미 취소됐는지」로 판정하는 것이 확실합니다(7장 코드 예). catch 절에서HttpRequestException만 보고 있으면 타임아웃을 놓치므로 주의하세요. Timeout은 「요청 전체」의 제한입니다. 연결 수립만 짧게 제한하고 싶을 때는SocketsHttpHandler.ConnectTimeout을 함께 씁니다. 「서버가 죽어 있을 때는 3초에 포기하고 싶지만, 정상일 때의 큰 응답은 60초 기다리고 싶다」는 요건은 이 둘의 조합으로 표현할 수 있습니다2.- 큰 파일 다운로드는 기본 버퍼링을 피하세요.
HttpClient는 기본적으로 응답 전체를 메모리에 읽으므로, 수십 MB 이상 다운로드에서는HttpCompletionOption.ResponseHeadersRead를 지정해 스트림으로 처리합니다2.
타임아웃 값을 appsettings.json으로 빼서 환경마다 바꾸는 구성은 「Windows 업무 앱의 구성 관리 실무」의 판단표를 그대로 쓸 수 있습니다.
덧붙여 WinForms/WPF에서 HTTP 통신을 호출할 때 .Result나 .Wait()로 막으면 UI 스레드 데드락을 일으킵니다. 이 흔한 함정은 「C# async/await 실무 판단표」에서 자세히 설명하므로, 통신 처리를 쓰기 전에 한 번 읽는 것을 권합니다.
6. 재시도 ── 직접 짠 루프가 아니라 표준 resilience 핸들러를 씁니다
네트워크는 일시적으로 실패하는 것이므로, 외부 API를 호출하는 처리에는 재시도가 필요합니다. 다만 for 루프와 Task.Delay로 직접 짠 재시도는 다음을 전부 스스로 올바르게 구현해야 해서 수지가 맞지 않습니다.
- 재시도해도 되는 실패(타임아웃, HTTP 408/429/5xx)와, 해도 소용없는 실패(HTTP 400/401/404)의 구분
- 다시 실행하면 사고가 되는 HTTP 메서드(POST에 의한 등록의 이중 실행 등)의 제외
- 지수 백오프와 지터(모든 클라이언트가 동시에 재전송해 서버를 다시 쓰러뜨리는 것을 막는 흔들림)
- 장애가 이어질 때 호출 자체를 멈추는 서킷 브레이커
Microsoft.Extensions.Http.Resilience 패키지의 표준 핸들러는 이 세트를 실적 있는 기본값으로 제공합니다4.
builder.Services.AddHttpClient("OrderApi", client =>
{
client.BaseAddress = new Uri("https://order.example.co.jp/");
})
.AddStandardResilienceHandler(); // 재시도+서킷 브레이커+타임아웃의 표준 세트
표준 핸들러 기본값은 요청 전체 타임아웃 30초, 최대 3회의 지수 백오프 재시도(최초 지연 2초·지터 포함), 시도마다 타임아웃 10초, 그리고 HTTP 408/429/5xx와 HttpRequestException을 일시 오류로 다루는 구성입니다4.
한 가지만 기본값에서 주의할 것은, 표준 핸들러는 기본적으로 모든 HTTP 메서드를 재시도한다는 점입니다. 등록용 POST가 이중 실행되면 곤란한 API에서는 안전하지 않은 메서드의 재시도를 끄세요4.
httpClientBuilder.AddStandardResilienceHandler(options =>
{
// POST/PUT/DELETE 등의 재실행을 끈다
options.Retry.DisableForUnsafeHttpMethods();
});
참고로 재시도로 해결하는 것은 어디까지나 일시적 실패입니다. 연결은 됐는데 데이터가 흐르지 않거나 응답이 극단적으로 느린 증상은 TCP 레이어 문제인 경우도 많아, 그 원인 분리는 「TCP 재전송으로 산업용 카메라 통신이 멈추는 원인 분리」에서 다룬 기법이 참고가 됩니다.
7. 오류 처리 ── 상태 코드를 어떻게 다룰까
HttpClient는 HTTP 404나 500처럼 「HTTP로서는 응답이 돌아온 실패」에서는 예외를 던지지 않습니다. 예외가 되는 것은 연결 실패·타임아웃·취소처럼 응답 자체를 얻지 못한 경우입니다. 이 두 종류를 의식해 나눠 씁니다.
try
{
using HttpResponseMessage response = await client.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
{
// 응답은 돌아왔지만 실패: 상태 코드로 분기할 수 있다
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null; // 「존재하지 않음」을 정상 경로로 다루는 예
}
response.EnsureSuccessStatusCode(); // 그 외는 HttpRequestException으로
}
return await response.Content.ReadFromJsonAsync<Order>(ct);
}
catch (HttpRequestException ex)
{
// 연결 실패, 또는 EnsureSuccessStatusCode에 의한 실패 상태.
// .NET 5 이후는 ex.StatusCode로 실패 때의 상태 코드를 참조할 수 있다
logger.LogError(ex, "주문 API 호출에 실패했습니다. StatusCode={StatusCode}", ex.StatusCode);
throw;
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
// 호출 측에서 넘긴 토큰에 의한 취소(화면을 닫은 경우 등).
// 오류가 아니므로 오류 로그를 남기지 않고 그대로 전파한다
throw;
}
catch (TaskCanceledException ex)
{
// HttpClient.Timeout, 또는 직접 만든 타임아웃용 CTS의 기한 만료
logger.LogError(ex, "주문 API가 타임아웃했습니다");
throw;
}
「404를 예외로 다룰지, null로 다룰지」 같은 판단은 연결 대상 API의 의미에 달렸습니다. 무엇이든 EnsureSuccessStatusCode 한 방으로 예외로 만들면 호출 측 catch 절이 비대해집니다. 예외로 만들 것과 반환값으로 표현할 것의 선 긋기는 「예외의 catch·로그·오류 처리 실무」의 생각을 그대로 적용할 수 있습니다.
JSON 송수신은 System.Net.Http.Json의 GetFromJsonAsync / PostAsJsonAsync / ReadFromJsonAsync를 쓰면 문자열을 거쳐 직렬화를 직접 쓰지 않아도 됩니다.
8. Windows 업무 앱 고유의 함정
마지막으로 Windows 환경 실무에서 자주 밟는 함정을 정리합니다.
- 프록시 자동 검색으로 첫 요청이 느리다. Windows 기본에서는
HttpClient가 OS의 프록시 설정(자동 검색 포함)을 씁니다. 프록시가 필요 없다는 것이 분명하면HttpClientHandler.UseProxy = false로 끄면 검색 대기가 사라집니다2. 반대로 사내 프록시가 필수인 환경에서는WebProxy로 명시하는 편이 「개발 기기에서는 되는데 서버에서는 안 된다」를 피할 수 있습니다. - 프록시 설정은 첫 요청 전에 끝냅니다. 핸들러의 연결 계열 설정은 한 번 요청을 보낸 뒤에 바꿔도 적용되지 않습니다2.
- 동시 연결 수 기본값은 .NET과 .NET Framework에서 정반대입니다. .NET(
SocketsHttpHandler)에서는 HTTP/1.1 동시 연결 수가 기본으로 무제한이라, 대량 병렬 요청에서 연결이 계속 늘어나 방화벽이나 서버 쪽 제한에 걸릴 수 있습니다. 병렬도가 높은 처리에서는MaxConnectionsPerServer로 상한을 두세요2. 반대로 .NET Framework에서는ServicePointManager.DefaultConnectionLimit의 기본값이 2(비 ASP.NET 환경)로 작아, 병렬 요청이 내부에서 기다려져 타임아웃하는 쪽 문제가 납니다. .NET Framework에서 병렬도를 올릴 때는 이 상한을 명시적으로 올리세요8. - Windows 서비스에서 호출할 때는 프록시와 TLS 컨텍스트가 사용자와 다릅니다. 서비스 실행 계정에는 사용자의 프록시 설정이나 자격 정보가 없으므로, 「대화형 사용자에서는 되는데 서비스에서는 안 된다」는 통신 장애의 흔한 원인이 됩니다. 서비스 특유의 실행 컨텍스트는 「Windows 서비스 만드는 법과 운영」을 참조하세요.
- 통신 대상 URL이나 API 키를 코드에 넣지 마세요. 연결 대상 전환은 구성 파일(「구성 관리 실무」), 비밀 정보 저장은 「DPAPI로 평문 설정을 피하기」의 방법에 맞추세요.
정리
HttpClient의 실무는 「어떻게 호출할지」보다 「어떻게 둘지」로 품질이 결정됩니다. 요청마다 생성은 소켓 고갈을, 무작정 static으로 두면 DNS 변경을 따라가지 못하게 되며, 둘 다 개발 중에는 보이지 않습니다. .NET이라면 PooledConnectionLifetime이 붙은 공유 인스턴스나 IHttpClientFactory, .NET Framework라면 IHttpClientFactory 도입이 답입니다. 그다음 타임아웃을 연결 대상마다 명시하고, 재시도는 표준 resilience 핸들러에 맡긴다──여기까지 해야 비로소 「가끔 실패하는 네트워크」를 전제로 한 업무 앱이 됩니다.
기존 앱의 통신 쪽 재검토(바쁜 시간대에만 떨어지는 통신, 타임아웃 설계 정리, 외부 API 연동의 신규 구현)는 실제 코드와 운영 환경을 보면서 판단해야 하는 경우가 많으므로, 고민되면 상담해 주세요.
관련 기사
- C# async/await 실무 판단표
- Generic Host란 무엇인가
- Generic Host + BackgroundService를 데스크톱 앱에서 쓰기
- appsettings.json만이 전부가 아닙니다 ── Windows 업무 앱의 구성 관리 실무
- WinForms/WPF 앱에 Entra ID 인증을 넣기
- TCP 재전송으로 산업용 카메라 통신이 멈추는 원인 분리
- TCP에서 Send한 단위마다 Receive할 수 있다는 오해
- 예외의 catch·로그·오류 처리 실무
- Windows 앱의 기밀 정보 저장 - DPAPI로 평문 설정을 피하기
관련 상담 영역
合同会社小村ソフト에서는 외부 API 연동을 포함한 Windows 업무 앱 개발, 기존 앱의 통신 장애(소켓 고갈·타임아웃·단속적 연결 실패) 조사와 수정 방침의 기술 상담을 다룹니다.
참고 링크
-
Microsoft Learn, Guidelines for using HttpClient. .NET Core/.NET 5+에서는
PooledConnectionLifetime을 설정한 수명이 긴 클라이언트 또는IHttpClientFactory가 만든 수명이 짧은 클라이언트를 쓸 것, .NET Framework에서는IHttpClientFactory이용이 권장되는 것, Cookie를 쓰는 앱에서는CookieContainer공유 때문에IHttpClientFactory를 피해야 하는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Microsoft Learn, HttpClient Class. 요청마다 생성이 소켓 고갈과
SocketException을 부르는 것, DNS를 연결 생성 때만 해석하고 TTL도 참조하지 않는 것, 핸들러의 연결 설정이 첫 요청 뒤에는 바꿀 수 없는 것, HTTP/1.1 동시 연결 수가 기본으로 무제한인 것, 큰 다운로드에서의 스트리밍 권장, 프록시 기본 동작과UseProxy로 끄는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn, IHttpClientFactory with .NET. 핸들러 기본 수명이 2분인 것, 팩토리가 만든
HttpClient는 짧게 쓰는 전제인 것, 팩토리가 만든 클라이언트의Dispose가 핸들러를 폐기하지 않는 것, 형식화된 클라이언트를 싱글톤에 주입하면 DNS 변경을 따라가지 않게 되는 것에 대해. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Build resilient HTTP apps: Key development patterns.
AddStandardResilienceHandler가 구성하는 5단계 전략(레이트 리미터/전체 타임아웃 30초/최대 3회의 지수 백오프 재시도/서킷 브레이커/시도 타임아웃 10초), 대상이 되는 상태 코드(408/429/5xx)와 예외,DisableForUnsafeHttpMethods로 POST 등의 재시도를 끄는 것에 대해. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, The default dynamic port range for TCP/IP has changed in Windows Vista and in Windows Server 2008. Windows Vista / Windows Server 2008 이후 기본 동적 포트 범위가 시작 49152·끝 65535인 것,
netsh int ipv4 show dynamicport tcp로 현재 범위를 확인할 수 있는 것에 대해. ↩ ↩2 -
IETF, RFC 9293 - Transmission Control Protocol (TCP), Section 3.3.2. TCP의 TIME-WAIT 상태와, 2×MSL(Maximum Segment Lifetime) 대기로 지연 패킷이 뒤이은 연결에 섞여 드는 것을 막는 구조에 대해. 아울러 TCP 연결이 양끝 소켓(주소와 포트의 쌍)의 쌍으로 식별되는 것에 대해. ↩ ↩2
-
Microsoft Learn, TCP/IP port exhaustion troubleshooting. 동적 포트 범위가 트랜스포트마다 설정되는 것, TIME_WAIT가 대량으로 있다는 것 자체는 고갈의 증거가 되지 않고 앞으로 고갈될 수 있음을 보일 뿐인 것, 아웃바운드 연결의 일제 실패·이벤트 ID 4227/4231·단일 PID로의 TIME_WAIT 편중으로 확인할 것, Windows 10 / Windows Server 2016 이후에는
netstat -anobq나Get-NetTCPConnection으로 BOUND 상태 포트도 볼 수 있는 것에 대해. ↩ ↩2 ↩3 -
Microsoft Learn, ServicePointManager.DefaultConnectionLimit Property. 기본 동시 연결 수가 ASP.NET에서 호스트된 애플리케이션에서는 10, 그 외(데스크톱 앱 등)에서는 2인 것에 대해. ↩ ↩2
-
Microsoft Learn, ServicePoint.ConnectionLeaseTimeout Property. 기본값이 -1(무기한)이고 밀리초 지정인 것, 지정 시간이 지난 뒤 요청을 처리한 연결이 닫히는 것, 로드 밸런싱처럼 연결을 주기적으로 다시 열고 싶은 장면용 설정인 것,
WebRequest/ServicePoint계열 API가 신규 개발에서는 비권장인 것, .NET 9 이후 이 속성이PooledConnectionLifetime에 매핑되지만 실질적 효과는 없는 것에 대해. ↩ ↩2 ↩3 -
Microsoft Learn, HttpClient.Timeout Property. 기본값이 100초인 것, 타임아웃 때 .NET 5 이후에는 내부 예외에
TimeoutException을 가진TaskCanceledException이 던져지는 것에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 프린터 드라이버 제공 종료 ── 업무 앱의 장표·라벨 인쇄는 어떻게 대비할 것인가
Microsoft는 v3/v4 프린터 드라이버의 제공 종료를 단계적으로 진행하고 있으며, 2026년 7월부터는 IPP 클래스 드라이버가 우선됩니다. Windows protected print mode에서 무엇이 사라지는지, 업무 앱의 장표·라벨 ...
네트워크 드라이브와 UNC 경로의 함정 ── 업무 앱에서 파일 서버(공유 폴더)를 다루는 실무
업무 앱에서 공유 폴더로 출력·감시할 때 자주 겪는 문제를 정리합니다. 드라이브 문자(Z:)가 서비스에서 보이지 않는 이유, 실행 계정별로 필요한 권한, 오류 1219, FileSystemWatcher 주의점까지 설명합니다.
WinForms/WPF 앱의 다국어화 ── resx·satellite assembly·culture 전환의 실무
Windows 데스크톱 앱의 다국어화를 정리합니다. CurrentCulture와 CurrentUICulture의 차이, resx와 satellite assembly의 구조, WPF에서 현실적인 방식 선택, 런타임 언어 전환, 서식·RTL까지 설명...
CSV는 「그냥 텍스트」가 아닙니다 ── C# 업무 앱의 CSV 실무(문자 코드·Excel 호환·인젝션 대책)
업무 앱 CSV 입출력의 전형적인 사고──Split(',') 자체 파싱, BOM 없는 UTF-8의 문자 깨짐, 앞자리 0 소실──을 정리하고, RFC 4180 규칙, Shift_JIS 취급, TextFieldParser를 이용한 안전한 읽기까지 ...
PerfView와 dotnet-trace로 「느림」을 특정한다 ── .NET 성능 조사의 실무 입문
업무 앱이 「느리다」「CPU가 붙어 있다」일 때, 어떤 도구로 무엇을 볼지. PerfView와 dotnet-trace의 역할 분담, CPU 샘플링 읽는 법, ThreadTime으로 블록 시간을 조사하는 절차까지 실무 조사 흐름을 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
장애 조사 & 장기 가동 장애
간헐적 장애, 통신 진단, 장기 가동 크래시, 실패 경로 테스트 기반을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
외부 API 연동·HTTP 통신 쪽 설계와 구현은 Windows 앱 개발의 실무 상담 범위이기 때문입니다.
기술 상담 & 설계 리뷰
기존 앱의 통신 장애(소켓 고갈·타임아웃 설계) 재검토는 설계 리뷰를 수반하는 기술 상담에 해당하기 때문입니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- HttpClient는 IDisposable인데, using으로 감싸면 안 되나요?
- 「요청마다 생성해서 폐기하는」 사용 방식이 문제입니다. HttpClient는 내부에 연결 풀을 가지며, 애플리케이션 수명 동안 재사용하는 전제로 설계되어 있습니다. 매번 생성·폐기하면, 폐기 후에도 소켓이 TIME_WAIT 상태로 한동안 남기 때문에 부하가 높을 때 소켓을 다 써 버려 SocketException이 발생합니다. 앱 종료 때 한 번만 폐기하거나, IHttpClientFactory를 통해 받는 형태로 하세요. 팩토리를 통해 받은 HttpClient는 Dispose해도 핸들러는 폐기되지 않으므로, using으로 감싸도 문제 없습니다.
- .NET Framework의 WebClient나 HttpWebRequest에서는 옮겨야 하나요?
- 신규 코드는 HttpClient로 통일하는 것을 권합니다. WebClient와 HttpWebRequest는 호환성을 위해 남겨 둔 옛 API이고, Microsoft도 신규 개발에서는 HttpClient 사용을 권장합니다. 기존 코드를 한꺼번에 바꿀 필요까지는 없지만, 통신 쪽에 손댈 타이밍에 차례로 HttpClient로 모으면 타임아웃 제어·async 대응·테스트 용이성 면에서 유지보수가 편해집니다.
- 재시도는 몇 번, 어느 간격으로 해야 하나요?
- 스스로 정하기보다 Microsoft.Extensions.Http.Resilience의 표준 핸들러 기본값(최대 3회, 지수 백오프+지터, 최초 지연 2초)을 출발점으로 삼는 편이 안전합니다. 중요한 것은 횟수보다 「재시도해도 되는 요청인가」를 가리는 일이며, POST처럼 다시 실행하면 이중 등록이 될 수 있는 조작은 기본으로 끄거나, 서버 쪽에서 멱등성(같은 요청을 두 번 받아도 결과가 바뀌지 않는 성질)을 확보한 뒤에 재시도를 켜세요.
- 사내 프록시 환경에서 첫 요청만 유난히 느린 이유는 무엇인가요?
- Windows 기본 설정에서는 HttpClient가 프록시 자동 검색을 시도하므로, 첫 연결 때 검색 처리에 시간이 걸릴 수 있습니다. 프록시가 필요 없다는 것이 분명한 환경(서버 내부 통신 등)에서는 HttpClientHandler의 UseProxy를 false로 해 자동 검색을 끄면 나아집니다. 반대로 프록시가 필수인 사내 환경에서는 자동 검색에 기대지 않고 WebProxy로 명시하는 편이 동작이 안정됩니다.