Files
XRLib/Assets/Scripts/UVC/Data/DataRepository.cs
2025-07-24 18:28:09 +09:00

429 lines
17 KiB
C#

#nullable enable
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Data.Mqtt;
using UVC.Log;
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()
{
// Best MQTT 초기화 작업을 Main 스레드에서 호출 해야 한다.
Best.HTTP.Shared.HTTPManager.Setup();
}
/// <summary>
/// DataRepository의 단일 인스턴스에 대한 접근자입니다.
/// </summary>
public static DataRepository Instance { get { return instance.Value; } }
#endregion
/// <summary>
/// 키로 식별되는 데이터 객체를 저장하는 컬렉션입니다.
/// </summary>
private Dictionary<string, IDataObject> dataObjects = new Dictionary<string, IDataObject>();
/// <summary>
/// 스레드 동기화를 위한 잠금 객체입니다.
/// </summary>
private readonly object syncLock = new object();
/// <summary>
/// 데이터 업데이트 시 호출될 핸들러 함수들을 저장하는 딕셔너리입니다.
/// 각 키에 연결된 데이터가 업데이트될 때 해당 핸들러가 호출됩니다.
/// </summary>
private Dictionary<string, Action<IDataObject>> dataUpdateHandlers = new Dictionary<string, Action<IDataObject>>();
private HttpDataFetcher httpFetcher = new HttpDataFetcher();
public HttpDataFetcher HttpFetcher => httpFetcher;
private MqttDataReceiver mqttReceiver = new MqttDataReceiver();
public MqttDataReceiver MqttReceiver => mqttReceiver;
/// <summary>
/// 저장소에 데이터 객체를 추가하거나 기존 객체를 업데이트합니다.
/// </summary>
/// <param name="key">데이터 객체를 식별하는 고유 키</param>
/// <param name="dataObject">저장할 데이터 객체</param>
/// <param name="updatedDataOnly">true인 경우 업데이트된 속성만 반환, false인 경우 전체 객체 반환</param>
/// <returns>새로 추가된 객체 또는 업데이트된 기존 객체</returns>
internal IDataObject AddOrUpdateData(string key, IDataObject dataObject, bool updatedDataOnly = true)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
if (dataObject == null)
throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다.");
lock (syncLock)
{
if (!dataObjects.ContainsKey(key))
{
dataObject.MarkAllAsUpdated();
var newData = dataObject.Clone(fromPool: false);
dataObjects.Add(key, newData);
NotifyDataUpdate(key, newData);
return dataObject;
}
else
{
IDataObject obj = dataObjects[key];
obj.UpdateDifferent(dataObject);
IDataObject newDataObject;
if (updatedDataOnly)
{
newDataObject = obj.GetUpdatedObject(fromPool: false);
}
else
{
newDataObject = dataObject;
}
bool shouldInvoke = !updatedDataOnly || newDataObject.UpdatedCount > 0;
if(shouldInvoke) NotifyDataUpdate(key, newDataObject);
return newDataObject;
}
}
}
/// <summary>
/// 지정된 키에 해당하는 데이터 객체를 저장소에서 제거합니다.
/// </summary>
/// <param name="key">제거할 데이터 객체의 키</param>
public void RemoveData(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
lock (syncLock)
{
if (dataObjects.ContainsKey(key))
{
dataObjects.Remove(key);
}
}
}
/// <summary>
/// 지정된 키에 해당하는 데이터 객체를 저장소에서 검색합니다.
/// </summary>
/// <param name="key">검색할 데이터 객체의 키</param>
/// <returns>키가 존재하면 해당 데이터 객체, 존재하지 않으면 null</returns>
public IDataObject? GetData(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
lock (syncLock)
{
if (dataObjects.ContainsKey(key))
{
return dataObjects[key];
}
return null;
}
}
/// <summary>
/// 지정된 키에 대한 데이터 업데이트 핸들러를 추가합니다.
/// </summary>
/// <param name="key">데이터 업데이트를 감시할 키</param>
/// <param name="handler">데이터가 업데이트되었을 때 호출될 콜백 함수</param>
/// <remarks>
/// 같은 키에 대해 여러 핸들러를 등록할 수 있습니다.
/// 핸들러는 해당 키의 데이터가 AddData를 통해 업데이트될 때마다 호출됩니다.
/// 전달 되는 데이터 객체는 업데이트된 상태고 DataRepository에 저장된 객체이기에 pool.ReturnToPool()을 호출하면 않됩니다.
/// </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>
/// 이 메서드는 주로 내부적으로 AddOrUpdateData 메서드에서 호출되어
/// 특정 키의 데이터가 변경되었을 때 등록된 핸들러들에게 알립니다.
/// </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일 수 없습니다.");
//Debug.Log($"NotifyDataUpdate: {key}, {dataObject.GetType().Name}");
lock (syncLock)
{
if (dataUpdateHandlers.ContainsKey(key))
{
UniTask.Post(() => 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 = DataObjectPool.Get();
dataObject.FromJObject(jObject);
AddOrUpdateData(key, dataObject, false);
}
else if (value is JArray jArray)
{
DataArray dataArray = DataArrayPool.Get().FromJArray(jArray);
AddOrUpdateData(key, dataArray, false);
}
}
}
catch (JsonException ex)
{
ULog.Error($"JSON 파싱 오류", ex);
}
}
}
}