.NET을 사용한 IHttpClientFactory

이 문서에서는 IHttpClientFactory를 사용하여 종속성 주입(DI), 로깅, 구성과 같이 .NET 기본 사항이 다양한 HttpClient 형식을 만드는 방법을 알아봅니다. HttpClient 형식은 2012년에 출시된 .NET Framework 4.5에 도입되었습니다. 즉, 한동안은 유지됩니다. HttpClient는 HTTP를 요청하고 Uri에 의해 식별된 웹 리소스에서 HTTP 응답을 처리하는 데 사용됩니다. HTTP 프로토콜은 모든 인터넷 트래픽의 대다수를 구성합니다.

모범 사례를 이끌어내는 최신 애플리케이션 개발 원칙에 따라 IHttpClientFactory는 사용자 지정 구성으로 HttpClient 인스턴스를 만들 수 있는 팩터리 추상화의 역할을 수행합니다. IHttpClientFactory는 .NET Core 2.1에서 도입되었습니다. 일반적인 HTTP 기반 .NET 워크로드는 복원력 및 transient-fault-handling 타사 미들웨어를 쉽게 활용할 수 있습니다.

참고

앱에 쿠키가 필요한 경우 앱에서 IHttpClientFactory를 사용하지 않는 것이 좋습니다. 클라이언트를 관리하는 다른 방법은 HTTP 클라이언트 사용 지침을 참조하세요.

중요

IHttpClientFactory에서 만든 HttpClient 인스턴스의 수명 관리는 수동으로 만든 인스턴스와 완전히 다릅니다. 전략은 IHttpClientFactory에서 만든 단기 클라이언트 또는 PooledConnectionLifetime이 설정된 장기 클라이언트를 사용하는 것입니다. 자세한 내용은 HttpClient 수명 관리 섹션 및 HTTP 클라이언트 사용에 대한 지침을 참조하세요.

IHttpClientFactory 형식

이 문서의 모든 샘플 소스 코드는 Microsoft.Extensions.Http NuGet 패키지를 사용합니다. 또한 무료 {JSON} 자리 표시자 API에 대해 사용자 Todo 개체를 가져오도록 HTTP GET 요청이 수행됩니다.

AddHttpClient 확장 메서드 중 하나를 호출하면 IHttpClientFactory 및 관련 서비스를 IServiceCollection에 추가하게 됩니다. IHttpClientFactory 형식은 다음과 같은 이점을 제공합니다.

  • HttpClient 클래스를 DI 준비 형식으로 노출합니다.
  • 논리적 HttpClient 인스턴스를 구성하고 이름을 지정하기 위한 중앙 위치를 제공합니다.
  • HttpClient에서 위임 처리기를 통해 나가는 미들웨어의 개념을 체계화합니다.
  • Polly 기반 미들웨어에 대한 확장 메서드를 제공하여 HttpClient에서의 처리기 위임을 활용합니다.
  • 기본 HttpClientHandler 인스턴스의 캐싱 및 수명을 관리합니다. 자동 관리가 HttpClient 수명을 수동으로 관리할 때 발생하는 일반적인 DNS(Domain Name System) 문제를 방지해 줍니다.
  • 팩터리에서 만든 클라이언트를 통해 전송된 모든 요청에 대해 구성 가능한 로깅 경험(ILogger을 통해)을 추가합니다.

사용 패턴

앱에서 IHttpClientFactory를 사용할 수 있는 몇 가지 방법이 있습니다.

가장 좋은 방법은 앱의 요구 사항에 따라서 달라집니다.

기본적인 사용 방법

IHttpClientFactory를 등록하려면 AddHttpClient를 호출합니다.

using Shared;
using BasicHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddTransient<TodoService>();

using IHost host = builder.Build();

서비스를 사용하려면 DI가 포함된 생성자 매개 변수로 IHttpClientFactory가 필요할 수 있습니다. 다음 코드는 IHttpClientFactory를 사용하여 HttpClient 인스턴스를 만듭니다.

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace BasicHttp.Example;

