HttpClient를 using으로 감싸면 안 되는 이유 ── C# 업무 앱의 HTTP 통신 실무(생성 패턴・타임아웃・재시도)
· Go Komura · CSharp, .NET, HttpClient, IHttpClientFactory, 네트워크, async/await, Windows 개발, 기술 상담
「오후가 되면 외부 API 연결이 SocketException으로 실패하기 시작한다」「접속 대상을 전환했는데 앱이 계속 예전 서버로 연결한다」──C#의 HttpClient는 GetAsync를 호출하는 것만이라면 간단하지만, 인스턴스를 어떻게 생성・유지할지를 잘못 다루면 이런 종류의 「그때는 동작하지만 운영에 들어간 뒤 문제가 생기는」 버그를 심어 놓게 됩니다.
이 글에서는 Windows 업무 앱이 외부 Web API나 사내 서비스를 호출하는 상황을 가정하여, HttpClient의 올바른 생성 패턴, 타임아웃 설계, 재시도, 오류 처리, 그리고 Windows 환경 특유의 함정까지 실무에서 판단하기 어려운 순서로 정리합니다.
1. 결론부터(판단표)
HttpClient를 어떻게 유지할지는 앱의 구성에 따라 답이 달라집니다. 먼저 판단표로 정리합니다.
| 앱의 형태 | 권장 패턴 | 이유 |
|---|---|---|
| 수 초~수 분 만에 끝나는 콘솔 도구(.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 |
| 접속 대상마다 프록시・쿠키・인증서 설정이 다름 | 설정별로 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.
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.
이 버그가 성가신 이유는 개발 중・테스트 중에는 거의 재현되지 않는다는 점입니다. 운영 환경의 혼잡 시간대에만 발생하거나, 월말 배치 작업 때만 발생하는 형태로 나타납니다. 조사할 때는 현상이 발생한 시점에 다음 명령으로 TIME_WAIT 소켓 수를 확인하면 원인을 짐작하는 데 도움이 됩니다.
# 접속 대상별 TIME_WAIT 소켓 수를 집계한다
netstat -ano | Select-String "TIME_WAIT" | Measure-Object
참고로 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.
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);
});
팩토리의 동작에서 짚어 둬야 할 점은 세 가지입니다.
- 핸들러는 풀링되며 기본적으로 2분마다 교체됩니다.
CreateClient를 호출할 때마다 새로운HttpClient가 반환되지만, 내부의 핸들러(연결 풀)는 공유되므로 소켓 고갈은 일어나지 않고, 정기적인 교체를 통해 DNS 변경에도 대응합니다3. - 팩토리가 생성한
HttpClient는 단명으로 사용하는 것이 전제입니다. 받은 인스턴스를 싱글턴 필드에 보관하면 핸들러 교체에 참여할 수 없게 되어 DNS 변경에 대응하지 못하게 됩니다. 타입 지정 클라이언트를 싱글턴 서비스에 주입하는 구성도 같은 이유로 피해야 합니다3. - 쿠키에 의존하는 앱에서는 주의가 필요합니다. 핸들러가 풀링되는 결과,
CookieContainer가 의도치 않게 공유됩니다. 쿠키를 사용하는 경우 팩토리를 피하거나, 쿠키 처리를 비활성화하고 직접 헤더를 붙이는 설계가 공식적으로 안내되고 있습니다1.
인증이 필요한 API(Microsoft Entra ID로 보호되는 API 등)를 호출할 때의 토큰 획득 관련 내용은 「WinForms/WPF 앱에 Entra ID 인증 넣기」에서 다루고 있으니 그쪽을 참조하세요.
5. 타임아웃 설계 ── 기본값 100초는 업무 앱에는 너무 길다
HttpClient.Timeout의 기본값은 100초입니다5. 화면 조작의 연장선에서 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이 담깁니다5. 다만 위와 같이 직접 만든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 재전송과 RFC1323 ── 산업용 카메라 통신 장애 조사」에서 다룬 방법이 참고가 됩니다.
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에서 병렬도를 높이려면 이 상한을 명시적으로 올려야 합니다6. - 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 재전송과 RFC1323 ── 산업용 카메라 통신 장애 조사
- TCP에서 Send한 단위대로 Receive할 수 있다는 오해
- 예외 catch・로그・에러 처리 실무
- Windows 앱의 기밀 정보 저장 - DPAPI로 평문 설정을 피하기
관련 상담 분야
코무라소프트 합동회사에서는 외부 API 연동을 포함한 Windows 업무 앱 개발, 기존 앱의 통신 문제(소켓 고갈・타임아웃・간헐적 연결 실패) 조사와 개선 방안에 대한 기술 상담을 다루고 있습니다.
참고 자료
-
Microsoft Learn, HttpClient 사용 지침. .NET Core/.NET 5+에서는
PooledConnectionLifetime을 설정한 장수명 클라이언트 또는IHttpClientFactory가 생성한 단명 클라이언트를 사용해야 하고, .NET Framework에서는IHttpClientFactory사용이 권장되며, 쿠키를 사용하는 앱에서는CookieContainer공유 문제로IHttpClientFactory를 피해야 한다는 내용에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, HttpClient 클래스. 요청마다 생성하면 소켓 고갈과
SocketException을 유발한다는 점, DNS를 연결 생성 시에만 해석하고 TTL도 참조하지 않는다는 점, 핸들러의 연결 설정은 최초 요청 이후에는 변경할 수 없다는 점, HTTP/1.1의 동시 연결 수가 기본적으로 무제한이라는 점, 큰 다운로드에서의 스트리밍 권장, 프록시의 기본 동작과UseProxy에 의한 비활성화에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn, .NET에서 IHttpClientFactory 사용하기. 핸들러의 기본 수명이 2분이라는 점, 팩토리가 생성한
HttpClient는 단명으로 사용하는 것이 전제라는 점, 팩토리가 생성한 클라이언트의Dispose는 핸들러를 폐기하지 않는다는 점, 타입 지정 클라이언트를 싱글턴에 주입하면 DNS 변경에 대응하지 못하게 된다는 점에 대해. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, 복원력 있는 HTTP 앱 구축: 주요 개발 패턴.
AddStandardResilienceHandler가 구성하는 5단계 전략(레이트 리미터/전체 타임아웃 30초/최대 3회의 지수 백오프 재시도/서킷 브레이커/시도 타임아웃 10초), 대상이 되는 상태 코드(408/429/5xx)와 예외,DisableForUnsafeHttpMethods에 의한 POST 등의 재시도 비활성화에 대해. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, HttpClient.Timeout 속성. 기본값이 100초라는 점, 타임아웃 시 .NET 5 이후로는 내부 예외에
TimeoutException을 담은TaskCanceledException이 발생한다는 점에 대해. ↩ ↩2 -
Microsoft Learn, ServicePointManager.DefaultConnectionLimit 속성. 기본 동시 연결 수가 ASP.NET에서 호스팅되는 애플리케이션에서는 10, 그 외(데스크톱 앱 등)에서는 2라는 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
WinForms/WPF 앱 다국어화 ── resx·새틀라이트 어셈블리·컬처 전환 실무
Windows 데스크톱 앱의 다국어화를 CurrentCulture와 CurrentUICulture의 차이, resx와 새틀라이트 어셈블리를 통한 리소스 구조, WinForms의 Localizable 속성, WPF에서의 현실적인 방식 선택, 실행 ...
CSV는 “그냥 텍스트”가 아니다 ── C# 업무 앱의 CSV 실무(문자 코드・Excel 호환・인젝션 대책)
업무 앱의 CSV 입출력에서 발생하는 전형적인 사고 패턴──Split(',')를 이용한 자체 파싱, BOM 없는 UTF-8의 Excel 문자 깨짐, 선행 0 소실, CSV 인젝션──을 정리하고, RFC 4180의 규칙, .NET에서의 Shift_...
PerfView와 dotnet-trace로 “느림”의 원인을 찾는다 ── .NET 성능 조사 실무 입문
업무 앱이 “느리다”, “CPU를 다 써버린다”, “가끔 멈춘다”고 할 때, 어떤 도구로 무엇을 봐야 할까요. PerfView와 dotnet-trace의 역할 분담, CPU 샘플링을 읽는 방법(inclusive/exclusive), ThreadT...
appsettings.json만으로는 부족하다 ── Windows 업무 앱 구성 관리 실무(환경별 설정・비밀 정보・쓰기 위치)
Windows 업무 앱의 구성 관리를 appsettings.json의 계층화, IConfiguration과 IOptions/IOptionsSnapshot/IOptionsMonitor의 구분 사용, 환경별 설정, 쓰기 가능한 설정의 배치 위치, 비...
Windows 업무 앱의 인쇄와 PDF 출력 - System.Drawing.Printing / WPF / 보고서 라이브러리 구분 사용법
PrintDocument를 이용한 WinForms 인쇄, WPF의 FlowDocument/FixedDocument 인쇄, PDF 출력의 선택지를 요구사항별 판단표로 정리합니다. 페이지 나눔 제어나 DPI 차이 같은 구현의 함정, Windows 서...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
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로 명시적으로 지정하는 편이 동작이 안정적입니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크