Refactroing - HttpClient(EP02)

2023. 10. 30. 00:00CSharp/Advance

반응형

이전 강의에서 HttpClient 를 사용할 때 경험할 수 있는 일반적인 오류에 대해 알아봤고

https://yogingang.tistory.com/461

 

Refactroing - HttpClient

개발을 하다보면 직접적으로 library 를 이용하여 개발하거나 직접 특정 기능을 개발 하기도 하지만 Http api 를 이용하여 외부 api 를 이용하는 경우가 있다. 아마 요즘에는 대부분의 개발에 external s

yogingang.tistory.com

이 문제를 해결하는 방법도 알아봤다. 

일반적인 post, put, get, delete 를 wrapping 한 간단한 class 를 생성할 것이다. 

일단 간단한 helper 를 만들어보자

public class HttpClientHelper
{
    private static readonly HttpClient _client =
        new HttpClient(new HttpClientTraceHandler(new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(1),
            MaxConnectionsPerServer = 10
        }));


    public static async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await _client.GetAsync(url);
    }
    public static async Task<HttpResponseMessage> DeleteAsync(string url)
    {
        return await _client.DeleteAsync(url);
    }

    public static async Task<HttpResponseMessage> PostAsync<Tinput>(string url, Tinput input)
    {
        StringContent stringContent = null;
        try
        {
            var jsonString = JsonSerializer.Serialize(input);
            stringContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
            return await _client.PostAsync(url, stringContent);
        }
        finally
        {
            stringContent?.Dispose();
        }
    }

    public static async Task<HttpResponseMessage> PutAsync<Tinput>(string url, Tinput input)
    {
        StringContent stringContent = null;
        try
        {
            var jsonString = JsonSerializer.Serialize(input);
            stringContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
            return await _client.PutAsync(url, stringContent);
        }
        finally
        {
            stringContent?.Dispose();
        }
    }
}

자 이 helper 를 가지고도 그대로 사용 가능 하다.

일단 강의에서  call 했던 Get 1000 번 호출을 다시 해보자

문제 없이 실행된다. 

문제는 실제로 HttpClient 를 직접 사용하는 것과 거의 다른점이 없다는 것이다. 

즉 편의성이 너무 떨어진다. 

**주의 !! 편의성에 집중하다 보면 재사용성과 확장성이 떨어질 수 있다. **

 

Get 으로 아주 간단한 내용을 했기 때문에 느끼지 못했겠지만

route path 나 query parameter 가 추가된 Get 이라면 복잡해진다.

그래서 약간에 편의성을 위한 기능을 추가해 보겠다. 

일단 Get과 Delete 를 수정한 class 를 만들어 보자

기존 helper class 를 수정해도 되지만 RestClient 라는 다른 class 를 만들어보자

RestClient.cs

public class RestClient
{
    private static readonly HttpClient _client = new(new HttpClientTraceHandler(new SocketsHttpHandler()
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(1),
        MaxConnectionsPerServer = 10
    }));

    public static async Task<T> GetAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
    {
        var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
        var request = new HttpRequestMessage(HttpMethod.Get, fullUrl);
        if (header != null)
        {
            foreach (var item in header)
            {
                request.Headers.Add(item.Key, item.Value);
            }
        }
        using var response = await _client.SendAsync(request);

        if (!response.IsSuccessStatusCode)
        {
            var message = await response.Content.ReadAsStringAsync();
            throw new Exception(message);
        }

        var data = await response.Content.ReadAsStringAsync();
        if (typeof(T) == typeof(string))
            return (T)Convert.ChangeType(data, typeof(T));

        var encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
        var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, WriteIndented = true, Encoder = encoder, IncludeFields = true };
        return JsonSerializer.Deserialize<T>(data, options);
    }
    public static async Task<T> DeleteAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
    {
        var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
        var request = new HttpRequestMessage(HttpMethod.Delete, fullUrl);
        if (header != null)
        {
            foreach (var item in header)
            {
                request.Headers.Add(item.Key, item.Value);
            }
        }
        using var response = await _client.SendAsync(request);

        if (!response.IsSuccessStatusCode)
        {
            var message = await response.Content.ReadAsStringAsync();
            throw new Exception(message);
        }

        var data = await response.Content.ReadAsStringAsync();
        if (typeof(T) == typeof(string))
            return (T)Convert.ChangeType(data, typeof(T)); ;

        var encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
        var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, WriteIndented = true, Encoder = encoder, IncludeFields = true };
        return JsonSerializer.Deserialize<T>(data, options);
    }
}

 

차이점이 보이는가? 단순히 url 만 있는 것은 아니고 QueryString 을 따로 처리 할 수 있도록 하였다. 

