2023. 10. 30. 00:00ㆍCSharp/Advance
이전 강의에서 HttpClient 를 사용할 때 경험할 수 있는 일반적인 오류에 대해 알아봤고
https://yogingang.tistory.com/461
이 문제를 해결하는 방법도 알아봤다.
일반적인 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 에 대한 구현도 각자 한번 진행해보자
관련영상
'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 |