public sealed class TodoService(
    IHttpClientFactory httpClientFactory,
    ILogger<TodoService> logger)
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        using HttpClient client = httpClientFactory.CreateClient();
        
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo types
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"https://jsonplaceholder.typicode.com/todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

앞서 나온 예제에서와 같이 IHttpClientFactory를 사용하는 것은 기존 앱을 리팩터링하는 좋은 방법입니다. HttpClient가 사용되는 방식에는 어떠한 영향도 없습니다. 기존 앱에서 HttpClient 인스턴스가 만들어지는 위치에서 해당 코드를 CreateClient에 대한 호출로 대체합니다.

명명된 클라이언트

명명된 클라이언트는 다음과 같은 경우에 적합합니다.

  • 앱에서 HttpClient를 서로 다른 곳에서 여러 번 사용해야 합니다.
  • 많은 HttpClient 인스턴스에 다양한 구성이 있습니다.

명명된 HttpClient에 대한 구성은 IServiceCollection에서 등록하는 동안 지정할 수 있습니다.

using Shared;
using NamedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

string? httpClientName = builder.Configuration["TodoHttpClientName"];
ArgumentException.ThrowIfNullOrEmpty(httpClientName);

builder.Services.AddHttpClient(
    httpClientName,
    client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

위의 코드에서 클라이언트가 다음을 사용하여 구성됩니다.

  • "TodoHttpClientName"의 구성에서 가져온 이름입니다.
  • 기본 주소 https://jsonplaceholder.typicode.com/.
  • "User-Agent" 헤더입니다.

구성을 사용하여 HTTP 클라이언트 이름을 지정할 수 있으며, 이는 클라이언트를 추가하고 만들 때 잘못된 명명을 방지하는 데 유용합니다. 이 예제에서는 HTTP 클라이언트 이름을 만드는 데 appsettings.json 파일이 사용됩니다.

{
    "TodoHttpClientName": "JsonPlaceholderApi"
}

손쉽게 이 구성을 확장하고 HTTP 클라이언트의 작동 방식에 대한 자세한 정보를 저장할 수 있습니다. 자세한 내용은 .NET의 구성을 참조하세요.

클라이언트 만들기

CreateClient가 호출될 때마다

  • HttpClient의 새 인스턴스가 만들어집니다.
  • 구성 작업이 호출됩니다.

명명된 클라이언트를 만들려면 CreateClient로 이름을 전달합니다.

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Shared;

namespace NamedHttp.Example;

public sealed class TodoService
{
    private readonly IHttpClientFactory _httpClientFactory = null!;
    private readonly IConfiguration _configuration = null!;
    private readonly ILogger<TodoService> _logger = null!;

    public TodoService(
        IHttpClientFactory httpClientFactory,
        IConfiguration configuration,
        ILogger<TodoService> logger) =>
        (_httpClientFactory, _configuration, _logger) =
            (httpClientFactory, configuration, logger);

    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        string? httpClientName = _configuration["TodoHttpClientName"];
        using HttpClient client = _httpClientFactory.CreateClient(httpClientName ?? "");

        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

위의 코드에서는 HTTP 요청이 호스트 이름을 지정할 필요가 없습니다. 클라이언트에 대해 구성된 기본 주소가 사용되었므로 코드는 경로만 전달할 수 있습니다.

형식화된 클라이언트

형식화된 클라이언트:

  • 문자열을 키로 사용할 필요가 없이 명명된 클라이언트와 동일한 기능을 제공합니다.
  • 클라이언트를 사용할 때 IntelliSense 및 컴파일러 도움말을 제공합니다.
  • 특정 HttpClient을 구성하고 상호 작용하기 위해 단일 위치를 제공합니다. 예를 들어, 다음과 같은 경우에 단일 형식화된 클라이언트를 사용할 수 있습니다.
    • 단일 백 엔드 엔드포인트에 대해.
    • 엔드포인트를 처리하는 모든 로직을 캡슐화하기 위해.
  • DI로 작업하고 앱에서 필요할 경우 삽입할 수 있습니다.

형식화된 클라이언트는 생성자에서 HttpClient 매개 변수를 받습니다.

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace TypedHttp.Example;

public sealed class TodoService(
    HttpClient httpClient,
    ILogger<TodoService> logger) : IDisposable
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await httpClient.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }

    public void Dispose() => httpClient?.Dispose();
}