Dictionary <string,string> 을 받아서 처리 하도록 하고 있다. 

header 도 마찬가지 이다. Dictionary 형태로 받아서 HttpRequestMessage 에 Headers 에 add 하고 있다. 

꽤 괜찮은 편의성을 추가한 것같다. 

이제 외부에서 사용할 때 Key, Value 형태로 queryString 과 header 를 넣을수 있다. 

 

그런데 위에 code 를 보면 중복된 부분들이 보인다. 

일단 System.Text.Json 의 JsonSerializer 를 이용해서 Deserialize 하는 부분부터 refactoring 하겠다. 

public static async Task<T> GetAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
{
    ...
    //var encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
    //var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, WriteIndented = true, Encoder = encoder, IncludeFields = true };
    //return JsonSerializer.Deserialize<T>(data, options);

    return DeserializeFromJson<T> (data);
    ...
}

public static async Task<T> DeleteAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
{
    ...
    //var encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
    //var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, WriteIndented = true, Encoder = encoder, IncludeFields = true };
    //return JsonSerializer.Deserialize<T>(data, options);

    return DeserializeFromJson<T> (data);
    ...
}
 
public static T DeserializeFromJson<T>(string value, bool writeIndented = true, bool propertyNameCaseInsensitive = true, JavaScriptEncoder encoder = null, bool includeFields = false)
{
     if (encoder == null)
         encoder = JavaScriptEncoder.Create(UnicodeRanges.All);

     var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = propertyNameCaseInsensitive, WriteIndented = writeIndented, Encoder = encoder, IncludeFields = includeFields };
     var retValue = JsonSerializer.Deserialize<T>(value, options);
     return retValue;
}

자 DeserializeFromJson 이 위에 코드를 refactoring 하였다. 

이제 Extension Method 를 이용하여 다시 수정하겠다. 

ObjectExtensions.cs

public static class ObjectExtensions
{
    public static T DeserializeFromJson<T>(this string value, bool writeIndented = true, bool propertyNameCaseInsensitive = true, JavaScriptEncoder encoder = null, bool includeFields = false)
    {
        if (encoder == null)
            encoder = JavaScriptEncoder.Create(UnicodeRanges.All);

        var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = propertyNameCaseInsensitive, WriteIndented = writeIndented, Encoder = encoder, IncludeFields = includeFields };
        var retValue = JsonSerializer.Deserialize<T>(value, options);
        return retValue;
    }
}

이제 code 는 다음과 같이 변한다. 

//return DeserializeFromJson<T> (data);
return data.DeserializeFromJson<T>();

이제 다음 code 를 수정하겠다.

HttpRequestMessage 를 생성하는 code 와

SendAsync 를 통해 response 를 받는 부분이 그렇다.

RestClient.cs 

public class RestClient
{
    private static readonly HttpClient _client = new(new HttpClientTraceHandler(new SocketsHttpHandler()
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(1),
        MaxConnectionsPerServer = 10
    }));

    public static async Task<T> GetAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
    {
        var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
        using HttpRequestMessage request = CreateRequestMessage(HttpMethod.Get, fullUrl, header);
        return await SendAsync<T>(fullUrl, request);
    }
    public static async Task<T> DeleteAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
    {
        var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
        using HttpRequestMessage request = CreateRequestMessage(HttpMethod.Delete, fullUrl, header);
        return await SendAsync<T>(fullUrl, request);
    }
    protected static HttpRequestMessage CreateRequestMessage(HttpMethod httpMethod, string fullUrl, IDictionary<string, string>? header)
    {
        var request = new HttpRequestMessage(httpMethod, fullUrl);
        if (header != null)
        {
            foreach (var item in header)
            {
                request.Headers.Add(item.Key, item.Value);
            }
        }
        return request;
    }
    protected static async Task<T> SendAsync<T>(string url, HttpRequestMessage request)
    {
        using var response = await _client.SendAsync(request);
        if (!response.IsSuccessStatusCode)
        {
            var message = await response.Content.ReadAsStringAsync();
            throw new Exception(message);
        }
        var data = await response.Content.ReadAsStringAsync();
        if (typeof(T) == typeof(string))
            return (T)Convert.ChangeType(data, typeof(T)); ;
        return data.DeserializeFromJson<T>();
    }
}

자 여기 까지 왔다면 기본적으로 사용가능한 Get 과 Delete 를 지원하는 RestClient 를 생성하였다. 

이제 Post 와 Put 을 구현해 보자.

