httppipeline 완료, mqttPipeline 개발 중. test 코드 작성 필요

This commit is contained in:
logonkhi
2025-06-09 19:29:59 +09:00
parent 4b490d79f4
commit ac3645577a
14 changed files with 2347 additions and 264 deletions

View File

@@ -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>
/// 항목을 추가합니다.

View File

@@ -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>

View File

@@ -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&lt;string, string&gt;
/// {
/// { "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&lt;string, string&gt;
/// {
/// { "first_name", "firstName" },
/// { "last_name", "lastName" },
/// { "birth_date", "birthDate" }
/// };
/// </code>
/// </example>
public Dictionary<string, string>? NamesForReplace { get; set; }
}

View File

@@ -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&lt;string, string&gt;
/// {
/// { "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}"));

View File

@@ -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}");
}
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0826ba2b5da8fad48aff9e3542115630