위의 코드에서

  • 구성은 형식화된 클라이언트가 서비스 컬렉션에 추가되는 경우에 설정됩니다.
  • HttpClient는 클래스 범위 변수(필드)로 할당되고 노출된 API와 함께 사용됩니다.

HttpClient 기능을 노출하는 API 특정 메서드를 만들 수 있습니다. 예를 들어 GetUserTodosAsync 메서드는 사용자별 Todo 개체를 검색하는 코드를 캡슐화합니다.

다음 코드는 AddHttpClient를 호출하여 형식화된 클라이언트 클래스를 등록합니다.

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>(
    client =>
    {
        // Set the base address of the typed client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

형식화된 클라이언트는 DI를 사용하여 일시적으로 등록됩니다. 위의 코드에서 AddHttpClientTodoService를 임시 서비스로 등록합니다. 이 등록에서는 팩터리 메서드를 사용하여 다음을 수행합니다.

  1. HttpClient의 인스턴스를 만듭니다.
  2. HttpClient의 인스턴스를 생성자에 전달하여 TodoService의 인스턴스를 만듭니다.

중요

싱글톤 서비스에서 형식화된 클라이언트를 사용하는 것은 위험할 수 있습니다. 자세한 내용은 싱글톤 서비스에서 형식화된 클라이언트 방지 섹션을 참조하세요.

참고 항목

AddHttpClient<TClient> 메서드를 사용하여 형식이 지정된 클라이언트를 등록할 때 TClient 형식에는 HttpClient 매개 변수를 허용하는 생성자가 있어야 합니다. 또한 TClient 형식은 DI 컨테이너에 별도로 등록하면 안 됩니다.

명명된 클라이언트와 형식화된 클라이언트

명명된 클라이언트와 형식화된 클라이언트에는 고유한 장점과 단점이 있습니다. 두 가지 클라이언트 유형을 결합하여 두 가지 장점을 최대한 활용하는 방법이 있습니다.

기본 사용 사례는 다음과 같습니다. 동일한 형식의 클라이언트를 사용하지만 다른 도메인에 대해 사용합니다. 예를 들어, 기본 서비스와 보조 서비스가 있고 이들은 정확히 동일한 기능을 제공합니다. 이는 동일한 형식의 클라이언트를 사용하여 HttpClient 사용법을 래핑하여 요청을 발급하고, 응답을 처리하고, 오류를 처리할 수 있음을 의미합니다. 완전히 동일한 코드가 사용되지만 구성은 다릅니다(예: 기준 주소, 시간 제한 및 자격 증명이 다름).

다음 예에서는 형식화된 클라이언트 섹션에 표시된 것과 동일한 TodoService 형식화된 클라이언트를 사용합니다.

먼저 명명된 클라이언트와 형식화된 클라이언트를 등록합니다.

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>("primary"
    client =>
    {
        // Configure the primary typed client
        client.BaseAddress = new Uri("https://primary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(3);
    });

// Register the same typed client but with different settings
builder.Services.AddHttpClient<TodoService>("secondary"
    client =>
    {
        // Configure the secondary typed client
        client.BaseAddress = new Uri("https://secondary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(10);
    });

위의 코드에서

  • 첫 번째 AddHttpClient 호출은 primary 이름으로 TodoService 형식화된 클라이언트를 등록합니다. 기본 HttpClient는 기본 서비스를 가리키며 시간 제한이 짧습니다.
  • 두 번째 AddHttpClient 호출은 secondary 이름으로 TodoService 형식화된 클라이언트를 등록합니다. 기본 HttpClient는 보조 서비스를 가리키며 시간 제한이 더 깁니다.
using IHost host = builder.Build();

// Fetch an IHttpClientFactory instance to create a named client
IHttpClientFactory namedClientFactory =
    host.Services.GetRequiredService<IHttpClientFactory>();

// Fetch an ITypedHttpClientFactory<TodoService> instance to create a named and typed client
ITypedHttpClientFactory<TodoService> typedClientFactory  =
    host.Services.GetRequiredService<ITypedHttpClientFactory<TodoService>>();

// Create a TodoService instance against the primary host
var primaryClient = namedClientFactory.CreateClient("primary");
var todoService = typedClientFactory.CreateClient(primaryClient);

위의 코드에서

  • CreateClient 메서드를 통해 명명된 클라이언트를 만들 수 있도록 IHttpClientFactory 인스턴스가 DI 컨테이너에서 검색됩니다.
  • ITypedHttpClientFactory<TodoService> 인스턴스는 해당 CreateClient 메서드를 통해 형식화된 클라이언트를 만들 수 있도록 DI 컨테이너에서 검색됩니다.
    • CreateClient 오버로드는 매개 변수로 명명된 HttpClient(적절한 구성 포함)를 수신했습니다.
    • 만들어진 todoService는 기본 서비스를 사용하도록 구성되었습니다.

참고 항목

IHttpClientFactory 형식은 System.Net.Http 네임스페이스 내에 있는 반면, ITypedHttpClientFactory 형식은 Microsoft.Extensions.Http 내에 있습니다.

Important

구현 클래스(이전 예에서는 TodoService)를 ITypedHttpClientFactory의 형식 매개 변수로 사용합니다. 추상화(예: ITodoService 인터페이스)가 있더라도 여전히 구현을 사용해야 합니다. 실수로 추상화(ITodoService)를 사용한 경우 해당 CreateClient를 호출하면 InvalidOperationException이 throw됩니다.

try
{
    Todo[] todos = await todoService.GetUserTodosAsync(4);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
    // The request timed out against the primary host

    // Create a TodoService instance against the secondary host
    var fallbackClient = namedClientFactory.CreateClient("secondary");
    var todoFallbackService = typedClientFactory.CreateClient(fallbackClient);

    // Issue request against the secondary host
    Todo[] todos = await todoFallbackService.GetUserTodosAsync(4);
}

위의 코드에서

  • 기본 서비스에 대한 요청을 발급하려고 시도합니다.
  • 요청 시간이 초과되면(3초 이상 소요) TimeoutException 내부가 포함된 TaskCanceledException이 throw됩니다.
  • 시간 제한이 발생하면 이제 보조 서비스를 대상으로 하는 새 클라이언트가 만들어져 사용됩니다.

생성된 클라이언트

IHttpClientFactoryRefit과 같은 타사 라이브러리와 함께 사용할 수 있습니다. Refit은 .NET용 REST 라이브러리입니다. 선언적 REST API 정의가 인터페이스 메서드를 엔드포인트에 매핑할 수 있습니다. 인터페이스의 구현은 HttpClient를 사용하여 외부 HTTP를 호출하도록 RestService에 의해 동적으로 생성됩니다.

다음 record 형식을 고려해 보세요.

namespace Shared;

public record class Todo(
    int UserId,
    int Id,
    string Title,
    bool Completed);

다음 예에서는 Refit.HttpClientFactory NuGet 패키지를 사용하며 간단한 인터페이스입니다.

using Refit;
using Shared;

namespace GeneratedHttp.Example;

public interface ITodoService
{
    [Get("/todos?userId={userId}")]
    Task<Todo[]> GetUserTodosAsync(int userId);
}

이전 C# 인터페이스:

  • Task<Todo[]> 인스턴스를 반환하는 GetUserTodosAsync라는 이름의 메서드를 정의합니다.
  • 외부 API에 대한 경로 및 쿼리 문자열로 Refit.GetAttribute 특성을 선언합니다.

구현을 생성하기 위해 Refit를 사용하여 형식화된 클라이언트를 추가할 수 있습니다.

using GeneratedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using Shared;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddRefitClient<ITodoService>()
    .ConfigureHttpClient(client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

DI 및 Refit에서 제공한 구현을 통해 필요한 곳에서 정의된 인터페이스를 사용할 수 있습니다.

POST, PUT 및 DELETE 요청 수행

위의 예제에서 모든 HTTP 요청은 GET HTTP 동사를 사용합니다. HttpClient는 다음을 비롯한 다른 HTTP 동사도 지원합니다.

  • POST
  • PUT
  • DELETE
  • PATCH

지원되는 HTTP 동사의 전체 목록은 HttpMethod를 참조하세요. HTTP 요청에 대한 자세한 내용은 HttpClient를 사용하여 요청 보내기를 참조하세요.

다음 예제에서는 HTTP POST 요청을 수행하는 방법을 보여 줍니다.

public async Task CreateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PostAsync("/api/items", json);

    httpResponse.EnsureSuccessStatusCode();
}

위의 코드에서 CreateItemAsync 메서드는 다음을 수행합니다.

  • System.Text.Json을 사용하여 Item 매개 변수를 JSON으로 serialize합니다. JsonSerializerOptions의 인스턴스를 사용하여 serialization 프로세스를 구성합니다.
  • HTTP 요청의 본문에서 전송하기 위해 serialize된 JSON을 패키지할 StringContent의 인스턴스를 만듭니다.
  • PostAsync를 호출하여 JSON 콘텐츠를 지정된 URL로 보냅니다. HttpClient.BaseAddress에 추가되는 상대 URL입니다.
  • 응답 상태 코드가 성공을 나타내지 않을 경우 EnsureSuccessStatusCode를 호출하여 예외를 throw합니다.

HttpClient는 다른 형식의 콘텐츠도 지원합니다. 예를 들어 MultipartContentStreamContent를 지정합니다. 지원되는 콘텐츠의 전체 목록은 HttpContent를 참조하세요.

다음 예제에서는 HTTP PUT 요청을 보여 줍니다.

public async Task UpdateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PutAsync($"/api/items/{item.Id}", json);

    httpResponse.EnsureSuccessStatusCode();
}

앞의 코드는 POST 예제와 매우 비슷합니다. UpdateItemAsync 메서드는 PostAsync 대신 PutAsync를 호출합니다.

다음 예제에서는 HTTP DELETE 요청을 보여 줍니다.

public async Task DeleteItemAsync(Guid id)
{
    using HttpResponseMessage httpResponse =
        await httpClient.DeleteAsync($"/api/items/{id}");

    httpResponse.EnsureSuccessStatusCode();
}

위의 코드에서 DeleteItemAsync 메서드는 DeleteAsync를 호출합니다. HTTP DELETE 요청은 일반적으로 본문을 포함하지 않기 때문에 DeleteAsync 메서드는 HttpContent의 인스턴스를 허용하는 오버로드를 제공하지 않습니다.

HttpClient에 다른 HTTP 동사를 사용하는 방법에 대한 자세한 내용은 HttpClient를 참조하세요.

HttpClient 수명 관리

IHttpClientFactory에서 CreateClient가 호출될 때마다 새로운 HttpClient 인스턴스가 반환됩니다. 클라이언트마다 HttpClientHandler 인스턴스가 하나씩 만들어집니다. 팩터리는 HttpClientHandler 인스턴스의 수명을 관리합니다.

IHttpClientFactory는 리소스 사용을 줄이기 위해 팩터리에서 만든 HttpClientHandler 인스턴스를 캐시합니다. 수명이 만료되지 않은 경우, 새 HttpClient 인스턴스를 만들 때 캐시에서 HttpClientHandler 인스턴스가 재사용될 수 있습니다.

일반적으로 각 처리기는 자체적인 기본 HTTP 연결 풀을 관리하므로 처리기의 캐싱이 적합합니다. 필요한 것보다 더 많은 처리기를 만들면 소켓 고갈 및 연결 지연이 발생할 수 있습니다. 또한 일부 처리기는 무한정으로 연결을 열어 놓아 처리기가 DNS 변경에 대응하는 것을 막을 수 있습니다.

기본 처리기 수명은 2분입니다. 기본값을 재정의하려면 IServiceCollection에서 각 클라이언트의 SetHandlerLifetime을 호출합니다.

services.AddHttpClient("Named.Client")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

중요

IHttpClientFactory에서 만든 HttpClient 인스턴스는 단기여야 합니다.

  • 처리기가 DNS 변경에 대응할 수 있도록 해당 수명이 만료될 때 IHttpClientFactory에 대해 HttpMessageHandler를 재생하고 다시 만들어야 합니다. HttpClient는 만들어질 때 특정 처리기 인스턴스에 연결되므로 클라이언트가 업데이트된 처리기를 가져올 수 있도록 새 HttpClient 인스턴스를 적시에 요청해야 합니다.

  • 팩터리에서 만든 이러한 HttpClient 인스턴스를 삭제해도 소켓 고갈로 이어지지는 않습니다. 이 인스턴스를 삭제해도 HttpMessageHandler의 삭제가 트리거되지 않기 때문입니다. IHttpClientFactoryHttpClient 인스턴스, 특히 HttpMessageHandler 인스턴스를 만드는 데 사용되는 리소스를 추적하고 삭제합니다. 이러한 인스턴스는 수명이 곧 만료되고 이러한 인스턴스를 사용하는 HttpClient가 더 이상 없기 때문입니다.

단일 HttpClient 인스턴스를 장기간 활성 상태로 유지하는 것은 IHttpClientFactory에 대한 대안으로 사용할 수 있는 일반적인 패턴입니다. 그러나 이 패턴을 사용하려면 PooledConnectionLifetime과 같은 추가 설정이 필요합니다. PooledConnectionLifetime과 함께 장기 클라이언트를 사용하거나 IHttpClientFactory에서 만든 단기 클라이언트를 사용할 수 있습니다. 앱에서 사용할 전략에 대한 내용은 HTTP 클라이언트 사용에 대한 지침을 참조하세요.

HttpMessageHandler 구성

클라이언트가 사용하는 내부 HttpMessageHandler의 구성을 제어해야 할 수도 있습니다.

IHttpClientBuilder는 명명된 또는 형식화된 클라이언트를 추가할 때 반환됩니다. ConfigurePrimaryHttpMessageHandler 확장 메서드는 IServiceCollection에서 대리자를 정의하는 데 사용될 수 있습니다. 대리자는 해당 클라이언트가 사용하는 기본 HttpMessageHandler을 만들고 구성하는 데 사용됩니다.

.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        AllowAutoRedirect = false,
        UseDefaultCredentials = true
    };
});

