#nullable enable using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using UVC.Log; namespace UVC.Data { /// /// 키-값 쌍의 데이터를 관리하고 변경 사항을 추적하는 동적 데이터 객체입니다. /// SortedDictionary를 상속하여 키를 기준으로 정렬된 데이터를 제공합니다. /// /// /// 이 클래스는 JSON과 호환되는 데이터 구조를 표현하며, 데이터 변경 추적 기능을 통해 /// 효율적인 데이터 동기화를 지원합니다. /// /// /// /// // DataMask 생성 및 설정 /// var mask = new DataMask(); /// mask.ObjectIdKey = "id"; /// mask.ObjectName = "users"; /// /// // 필드 이름 변환 설정 /// mask.NamesForReplace = new Dictionary /// { /// { "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는 제외됨 /// /// public class DataObject : SortedDictionary, IDataObject { /// /// 객체의 고유 식별자를 나타내는 속성입니다. DataArray에서 데이터를 식별하는 데 사용됩니다. /// public string Id { get => (IdKey != null && ContainsKey(IdKey)) ? this[IdKey].ToString() : this.First().Value.ToString(); } /// /// Id에 해당하는 key 문자열 /// internal string? IdKey { get; set; } = null; /// /// DataObject의 이름을 나타내는 속성입니다. /// public string Name { get; internal set; } = string.Empty; /// /// 직접적인 변경이 있었던 키를 저장하는 리스트입니다. /// protected List changedProperies = new List(); /// /// 변경된 속성의 키 목록을 읽기 전용으로 반환합니다. /// public ReadOnlyCollection ChangedProperies => changedProperies.AsReadOnly(); // 기존에 있던 속성 키를 추적하기 위한 집합 private HashSet existingProperties = new HashSet(); /// /// 기본 생성자입니다. 빈 데이터 객체를 생성합니다. /// public DataObject() { } /// /// JObject로부터 데이터 객체를 생성합니다. /// /// 복사할 JObject 객체 public DataObject(JObject other) { // JObject로부터 속성을 복사 foreach (var prop in other.Properties()) { // JToken을 object로 변환 this[prop.Name] = ConvertJTokenToObject(prop.Value); existingProperties.Add(prop.Name); } } /// /// Dictionary로 데이터 객체를 초기화합니다. /// /// 초기화에 사용할 Dictionary 객체 public DataObject(Dictionary dictionary) : base(dictionary) { // 생성자에서 초기 속성들을 기존 속성으로 등록 foreach (var key in dictionary.Keys) { existingProperties.Add(key); } } /// /// JToken을 적절한 C# 객체 타입으로 변환하는 헬퍼 메서드입니다. /// /// 변환할 JToken 객체 /// 변환된 C# 객체 private object ConvertJTokenToObject(JToken token) { switch (token.Type) { case JTokenType.String: return token.ToString(); case JTokenType.Integer: return token.ToObject(); case JTokenType.Float: return token.ToObject(); case JTokenType.Boolean: return token.ToObject(); case JTokenType.Object: return new DataObject((JObject)token); case JTokenType.Array: JArray array = (JArray)token; return new DataArray(array); default: return token.ToString(); } } /// /// 모든 프로퍼티를 변경된 것으로 표시합니다. /// 전체 데이터가 갱신되었을 때 사용합니다. /// public void InitData() { changedProperies.Clear(); changedProperies.AddRange(this.Keys); } /// /// 속성이 변경될 때 호출되는 메서드입니다. /// 파생 클래스에서 오버라이드하여 추가 동작을 정의할 수 있습니다. /// /// 변경된 속성의 이름 protected virtual void OnPropertyChanged(string propertyName) { // 기존에 존재하던 속성인 경우에만 변경된 것으로 추적 if (existingProperties.Contains(propertyName)) { if (!changedProperies.Contains(propertyName)) { changedProperies.Add(propertyName); } } else { // 새로 추가된 속성은 기존 속성 목록에 추가 existingProperties.Add(propertyName); } } /// /// 새 속성을 추가하고 변경 사항을 추적합니다. /// /// 추가할 속성의 이름 /// 속성의 값 public new void Add(string propertyName, object value) { // 추가하기 전에 확인 - 속성이 이미 있는지 확인 bool isExisting = ContainsKey(propertyName); // 기본 구현 호출 base.Add(propertyName, value); // 새로 추가된 속성이면 기존 속성 목록에 추가 if (!isExisting) { existingProperties.Add(propertyName); } } /// /// 인덱서를 통해 속성값에 접근하고 설정합니다. /// 속성값이 변경될 때마다 변경 사항을 자동으로 추적합니다. /// /// 접근할 속성의 키 /// 속성값 public new object this[string key] { get => base[key]; set { // 속성 설정 전에 기존에 있는 속성인지 확인 bool isExisting = ContainsKey(key) || existingProperties.Contains(key); // 기본 구현 호출 base[key] = value; // 기존 속성이었다면 변경된 것으로 처리 if (isExisting) { if (!changedProperies.Contains(key)) { changedProperies.Add(key); } } else { // 새로 추가된 속성은 기존 속성으로 등록 existingProperties.Add(key); } OnPropertyChanged(key); } } /// /// 지정된 속성의 값을 정수(int)로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 정수 값 또는 기본값 public int GetInt(string propertyName, int defaultValue = 0) { if (TryGetValue(propertyName, out object value) && value != null) { if (value is int intValue) return intValue; return Convert.ToInt32(value); } return defaultValue; } /// /// 지정된 속성의 값을 문자열(string)로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 null인 경우 반환할 기본값 /// 변환된 문자열 값 또는 기본값 public string? GetString(string propertyName, string? defaultValue = null) { if (TryGetValue(propertyName, out object value) && value != null) { return value.ToString(); } return defaultValue; } /// /// 지정된 속성의 값을 불리언(bool)으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 불리언 값 또는 기본값 public bool GetBool(string propertyName, bool defaultValue = false) { if (TryGetValue(propertyName, out object value) && value != null) { if (value is bool boolValue) return boolValue; return Convert.ToBoolean(value); } return defaultValue; } /// /// 지정된 속성의 값을 부동 소수점(float)으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 부동 소수점 값 또는 기본값 public float GetFloat(string propertyName, float defaultValue = 0f) { if (TryGetValue(propertyName, out object value) && value != null) { if (value is float floatValue) return floatValue; return Convert.ToSingle(value); } return defaultValue; } /// /// 지정된 속성의 값을 더블 정밀도 부동 소수점(double)으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 더블 값 또는 기본값 public double GetDouble(string propertyName, double defaultValue = 0.0) { if (TryGetValue(propertyName, out object value) && value != null) { if (value is double doubleValue) return doubleValue; return Convert.ToDouble(value); } return defaultValue; } /// /// 지정된 속성의 값을 DateTime으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 DateTime 값 또는 기본값 public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null) { if (TryGetValue(propertyName, out object value) && value != null) { if (value is DateTime dateTime) return dateTime; return Convert.ToDateTime(value); } return defaultValue; } /// /// 지정된 속성의 값을 열거형(Enum)으로 변환하여 반환합니다. /// /// 변환할 열거형 타입 /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 열거형 값 또는 기본값 public T GetEnum(string propertyName, T defaultValue = default) where T : Enum { if (TryGetValue(propertyName, out object value) && value != null) { if (value is T enumValue) return enumValue; return (T)Enum.Parse(typeof(T), value.ToString()); } return defaultValue; } /// /// 지정된 속성의 값을 IDataObject로 반환합니다. /// /// 속성 이름 /// 속성이 없거나 IDataObject가 아닌 경우 반환할 기본값 /// IDataObject 인터페이스를 구현한 객체 또는 기본값 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; } /// /// 지정된 속성의 값을 DataArray로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// DataArray 객체 또는 기본값 public DataArray? GetDataArray(string propertyName, DataArray? defaultValue = null) { if (TryGetValue(propertyName, out object value) && value != null) { if (value is DataArray dataArray) return dataArray; if (value is JArray jArray) return new DataArray(jArray); } return defaultValue; } /// /// 지정된 속성의 값을 DataObject로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// DataObject 객체 또는 기본값 public DataObject? GetDataObject(string propertyName, DataObject? defaultValue = null) { if (TryGetValue(propertyName, out object value) && value != null) { if (value is DataObject dataObject) return dataObject; if (value is JObject jObject) return new DataObject(jObject); if (value is Dictionary dict) return new DataObject(dict); } // propertyName으로 직접 객체를 찾지 못했거나, 찾았지만 적절한 타입이 아닌 경우, // 현재 DataObject의 값들 중 DataObject 타입인 것들을 순회하며 내부에서 propertyName을 다시 검색합니다. foreach (KeyValuePair keyValue in this) { if (keyValue.Value is DataObject nestedDataObject) { // 중첩된 DataObject에서 propertyName을 검색합니다. // 여기서 defaultValue로 null을 전달하여, 이 탐색 단계에서 찾지 못하면 // 외부 호출의 defaultValue가 사용되도록 합니다. DataObject? foundInNested = nestedDataObject.GetDataObject(propertyName, null); if (foundInNested != null) { return foundInNested; // 중첩된 객체에서 찾았으면 반환 } } } return defaultValue; } /// /// 속성이 제거될 때 기존 속성 목록에서도 제거합니다. /// /// 제거할 속성의 키 /// 제거 성공 여부 public new bool Remove(string key) { bool result = base.Remove(key); if (result) { existingProperties.Remove(key); changedProperies.Remove(key); } return result; } /// /// 모든 속성을 제거하고 추적 리스트를 초기화합니다. /// public void RemoveAll() { base.Clear(); existingProperties.Clear(); changedProperies.Clear(); } /// /// 현재 DataObject를 JObject로 변환합니다. /// /// 현재 객체의 데이터를 담은 JObject public JObject ToJObject() { JObject result = new JObject(); foreach (var kvp in this) { result[kvp.Key] = JToken.FromObject(kvp.Value); } return result; } /// /// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다. /// 변경된 키는 자동으로 추적됩니다. /// /// 비교할 DataObject public void UpdateDifferent(IDataObject other) { if (other == null || other is not DataObject) return; changedProperies.Clear(); foreach (var keyValue in (DataObject)other) { if (!this.ContainsKey(keyValue.Key) || !this[keyValue.Key].Equals(keyValue.Value)) { this[keyValue.Key] = keyValue.Value; changedProperies.Add(keyValue.Key); } } } /// /// 업데이트된 속성만 포함하는 새로운 DataObject를 반환합니다. /// /// 업데이트 된 항목만 가지고 있는 DataObject public IDataObject GetUpdatedObject() { DataObject updated = new DataObject(); foreach (var key in changedProperies) { if (this.ContainsKey(key)) { updated[key] = this[key]; } } return updated; } /// /// 업데이트 된 속성의 수. /// /// 업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다. public int UpdatedCount { get => changedProperies.Count; } /// /// DataObject의 내용을 문자열로 반환합니다. /// 각 키-값 쌍이 "키:값" 형식으로 쉼표로 구분되어 표시됩니다. /// /// DataObject의 내용을 나타내는 문자열 public override string ToString() { return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}")); } } /// /// DataObject 인스턴스를 재사용하기 위한 객체 풀 클래스입니다. /// 메모리 할당과 가비지 컬렉션을 최소화하기 위해 DataObject 인스턴스를 관리합니다. /// /// /// 객체 풀링은 자주 생성 및 삭제되는 객체의 성능을 향상시키는 패턴입니다. /// 사용이 끝난 객체를 삭제하는 대신 풀에 반환하고, 새 객체가 필요할 때 풀에서 꺼내 재사용합니다. /// /// /// /// // 객체 풀에서 DataObject 가져오기 /// DataObject obj = DataObjectPool.GetDataObjectFromPool(); /// /// // 객체 사용 /// obj["name"] = "홍길동"; /// obj["age"] = 30; /// /// // 작업 완료 후 풀에 반환 /// DataObjectPool.ReturnToPool(obj); /// /// public static class DataObjectPool { /// /// DataObject 인스턴스를 저장하는 큐입니다. /// private static Queue dataObjectPool = new Queue(); /// /// 풀의 최대 크기입니다. 이 크기를 초과하는 객체는 풀에 저장되지 않습니다. /// private static int maxPoolSize = 1000; /// /// 풀에서 DataObject 인스턴스를 가져옵니다. /// 풀이 비어있으면 새 인스턴스를 생성하여 반환합니다. /// /// 재사용 가능한 DataObject 인스턴스 public static DataObject GetDataObjectFromPool() { if (dataObjectPool.Count > 0) { return dataObjectPool.Dequeue(); } return new DataObject(); } /// /// 사용이 완료된 DataObject를 풀에 반환합니다. /// 객체는 반환 전에 초기화되어 모든 속성이 제거됩니다. /// /// 풀에 반환할 DataObject 인스턴스 public static void ReturnToPool(DataObject obj) { if (obj != null && dataObjectPool.Count < maxPoolSize) { obj.RemoveAll(); // 재사용 전 정리 dataObjectPool.Enqueue(obj); } } } }