public static async Task<T> PostAsync<TInput, T>(string url, TInput input, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
{
    var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
    using var request = CreateRequestMessage(HttpMethod.Post, fullUrl, header);
    request.Content = new StringContent(input.SerializeToJson(), Encoding.UTF8, "application/json");
    return await SendAsync<T>(fullUrl, request);
}
public static async Task<T> PutAsync<TInput, T>(string url, TInput input, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
{
    var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
    using var request = CreateRequestMessage(HttpMethod.Put, fullUrl, header);
    request.Content = new StringContent(input.SerializeToJson(), Encoding.UTF8, "application/json");
    return await SendAsync<T>(fullUrl, request);
}

자 이쯤 되면 사용가능한 helper 를 구현했다고 보인다. 

그런데 아무래도 static 이다 보니 oop 상 여러가지 추상화에 걸린다. 

일반 class 로 수정해보자

확장성을 위해서 interface 를 만들고 , class 구현해보자

모든 method 에서 static 을  빼자

RestClinet class 에서 Ctrl + . 을 눌러보자

interface 를 먼저 추출하자

이제 base class 를 추출하자

 

오류가 좀 있을수 있다. 수정해 주자

// 최종본
public interface IRestClient
{
    Task<T> DeleteAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null);
    Task<T> GetAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null);
    Task<T> PostAsync<TInput, T>(string url, TInput input, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null);
    Task<T> PutAsync<TInput, T>(string url, TInput input, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null);
}

public abstract class RestClientBase:IRestClient
{
    private static readonly HttpClient _client = new(new HttpClientTraceHandler(new SocketsHttpHandler()
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(1),
        MaxConnectionsPerServer = 10
    }));
    public async Task<T> DeleteAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
    {
        var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
        using HttpRequestMessage request = CreateRequestMessage(HttpMethod.Delete, fullUrl, header);
        return await SendAsync<T>(fullUrl, request);
    }

    public async Task<T> GetAsync<T>(string url, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
    {
        var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
        using HttpRequestMessage request = CreateRequestMessage(HttpMethod.Get, fullUrl, header);
        return await SendAsync<T>(fullUrl, request);
    }

    public abstract  Task<T> PostAsync<TInput, T>(string url, TInput input, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null);
    public abstract  Task<T> PutAsync<TInput, T>(string url, TInput input, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null);

    protected HttpRequestMessage CreateRequestMessage(HttpMethod httpMethod, string fullUrl, IDictionary<string, string>? header)
    {
        var request = new HttpRequestMessage(httpMethod, fullUrl);
        if (header != null)
        {
            foreach (var item in header)
            {
                request.Headers.Add(item.Key, item.Value);
            }
        }
        return request;
    }
    protected async Task<T> SendAsync<T>(string url, HttpRequestMessage request)
    {
        using var response = await _client.SendAsync(request);
        if (!response.IsSuccessStatusCode)
        {
            var message = await response.Content.ReadAsStringAsync();
            throw new Exception(message);
        }
        var data = await response.Content.ReadAsStringAsync();
        if (typeof(T) == typeof(string))
            return (T)Convert.ChangeType(data, typeof(T)); ;
        return data.DeserializeFromJson<T>();
    }
}

public class JsonClient : RestClientBase
{
    public override async Task<T> PostAsync<TInput, T>(string url, TInput input, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
    {
        var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
        using var request = CreateRequestMessage(HttpMethod.Post, fullUrl, header);
        request.Content = new StringContent(input.SerializeToJson(), Encoding.UTF8, "application/json");
        return await SendAsync<T>(fullUrl, request);
    }
    public override async Task<T> PutAsync<TInput, T>(string url, TInput input, IDictionary<string, string>? queryString = null, IDictionary<string, string>? header = null)
    {
        var fullUrl = queryString == null ? url : QueryHelpers.AddQueryString(url, queryString);
        using var request = CreateRequestMessage(HttpMethod.Put, fullUrl, header);
        request.Content = new StringContent(input.SerializeToJson(), Encoding.UTF8, "application/json");
        return await SendAsync<T>(fullUrl, request);
    }
}

자 이제 JsonClient 라는 개체가 생성되었다.

이것은 application/json 을 header 보내고 response 들을 처리 하는 httpclient 이다. 

기본적인 refactoring 은 여기 까지이다.

multipart/form-data 다른 content-type 에 대한 구현도 각자 한번 진행해보자

 

관련영상

https://youtu.be/Odbp8rgd7-w

반응형

'CSharp > Advance' 카테고리의 다른 글

Json to CSharp  (0) 2023.11.13
알수 없는 문제를 해결하는 방법 - vite dev server 의 404 오류 해결  (0) 2023.11.06
Refactroing - HttpClient  (1) 2023.10.23
Refactoring - Encryption Hash Helper  (0) 2023.10.16
C# deadlock prevent  (0) 2023.06.23