HttClientHandler를 구성하면 처리기의 다양한 속성 중에서 HttpClient 인스턴스에 대한 프록시를 지정할 수 있습니다. 자세한 내용은 클라이언트당 프록시를 참조하세요.

추가 구성

IHttpClientHandler를 제어하기 위한 몇 가지 추가 구성 옵션이 있습니다.

메서드 설명
AddHttpMessageHandler 명명된 HttpClient에 대한 추가 메시지 처리기를 추가합니다.
AddTypedClient IHttpClientBuilder와 연결된 명명된 HttpClientTClient 간의 바인딩을 구성합니다.
ConfigureHttpClient 명명된 HttpClient를 구성하는 데 사용되는 대리자를 추가합니다.
ConfigureHttpMessageHandlerBuilder HttpMessageHandlerBuilder를 사용하여 명명된 HttpClient에 대한 메시지 처리기를 구성하는 데 사용되는 대리자를 추가합니다.
ConfigurePrimaryHttpMessageHandler 명명된 HttpClient에 대해 종속성 주입 컨테이너에서 기본 HttpMessageHandler를 구성합니다.
RedactLoggedHeaders 로깅하기 전에 값을 수정해야 하는 HTTP 헤더 이름의 컬렉션을 설정합니다.
SetHandlerLifetime HttpMessageHandler 인스턴스를 다시 사용할 수 있는 시간을 설정합니다. 명명된 클라이언트마다 고유하게 구성된 처리기 수명 값이 있을 수 있습니다.

