httppipeline 완료, mqttPipeline 개발 중. test 코드 작성 필요
This commit is contained in:
@@ -212,7 +212,12 @@ namespace UVC.Data
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 업데이트 된 속성의 수.
|
||||
/// </summary>
|
||||
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
|
||||
public int UpdatedCount { get => addedList.Count + modifiedList.Count + removedList.Count; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 항목을 추가합니다.
|
||||
|
||||
@@ -41,10 +41,19 @@ namespace UVC.Data
|
||||
|
||||
// 재귀 호출 제한하기 위한 설정 추가
|
||||
private int maxRecursionDepth = 10;
|
||||
private int currentDepth = 0;
|
||||
|
||||
private DataMask mask;
|
||||
|
||||
/// <summary>
|
||||
/// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다.
|
||||
/// </summary>
|
||||
public bool SupportsStreamParsing { get; internal set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 대용량 JSON 스트림을 판단 할때 스트림 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다.
|
||||
/// </summary>
|
||||
public int SupportsStreamLength { get; internal set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
@@ -116,7 +125,7 @@ namespace UVC.Data
|
||||
/// </summary>
|
||||
/// <param name="jsonStream">JSON 데이터 스트림</param>
|
||||
/// <returns>매핑된 DataObject</returns>
|
||||
public DataObject MapStream(System.IO.Stream jsonStream)
|
||||
public DataObject MapObjectStream(System.IO.Stream jsonStream)
|
||||
{
|
||||
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
||||
{
|
||||
@@ -126,6 +135,21 @@ namespace UVC.Data
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다.
|
||||
/// </summary>
|
||||
/// <param name="jsonStream">JSON 데이터 스트림</param>
|
||||
/// <returns>매핑된 DataObject</returns>
|
||||
public DataArray MapArrayStream(System.IO.Stream jsonStream)
|
||||
{
|
||||
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
||||
{
|
||||
var serializer = new Newtonsoft.Json.JsonSerializer();
|
||||
var sourceArray = serializer.Deserialize<JArray>(reader);
|
||||
return Mapping(sourceArray);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 객체를 재귀적으로 매핑합니다.
|
||||
/// </summary>
|
||||
|
||||
@@ -5,21 +5,96 @@ using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 데이터의 구조와 변환 규칙을 정의하는 마스크 클래스입니다.
|
||||
/// Newtonsoft.Json.Linq.JObject를 상속하여 JSON 데이터 구조를 표현하고,
|
||||
/// 데이터 매핑, 필드 이름 변환, 그리고 타입 변환을 위한 메타데이터를 제공합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// DataMask는 주로 DataMapper 클래스와 함께 사용되어 서로 다른 JSON 형식 간의
|
||||
/// 데이터 변환 및 매핑 규칙을 정의합니다. 기본 JSON 구조 외에도 추가 속성들을
|
||||
/// 통해 매핑 과정에서 필요한 메타데이터를 제공합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// 기본 사용 예시:
|
||||
/// <code>
|
||||
/// // 마스크 객체 생성 및 설정
|
||||
/// var mask = new DataMask();
|
||||
/// mask["name"] = ""; // 문자열 타입 지정
|
||||
/// mask["age"] = 0; // 정수 타입 지정
|
||||
/// mask["isActive"] = false; // 불리언 타입 지정
|
||||
/// mask["height"] = 0.0; // 실수 타입 지정
|
||||
///
|
||||
/// // 메타 속성 설정
|
||||
/// mask.ObjectIdKey = "name"; // DataObject의 ID로 사용할 필드 지정
|
||||
/// mask.ObjectName = "employees"; // 데이터 객체의 이름 지정
|
||||
///
|
||||
/// // 필드 이름 변환 규칙 설정
|
||||
/// mask.NamesForReplace = new Dictionary<string, string>
|
||||
/// {
|
||||
/// { "full_name", "name" }, // JSON의 full_name을 name으로 변환
|
||||
/// { "employee_age", "age" } // JSON의 employee_age를 age로 변환
|
||||
/// };
|
||||
///
|
||||
/// // 매핑 예시 (DataMapper 클래스와 함께 사용)
|
||||
/// var sourceJson = JObject.Parse(@"{
|
||||
/// ""full_name"": ""김철수"",
|
||||
/// ""employee_age"": 30,
|
||||
/// ""isActive"": true,
|
||||
/// ""height"": 175.5
|
||||
/// }");
|
||||
///
|
||||
/// var mapper = new DataMapper(mask);
|
||||
/// DataObject result = mapper.Mapping(sourceJson);
|
||||
/// // result는 변환된 필드 이름과 타입을 가진 DataObject 객체
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class DataMask : JObject
|
||||
{
|
||||
/// <summary>
|
||||
/// DataObject의 Id에 해당하는 key 문자열
|
||||
/// DataObject의 Id에 해당하는 key 문자열입니다.
|
||||
/// 이 속성은 매핑된 DataObject에서 고유 식별자로 사용될 필드를 지정합니다.
|
||||
/// null인 경우 DataObject의 기본 동작을 따릅니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var mask = new DataMask();
|
||||
/// mask.ObjectIdKey = "userId"; // DataObject에서 "userId" 필드가 Id로 사용됨
|
||||
/// </code>
|
||||
/// </example>
|
||||
public string? ObjectIdKey { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// DataObject의 이름을 나타내는 속성입니다. DataRepository에서 사용됩니다.
|
||||
/// 이 이름은 매핑된 DataObject를 분류하거나 식별하는 데 사용될 수 있습니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var mask = new DataMask();
|
||||
/// mask.ObjectName = "users"; // 매핑된 DataObject는 "users"라는 이름을 가짐
|
||||
/// </code>
|
||||
/// </example>
|
||||
public string ObjectName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 교체 할 이름을 나타내는 딕셔너리입니다.
|
||||
/// 원본 JSON에서 매핑된 DataObject로 필드 이름을 변환하는 규칙을 정의하는 딕셔너리입니다.
|
||||
/// 키는 원본 JSON의 필드 이름이고, 값은 변환될 대상 필드 이름입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 속성을 통해 서로 다른 명명 규칙을 사용하는 JSON 구조 간의 매핑을 용이하게 할 수 있습니다.
|
||||
/// 예를 들어, snake_case를 사용하는 API 응답을 camelCase로 변환하는 데 사용할 수 있습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var mask = new DataMask();
|
||||
/// mask.NamesForReplace = new Dictionary<string, string>
|
||||
/// {
|
||||
/// { "first_name", "firstName" },
|
||||
/// { "last_name", "lastName" },
|
||||
/// { "birth_date", "birthDate" }
|
||||
/// };
|
||||
/// </code>
|
||||
/// </example>
|
||||
public Dictionary<string, string>? NamesForReplace { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,44 @@ using System.Linq;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 키-값 쌍의 데이터를 관리하고 변경 사항을 추적하는 동적 데이터 객체입니다.
|
||||
/// SortedDictionary를 상속하여 키를 기준으로 정렬된 데이터를 제공합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 JSON과 호환되는 데이터 구조를 표현하며, 데이터 변경 추적 기능을 통해
|
||||
/// 효율적인 데이터 동기화를 지원합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // DataMask 생성 및 설정
|
||||
/// var mask = new DataMask();
|
||||
/// mask.ObjectIdKey = "id";
|
||||
/// mask.ObjectName = "users";
|
||||
///
|
||||
/// // 필드 이름 변환 설정
|
||||
/// mask.NamesForReplace = new Dictionary<string, string>
|
||||
/// {
|
||||
/// { "userName", "name" },
|
||||
/// { "userEmail", "email" }
|
||||
/// };
|
||||
///
|
||||
/// // JObject 속성으로 마스킹 규칙 추가
|
||||
/// mask["include"] = new JArray("name", "email", "age");
|
||||
/// mask["exclude"] = new JArray("password", "token");
|
||||
///
|
||||
/// // DataObject에 마스크 적용하는 예시
|
||||
/// var dataObject = new DataObject();
|
||||
/// dataObject["userName"] = "홍길동";
|
||||
/// dataObject["userEmail"] = "hong@example.com";
|
||||
/// dataObject["password"] = "secret123";
|
||||
///
|
||||
/// // 마스크 적용 결과 (개념적 예시):
|
||||
/// // - userName은 name으로 변환됨
|
||||
/// // - userEmail은 email로 변환됨
|
||||
/// // - password는 제외됨
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class DataObject : SortedDictionary<string, object>, IDataObject
|
||||
{
|
||||
|
||||
@@ -27,7 +65,9 @@ namespace UVC.Data
|
||||
public string Name { get; internal set; } = string.Empty;
|
||||
|
||||
|
||||
// 직접적인 변경이 있었던 키를 저장하는 리스트
|
||||
/// <summary>
|
||||
/// 직접적인 변경이 있었던 키를 저장하는 리스트입니다.
|
||||
/// </summary>
|
||||
protected List<string> changedProperies = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
@@ -132,7 +172,7 @@ namespace UVC.Data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 새 속성을 추가할 때 이벤트 연결을 처리합니다.
|
||||
/// 새 속성을 추가하고 변경 사항을 추적합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">추가할 속성의 이름</param>
|
||||
/// <param name="value">속성의 값</param>
|
||||
@@ -152,9 +192,11 @@ namespace UVC.Data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인덱서를 통한 속성 설정을 처리합니다.
|
||||
/// 속성 변경 시 변경 사항을 자동으로 추적합니다.
|
||||
/// 인덱서를 통해 속성값에 접근하고 설정합니다.
|
||||
/// 속성값이 변경될 때마다 변경 사항을 자동으로 추적합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">접근할 속성의 키</param>
|
||||
/// <returns>속성값</returns>
|
||||
public new object this[string key]
|
||||
{
|
||||
get => base[key];
|
||||
@@ -184,7 +226,12 @@ namespace UVC.Data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 정수(int)로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||
/// <returns>변환된 정수 값 또는 기본값</returns>
|
||||
public int GetInt(string propertyName, int defaultValue = 0)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -196,6 +243,12 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 문자열(string)로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 null인 경우 반환할 기본값</param>
|
||||
/// <returns>변환된 문자열 값 또는 기본값</returns>
|
||||
public string? GetString(string propertyName, string? defaultValue = null)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -205,6 +258,12 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 불리언(bool)으로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||
/// <returns>변환된 불리언 값 또는 기본값</returns>
|
||||
public bool GetBool(string propertyName, bool defaultValue = false)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -216,6 +275,12 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 부동 소수점(float)으로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||
/// <returns>변환된 부동 소수점 값 또는 기본값</returns>
|
||||
public float GetFloat(string propertyName, float defaultValue = 0f)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -227,6 +292,12 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 더블 정밀도 부동 소수점(double)으로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||
/// <returns>변환된 더블 값 또는 기본값</returns>
|
||||
public double GetDouble(string propertyName, double defaultValue = 0.0)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -238,6 +309,12 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 DateTime으로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||
/// <returns>변환된 DateTime 값 또는 기본값</returns>
|
||||
public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -249,6 +326,13 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 열거형(Enum)으로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">변환할 열거형 타입</typeparam>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||
/// <returns>변환된 열거형 값 또는 기본값</returns>
|
||||
public T GetEnum<T>(string propertyName, T defaultValue = default) where T : Enum
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -260,6 +344,27 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 IDataObject로 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 IDataObject가 아닌 경우 반환할 기본값</param>
|
||||
/// <returns>IDataObject 인터페이스를 구현한 객체 또는 기본값</returns>
|
||||
public IDataObject? Get(string propertyName, IDataObject? defaultValue = null)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is IDataObject dataObject) return dataObject;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 DataArray로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||
/// <returns>DataArray 객체 또는 기본값</returns>
|
||||
public DataArray? GetDataArray(string propertyName, DataArray? defaultValue = null)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -272,6 +377,12 @@ namespace UVC.Data
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 속성의 값을 DataObject로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||
/// <returns>DataObject 객체 또는 기본값</returns>
|
||||
public DataObject? GetDataObject(string propertyName, DataObject? defaultValue = null)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
@@ -303,7 +414,7 @@ namespace UVC.Data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 속성을 제거할 때 기존 속성 목록과 변경된 키 목록도 초기화합니다.
|
||||
/// 모든 속성을 제거하고 추적 리스트를 초기화합니다.
|
||||
/// </summary>
|
||||
public void RemoveAll()
|
||||
{
|
||||
@@ -362,6 +473,17 @@ namespace UVC.Data
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 업데이트 된 속성의 수.
|
||||
/// </summary>
|
||||
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
|
||||
public int UpdatedCount { get => changedProperies.Count; }
|
||||
|
||||
/// <summary>
|
||||
/// DataObject의 내용을 문자열로 반환합니다.
|
||||
/// 각 키-값 쌍이 "키:값" 형식으로 쉼표로 구분되어 표시됩니다.
|
||||
/// </summary>
|
||||
/// <returns>DataObject의 내용을 나타내는 문자열</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}"));
|
||||
|
||||
@@ -1,50 +1,398 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 데이터 객체들을 키-값 쌍으로 관리하는 중앙 저장소입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 싱글톤 패턴으로 구현되어 있어 애플리케이션 전체에서
|
||||
/// 하나의 인스턴스만 존재합니다. IDataObject 인터페이스를 구현하는
|
||||
/// 모든 데이터 객체를 저장하고 검색할 수 있습니다.
|
||||
/// </remarks>
|
||||
public class DataRepository
|
||||
{
|
||||
#region Singleton
|
||||
/// <summary>
|
||||
/// DataRepository 싱글톤 인스턴스를 생성하는 지연 초기화 객체입니다.
|
||||
/// </summary>
|
||||
protected static readonly Lazy<DataRepository> instance = new Lazy<DataRepository>(() => new DataRepository());
|
||||
/// <summary>
|
||||
/// 외부에서의 인스턴스 생성을 방지하는 보호된 생성자입니다.
|
||||
/// </summary>
|
||||
protected DataRepository() { }
|
||||
/// <summary>
|
||||
/// DataRepository의 단일 인스턴스에 대한 접근자입니다.
|
||||
/// </summary>
|
||||
public static DataRepository Instance { get { return instance.Value; } }
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 키로 식별되는 데이터 객체를 저장하는 컬렉션입니다.
|
||||
/// </summary>
|
||||
private Dictionary<string, IDataObject> dataObjects = new Dictionary<string, IDataObject>();
|
||||
|
||||
public IDataObject AddData(string key, IDataObject dataObject)
|
||||
/// <summary>
|
||||
/// 스레드 동기화를 위한 잠금 객체입니다.
|
||||
/// </summary>
|
||||
private readonly object syncLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 업데이트 시 호출될 핸들러 함수들을 저장하는 딕셔너리입니다.
|
||||
/// 각 키에 연결된 데이터가 업데이트될 때 해당 핸들러가 호출됩니다.
|
||||
/// </summary>
|
||||
private Dictionary<string, Action<IDataObject>> dataUpdateHandlers = new Dictionary<string, Action<IDataObject>>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 저장소에 데이터 객체를 추가하거나 기존 객체를 업데이트합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">데이터 객체를 식별하는 고유 키</param>
|
||||
/// <param name="dataObject">저장할 데이터 객체</param>
|
||||
/// <param name="updatedDataOnly">true인 경우 업데이트된 속성만 반환, false인 경우 전체 객체 반환</param>
|
||||
/// <returns>새로 추가된 객체 또는 업데이트된 기존 객체</returns>
|
||||
public IDataObject AddData(string key, IDataObject dataObject, bool updatedDataOnly = true)
|
||||
{
|
||||
if (!dataObjects.ContainsKey(key))
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||
|
||||
if (dataObject == null)
|
||||
throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다.");
|
||||
|
||||
lock (syncLock)
|
||||
{
|
||||
dataObject.InitData();
|
||||
dataObjects.Add(key, dataObject);
|
||||
return dataObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
IDataObject obj = dataObjects[key];
|
||||
obj.UpdateDifferent(dataObject);
|
||||
return obj.GetUpdatedObject();
|
||||
if (!dataObjects.ContainsKey(key))
|
||||
{
|
||||
dataObject.InitData();
|
||||
dataObjects.Add(key, dataObject);
|
||||
return dataObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
IDataObject obj = dataObjects[key];
|
||||
obj.UpdateDifferent(dataObject);
|
||||
var newDataObject = dataObject;
|
||||
if (updatedDataOnly) newDataObject = obj.GetUpdatedObject();
|
||||
NotifyDataUpdate(key, newDataObject);
|
||||
return newDataObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 키에 해당하는 데이터 객체를 저장소에서 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">제거할 데이터 객체의 키</param>
|
||||
public void RemoveData(string key)
|
||||
{
|
||||
if (dataObjects.ContainsKey(key))
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||
|
||||
lock (syncLock)
|
||||
{
|
||||
dataObjects.Remove(key);
|
||||
if (dataObjects.ContainsKey(key))
|
||||
{
|
||||
dataObjects.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 키에 해당하는 데이터 객체를 저장소에서 검색합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">검색할 데이터 객체의 키</param>
|
||||
/// <returns>키가 존재하면 해당 데이터 객체, 존재하지 않으면 null</returns>
|
||||
public IDataObject GetData(string key)
|
||||
{
|
||||
if (dataObjects.ContainsKey(key))
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||
|
||||
lock (syncLock)
|
||||
{
|
||||
return dataObjects[key];
|
||||
if (dataObjects.ContainsKey(key))
|
||||
{
|
||||
return dataObjects[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 키에 대한 데이터 업데이트 핸들러를 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">데이터 업데이트를 감시할 키</param>
|
||||
/// <param name="handler">데이터가 업데이트되었을 때 호출될 콜백 함수</param>
|
||||
/// <remarks>
|
||||
/// 같은 키에 대해 여러 핸들러를 등록할 수 있습니다.
|
||||
/// 핸들러는 해당 키의 데이터가 AddData를 통해 업데이트될 때마다 호출됩니다.
|
||||
/// </remarks>
|
||||
public void AddDataUpdateHandler(string key, Action<IDataObject> handler)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||
if (handler == null)
|
||||
throw new ArgumentNullException(nameof(handler), "핸들러는 null일 수 없습니다.");
|
||||
lock (syncLock)
|
||||
{
|
||||
if (!dataUpdateHandlers.ContainsKey(key))
|
||||
{
|
||||
dataUpdateHandlers[key] = handler;
|
||||
}
|
||||
else
|
||||
{
|
||||
dataUpdateHandlers[key] += handler; // 기존 핸들러에 추가
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 키에서 데이터 업데이트 핸들러를 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">핸들러를 제거할 데이터 키</param>
|
||||
/// <param name="handler">제거할 핸들러 함수</param>
|
||||
/// <remarks>
|
||||
/// 지정된 키에 연결된 핸들러가 없거나, 제거하려는 핸들러가 등록되지 않은 경우 아무 작업도 수행하지 않습니다.
|
||||
/// 키에 연결된 마지막 핸들러가 제거되면 해당 키는 dataUpdateHandlers 딕셔너리에서 삭제됩니다.
|
||||
/// </remarks>
|
||||
public void RemoveDataUpdateHandler(string key, Action<IDataObject> handler)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||
if (handler == null)
|
||||
throw new ArgumentNullException(nameof(handler), "핸들러는 null일 수 없습니다.");
|
||||
lock (syncLock)
|
||||
{
|
||||
if (dataUpdateHandlers.ContainsKey(key))
|
||||
{
|
||||
dataUpdateHandlers[key] -= handler; // 기존 핸들러에서 제거
|
||||
if (dataUpdateHandlers[key] == null)
|
||||
{
|
||||
dataUpdateHandlers.Remove(key); // 핸들러가 없으면 제거
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 데이터 업데이트 핸들러를 제거합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 모든 키에 대한 모든 핸들러를 한 번에 제거합니다.
|
||||
/// 예를 들어, 씬이 변경되거나 애플리케이션이 종료될 때
|
||||
/// 모든 핸들러를 정리하는 데 유용합니다.
|
||||
/// </remarks>
|
||||
public void ClearDataUpdateHandlers()
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
dataUpdateHandlers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 키의 데이터가 업데이트되었음을 알리고 등록된 핸들러들을 호출합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">업데이트된 데이터의 키</param>
|
||||
/// <param name="dataObject">업데이트된 데이터 객체</param>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 주로 내부적으로 AddData 메서드에서 호출되어
|
||||
/// 특정 키의 데이터가 변경되었을 때 등록된 핸들러들에게 알립니다.
|
||||
/// </remarks>
|
||||
private void NotifyDataUpdate(string key, IDataObject dataObject)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||
if (dataObject == null)
|
||||
throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다.");
|
||||
lock (syncLock)
|
||||
{
|
||||
if (dataUpdateHandlers.ContainsKey(key))
|
||||
{
|
||||
dataUpdateHandlers[key]?.Invoke(dataObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 저장소에 저장된 모든 데이터 객체의 키 목록을 반환합니다.
|
||||
/// </summary>
|
||||
/// <returns>저장소에 저장된 모든 데이터 객체의 키 목록</returns>
|
||||
public IEnumerable<string> GetAllKeys()
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
return new List<string>(dataObjects.Keys);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 저장된 모든 데이터 객체를 반환합니다.
|
||||
/// </summary>
|
||||
/// <returns>저장소의 모든 데이터 객체</returns>
|
||||
public IEnumerable<IDataObject> GetAllData()
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
return new List<IDataObject>(dataObjects.Values);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 키에 대한 데이터 객체가 저장소에 존재하는지 확인합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">확인할 키</param>
|
||||
/// <returns>키가 존재하면 true, 그렇지 않으면 false</returns>
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
return dataObjects.ContainsKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 저장소의 모든 데이터를 삭제합니다.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
dataObjects.Clear();
|
||||
dataUpdateHandlers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 키의 데이터 객체를 특정 타입으로 변환하여 반환합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">변환할 타입 (IDataObject를 구현해야 함)</typeparam>
|
||||
/// <param name="key">검색할 키</param>
|
||||
/// <returns>변환된 객체 또는 null</returns>
|
||||
public T GetDataAs<T>(string key) where T : class, IDataObject
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
if (dataObjects.ContainsKey(key) && dataObjects[key] is T typedObject)
|
||||
{
|
||||
return typedObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 조건을 만족하는 첫 번째 데이터 객체를 찾습니다.
|
||||
/// </summary>
|
||||
/// <param name="predicate">데이터 객체를 필터링할 조건</param>
|
||||
/// <returns>조건을 만족하는 첫 번째 데이터 객체와 해당 키, 없으면 null</returns>
|
||||
public (string Key, IDataObject Data) FindFirst(Func<IDataObject, bool> predicate)
|
||||
{
|
||||
if (predicate == null)
|
||||
throw new ArgumentNullException(nameof(predicate));
|
||||
|
||||
lock (syncLock)
|
||||
{
|
||||
foreach (var pair in dataObjects)
|
||||
{
|
||||
if (predicate(pair.Value))
|
||||
{
|
||||
return (pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 조건을 만족하는 모든 데이터 객체를 찾습니다.
|
||||
/// </summary>
|
||||
/// <param name="predicate">데이터 객체를 필터링할 조건</param>
|
||||
/// <returns>조건을 만족하는 모든 데이터 객체와 해당 키</returns>
|
||||
public IEnumerable<(string Key, IDataObject Data)> FindAll(Func<IDataObject, bool> predicate)
|
||||
{
|
||||
if (predicate == null)
|
||||
throw new ArgumentNullException(nameof(predicate));
|
||||
|
||||
lock (syncLock)
|
||||
{
|
||||
List<(string, IDataObject)> result = new List<(string, IDataObject)>();
|
||||
foreach (var pair in dataObjects)
|
||||
{
|
||||
if (predicate(pair.Value))
|
||||
{
|
||||
result.Add((pair.Key, pair.Value));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 저장소의 전체 내용을 JSON 문자열로 직렬화합니다.
|
||||
/// </summary>
|
||||
/// <returns>저장소 데이터의 JSON 표현</returns>
|
||||
public string ToJson()
|
||||
{
|
||||
JObject result = new JObject();
|
||||
|
||||
lock (syncLock)
|
||||
{
|
||||
foreach (var pair in dataObjects)
|
||||
{
|
||||
if (pair.Value is DataObject dataObject)
|
||||
{
|
||||
result[pair.Key] = JObject.FromObject(dataObject);
|
||||
}
|
||||
else if (pair.Value is DataArray dataArray)
|
||||
{
|
||||
result[pair.Key] = JArray.FromObject(dataArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 문자열에서 데이터를 불러와 저장소에 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="json">저장소 데이터의 JSON 표현</param>
|
||||
public void LoadFromJson(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
JObject data = JObject.Parse(json);
|
||||
|
||||
foreach (var property in data.Properties())
|
||||
{
|
||||
string key = property.Name;
|
||||
JToken value = property.Value;
|
||||
|
||||
if (value is JObject jObject)
|
||||
{
|
||||
DataObject dataObject = new DataObject(jObject);
|
||||
AddData(key, dataObject, false);
|
||||
}
|
||||
else if (value is JArray jArray)
|
||||
{
|
||||
DataArray dataArray = new DataArray(jArray);
|
||||
AddData(key, dataArray, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Debug.LogError($"JSON 파싱 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,114 @@
|
||||
#nullable enable
|
||||
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using UVC.Network;
|
||||
using UVC.Tests;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP 요청 파이프라인을 관리하는 클래스입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 HTTP 요청의 실행 및 반복 요청을 관리합니다.
|
||||
/// 등록된 요청(HttpPipeLineInfo)을 키 값으로 관리하며,
|
||||
/// 주기적 데이터 수집을 위한 반복 요청 기능을 제공합니다.
|
||||
///
|
||||
/// 주요 기능:
|
||||
/// - 단일 및 반복 HTTP 요청 관리
|
||||
/// - 요청 결과의 JSON 데이터를 IDataObject로 변환
|
||||
/// - 안전한 요청 취소 및 자원 정리
|
||||
/// - 테스트를 위한 목업 기능 지원
|
||||
///
|
||||
/// 모든 반복 실행은 CancellationTokenSource를 통해 취소할 수 있으며,
|
||||
/// 취소 후 현재 진행 중인 모든 요청이 안전하게 완료되는 것을 보장합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // HttpPipeLine 인스턴스 생성
|
||||
/// var httpPipeline = new HttpPipeLine();
|
||||
///
|
||||
/// // 데이터 매퍼 설정 (응답 데이터 변환용)
|
||||
/// var dataMask = new DataMask();
|
||||
/// dataMask["name"] = "이름";
|
||||
/// dataMask["value"] = 0;
|
||||
/// var dataMapper = new DataMapper(dataMask);
|
||||
///
|
||||
/// // 단일 요청 설정 및 등록
|
||||
/// var singleRequest = new HttpPipeLineInfo("https://api.example.com/data")
|
||||
/// .setDataMapper(dataMapper)
|
||||
/// .setHandler(data => {
|
||||
/// // 데이터 처리 로직
|
||||
/// Debug.Log($"데이터 수신: {data?.ToString() ?? "null"}");
|
||||
/// });
|
||||
/// httpPipeline.Add("dataRequest", singleRequest);
|
||||
///
|
||||
/// // 반복 요청 설정 및 등록
|
||||
/// var repeatingRequest = new HttpPipeLineInfo("https://api.example.com/status")
|
||||
/// .setDataMapper(dataMapper)
|
||||
/// .setHandler(data => {
|
||||
/// // 상태 데이터 처리
|
||||
/// Debug.Log($"상태 업데이트: {data?.ToString() ?? "null"}");
|
||||
/// })
|
||||
/// .setRepeat(true, 0, 5000); // 5초마다 무한 반복
|
||||
/// httpPipeline.Add("statusMonitor", repeatingRequest);
|
||||
///
|
||||
/// // 요청 실행
|
||||
/// await httpPipeline.Excute("dataRequest"); // 단일 실행
|
||||
/// await httpPipeline.Excute("statusMonitor"); // 반복 실행 시작
|
||||
///
|
||||
/// // 나중에 반복 요청 중지
|
||||
/// httpPipeline.StopRepeat("statusMonitor");
|
||||
///
|
||||
/// // 더 이상 필요없는 요청 제거
|
||||
/// httpPipeline.Remove("dataRequest");
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class HttpPipeLine
|
||||
{
|
||||
/// <summary>
|
||||
/// 테스트를 위한 목업 모드 활성화 여부를 설정하거나 가져옵니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// true로 설정하면 실제 HTTP 요청 대신 MockHttpRequester를 사용합니다.
|
||||
/// 테스트 환경에서 외부 의존성 없이 HTTP 요청을 시뮬레이션할 때 유용합니다.
|
||||
/// </remarks>
|
||||
public bool UseMockup { get; internal set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 등록된 HTTP 파이프라인 정보를 저장하는 사전
|
||||
/// </summary>
|
||||
private Dictionary<string, HttpPipeLineInfo> infoList = new Dictionary<string, HttpPipeLineInfo>();
|
||||
/// <summary>
|
||||
/// 실행 중인 반복 작업의 취소 토큰을 관리하는 사전
|
||||
/// </summary>
|
||||
private Dictionary<string, CancellationTokenSource> repeatTokenSources = new Dictionary<string, CancellationTokenSource>();
|
||||
|
||||
/// <summary>
|
||||
/// 진행 중인 요청의 상태를 추적하는 사전입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 키는 요청 식별자이고, 값은 현재 요청이 실행 중인지 여부를 나타냅니다.
|
||||
/// 이 상태 추적은 StopRepeat 메서드가 요청의 완전한 종료를 보장하기 위해 사용됩니다.
|
||||
/// </remarks>
|
||||
private Dictionary<string, bool> requestInProgress = new Dictionary<string, bool>();
|
||||
|
||||
/// <summary>
|
||||
/// 새로운 HTTP 요청 정보를 추가하거나 기존 정보를 업데이트합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">요청을 식별하는 키</param>
|
||||
/// <param name="info">HTTP 요청 정보</param>
|
||||
/// <remarks>
|
||||
/// 동일한 키가 이미 존재하는 경우 새로운 정보로 대체됩니다.
|
||||
/// </remarks>
|
||||
public void Add(string key, HttpPipeLineInfo info)
|
||||
{
|
||||
if (!infoList.ContainsKey(key))
|
||||
@@ -21,42 +121,448 @@ namespace UVC.Data
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
/// <summary>
|
||||
/// 지정한 키의 HTTP 요청 정보를 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">제거할 요청의 키</param>
|
||||
/// <remarks>
|
||||
/// 실행 중인 반복 작업이 있다면 함께 중지됩니다.
|
||||
/// </remarks>
|
||||
public async UniTask RemoveAsync(string key)
|
||||
{
|
||||
if (infoList.ContainsKey(key))
|
||||
{
|
||||
await StopRepeat(key);
|
||||
infoList.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public async void Excute(string key)
|
||||
/// <summary>
|
||||
/// 지정한 키의 HTTP 요청을 실행합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">실행할 요청의 키</param>
|
||||
/// <returns>비동기 작업</returns>
|
||||
/// <remarks>
|
||||
/// 요청 정보의 repeat 속성에 따라 단일 실행 또는 반복 실행을 시작합니다.
|
||||
/// 이미 실행 중인 반복 작업이 있다면 먼저 중지하고 완료를 대기한 후 새로운 요청을 시작합니다.
|
||||
/// 단일 실행의 경우 완료될 때까지 대기하지만, 반복 실행은 백그라운드에서 실행됩니다.
|
||||
/// </remarks>
|
||||
/// <exception cref="KeyNotFoundException">지정된 키가 등록되어 있지 않은 경우</exception>
|
||||
public async UniTask Excute(string key)
|
||||
{
|
||||
if (infoList.ContainsKey(key))
|
||||
{
|
||||
HttpPipeLineInfo info = infoList[key];
|
||||
|
||||
string result = await HttpRequester.Request<string>(info.url, info.method, info.body, info.headers);
|
||||
|
||||
IDataObject? dataObject = null;
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
// 반복 설정에 관계없이 이전에 실행 중인 반복 작업이 있다면 중지
|
||||
await StopRepeat(key);
|
||||
|
||||
if (!info.Repeat)
|
||||
{
|
||||
result = result.Trim();
|
||||
if (result.StartsWith("{"))
|
||||
// 단일 실행 로직 호출
|
||||
await ExecuteSingle(key, info);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 반복 설정이 있는 경우에만 StartRepeat 호출
|
||||
StartRepeat(key).Forget();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException($"No HTTP request found with key '{key}'.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 단일 HTTP 요청을 실행하고 결과를 처리합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">요청을 식별하는 키</param>
|
||||
/// <param name="info">HTTP 요청 정보</param>
|
||||
/// <param name="cancellationToken">요청 취소를 위한 취소 토큰</param>
|
||||
/// <returns>비동기 작업</returns>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 HTTP 요청을 보내고, 응답 데이터를 파싱하여 IDataObject로 변환합니다.
|
||||
/// JSON 객체 또는 배열 형식의 응답을 처리할 수 있으며, 취소 토큰을 통해 언제든지 작업을 취소할 수 있습니다.
|
||||
/// </remarks>
|
||||
/// <exception cref="OperationCanceledException">작업이 취소된 경우 발생</exception>
|
||||
/// <exception cref="JsonException">JSON 응답 파싱 중 오류가 발생한 경우</exception>
|
||||
/// <exception cref="Exception">HTTP 요청 중 다른 예외가 발생한 경우</exception>
|
||||
private async UniTask ExecuteSingle(string key, HttpPipeLineInfo info, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
int retryCount = 0;
|
||||
Exception lastException = null;
|
||||
|
||||
while (retryCount <= info.MaxRetryCount)
|
||||
{
|
||||
// 취소 요청 확인
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new OperationCanceledException("Operation cancelled", cancellationToken);
|
||||
}
|
||||
|
||||
lock (requestInProgress)
|
||||
{
|
||||
requestInProgress[key] = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string result = string.Empty;
|
||||
if (!UseMockup)
|
||||
{
|
||||
JObject source = JObject.Parse(result);
|
||||
if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source);
|
||||
result = await HttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
|
||||
}
|
||||
else if(result.StartsWith("["))
|
||||
else
|
||||
{
|
||||
JArray source = JArray.Parse(result);
|
||||
if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source);
|
||||
result = await MockHttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
|
||||
}
|
||||
|
||||
// 응답 처리 전에 다시 취소 요청 확인
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new OperationCanceledException("Operation cancelled", cancellationToken);
|
||||
}
|
||||
|
||||
IDataObject? dataObject = null;
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
result = result.Trim();
|
||||
try
|
||||
{
|
||||
if (result.StartsWith("{"))
|
||||
{
|
||||
if (info.DataMapper != null)
|
||||
{
|
||||
if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength)
|
||||
{
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result)))
|
||||
{
|
||||
dataObject = info.DataMapper.MapObjectStream(stream);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JObject source = JObject.Parse(result);
|
||||
dataObject = info.DataMapper.Mapping(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (result.StartsWith("["))
|
||||
{
|
||||
if (info.DataMapper != null)
|
||||
{
|
||||
if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength)
|
||||
{
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result)))
|
||||
{
|
||||
dataObject = info.DataMapper.MapArrayStream(stream);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JArray source = JArray.Parse(result);
|
||||
dataObject = info.DataMapper.Mapping(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Debug.LogError($"JSON parsing error in ExecuteSingle for {key}: {ex.Message}\nResponse: {result}");
|
||||
throw; // 상위에서 처리하도록 다시 throw
|
||||
}
|
||||
}
|
||||
|
||||
// 핸들러 호출 전에 다시 취소 요청 확인
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new OperationCanceledException("Operation cancelled", cancellationToken);
|
||||
}
|
||||
if (dataObject != null) dataObject = DataRepository.Instance.AddData(key, dataObject, info.UpdatedDataOnly);
|
||||
|
||||
// 갱신 된 데이터가 있는 경우 핸들러 호출
|
||||
if (info.UpdatedDataOnly)
|
||||
{
|
||||
if (dataObject != null && dataObject.UpdatedCount > 0) info.Handler?.Invoke(dataObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Handler?.Invoke(dataObject);
|
||||
}
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 취소 예외는 그대로 전파
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
retryCount++;
|
||||
|
||||
if (retryCount <= info.MaxRetryCount)
|
||||
{
|
||||
// 재시도 전에 취소 요청 확인
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new OperationCanceledException("Operation cancelled", cancellationToken);
|
||||
}
|
||||
Debug.LogWarning($"Request failed for '{key}', retry {retryCount}/{info.MaxRetryCount} after {info.RetryDelay}ms: {ex.Message}");
|
||||
await UniTask.Delay(info.RetryDelay);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (requestInProgress)
|
||||
{
|
||||
requestInProgress[key] = false;
|
||||
}
|
||||
}
|
||||
if(dataObject != null) dataObject = DataRepository.Instance.AddData(key, dataObject);
|
||||
info.handler?.Invoke(dataObject);
|
||||
}
|
||||
|
||||
|
||||
// 모든 재시도 후에도 실패
|
||||
Debug.LogError($"Request failed for '{key}' after {info.MaxRetryCount} retries: {lastException?.Message}");
|
||||
throw lastException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 반복 실행을 시작합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">반복 실행할 요청의 키</param>
|
||||
/// <returns>비동기 작업</returns>
|
||||
/// <remarks>
|
||||
/// 지정된 간격(repeatInterval)으로 HTTP 요청을 반복 실행합니다.
|
||||
/// repeatCount가 0인 경우 무한 반복하며, 0보다 큰 경우 지정된 횟수만큼만 실행합니다.
|
||||
/// 작업 실행 중 예외가 발생하면 로그를 기록하고 다음 실행을 시도합니다.
|
||||
/// 취소 요청이 있거나 최대 실행 횟수에 도달하면 반복이 종료됩니다.
|
||||
/// </remarks>
|
||||
/// <exception cref="KeyNotFoundException">지정된 키가 등록되어 있지 않은 경우</exception>
|
||||
private async UniTask StartRepeat(string key)
|
||||
{
|
||||
if (!infoList.ContainsKey(key))
|
||||
{
|
||||
throw new KeyNotFoundException($"No HTTP request found with key '{key}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
HttpPipeLineInfo info = infoList[key];
|
||||
if (!info.Repeat) return;
|
||||
|
||||
// 새 취소 토큰 생성
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
repeatTokenSources[key] = cts;
|
||||
|
||||
int executionCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 단일 실행 로직 호출
|
||||
await ExecuteSingle(key, info, cts.Token);
|
||||
|
||||
// 지정된 횟수만큼 반복한 경우 중지
|
||||
if (info.RepeatCount > 0)
|
||||
{
|
||||
executionCount++;
|
||||
if (executionCount >= info.RepeatCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 토큰이 취소되지 않은 경우에만 지연
|
||||
if (!cts.IsCancellationRequested)
|
||||
{
|
||||
// 지정된 간격만큼 대기
|
||||
await UniTask.Delay(info.RepeatInterval, cancellationToken: cts.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 취소된 경우 루프 종료
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 다른 예외 처리
|
||||
Debug.LogError($"Error in repeat execution for '{key}': {ex.Message}");
|
||||
if (cts.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await UniTask.Delay(info.RepeatInterval, cancellationToken: cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 취소된 경우 무시하고 루프 종료
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (repeatTokenSources) // 스레드 안전성 확보
|
||||
{
|
||||
if (repeatTokenSources.TryGetValue(key, out var currentCts) && currentCts == cts)
|
||||
{
|
||||
repeatTokenSources.Remove(key);
|
||||
}
|
||||
}
|
||||
cts.Dispose(); // 여기서 최종적으로 Dispose 호출
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 반복 실행 중인 요청을 중지합니다.
|
||||
/// </summary>
|
||||
/// <param name="key">중지할 요청의 키</param>
|
||||
/// <returns>요청 중지 작업을 나타내는 비동기 작업</returns>
|
||||
/// <remarks>
|
||||
/// 해당 키로 실행 중인 반복 작업이 없는 경우 아무 작업도 수행하지 않습니다.
|
||||
/// 요청이 중지되었더라도 현재 실행 중인 작업이 완전히 종료될 때까지 대기합니다.
|
||||
/// 이를 통해 작업 중단 후 자원이 안전하게 정리되는 것을 보장합니다.
|
||||
/// </remarks>
|
||||
public async UniTask StopRepeat(string key)
|
||||
{
|
||||
CancellationTokenSource cts = null;
|
||||
lock (repeatTokenSources) // 스레드 안전성 확보
|
||||
{
|
||||
if (repeatTokenSources.TryGetValue(key, out cts) && !cts.IsCancellationRequested)
|
||||
{
|
||||
cts.Cancel();
|
||||
repeatTokenSources.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 진행 중인 요청이 완료될 때까지 대기
|
||||
if (cts != null)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
lock (requestInProgress)
|
||||
{
|
||||
if (!requestInProgress.ContainsKey(key) || !requestInProgress[key])
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
await UniTask.Delay(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 반복 실행 중인 요청을 중지합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 애플리케이션 종료 시 또는 모든 반복 작업을 일괄 중지해야 할 때 사용합니다.
|
||||
/// 이 메서드는 비동기적으로 작동하지만 완료를 대기하지 않습니다.
|
||||
/// 모든 작업이 완전히 종료될 때까지 기다려야 하는 경우, 각 키에 대해 개별적으로 StopRepeat를 호출하고 대기해야 합니다.
|
||||
/// </remarks>
|
||||
public async UniTask StopAllRepeats()
|
||||
{
|
||||
foreach (var key in new List<string>(repeatTokenSources.Keys))
|
||||
{
|
||||
await StopRepeat(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성화된 요청 목록과 상태 정보를 반환합니다.
|
||||
/// </summary>
|
||||
/// <returns>키와 요청 상태 정보를 포함하는 딕셔너리</returns>
|
||||
/// <remarks>
|
||||
/// 반환되는 딕셔너리는 등록된 모든 HTTP 요청에 대한 상태 정보를 제공합니다.
|
||||
/// 각 요청에 대해 활성 상태, 반복 설정, 반복 횟수, 반복 간격을 확인할 수 있습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var httpPipeline = new HttpPipeLine();
|
||||
/// // 파이프라인에 요청 추가 후...
|
||||
///
|
||||
/// // 모든 활성 요청 확인
|
||||
/// var activeRequests = httpPipeline.GetActiveRequests();
|
||||
/// foreach (var request in activeRequests)
|
||||
/// {
|
||||
/// Debug.Log($"요청 키: {request.Key}, 활성 상태: {request.Value.IsActive}, " +
|
||||
/// $"반복 중: {request.Value.IsRepeating}, 반복 간격: {request.Value.RepeatInterval}ms");
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public Dictionary<string, HttpPipeLineRequestStatus> GetActiveRequests()
|
||||
{
|
||||
var result = new Dictionary<string, HttpPipeLineRequestStatus>();
|
||||
foreach (var key in infoList.Keys)
|
||||
{
|
||||
bool isRepeating = repeatTokenSources.ContainsKey(key);
|
||||
result[key] = new HttpPipeLineRequestStatus
|
||||
{
|
||||
IsActive = isRepeating,
|
||||
IsRepeating = isRepeating,
|
||||
RepeatCount = isRepeating ? infoList[key].RepeatCount : 0,
|
||||
RepeatInterval = isRepeating ? infoList[key].RepeatInterval : 0
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다.
|
||||
/// <see cref="Dispose"/>를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.</remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
// 모든 반복 작업 중지
|
||||
StopAllRepeats().Forget();
|
||||
// 요청 상태 초기화
|
||||
requestInProgress.Clear();
|
||||
// 등록된 요청 정보 초기화
|
||||
infoList.Clear();
|
||||
// 취소 토큰 소스 정리
|
||||
foreach (var cts in repeatTokenSources.Values)
|
||||
{
|
||||
cts.Dispose();
|
||||
}
|
||||
repeatTokenSources.Clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청의 현재 상태 정보를 나타내는 클래스입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 HTTP 파이프라인에 등록된 요청의 활성 상태, 반복 설정,
|
||||
/// 반복 횟수, 반복 간격에 관한 정보를 제공합니다.
|
||||
/// </remarks>
|
||||
public class HttpPipeLineRequestStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 요청이 현재 활성 상태인지 여부를 나타냅니다.
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
/// <summary>
|
||||
/// 요청이 반복 실행 중인지 여부를 나타냅니다.
|
||||
/// </summary>
|
||||
public bool IsRepeating { get; set; }
|
||||
/// <summary>
|
||||
/// 반복 설정된 횟수를 나타냅니다. 0은 무한 반복을 의미합니다.
|
||||
/// </summary>
|
||||
public int RepeatCount { get; set; }
|
||||
/// <summary>
|
||||
/// 반복 요청 간의 간격을 밀리초 단위로 나타냅니다.
|
||||
/// </summary>
|
||||
public int RepeatInterval { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,47 +5,185 @@ using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP 요청 파이프라인 정보를 관리하는 클래스입니다.
|
||||
/// 요청 URL, 메서드, 헤더, 본문과 같은 HTTP 요청 정보 및
|
||||
/// 반복 실행, 재시도, 데이터 매핑 등 파이프라인 동작을 구성합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 파이프라인 빌더 패턴을 사용하여 HTTP 요청 설정을 구성할 수 있도록 합니다.
|
||||
/// 각 설정 메서드는 체이닝을 통해 유연하게 요청을 정의할 수 있습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/data", "GET")
|
||||
/// .setDataMapper(new DataMapper(dataMask))
|
||||
/// .setHandler(data => Console.WriteLine(data))
|
||||
/// .setRetry(5, 2000)
|
||||
/// .setRepeat(true, 10, 5000);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class HttpPipeLineInfo
|
||||
{
|
||||
public string url;
|
||||
public string method;
|
||||
public Dictionary<string, string>? headers = null;
|
||||
public string? body = null;
|
||||
public Action<IDataObject?>? handler = null;
|
||||
public bool repeat = false; // 반복 실행 여부
|
||||
public int repeatCount = 0; // 반복 횟수, 0은 무한 반복
|
||||
public int repeatInterval = 1000; // 반복 간격 (ms)
|
||||
private string _url;
|
||||
private string _method;
|
||||
private Dictionary<string, string>? _headers = null;
|
||||
private string? _body = null;
|
||||
private Action<IDataObject?>? _handler = null;
|
||||
private bool _repeat = false; // 반복 실행 여부
|
||||
private int _repeatCount = 0; // 반복 횟수, 0은 무한 반복
|
||||
private int _repeatInterval = 1000; // 반복 간격 (ms)
|
||||
private DataMapper? _dataMapper = null; // 데이터 매퍼
|
||||
private int _maxRetryCount = 3;
|
||||
private int _retryDelay = 1000; // 밀리초
|
||||
private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부
|
||||
|
||||
public DataMapper? dataMapper = null; // 데이터 매퍼
|
||||
/// <summary>
|
||||
/// 요청을 보낼 URL 주소
|
||||
/// </summary>
|
||||
public string Url => _url;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청 메서드 (GET, POST 등)
|
||||
/// </summary>
|
||||
public string Method => _method;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청 헤더
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? Headers => _headers;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청 본문
|
||||
/// </summary>
|
||||
public string? Body => _body;
|
||||
|
||||
/// <summary>
|
||||
/// 요청 완료 후 호출될 핸들러
|
||||
/// </summary>
|
||||
public Action<IDataObject?>? Handler => _handler;
|
||||
|
||||
/// <summary>
|
||||
/// 반복 실행 여부
|
||||
/// </summary>
|
||||
public bool Repeat => _repeat;
|
||||
|
||||
/// <summary>
|
||||
/// 반복 횟수, 0은 무한 반복
|
||||
/// </summary>
|
||||
public int RepeatCount => _repeatCount;
|
||||
|
||||
/// <summary>
|
||||
/// 반복 간격 (ms)
|
||||
/// </summary>
|
||||
public int RepeatInterval => _repeatInterval;
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 매퍼 객체
|
||||
/// </summary>
|
||||
public DataMapper? DataMapper => _dataMapper;
|
||||
|
||||
/// <summary>
|
||||
/// 최대 재시도 횟수
|
||||
/// </summary>
|
||||
public int MaxRetryCount => _maxRetryCount;
|
||||
|
||||
/// <summary>
|
||||
/// 재시도 간격 (ms)
|
||||
/// </summary>
|
||||
public int RetryDelay => _retryDelay;
|
||||
|
||||
/// <summary>
|
||||
/// 업데이트된 데이터만 받을 여부 (true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다)
|
||||
/// </summary>
|
||||
public bool UpdatedDataOnly => _updatedDataOnly;
|
||||
|
||||
/// <summary>
|
||||
/// HttpPipeLineInfo 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="url">HTTP 요청을 보낼 URL</param>
|
||||
/// <param name="method">HTTP 요청 메서드 (기본값: "post")</param>
|
||||
/// <param name="headers">HTTP 요청 헤더 (선택사항)</param>
|
||||
/// <param name="body">HTTP 요청 본문 (선택사항)</param>
|
||||
public HttpPipeLineInfo(string url, string method = "post", Dictionary<string, string>? headers = null, string? body = null)
|
||||
{
|
||||
this.url = url;
|
||||
this.method = method;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
_url = url;
|
||||
_method = method;
|
||||
_headers = headers;
|
||||
_body = body;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청 응답을 처리할 데이터 매퍼를 설정합니다.
|
||||
/// 데이터 매퍼는 HTTP 응답을 IDataObject로 변환하는 역할을 합니다.
|
||||
/// </summary>
|
||||
/// <param name="dataMapper">사용할 데이터 매퍼 객체</param>
|
||||
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
public HttpPipeLineInfo setDataMapper(DataMapper dataMapper)
|
||||
{
|
||||
this.dataMapper = dataMapper;
|
||||
_dataMapper = dataMapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpPipeLineInfo setHandler(Action<IDataObject> handler)
|
||||
/// <summary>
|
||||
/// HTTP 요청이 완료된 후 호출될 핸들러를 설정합니다.
|
||||
/// 변경 된 데이터는 IDataObject로 전달됩니다.
|
||||
/// 변경 된 데이터가 없으면 호출 되지 않습니다.
|
||||
/// </summary>
|
||||
/// <param name="handler">응답 데이터를 처리할 콜백 함수</param>
|
||||
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
public HttpPipeLineInfo setHandler(Action<IDataObject?>? handler)
|
||||
{
|
||||
this.handler = handler;
|
||||
_handler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpPipeLineInfo setRepeat(bool repeat, int count = 0, int interval = 1000)
|
||||
/// <summary>
|
||||
/// HTTP 요청 실패 시 재시도 정책을 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="maxRetryCount">최대 재시도 횟수 (기본값: 3)</param>
|
||||
/// <param name="retryDelay">재시도 간 대기 시간(밀리초) (기본값: 1000)</param>
|
||||
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
public HttpPipeLineInfo setRetry(int maxRetryCount = 3, int retryDelay = 1000)
|
||||
{
|
||||
this.repeat = repeat;
|
||||
this.repeatCount = count;
|
||||
this.repeatInterval = interval;
|
||||
_maxRetryCount = maxRetryCount;
|
||||
_retryDelay = retryDelay;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청의 반복 실행 설정을 구성합니다.
|
||||
/// </summary>
|
||||
/// <param name="repeat">반복 실행 여부</param>
|
||||
/// <param name="count">반복 횟수 (0은 무한 반복) (기본값: 0)</param>
|
||||
/// <param name="interval">반복 실행 간 대기 시간(밀리초) (기본값: 1000)</param>
|
||||
/// <param name="updatedDataOnly">변경된 데이터만 처리할지 여부 (기본값: true)</param>
|
||||
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
/// <remarks>
|
||||
/// 반복 요청 시 updatedDataOnly가 true인 경우, 서버 응답에서 데이터가 변경된 경우에만 핸들러가 호출됩니다.
|
||||
/// 이는 불필요한 데이터 처리를 방지하고 성능을 향상시키는 데 도움이 됩니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 5초마다 10번 반복 요청, 변경된 데이터만 처리
|
||||
/// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/data", "GET")
|
||||
/// .setHandler(data => ProcessData(data))
|
||||
/// .setRepeat(true, 10, 5000, true);
|
||||
///
|
||||
/// // 3초마다 무한 반복, 모든 응답 데이터 처리
|
||||
/// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/status", "GET")
|
||||
/// .setHandler(data => UpdateStatus(data))
|
||||
/// .setRepeat(true, 0, 3000, false);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public HttpPipeLineInfo setRepeat(bool repeat, int count = 0, int interval = 1000, bool updatedDataOnly = true)
|
||||
{
|
||||
_repeat = repeat;
|
||||
_repeatCount = count;
|
||||
_repeatInterval = interval;
|
||||
_updatedDataOnly = updatedDataOnly; // true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다.
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,31 @@
|
||||
/// </remarks>
|
||||
public interface IDataObject
|
||||
{
|
||||
/// <summary>
|
||||
/// 모든 프로퍼티를 변경된 것으로 표시합니다.
|
||||
/// 전체 데이터가 갱신되었을 때 사용합니다.
|
||||
/// </summary>
|
||||
public void InitData();
|
||||
|
||||
/// <summary>
|
||||
/// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다.
|
||||
/// 변경된 키는 자동으로 추적됩니다.
|
||||
/// </summary>
|
||||
/// <param name="other">비교할 DataObject</param>
|
||||
public void UpdateDifferent(IDataObject other);
|
||||
|
||||
/// <summary>
|
||||
/// 업데이트된 속성만 포함하는 새로운 DataObject를 반환합니다.
|
||||
/// </summary>
|
||||
/// <returns>업데이트 된 항목만 가지고 있는 DataObject</returns>
|
||||
public IDataObject GetUpdatedObject();
|
||||
|
||||
/// <summary>
|
||||
/// /// <summary>
|
||||
/// 업데이트 된 속성의 수.
|
||||
/// </summary>
|
||||
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
|
||||
public int UpdatedCount { get; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,30 +3,76 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SampleProject.Config;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.InputSystem;
|
||||
using UVC.network;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// MQTT 통신을 통해 데이터를 수신하고 처리하는 파이프라인을 관리하는 클래스입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 MQTT 브로커에 연결하여 등록된 토픽으로 들어오는 메시지를 수신하고,
|
||||
/// 해당 메시지를 지정된 데이터 매퍼를 통해 변환한 후 핸들러에게 전달합니다.
|
||||
/// 여러 MQTT 토픽을 동시에 관리하고 각각에 대한 처리 방식을 개별적으로 설정할 수 있습니다.
|
||||
/// </remarks>
|
||||
public class MQTTPipeLine
|
||||
{
|
||||
/// <summary>
|
||||
/// MQTT 브로커의 도메인 주소
|
||||
/// </summary>
|
||||
private string domain;
|
||||
|
||||
private Dictionary<string, MQTTPipeLineInfo> infoList = new Dictionary<string, MQTTPipeLineInfo>();
|
||||
/// <summary>
|
||||
/// MQTT 브로커의 포트 번호
|
||||
/// </summary>
|
||||
public int port;
|
||||
|
||||
private MQTTService mqtt = new MQTTService(Constants.MQTT_DOMAIN, Constants.MQTT_PORT);
|
||||
/// <summary>
|
||||
/// 토픽별 파이프라인 정보를 저장하는 딕셔너리
|
||||
/// </summary>
|
||||
private Dictionary<string, MQTTPipeLineInfo> infoList;
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 통신을 처리하는 서비스 객체
|
||||
/// </summary>
|
||||
private MQTTService mqtt;
|
||||
|
||||
/// <summary>
|
||||
/// MQTTPipeLine 인스턴스를 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="domain">MQTT 브로커의 도메인 주소, 기본값은 "localhost"입니다.</param>
|
||||
/// <param name="port">MQTT 브로커의 포트 번호, 기본값은 1883입니다.</param>
|
||||
public MQTTPipeLine(string domain = "localhost", int port = 1883)
|
||||
{
|
||||
this.domain = string.IsNullOrEmpty(domain) ? Constants.MQTT_DOMAIN : domain;
|
||||
this.port = port;
|
||||
mqtt = new MQTTService(Constants.MQTT_DOMAIN, Constants.MQTT_PORT);
|
||||
infoList = new Dictionary<string, MQTTPipeLineInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 토픽에 대한 파이프라인 정보를 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="info">추가할 MQTTPipeLineInfo 객체</param>
|
||||
/// <remarks>
|
||||
/// 동일한 토픽에 대한 정보가 이미 존재하는 경우 덮어씁니다.
|
||||
/// </remarks>
|
||||
public void Add(MQTTPipeLineInfo info)
|
||||
{
|
||||
if (!infoList.ContainsKey(info.topic))
|
||||
if (!infoList.ContainsKey(info.Topic))
|
||||
{
|
||||
infoList.Add(info.topic, info);
|
||||
infoList.Add(info.Topic, info);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoList[info.topic] = info;
|
||||
infoList[info.Topic] = info;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 토픽에 대한 파이프라인 정보를 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="topic">제거할 토픽 이름</param>
|
||||
public void Remove(string topic)
|
||||
{
|
||||
if (infoList.ContainsKey(topic))
|
||||
@@ -35,6 +81,9 @@ namespace UVC.Data
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파이프라인을 실행하여 MQTT 브로커에 연결하고 등록된 모든 토픽을 구독합니다.
|
||||
/// </summary>
|
||||
public void Execute()
|
||||
{
|
||||
foreach (var topic in infoList.Keys)
|
||||
@@ -44,6 +93,16 @@ namespace UVC.Data
|
||||
mqtt.Connect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 토픽으로 메시지가 수신되었을 때 호출되는 콜백 메서드입니다.
|
||||
/// </summary>
|
||||
/// <param name="topic">수신된 메시지의 토픽</param>
|
||||
/// <param name="message">수신된 메시지 내용</param>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 수신된 메시지의 형식(JSON 객체 또는 배열)에 따라 적절한 파싱을 수행하고,
|
||||
/// 등록된 데이터 매퍼를 통해 메시지를 변환한 후, 해당 토픽에 등록된 핸들러에게 전달합니다.
|
||||
/// 'UpdatedDataOnly' 설정에 따라 데이터가 변경된 경우에만 핸들러를 호출할 수도 있습니다.
|
||||
/// </remarks>
|
||||
private void OnTopicMessage(string topic, string message)
|
||||
{
|
||||
if (infoList.ContainsKey(topic))
|
||||
@@ -56,19 +115,49 @@ namespace UVC.Data
|
||||
if (message.StartsWith("{"))
|
||||
{
|
||||
JObject source = JObject.Parse(message);
|
||||
if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source);
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Mapping(source);
|
||||
}
|
||||
else if (message.StartsWith("["))
|
||||
{
|
||||
JArray source = JArray.Parse(message);
|
||||
if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source);
|
||||
if (info.DataMapper != null) dataObject = info.DataMapper.Mapping(source);
|
||||
}
|
||||
}
|
||||
if (dataObject != null) dataObject = DataRepository.Instance.AddData(topic, dataObject);
|
||||
|
||||
info.handler?.Invoke(dataObject);
|
||||
if (dataObject != null) dataObject = DataRepository.Instance.AddData(topic, dataObject, info.UpdatedDataOnly);
|
||||
// 갱신 된 데이터가 있는 경우 핸들러 호출
|
||||
if (info.UpdatedDataOnly)
|
||||
{
|
||||
if (dataObject != null && dataObject.UpdatedCount > 0) info.Handler?.Invoke(dataObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Handler?.Invoke(dataObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파이프라인을 중지하고 모든 토픽 구독을 해제한 후 MQTT 브로커와의 연결을 종료합니다.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
foreach (var topic in infoList.Keys)
|
||||
{
|
||||
mqtt.RemoveTopicHandler(topic, OnTopicMessage);
|
||||
}
|
||||
mqtt.Disconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다.
|
||||
/// <see cref="Dispose"/>를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.</remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
mqtt.Disconnect();
|
||||
infoList.Clear();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,103 @@ using System;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// MQTT 파이프라인 정보를 관리하는 클래스입니다.
|
||||
/// MQTT 토픽 구독, 메시지 핸들링 및 데이터 매핑에 필요한 설정을 제공합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 빌더 패턴을 사용하여 MQTT 구독 설정을 유연하게 구성할 수 있게 합니다.
|
||||
/// 각 설정 메서드는 체이닝을 통해 파이프라인을 정의할 수 있도록 합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var pipelineInfo = new MQTTPipeLineInfo("device/status")
|
||||
/// .setDataMapper(new DataMapper(dataMask))
|
||||
/// .setHandler(data => Console.WriteLine(data));
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class MQTTPipeLineInfo
|
||||
{
|
||||
public string topic; // MQTT 토픽
|
||||
public Action<IDataObject?>? handler = null; // 메시지 핸들러
|
||||
public DataMapper? dataMapper = null; // 데이터 매퍼
|
||||
private string _topic; // MQTT 토픽
|
||||
private Action<IDataObject?>? _handler = null; // 메시지 핸들러
|
||||
private DataMapper? _dataMapper = null; // 데이터 매퍼
|
||||
private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부
|
||||
|
||||
public MQTTPipeLineInfo(string topic)
|
||||
/// <summary>
|
||||
/// MQTT 토픽
|
||||
/// </summary>
|
||||
public string Topic => _topic;
|
||||
|
||||
/// <summary>
|
||||
/// 메시지 핸들러
|
||||
/// </summary>
|
||||
public Action<IDataObject?>? Handler => _handler;
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 매퍼
|
||||
/// </summary>
|
||||
public DataMapper? DataMapper => _dataMapper;
|
||||
|
||||
/// <summary>
|
||||
/// 업데이트된 데이터만 받을 여부 (true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다)
|
||||
/// </summary>
|
||||
public bool UpdatedDataOnly => _updatedDataOnly;
|
||||
|
||||
/// <summary>
|
||||
/// MQTTPipeLineInfo 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="topic">구독할 MQTT 토픽</param>
|
||||
/// <param name="updatedDataOnly">변경된 데이터만 처리할지 여부 (기본값: true)</param>
|
||||
/// <remarks>
|
||||
/// updatedDataOnly가 true인 경우, 이전 데이터와 동일한 메시지는 핸들러에 전달되지 않습니다.
|
||||
/// 이는 불필요한 데이터 처리를 방지하고 성능을 향상시킵니다.
|
||||
/// </remarks>
|
||||
public MQTTPipeLineInfo(string topic, bool updatedDataOnly = true)
|
||||
{
|
||||
this.topic = topic;
|
||||
_topic = topic;
|
||||
_updatedDataOnly = updatedDataOnly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 메시지를 수신했을 때 호출될 핸들러를 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="handler">메시지 데이터를 처리할 콜백 함수</param>
|
||||
/// <returns>현재 MQTTPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
/// <remarks>
|
||||
/// 핸들러는 메시지가 수신되고 DataMapper에 의해 변환된 후 호출됩니다.
|
||||
/// UpdatedDataOnly 속성이 true인 경우, 데이터가 변경된 경우에만 호출됩니다.
|
||||
/// </remarks>
|
||||
public MQTTPipeLineInfo setHandler(Action<IDataObject?> handler)
|
||||
{
|
||||
this.handler = handler;
|
||||
_handler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT 메시지 데이터를 처리할 데이터 매퍼를 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="dataMapper">사용할 데이터 매퍼 객체</param>
|
||||
/// <returns>현재 MQTTPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||
/// <remarks>
|
||||
/// 데이터 매퍼는 수신된 MQTT 메시지(JSON 형식)를 IDataObject로 변환하는 역할을 합니다.
|
||||
/// DataMask를 사용하여 특정 필드에 대한 타입 변환 및 필드 이름 매핑을 처리할 수 있습니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // DataMask를 사용한 데이터 매퍼 설정
|
||||
/// var dataMask = new DataMask();
|
||||
/// dataMask["temperature"] = 0.0; // 실수 타입 지정
|
||||
/// dataMask["humidity"] = 0; // 정수 타입 지정
|
||||
/// dataMask["timestamp"] = ""; // 문자열 타입 지정
|
||||
///
|
||||
/// var pipelineInfo = new MQTTPipeLineInfo("sensor/data")
|
||||
/// .setDataMapper(new DataMapper(dataMask))
|
||||
/// .setHandler(data => ProcessSensorData(data));
|
||||
/// </code>
|
||||
/// </example>
|
||||
public MQTTPipeLineInfo setDataMapper(DataMapper dataMapper)
|
||||
{
|
||||
this.dataMapper = dataMapper;
|
||||
_dataMapper = dataMapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -363,5 +363,17 @@ namespace UVC.network
|
||||
.WithPayload(message)
|
||||
.BeginPublish();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다.
|
||||
/// <see cref="Dispose"/>를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.</remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
topicHandler.Clear();
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
154
Assets/Scripts/UVC/Tests/MockHttpRequester.cs
Normal file
154
Assets/Scripts/UVC/Tests/MockHttpRequester.cs
Normal file
File diff suppressed because one or more lines are too long
2
Assets/Scripts/UVC/Tests/MockHttpRequester.cs.meta
Normal file
2
Assets/Scripts/UVC/Tests/MockHttpRequester.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0826ba2b5da8fad48aff9e3542115630
|
||||
Reference in New Issue
Block a user