재우니의 블로그

 

 

ASP.NET Core에서 HTTP Request Cancel 을 위한 CancellationToken 사용법 

 

 

앱이 요청을 처리하는 동안 사용자는 해당 페이지에서 이동할 수 있습니다. 이 경우 응답이 해당 사용자에게 더 이상 중요하지 않으므로 HTTP 요청을 취소(Cancel)하려고합니다. 물론 이것은 요청을 취소하려는 실제 애플리케이션에서 발생할 수있는 많은 상황 중 하나 일뿐입니다. 따라서이 기사에서는 CancellationToken을 사용하여 클라이언트 애플리케이션에서 HTTP 요청을 취소하는 방법을 알아볼 것입니다.

 

 

소스 코드를 다운로드하려면 HttpClient 저장소가있는 CancellationToken을 방문하세요 .

 

또한 HttpClient 자습서 페이지를 방문 하여이 자습서의 모든 기사를 볼 수 있습니다 .

이 기사는 다음 섹션으로 나눌 것입니다.

자, 바로 그것에 뛰어 들자.

 

CancellationToken 을 사용하여 HttpClient 로 보낸 요청 취소 (Using CancellationToken to Cancel Requests Sent with HttpClient)

 

 

도입부에서 사용자가 페이지를 벗어나면 더 이상 응답이 필요하지 않으므로 해당 요청을 취소하는 것이 좋습니다. 하지만 그 이상이 있습니다. HttpClient는 비동기 작업을 사용하므로 더 이상 필요하지 않은 작업을 취소하면 작업을 실행하는 데 사용하는 스레드가 해제됩니다. 이는 스레드가 다른 작업에 사용될 수있는 스레드 풀로 반환 될 것임을 의미합니다. 이것은 확실히 우리 응용 프로그램의 확장 성을 향상시킬 것입니다.

 

물론 그렇게 요청을 취소 할 수는 없습니다. 이러한 작업을 실행하려면 CancellationTokenSource 및 CancellationToken을 사용해야합니다.

 

CancellationTokenSource를 사용하여 CancellationToken을 만들고 모든 소비자에게 CancellationToken 요청이 취소되었음을 알립니다. 우리의 경우 HttpClient는 CancellationToken을 사용하고 알림을 수신합니다. 요청 취소 알림이 수신되는 즉시 HttpClient를 사용하여 해당 요청을 취소합니다.

 

그럼 어떻게하는지 봅시다.

 

 

HttpClient로 CancellationToken 논리 구현 (Implementing CancellationToken Logic with HttpClient)

 

 

가장 먼저 할 일은이 예제에 대한 새 서비스를 만드는 것입니다.

 

public class HttpClientCancellationService : IHttpClientServiceImplementation
{
    private static readonly HttpClient _httpClient = new HttpClient();
    private readonly JsonSerializerOptions _options;
    public HttpClientCancellationService()
    {
        _httpClient.BaseAddress = new Uri("https://localhost:5001/api/");
        _httpClient.Timeout = new TimeSpan(0, 0, 30);
        _httpClient.DefaultRequestHeaders.Clear();
        _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
    }
    public async Task Execute()
    {
        throw new NotImplementedException();
    }
}

 

HttpClient 인스턴스를 생성하고 이에 대한 configuration 을 제공합니다. 또한 JSON 직렬화(serialization)  옵션에 대해서도 동일하게 수행합니다. 다음 기사에서는 HttpClientFactory 모든 파일에서 이 configuration 을 반복하지 않고 단일 위치로 이 configuration 을 이동하는 방법에 대해 배우고 HttpClient로 인해 발생할 수있는 문제를 해결하는 방법도 알아 봅니다. 지금은 그대로 두겠습니다.

 

이제 모든 companies 를 가져 오는 메서드를 추가해 보겠습니다.

 

private async Task GetCompaniesAndCancel()
{
    using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();
        var stream = await response.Content.ReadAsStreamAsync();
        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}

 

이것은 또한 이전 기사 의 익숙한 코드 이며 여기서는 설명하지 않겠습니다. streams 에 익숙하지 않은 경우 언제든지 링크 된 기사를 읽을 수 있습니다.

 

이제이 요청을 취소한다고 가정 해 보겠습니다. 이미 말했듯이 요청을 취소하려면 CancellationTokenSource 가 필요합니다. 그래서 그것을 구현합시다.

 

 