SocketsHttpHandler와 함께 IHttpClientFactory 사용

HttpMessageHandlerSocketsHttpHandler 구현이 .NET Core 2.1에 추가되어 PooledConnectionLifetime을 구성할 수 있게 되었습니다. 이 설정은 처리기가 DNS 변경에 반응하도록 하는 데 사용되므로 SocketsHttpHandler 사용은 IHttpClientFactory 사용의 대안으로 간주됩니다. 자세한 내용은 HTTP 클라이언트 사용 지침을 참조하세요.

그러나 SocketsHttpHandlerIHttpClientFactory를 함께 사용하여 구성 가능성을 개선할 수 있습니다. 이러한 API를 모두 사용하면 낮은 수준(예: 동적 인증서 선택에 대해 LocalCertificateSelectionCallback 사용) 및 높은 수준(예: DI 통합 및 여러 클라이언트 구성 활용) 둘 다에서 구성할 수 있습니다.

두 API를 모두 사용하려면 다음을 수행합니다.

  1. SocketsHttpHandlerPrimaryHandler로 지정하고 해당 PooledConnectionLifetime을 설정합니다(예: 이전에 HandlerLifetime에 있던 값으로).
  2. SocketsHttpHandler가 연결 풀링 및 재생을 처리하므로 IHttpClientFactory 수준에서 처리기 재생이 더 이상 필요하지 않습니다. HandlerLifetimeTimeout.InfiniteTimeSpan로 설정하여 사용하지 않도록 설정할 수 있습니다.
