IHttpClientFactory
키 방식 DI 지원
이 문서에서는 keyed Services와 IHttpClientFactory
통합하는 방법에 대해 알아봅니다.
Keyed Services(Keyed DI라고도 함)는 단일 서비스의 여러 구현으로 편리하게 작동할 수 있는 DI(종속성 주입) 기능입니다. 등록 시 다른 서비스 키 특정 구현과 연결할 수 있습니다. 런타임에 이 키는 서비스 유형과 함께 조회에 사용되므로 일치하는 키를 전달하여 특정 구현을 검색할 수 있습니다. Keyed Services 및 DI에 대한 자세한 내용은 .NET 종속성 주입참조하세요.
.NET 애플리케이션에서 IHttpClientFactory
사용하는 방법에 대한 개요는 .NET IHttpClientFactory를 참조하세요.
배경
IHttpClientFactory
및 명명된 HttpClient
인스턴스는 당연히 Keyed Services 아이디어와 잘 일치합니다. 역사적으로, 무엇보다도, IHttpClientFactory
오랫동안 누락 된 DI 기능을 극복 할 수있는 방법이었습니다. 그러나 일반적인 명명된 클라이언트는 구성된 HttpClient
을 주입하는 대신 IHttpClientFactory
인스턴스를 가져오고 저장하고 쿼리해야 합니다. 이 과정은 불편할 수 있습니다. 형식화된 클라이언트는 해당 부분을 단순화하려고 시도하지만 catch가 함께 제공됩니다. 형식화된 클라이언트는 잘못 구성하고 오용하기 쉽고, 지원 인프라는 특정 시나리오(예: 모바일 플랫폼)에서 실질적인 오버헤드가 될 수도 있습니다.
.NET 9(Microsoft.Extensions.Http
및 Microsoft.Extensions.DependencyInjection
패키지 버전 9.0.0+
)부터 IHttpClientFactory
Keyed DI를 직접 활용하여 새로운 "Keyed DI 접근 방식"("명명됨" 및 "형식화된" 접근 방식과 반대)을 도입할 수 있습니다. "Keyed DI 접근 방식은 편리하고 매우 유연하게 구성할 수 있는 HttpClient
등록을 특정하게 구성된 HttpClient
인스턴스의 직관적인 주입과 결합합니다."
기본 사용량
.NET 9에서는 AddAsKeyed 확장 메서드를 호출하여 기능에 옵트인해야 합니다. 옵트인하면 구성을 적용하는 명명된 클라이언트가 서비스 키로 클라이언트의 이름을 사용하여 DI 컨테이너에 키 HttpClient
서비스로 추가되므로 표준 키 서비스 API(예: FromKeyedServicesAttribute)를 사용하여 원하는 명명된 HttpClient
인스턴스(IHttpClientFactory
생성 및 구성)를 가져올 수 있습니다. 기본적으로 클라이언트는 영역 수명으로 등록됩니다.
다음 코드는 IHttpClientFactory
, Keyed DI 및 ASP.NET Core 9.0 최소 API 간의 통합을 보여 줍니다.
var builder = WebApplication.CreateBuilder(args);
// --- (1) Registration ---
builder.Services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "dotnet");
})
.AddAsKeyed(); // Add HttpClient as a Keyed Scoped service for key="github"
var app = builder.Build();
// --- (2) Obtaining HttpClient instance ---
// Directly inject the Keyed HttpClient by its name
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) =>
// --- (3) Using HttpClient instance ---
httpClient.GetFromJsonAsync<Repo>("/repos/dotnet/runtime"));
app.Run();
record Repo(string Name, string Url);
엔드포인트 응답:
> ~ curl http://localhost:5000/
{"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"}
이 예제에서 구성된 HttpClient
ASP.NET Core 매개 변수 바인딩에 통합된 표준 Keyed DI 인프라를 통해 요청 처리기에 삽입됩니다. ASP.NET Core에서 Keyed Services에 대한 자세한 내용은 ASP.NET Core의 종속성 주입을 참조하세요.
키드(Keyed), 네임드(Named), 타이핑(Typed) 접근 방식 비교
Basic Usage 예제의 IHttpClientFactory
관련 코드만 고려합니다.
services.AddHttpClient("github", /* ... */).AddAsKeyed(); // (1)
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2)
//httpClient.Get.... // (3)
이 코드 조각은 Keyed DI 접근 방식을 사용할 때 등록 (1)
, 구성된 HttpClient
인스턴스 (2)
가져오고, 필요에 따라 얻은 클라이언트 인스턴스를 사용하는 (3)
방법을 보여 줍니다.
두 가지 "이전" 접근 방식과 동일한 단계를 수행하는 방법을 비교합니다.
먼저 명명된 접근 방식을.
services.AddHttpClient("github", /* ... */); // (1)
app.MapGet("/github", (IHttpClientFactory httpClientFactory) =>
{
HttpClient httpClient = httpClientFactory.CreateClient("github"); // (2)
//return httpClient.Get.... // (3)
});
둘째, 유형 접근 방식:
services.AddHttpClient<GitHubClient>(/* ... */); // (1)
app.MapGet("/github", (GitHubClient gitHubClient) =>
gitHubClient.GetRepoAsync());
public class GitHubClient(HttpClient httpClient) // (2)
{
private readonly HttpClient _httpClient = httpClient;
public Task<Repo> GetRepoAsync() =>
//_httpClient.Get.... // (3)
}
세 가지 중에서 Keyed DI 접근 방식은 동일한 동작을 달성하는 가장 간결한 방법을 제공합니다.
기본 제공 DI 컨테이너 유효성 검사
특정 명명된 클라이언트에 대해 키 등록을 사용하도록 설정한 경우 기존 Keyed DI API를 사용하여 액세스할 수 있습니다. 그러나 아직 사용하도록 설정되지 않은 이름을 잘못 사용하려고 하면 표준 Keyed DI 예외가 발생합니다.
services.AddHttpClient("keyed").AddAsKeyed();
services.AddHttpClient("not-keyed");
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
// Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("not-keyed");
또한 클라이언트의 범위가 지정된 수명은 의존 관계의 문제를 발견하는 데 도움이 될 수 있습니다.
services.AddHttpClient("scoped").AddAsKeyed();
services.AddSingleton<CapturingSingleton>();
// Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider.
rootProvider.GetRequiredKeyedService<HttpClient>("scoped");
using var scope = provider.CreateScope();
scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("scoped"); // OK
// Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'.
public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpClient)
//{ ...
서비스 수명 선택
기본적으로 AddAsKeyed()
는 HttpClient
을 키 범위가 지정된 서비스로 등록합니다.
ServiceLifetime
매개 변수를 AddAsKeyed()
메서드에 전달하여 수명을 명시적으로 지정할 수도 있습니다.
services.AddHttpClient("explicit-scoped")
.AddAsKeyed(ServiceLifetime.Scoped);
services.AddHttpClient("singleton")
.AddAsKeyed(ServiceLifetime.Singleton);
형식화된 클라이언트 등록 내에서 AddAsKeyed()
호출하는 경우 기본 명명된 클라이언트만 Keyed로 등록됩니다. 형식화된 클라이언트 자체는 계속해서 일반 임시 서비스로 등록됩니다.
일시적인 HttpClient 메모리 누수 방지
중요하다
HttpClient
는 IDisposable
이므로, Keyed HttpClient
인스턴스에 대해서 일시적인 수명을 피하는 것이 가장 추천해 드립니다.
클라이언트를 Keyed Transient 서비스로 등록하면 둘 다 IDisposable
구현하므로 HttpClient
및 HttpMessageHandler
인스턴스가 DI 컨테이너 캡처됩니다. 이로 인해 클라이언트가 Singleton 서비스 내에서 여러 번 해결되는 경우 메모리 누수가 발생할 수 있습니다.
포로 종속성 방지
중요하다
다음 중 하나가 적용되면 HttpClient
이 등록됩니다.
- Keyed 싱글톤으로서 - 또는 -
- Keyed 로 범위 지정된 또는 임시, 그리고 장기 실행(
HandlerLifetime
이상) 애플리케이션 범위 내에 삽입, -OR- - Keyed 임시로서 Singleton 서비스에 삽입합니다.
- HttpClient
인스턴스는 포로되며 예상 수명 HandlerLifetime
넘길 가능성이 있습니다.
IHttpClientFactory
은 제한된 클라이언트를 제어할 수 없으므로 처리기 회전에 참여할 수 없으며, 그로 인해 DNS 변경이 손실될 수있습니다. 임시 서비스로 등록된 Typed 클라이언트에 대해 유사한 문제가 이미 있습니다.
클라이언트의 장수를 피할 수 없거나 의식적으로 원하는 경우(예: Keyed Singleton)의 경우 PooledConnectionLifetime
적절한 값으로 설정하여 SocketsHttpHandler
활용하는 것이 좋습니다.
services.AddHttpClient("shared")
.AddAsKeyed(ServiceLifetime.Singleton) // explicit singleton
.UseSocketsHttpHandler((h, _) => h.PooledConnectionLifetime = TimeSpan.FromMinutes(2))
.SetHandlerLifetime(Timeout.InfiniteTimeSpan); // disable rotation
services.AddSingleton<MySingleton>();
public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ...
범위 불일치 주의
스코프 지정 수명은 Named HttpClient
의 경우(싱글톤 및 일시적인 문제에 비해) 훨씬 덜 문제가 되지만, 자체적인 문제가 있습니다.
중요하다
특정 HttpClient
인스턴스의 키 범위 수명은 예상대로 확인된 "일반" 애플리케이션 범위(예: 들어오는 요청 범위)에 바인딩됩니다. 그러나 공장에서 직접 생성한 명명된 클라이언트와 동일한 방식으로 IHttpClientFactory
에서 관리하는 기본 메시지 핸들러 체인에는 적용되지 않습니다.
HttpClient
가 동일한 이름을 사용하지만, 두 개의 서로 다른 범위(예: 동일한 엔드포인트에 대한 두 개의 동시 요청)에서 HandlerLifetime
시간 범위 내에 확인되는 경우에는, 동일한 HttpMessageHandler
인스턴스를재사용할 수 있습니다. 이 인스턴스에는 메시지 처리기 범위에 설명된 대로 고유한 별도의 범위가 있습니다.
메모
범위 불일치 문제는 불쾌하고 오래 지속되는 문제이며.NET 9에서는 아직 해결되지 않은 남아 있습니다. 일반 DI 인프라를 통해 주입된 서비스에서는 모든 종속성이 동일한 범위에서 충족될 것으로 예상하지만 키 범위가 지정된 HttpClient
인스턴스의 경우 그렇지 않습니다.
키 지정 메시지 처리기 체인
일부 고급 시나리오의 경우 HttpClient
개체 대신 HttpMessageHandler
체인에 직접 액세스할 수 있습니다.
IHttpClientFactory
처리기를 만드는 IHttpMessageHandlerFactory
인터페이스를 제공합니다. Keyed DI를 사용하도록 설정하면 HttpClient
뿐만 아니라 해당 HttpMessageHandler
체인도 Keyed 서비스로 등록됩니다.
services.AddHttpClient("keyed-handler").AddAsKeyed();
var handler = provider.GetRequiredKeyedService<HttpMessageHandler>("keyed-handler");
var invoker = new HttpMessageInvoker(handler, disposeHandler: false);
방법: 형식화된 접근 방식에서 Keyed DI로 전환
메모
현재 형식화된 클라이언트 대신 Keyed DI 접근 방식을 사용하는 것이 좋습니다.
기존 형식화된 클라이언트에서 키 종속성으로의 최소 변경 스위치는 다음과 같습니다.
- services.AddHttpClient<Service>( // (1) Typed client
+ services.AddHttpClient(nameof(Service), // (1) Named client
c => { /* ... */ } // HttpClient configuration
//).Configure....
- );
+ ).AddAsKeyed(); // (1) + Keyed DI opt-in
+ services.AddTransient<Service>(); // (1) Plain Transient service
public class Service(
- // (2) "Hidden" Named dependency
+ [FromKeyedServices(nameof(Service))] // (2) Explicit Keyed dependency
HttpClient httpClient) // { ...
예제에서는 다음을 수행합니다.
- Typed 클라이언트
Service
의 등록은 다음과 같이 분할됩니다.- 동일한
HttpClient
구성을 사용하여 명명된 클라이언트nameof(Service)
등록하고 Keyed DI에 옵트인합니다. 그리고 - 일반 임시 서비스
Service
.
- 동일한
-
Service
에서HttpClient
종속성은 키nameof(Service)
가 있는 서비스에 명확하게 바인딩됩니다.
이름은 nameof(Service)
필요가 없지만 동작 변경을 최소화하기 위한 예제입니다. 내부적으로 형식화된 클라이언트는 명명된 클라이언트를 사용하며, 기본적으로 이러한 "숨겨진" 명명된 클라이언트는 연결된 형식화된 클라이언트의 형식 이름으로 이동합니다. 이 경우 "숨김" 이름은 nameof(Service)
이었고, 예제에서는 이를 그대로 유지했습니다.
기술적으로는 이전에 "숨겨진" 명명된 클라이언트가 "노출"되고 종속성이 Typed 클라이언트 인프라 대신 Keyed DI 인프라를 통해 충족되도록 Typed 클라이언트를 "래프 해제"하는 예제입니다.
방법: 기본적으로 Keyed DI에 옵트인
모든 단일 클라이언트에 대해 AddAsKeyed 호출할 필요가 없습니다. ConfigureHttpClientDefaults통해 "전역적으로"(모든 클라이언트 이름에 대해) 쉽게 옵트인할 수 있습니다. Keyed Services의 관점에서 보면, 이는 KeyedService.AnyKey 등록을 의미합니다.
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
services.AddHttpClient("first", /* ... */);
services.AddHttpClient("second", /* ... */);
services.AddHttpClient("third", /* ... */);
public class MyController(
[FromKeyedServices("first")] HttpClient first,
[FromKeyedServices("second")] HttpClient second,
[FromKeyedServices("third")] HttpClient third)
//{ ...
"알 수 없는" 클라이언트 주의
메모
KeyedService.AnyKey
등록은 임의의 키 값을 특정 서비스 인스턴스로 매핑을 정의합니다. 그러나 결과적으로 컨테이너 유효성 검사가 적용되지 않으며, 잘못된 키 값이 자동으로 잘못된 인스턴스가 삽입될 있습니다.
중요하다
Keyed HttpClient
경우 클라이언트 이름의 실수로 인해 이름이 등록되지 않은 클라이언트인 "알 수 없는" 클라이언트가 잘못 삽입될 수 있습니다.
일반적으로 이름이 지정된 클라이언트에 대해서도 마찬가지입니다. IHttpClientFactory
는 옵션 패턴이와 함께 작동하는 방식과 일치하여 클라이언트 이름을 명시적으로 등록할 필요가 없습니다. 공장은 알 수 없는 이름에 대해 구성되지 않았거나, 보다 정확히는 기본 구성된HttpClient
을 제공합니다.
메모
따라서 "기본적으로 키 입력" 접근 방식은 등록된 모든 HttpClient
뿐만 아니라 IHttpClientFactory
모든 클라이언트가 만들 수있습니다.
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
services.AddHttpClient("known", /* ... */);
provider.GetRequiredKeyedService<HttpClient>("known"); // OK
provider.GetRequiredKeyedService<HttpClient>("unknown"); // OK (unconfigured instance)
"옵트인" 전략 고려 사항
"글로벌" 옵트인은 한 줄로 된 옵트인이지만, "기본 제공"으로 작업하는 대신 이 기능이 여전히 필요한 것은 불행한 일입니다. 해당 결정에 대한 전체 컨텍스트 및 추론은 dotnet/runtime#89755 및 dotnet/runtime#104943참조하세요. 요컨대, "기본 설정으로 켜기"의 주요 차단기는 ServiceLifetime
"논쟁"입니다: DI 및 IHttpClientFactory
구현의 현재 (9.0.0
) 상태에서는, 가능한 모든 상황에서 모든 HttpClient
에 대해 합리적으로 안전한 단일 ServiceLifetime
가 존재하지 않습니다. 그러나 향후 릴리스의 주의 사항을 해결하고 전략을 "옵트인"에서 "옵트아웃"으로 전환하려는 의도가 있습니다.
방법: 키 등록에서 옵트아웃
각 클라이언트 이름별로 RemoveAsKeyed 확장 메서드를 호출하여 HttpClient
키드 DI에서 명시적으로 옵트아웃할 수 있습니다.
services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); // opt IN by default
services.AddHttpClient("keyed", /* ... */);
services.AddHttpClient("not-keyed", /* ... */).RemoveAsKeyed(); // opt OUT per name
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown"); // OK (unconfigured instance)
또는 ConfigureHttpClientDefaults사용하여 "전역적으로" 다음을 수행합니다.
services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default
services.AddHttpClient("keyed", /* ... */).AddAsKeyed(); // opt IN per name
services.AddHttpClient("not-keyed", /* ... */);
provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
우선 순위
함께 호출되거나 두 번 이상 호출되는 경우 AddAsKeyed()
및 RemoveAsKeyed()
일반적으로 IHttpClientFactory
구성 및 DI 등록 규칙을 따릅니다.
- 동일한 이름으로 호출되는 경우 마지막 설정이 우선합니다. 마지막
AddAsKeyed()
수명은 키 등록을 만드는 데 사용됩니다(RemoveAsKeyed()
마지막으로 호출되지 않은 경우 이 경우 이름은 제외됨). -
ConfigureHttpClientDefaults
내에서만 사용하는 경우 마지막 설정이 우선합니다. -
ConfigureHttpClientDefaults
및 특정 클라이언트 이름을 모두 사용한 경우 모든 기본값은 모든 이름별 설정 전에 "발생"하는 것으로 간주됩니다. 따라서 기본값을 무시할 수 있으며 이름별 설정의 마지막이 우선합니다.
참조
- .NET 사용하여 IHttpClientFactory
- .NET에서의 종속성 주입
- IHttpClientFactory
-
일반적인
IHttpClientFactory
사용상의 문제
.NET