private async Task GetCompaniesAndCancel()
{
    var cancellationTokenSource = new CancellationTokenSource();
    cancellationTokenSource.CancelAfter(2000);
    using (var response = await _httpClient.GetAsync("companies", 
        HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token))
    {
        response.EnsureSuccessStatusCode();
        var stream = await response.Content.ReadAsStreamAsync();
        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}

 

여기에서 cancellationTokenSource 개체를 만듭니다. 이것이 작동하려면 using System.Threading.Tasks 네임 스페이스 를 포함해야 합니다. 객체를 생성 한 후 요청을 취소하려고합니다. 일반적으로 사용자가 취소 버튼을 누르거나 페이지에서 다른 곳으로 이동하여 실행하지만 예제 목적으로 여기에서 수행합니다. request 에 대한 Cancel() 을 취소하려면 Request 을 즉시 취소하는, 및 CancelAfter(). 이 예에서는 CancelAfter 메서드를 사용하고 인수로 2초를 제공합니다. 마지막으로 취소 작업에 대해 HttpClient에 알려야합니다. 이를 위해 GetAsync shortcut 메서드에 대한 추가 인수로 cancellation token 을 제공합니다 .

 

그게 다야. 지금 테스트 할 수 있습니다.

 

 

요청 취소 테스트 (Testing Cancelling the Request)

 

 

애플리케이션을 시작하기 전에 앱이 시작될 때 메서드가 호출되는지 확인해야합니다. 이를 위해 Execute 메서드를 수정해야 합니다.

 

public async Task Execute()
{
    await GetCompaniesAndCancel();
}

 

또한 이 서비스를 Program 클래스에 등록 해야합니다.

 

private static void ConfigureServices(IServiceCollection services)
{
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>();
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientPatchService>();
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientStreamService>();
    services.AddScoped<IHttpClientServiceImplementation, HttpClientCancellationService>();
}

 

이제 두 응용 프로그램을 시작해 보겠습니다.

 

 

그리고 request 이 cancel 되었음을 알 수 있습니다. 다시 테스트하려면 더 긴 첫번째 request 를 시뮬레이션 할 수 있도록 API 도 다시 시작해야합니다.

 

CancellationToken을 공유하여 솔루션 개선 (Improving the Solution by Sharing the CancellationToken)

 

있는 그대로의 구현은 학습 예제에 적합합니다. 그러나 실제 응용 프로그램에서는 모든 요청에 ​​토큰을 전달하여 다른 요청을 취소 할 수 있기를 원합니다. 이렇게하면 필요한 경우 이러한 모든 요청을 취소 할 수 있습니다. 또한 사용자가 취소 버튼을 클릭하거나 페이지를 벗어날 때와 같이 애플리케이션의 다른 부분에서이 CancellationTokenSource에 액세스 할 수 있기를 원합니다. 이 경우 단일 메서드 내에서 CancellationTokenSource 를 숨기고 싶지 않습니다.

 

즉, 서비스에 몇 가지 수정 사항을 추가하겠습니다.

 

private static readonly HttpClient _httpClient = new HttpClient();
private readonly JsonSerializerOptions _options;
private readonly CancellationTokenSource _cancellationTokenSource;
public HttpClientCancellationService()
{
    _httpClient.BaseAddress = new Uri("https://localhost:5001/api/");
    _httpClient.Timeout = new TimeSpan(0, 0, 30);
    _httpClient.DefaultRequestHeaders.Clear();
    _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
    _cancellationTokenSource = new CancellationTokenSource();
}

 

여기에서 CancellationTokenSource 읽기 전용 변수를 만들고 생성자에서 인스턴스화합니다.

그런 다음 Execute 메서드 를 수정하려고합니다 .

 

public async Task Execute()
{
    _cancellationTokenSource.CancelAfter(2000);
    await GetCompaniesAndCancel(_cancellationTokenSource.Token);
}

 

이 메서드에서 CancelAfter 메서드를 호출하여 요청을 cancel 할 기간을 지정하고 token 을 GetCompaniesAndCancel 메서드에 전달합니다 . 물론 GetCompaniesAndCancel 메서드도 수정해야합니다 .

 

 

private async Task GetCompaniesAndCancel(CancellationToken token)
{
    using (var response = await _httpClient.GetAsync("companies", 
        HttpCompletionOption.ResponseHeadersRead, token))
    {
        response.EnsureSuccessStatusCode();
        var stream = await response.Content.ReadAsStreamAsync();
        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}

 

보시다시피이 시점에서 메서드는 토큰을 수락하고 이를 사용하여 cancel 알림을 수신합니다.

이제 API와 클라이언트 앱을 다시 시작할 수 있습니다.

 

그렇게 하자마자 동일한 예외가 표시됩니다.

 

 

잘 됐네요. 애플리케이션에서이 예외를 처리하는 방법을 계속 볼 수 있습니다.

 

TaskCanceledException 처리

 

요청을 취소 한 후 애플리케이션에서 발생하는 예외를 처리하려면 try-catch 블록 안에 요청을 래핑하면됩니다.

 

private async Task GetCompaniesAndCancel(CancellationToken token)
{
    try
    {
        using (var response = await _httpClient.GetAsync("companies",
        HttpCompletionOption.ResponseHeadersRead, token))
        {
            response.EnsureSuccessStatusCode();
            var stream = await response.Content.ReadAsStreamAsync();
            var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
        }
    }
    catch (OperationCanceledException ocex)
    {
        Console.WriteLine(ocex.Message);
    }
}

 

앱이 throws 를 던지는 것을 TaskCanceledException 보았지만 OperationCanceledException 클래스에서 상속되었으므로 해당 클래스를 사용하여 예외를 포착 할 수 있습니다. 물론 catch 블록에서 많은 작업을 수행 할 수 있지만 이 예제에서는 메시지를 기록하는 것으로 충분합니다.

이제 두 애플리케이션을 모두 시작하고 결과를 살펴 보겠습니다.

 

 

Great..

 

 

응답에서 상태 코드 검사 (Inspecting Status Codes from Responses)

 

 

현재 구현 한 상태에서 응답이 성공하지 못하면 예외를 throw합니다. 글쎄, 100 % 정확하기 위해 EnsureSuccessStatusCode() 방법은 그것을 할 것입니다. 그러나 대부분의 경우 응답 실패의 실제 원인에 따라보다 사용자 친화적 인 메시지를 보장하고자합니다. 이를 위해 응답의 상태 코드를 확인할 수 있습니다.

여기에서 모든 상태 코드를 다루지는 않을 것 입니다. HTTP 참조 테이블을 방문 할 수 있기 때문 입니다. 즉, 여기서는 상태 코드 중 하나를 사용하고보다 의미있는 메시지로 더 나은 사용자 경험을 제공하는 방법을 보여줄 것입니다.

이 예에서는 HttpClientStreamService 클래스 를 사용합니다 . 따라서 해당 클래스에 새 메서드를 생성 해 보겠습니다.

 

 

private async Task GetNonExistentCompany()
{
    var uri = Path.Combine("companies", "F8088E81-7EFA-4E49-F824-08D8C38D155C");
    using (var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();
        var stream = await response.Content.ReadAsStreamAsync();
        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}

 

이 전체 코드는 이제 우리에게 익숙하지만 제공된 Id (Guid 값)가 데이터베이스에 존재하지 않는다는 점을 언급하고 싶습니다. 따라서 API는 찾을 수없는 결과 (404)를 반환해야합니다.

 

테스트하기 전에 Execute메서드 를 수정해야 합니다.

 

public async Task Execute()
{
    //await GetCompaniesWithStream();
    //await CreateCompanyWithStream();
    await GetNonExistentCompany();
}

 

또한 Program클래스 에서이 서비스를 활성화해야합니다 .

 

private static void ConfigureServices(IServiceCollection services)
{
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>();
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientPatchService>();
    services.AddScoped<IHttpClientServiceImplementation, HttpClientStreamService>();
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientCancellationService>();
}

 

 

Great.

 

두 응용 프로그램을 모두 시작하고 결과를 살펴 보겠습니다.

 

보시다시피 404 응답을 받았지만 여전히 예외가 발생합니다. 글쎄, 우리는 그것을 바꿀 수 있습니다.

 

 

상태 코드 작업 (Working with Status Codes)

 

 

메서드에 약간의 수정을 추가해 보겠습니다.

 

private async Task GetNonExistentCompany()
{
    var uri = Path.Combine("companies", "F8088E81-7EFA-4E49-F824-08D8C38D155C");
    using (var response = await _httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead))
    {
        if(!response.IsSuccessStatusCode)
        {
            if (response.StatusCode.Equals(HttpStatusCode.NotFound))
            {
                Console.WriteLine("The company you are searching for couldn't be found.");
                return;
            }
            response.EnsureSuccessStatusCode();
        }
        var stream = await response.Content.ReadAsStreamAsync();
        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}

 

먼저 응답에 IsSuccessStatusCode 속성의 Success Status Code 가 포함되어 있지 않은지 확인합니다 . 그렇지 않은 경우 처리하려는 Status Code (이 경우 Status Code)를 명시적으로 확인 NotFound 합니다. 이 경우 콘솔 창에 정보 메시지를 작성합니다. 다른 모든 실패한 상태 코드의 경우 EnsureSuccessStatusCode 메서드와 함께 예외가 발생합니다 .

 

물론 다른 상태 코드를 사용하여 이 조건을 항상 확장 할 수 있지만 이 경우 해당 논리를 다른 메서드로 추출하여 이 메서드를 더 읽기 쉽게 만드는 것이 좋습니다.

 

이제 애플리케이션을 시작하면 :

 

화면에서 메시지를 볼 수 있습니다.

 

Greats...

 

모든 것이 예상대로 작동합니다.

 

결론

 

 

이제 CancellationToken 및 CancellationTokenSource 를 사용하여 요청을 취소하는 방법과 CancellationTokenSource 를 사용하여 서로 다른 Request 간에 Token 을 공유하는 방법을 알고 있습니다. 또한 Fail 한 각 Response 에 대해 Exception 가 발생하지 않도록 Response 과 다른 Status Code  를 사용하는 방법을 알고 있습니다.

다음 기사에서는 HttpClientFactory 에 대해 자세히 알아보고 해당 접근 방식의 장점이 무엇인지 살펴볼 것 입니다.

 

 

원본문서 

 

code-maze.com/canceling-http-requests-in-asp-net-core-with-cancellationtoken/