services.AddHttpClient(name)
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(2)
        };
    })
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

싱글톤 서비스에서는 형식이 지정된 클라이언트를 방지합니다.

명명된 클라이언트 접근 방식을 사용하는 경우 IHttpClientFactory는 서비스에 주입되고 HttpClient 인스턴스는 HttpClient가 필요할 때마다 CreateClient를 호출하여 생성됩니다.

그러나 형식화된 클라이언트 접근 방식을 사용할 경우 형식화된 클라이언트는 일반적으로 서비스에 주입되는 일시적인 개체입니다. 형식화된 클라이언트를 싱글톤 서비스에 삽입할 수 있기 때문에 문제가 발생할 수 있습니다.

중요

형식화된 클라이언트는 IHttpClientFactory에서 만든 HttpClient 인스턴스와 같은 의미에서 수명이 짧은 것으로 예상됩니다(자세한 내용은 HttpClient 수명 관리 참조). 형식화된 클라이언트 인스턴스를 만드는 즉시 IHttpClientFactory에서 제어할 수 없게 됩니다. 형식화된 클라이언트 인스턴스가 싱글톤으로 캡처되는 경우 DNS 변경에 반응하지 못하게 하여 IHttpClientFactory의 용도 중 하나를 무효화할 수 있습니다.

