일반적인 IHttpClientFactory
사용 문제
IHttpClientFactory
을(를) HttpClient
인스턴스를 만들기 위해 사용하는 경우에 발생할 수 있는 가장 일반적인 문제 중 일부를 이 문서에서 알아봅니다.
DI 컨테이너에서 여러 HttpClient
구성을 설정하고, 로깅을 구성하며, 복원력 전략을 설정하는 편리한 방법으로 IHttpClientFactory
이(가) 있습니다. IHttpClientFactory
또한 소켓 소모 및 DNS 변경 손실과 HttpMessageHandler
같은 문제를 방지하기 위해 인스턴스 및 인스턴스의 HttpClient
수명 관리를 캡슐화합니다. .NET을 사용하는 IHttpClientFactory를 참조하여 .NET 애플리케이션에서 IHttpClientFactory
을(를) 사용하는 방법에 대한 개요를 확인하세요.
catch 및 문제 해결이 어려울 수 있는 몇 가지 문제를 DI와의 IHttpClientFactory
통합의 복잡한 특성을 통해 해결 가능합니다. 잠재적인 문제를 방지하고자 사전에 적용할 수 있는 권장 사항도 이 문서에 나열된 시나리오에 포함되어 있습니다.
HttpClient
은(는) Scoped
수명을 존중하지 않습니다.
예를 들어, HttpContext
또는 일부 범위 지정된 캐시에 접근해야 할 경우, HttpMessageHandler
내부에서 문제가 발생할 수 있습니다. 저장된 데이터는 "사라지거나" 그렇지 않은 경우 "지속"될 수 있습니다. 이는 애플리케이션 컨텍스트와 처리기 인스턴스 간의 DI(종속성 주입) 범위 불일치로 인해 발생되며, IHttpClientFactory
에 대한 제한 사항으로 알려져 있습니다.
IHttpClientFactory
는 각 HttpMessageHandler
인스턴스당 별도의 DI 범위를 만듭니다. 이러한 처리기 범위는 범위가 지정된 서비스 인스턴스를 공유하지 않으며, 이는 애플리케이션 컨텍스트 범위(예: ASP.NET Core의 수신 요청 범위 또는 사용자에 의해 생성된 수동 DI 범위)에서 분리되기 때문입니다.
다음은 이러한 제한으로 인한 결과입니다.
- 범위가 지정된 서비스에서 "외부적으로" 캐시된 데이터는 해당
HttpMessageHandler
내에서 사용할 수 없습니다. - "내부적으로"
HttpMessageHandler
내부의 캐시된 모든 데이터는 여러 애플리케이션 DI 범위(예: 다른 들어오는 요청)에서 관찰할 수 있으며, 이는 동일한 처리기를 공유할 수 있기 때문입니다.
다음과 같은 권장 사항을 고려하여 알려진 제한 사항을 완화하세요.
❌ 중요한 정보가 유출되지 않도록 범위 관련 정보(HttpContext
의 데이터 등)를 HttpMessageHandler
인스턴스 또는 해당 종속성 내부에 캐시하지 마세요.
❌ CookieContainer
이(가) 처리기와 함께 공유되므로 쿠키를 사용하지 않아야 합니다.
✔️ 정보를 저장하지 않거나 HttpRequestMessage
인스턴스 내에서 해당 사항만 전달하는 것이 권장됩니다.
HttpRequestMessage.Options 속성을 사용하여 HttpRequestMessage
와(과) 함께 임의 정보를 전달할 수 있습니다.
✔️ 모든 범위 관련(인증 등) 논리를 IHttpClientFactory
에 의해 생성되지 않은 별도의 DelegatingHandler
내에 캡슐화하는 것을 고려해야 하며, 이를 사용하여 IHttpClientFactory
에 의해 생성된 처리기를 래핑하는 것이 권장됩니다.
등록된 명명된 클라이언트에 대해 IHttpMessageHandlerFactory.CreateHandler을(를) 호출하여 HttpClient
없이 오직 HttpMessageHandler
을(를) 생성하세요. 그러한 경우 직접 HttpClient
인스턴스를 만들기 위해 결합된 처리기를 사용할 필요가 있습니다. 이 해결 방법을 위한 완전히 실행 가능한 예제를 GitHub에서 찾을 수 있습니다.
IHttpClientFactory
지침의 IHttpClientFactory 에서 메시지 처리기 범위 섹션을 참조하여 자세한 내용을 확인하세요.
HttpClient
은(는) DNS 변경 내용을 존중하지 않습니다.
IHttpClientFactory
이(가) 사용되는 경우에도 부실 DNS 문제를 해결 가능합니다. 이는 일반적으로 HttpClient
인스턴스가 Singleton
서비스에서 캡처되거나, 일반적으로 지정된 HandlerLifetime
보다 긴 기간 동안 어딘가에 저장될 때 발생 가능합니다. 해당 형식화된 클라이언트가 싱글톤에 의해 캡처되는 경우에도 HttpClient
이(가) 캡처됩니다.
❌ IHttpClientFactory
에 의해 장기간 생성된 HttpClient
인스턴스는 캐시하지 마세요.
❌ 형식화된 클라이언트 인스턴스를 Singleton
서비스에 삽입하지 마세요.
✔️ 적시에 또는 필요할 때마다 IHttpClientFactory
(으)로부터 클라이언트를 요청하는 것을 고려하세요. 팩터리에 의해 생성된 클라이언트는 제거해도 안전합니다.
IHttpClientFactory
에서 만든 HttpClient
인스턴스는 단기여야 합니다.
처리기가 DNS 변경에 대응할 수 있도록 해당 수명이 만료될 때
IHttpClientFactory
에 대해HttpMessageHandler
를 재생하고 다시 만들어야 합니다.HttpClient
는 만들어질 때 특정 처리기 인스턴스에 연결되므로 클라이언트가 업데이트된 처리기를 가져올 수 있도록 새HttpClient
인스턴스를 적시에 요청해야 합니다.팩터리에 의해 생성된 이러한
HttpClient
인스턴스를 삭제해도 소켓 소진으로 이어지지는 않으며, 이는 해당 인스턴스를 제거해도HttpMessageHandler
의 제거가 트리거되지 않기 때문입니다.IHttpClientFactory
는HttpClient
인스턴스, 특히HttpMessageHandler
인스턴스를 만드는 데 사용되는 리소스를 추적하고 삭제합니다. 이러한 인스턴스는 수명이 곧 만료되고 이러한 인스턴스를 사용하는HttpClient
가 더 이상 없기 때문입니다.
형식화된 클라이언트는 형식화된 클라이언트 수명을 공유하며, 이는 HttpClient
인스턴스가 생성자에 삽입되므로 수명이 짧기 때문입니다.
IHttpClientFactory
지침의 HttpClient
수명 관리 및 형식화된 클라이언트를 싱글톤 서비스에서 사용하지 않기 섹션을 참조하여 자세한 내용을 확인하세요.
HttpClient
이(가) 소켓을 너무 많이 사용합니다.
IHttpClientFactory
이(가) 사용되는 경우에도 특정 사용 시나리오에서 소켓 소진 문제에 대해 해결 가능합니다. 기본적으로 HttpClient
은(는) 동시 요청 수를 제한하지 않습니다. 동시에 많은 수의 HTTP/1.1 요청이 시작될 경우, 연결 풀에 사용 가능한 연결이 없으며 제한이 설정되어 있지 않기 때문에 새로운 HTTP 연결 시도를 각 요청이 트리거합니다.
❌ 동시에 많은 수의 HTTP/1.1 요청을 제한을 지정하지 않은 상태로 시작하지 마세요.
✔️ HttpClientHandler.MaxConnectionsPerServer을(를) 적절한 값으로 설정하는 것을 고려하세요(또는 기본 처리기로 사용하는 경우에는 SocketsHttpHandler.MaxConnectionsPerServer). 특정 처리기 인스턴스에만 이러한 제한이 적용됩니다.
✔️ 단일 TCP 연결을 통해 멀티플렉싱 요청을 허용하는 HTTP/2를 사용하는 것이 권장됩니다.
잘못된 HttpClient
삽입이 형식화된 클라이언트에 있습니다.
형식화된 클라이언트에 예기치 않은 HttpClient
삽입을 가져올 수 있는 다양한 상황이 존재할 수 있습니다. 근본 원인은 대부분의 경우 잘못된 구성에 있으며, 이는 DI 설계에 따라 서비스의 후속 등록이 이전 등록을 재정의하기 때문입니다.
형식화된 클라이언트는 "내부적으로" 명명된 클라이언트를 사용하며, 이는 형식화된 클라이언트를 추가함으로써 암시적으로 등록되며 명명된 클라이언트에 연결됩니다. 명시적으로 제공되지 않는 한, TClient
의 형식 이름으로 클라이언트 이름이 설정됩니다. AddHttpClient<TClient,TImplementation>
오버로드가 사용되는 경우, TClient,TImplementation
쌍에서 첫 번째가 됩니다.
그러므로, 형식화된 클라이언트를 등록함으로써 다음의 두 가지 개별 작업이 수행됩니다.
- 명명된 클라이언트를 등록합니다(간단한 기본 경우 이름은
typeof(TClient).Name
입니다). - 제공된
TClient
또는TClient,TImplementation
을(를) 활용하여Transient
서비스를 등록합니다.
다음과 같은 두 가지 문은 기술적으로 동일합니다.
services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));
// -OR-
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
.AddTypedClient<ExampleClient>(); // link the named client to a typed client
간단한 경우, 다음과 유사합니다.
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client
// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
new ExampleClient(
s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));
형식화된 클라이언트와 명명된 클라이언트 간의 연결이 어떻게 끊어질 수 있는지에 관한 다음과 같은 예시를 고려하세요.
형식화된 클라이언트가 두 번째로 등록됨
❌ 이미 AddHttpClient<T>
호출에 의해 자동으로 등록되어 있으므로, 형식화된 클라이언트를 별도로 등록하지 마세요.
일반적인 임시 서비스로서 형식화된 클라이언트가 두 번째로 잘못 등록되는 경우, 해당 HttpClientFactory
에서 추가한 등록을 덮어쓰며 명명된 클라이언트에 대한 링크를 끊습니다. 구성되지 않은 경우, HttpClient
의 구성이 손실된 것처럼 표시되며, 이는 HttpClient
이(가) 대신 형식화된 클라이언트에 삽입되기 때문입니다.
"잘못된" HttpClient
이(가) 예외를 throw하는 대신 사용되는 것이 혼란스러울 수 있습니다. 이는 "기본"으로 구성되지 않은 HttpClient
, 즉 Options.DefaultName 이름(string.Empty
)을 가진 클라이언트가 가장 기본적인 HttpClientFactory
사용 시나리오를 가능하게 하기 위해 일시적인 서비스로 등록되기 때문입니다. 그러므로 링크가 끊어지며 형식화된 클라이언트가 일반 서비스가 되는 경우, 자연스럽게 해당 생성자 매개 변수에 이 "기본값" HttpClient
이(가) 삽입됩니다.
공통 인터페이스에 다른 형식화된 클라이언트가 등록됩니다.
공통 인터페이스에 두 개의 서로 다른 형식화된 클라이언트가 등록된 경우에는 동일한 명명된 클라이언트를 다시 사용합니다. 이는 두 번째 명명된 클라이언트를 "잘못" 삽입하는 첫 번째 형식화된 클라이언트처럼 보일 수 있습니다.
❌ 여러 형식화된 클라이언트를 단일 인터페이스에 등록하지 않고, 이름을 명시적으로 지정하지 않아야 합니다.
✔️ 별도로 명명된 클라이언트를 등록하고 구성한 뒤, AddHttpClient<T>
호출에서 이름을 지정하거나 명명된 클라이언트 설정 중에 AddTypedClient
을(를) 호출하여 하나 이상의 형식화된 클라이언트에 연결하는 것이 권장됩니다.
기본적으로 동일한 이름을 가진 명명된 클라이언트를 여러 번 등록하고 구성하면 기존 클라이언트 목록에 구성 작업이 추가됩니다. HttpClientFactory
의 이러한 동작은 명확하지 않을 수 있지만, 옵션 패턴 및 Configure와(과) 같은 구성 API에서 사용하는 것과 동일한 방식입니다.
이는 고급 처리기 구성에 주로 유용합니다. 그 예시로, 사용자 지정 처리기를 외부에 정의된 명명된 클라이언트에 추가하거나 테스트에 대한 기본 처리기를 모의하는 경우가 있지만 HttpClient
인스턴스 구성에서도 작동합니다. 예를 들어, 다음과 같은 세 가지 예시는 동일한 방식으로 구성되어 HttpClient
이(가) 됩니다(BaseAddress
및 DefaultRequestHeaders
둘 다 설정됩니다).
// one configuration callback
services.AddHttpClient("example", c =>
{
c.BaseAddress = new Uri("http://example.com");
c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
});
// -OR-
// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
// -OR-
// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
이렇게 하면 이미 정의된 명명된 클라이언트에 형식화된 클라이언트를 연결하고 단일 명명된 클라이언트에 여러 형식화된 클라이언트를 연결 가능합니다. 이는 다음과 같은 name
매개 변수가 있는 오버로드가 사용되는 경우 더 분명해집니다.
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));
services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");
동일한 작업을 수행하기 위해 다음과 같이 명명된 클라이언트 구성 중에 AddTypedClient을(를) 호출할 수도 있습니다.
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
.AddTypedClient<FooLogger>()
.AddTypedClient<BarLogger>();
하지만 동일한 인터페이스에 클라이언트를 등록하려는 경우, 동일한 명명된 클라이언트를 다시 사용하지 않으려는 경우에도 이 작업을 수행하기 위해 다른 이름을 명시적으로 지정할 수 있습니다.
services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
c => c.BaseAddress = new Uri("https://github.com"));
참고 항목
.NET