싱글톤 서비스에서 HttpClient 인스턴스를 사용해야 하는 경우 다음 옵션을 고려합니다.

  • 대신 명명된 클라이언트 접근 방식을 사용하고, 필요한 경우 싱글톤 서비스에 IHttpClientFactory를 주입하고 HttpClient 인스턴스를 다시 만듭니다.
  • 형식화된 클라이언트 접근 방식이 필요한 경우 기본 처리기로 구성한 PooledConnectionLifetime과 함께 SocketsHttpHandler를 사용합니다. IHttpClientFactory에서 SocketsHttpHandler를 사용하는 방법에 대한 자세한 내용은 SocketsHttpHandler와 함께 IHttpClientFactory 사용 섹션을 참조하세요.

IHttpClientFactory의 메시지 처리기 범위

IHttpClientFactory는 각 HttpMessageHandler 인스턴스당 별도의 DI 범위를 만듭니다. 이러한 DI 범위는 애플리케이션 DI 범위(예: ASP.NET의 들어오는 요청 범위 또는 사용자가 만든 수동 DI 범위)에서 분리되므로 범위가 지정된 서비스 인스턴스를 공유하지 않습니다. 메시지 처리기 범위는 처리기 수명에 연결되며 애플리케이션 범위보다 오래 지속될 수 있습니다. 예를 들어 들어오는 여러 요청 간에 동일한 주입된 범위 종속성이 있는 동일한 HttpMessageHandler 인스턴스를 다시 사용할 수 있습니다.

두 개의 애플리케이션 DI 범위와 별도의 메시지 처리기 범위를 보여 주는 다이어그램

사용자는 HttpMessageHandler 인스턴스 내에서 범위 관련 정보(예: HttpContext의 데이터)를 캐시하지 말고 중요한 정보 유출을 방지하지 않도록 범위가 지정된 종속성을 주의해서 사용해야 합니다.

예를 들어 인증을 위해 메시지 처리기에서 앱 DI 범위에 액세스해야 하는 경우 별도의 임시 DelegatingHandler에 범위 인식 논리를 캡슐화하고 이를 IHttpClientFactory 캐시의 HttpMessageHandler 인스턴스에 래핑합니다. 등록된 이름이 지정된 클라이언트에 대해 처리기 호출 IHttpMessageHandlerFactory.CreateHandler에 액세스합니다. 이 경우 구성된 처리기를 사용하여 직접 HttpClient 인스턴스를 만듭니다.

별도의 임시 메시지 처리기 및 IHttpMessageHandlerFactory를 통해 앱 DI 범위에 대한 액세스 권한을 얻는 것을 보여 주는 다이어그램

다음 예에서는 범위 인식 DelegatingHandler를 사용하여 HttpClient를 만드는 방법을 보여 줍니다.

if (scopeAwareHandlerType != null)
{
    if (!typeof(DelegatingHandler).IsAssignableFrom(scopeAwareHandlerType))
    {
        throw new ArgumentException($"""
            Scope aware HttpHandler {scopeAwareHandlerType.Name} should
            be assignable to DelegatingHandler
            """);
    }

    // Create top-most delegating handler with scoped dependencies
    scopeAwareHandler = (DelegatingHandler)_scopeServiceProvider.GetRequiredService(scopeAwareHandlerType); // should be transient
    if (scopeAwareHandler.InnerHandler != null)
    {
        throw new ArgumentException($"""
            Inner handler of a delegating handler {scopeAwareHandlerType.Name} should be null.
            Scope aware HttpHandler should be registered as Transient.
            """);
    }
}

// Get or create HttpMessageHandler from HttpClientFactory
HttpMessageHandler handler = _httpMessageHandlerFactory.CreateHandler(name);

if (scopeAwareHandler != null)
{
    scopeAwareHandler.InnerHandler = handler;
    handler = scopeAwareHandler;
}

HttpClient client = new(handler);

범위 인식 DelegatingHandler를 등록하고 현재 앱 범위에 액세스할 수 있는 임시 서비스로 기본 IHttpClientFactory 등록을 재정의하는 확장 메서드를 사용하면 추가 해결 방법을 사용할 수 있습니다.

public static IHttpClientBuilder AddScopeAwareHttpHandler<THandler>(
    this IHttpClientBuilder builder) where THandler : DelegatingHandler
{
    builder.Services.TryAddTransient<THandler>();
    if (!builder.Services.Any(sd => sd.ImplementationType == typeof(ScopeAwareHttpClientFactory)))
    {
        // Override default IHttpClientFactory registration
        builder.Services.AddTransient<IHttpClientFactory, ScopeAwareHttpClientFactory>();
    }

    builder.Services.Configure<ScopeAwareHttpClientFactoryOptions>(
        builder.Name, options => options.HttpHandlerType = typeof(THandler));

    return builder;
}

자세한 내용은 전체 예제를 참조하세요.

참고 항목