테스트 중

This commit is contained in:
logonkhi
2025-11-10 20:32:10 +09:00
parent 9277bab6dd
commit 12c0b8c429
17 changed files with 584 additions and 1390 deletions

View File

@@ -1,15 +1,18 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
namespace UVC.Data.Core
{
/// <summary>
/// DataObject 객체 컬렉션의 변경사항을 추적하는 데이터 배열 클래스
/// </summary>
public class DataArray : List<DataObject>, IDataObject
public partial class DataArray : List<DataObject>, IDataObject
{
private bool isInPool = false;
/// <summary>
@@ -164,6 +167,77 @@ namespace UVC.Data.Core
return this;
}
/// <summary>
/// UniTask 기반 비차단(코루틴 대체) 방식으로 JArray로부터 데이터를 채웁니다. WebGL 같은 싱글스레드 환경에서
/// UI 블로킹을 피하기 위해 일정 수의 항목마다 await로 제어권을 양보합니다.
/// await myDataArray.FromJArrayIncrementalAsync(jArray, 100, cancellationToken);
/// </summary>
/// <param name="jArray">JArray 소스</param>
/// <param name="batchSize">한 번에 처리할 항목 수</param>
/// <param name="cancellationToken">취소 토큰</param>
public async UniTask FromJArrayIncrementalAsync(JArray jArray, int batchSize = 50, CancellationToken cancellationToken = default)
{
if (jArray == null) return;
int count = 0;
foreach (var item in jArray)
{
cancellationToken.ThrowIfCancellationRequested();
base.Add(ConvertToDataObject(item));
count++;
if (count % batchSize == 0)
{
await UniTask.Yield(cancellationToken);
}
}
}
/// <summary>
/// UniTask 기반 비차단 방식으로 JSON 문자열을 스트리밍하여 파싱합니다.
/// JsonTextReader를 사용해 토큰 단위로 읽고, 일정 수의 항목마다 await로 제어권을 양보합니다.
/// await myDataArray.FromJsonStringIncrementalAsync(jArray, 100, cancellationToken);
/// </summary>
/// <param name="jsonString">JSON 문자열</param>
/// <param name="batchSize">한 번에 처리할 항목 수</param>
/// <param name="cancellationToken">취소 토큰</param>
public async UniTask FromJsonStringIncrementalAsync(string jsonString, int batchSize = 50, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(jsonString)) return;
using (var sr = new System.IO.StringReader(jsonString))
using (var reader = new Newtonsoft.Json.JsonTextReader(sr))
{
reader.SupportMultipleContent = false;
// 기대: StartArray
if (!reader.Read() || reader.TokenType != Newtonsoft.Json.JsonToken.StartArray) return;
int count = 0;
while (reader.Read())
{
cancellationToken.ThrowIfCancellationRequested();
if (reader.TokenType == Newtonsoft.Json.JsonToken.StartObject || reader.TokenType == Newtonsoft.Json.JsonToken.StartArray ||
reader.TokenType == Newtonsoft.Json.JsonToken.String || reader.TokenType == Newtonsoft.Json.JsonToken.Integer ||
reader.TokenType == Newtonsoft.Json.JsonToken.Float || reader.TokenType == Newtonsoft.Json.JsonToken.Boolean)
{
// JToken.ReadFrom는 현재 토큰에서 전체 값(객체나 배열 등)을 읽습니다.
JToken token = JToken.ReadFrom(reader);
base.Add(ConvertToDataObject(token));
count++;
if (count % batchSize == 0)
{
await UniTask.Yield(cancellationToken);
}
}
else if (reader.TokenType == Newtonsoft.Json.JsonToken.EndArray)
{
break;
}
}
}
}
public DataArray FromCapacity(int capacity)
{
Capacity = capacity;
@@ -333,7 +407,7 @@ namespace UVC.Data.Core
/// <summary>
/// 현재 데이터 객체의 업데이트된 버전을 생성하고 반환합니다. 선택적으로 메모리 할당 최적화를 위해 풀을 사용합니다.
// </summary>
/// </summary>
/// <remarks>이 메서드는 현재 데이터 객체와 관련 요소의 깊은 복사를 수행합니다.
/// <paramref name="fromPool"/>이 <see langword="true"/>이면 메서드는 메모리 사용을 최적화하기 위해 객체 풀에서 새 인스턴스를 검색합니다. 그렇지 않으면 새 인스턴스가 직접 생성됩니다. 반환된
/// 객체는 추가, 제거 및 수정을 포함하여 현재 객체의 상태에 대한 업데이트를 반영합니다.
@@ -377,7 +451,7 @@ namespace UVC.Data.Core
/// <summary>
/// 업데이트 된 속성의 수.
/// </summary>
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면0을 반환합니다.</returns>
public int UpdatedCount { get => addedList.Count + modifiedList.Count + removedList.Count; }
private bool isUpdateImmediately = false;
@@ -471,12 +545,11 @@ namespace UVC.Data.Core
/// 현재 <see cref="DataArray"/> 인스턴스의 요소 및 관련 상태를 포함한 깊은 복사본을 생성합니다.
///
/// </summary>
/// <remarks>이 메서드는 현재 배열에 있는 모든 <see cref="DataObject"/> 요소의 깊은 복사본을 포함하는 새로운 <see cref="DataArray"/> 인스턴스를 반환합니다.
/// <remarks>이 메서드는 현재 배열에 있는 모든 <see cref="DataObject"/> 요소의 깊은 복사본을 포함하는 새로운 <see cref=\"DataArray\"/> 인스턴스를 반환합니다.
/// 복사된 인스턴스는 내부 목록에서 추적하는 추가, 제거 또는 수정된 요소를 포함하여 원본 인스턴스의 상태도 복제합니다.
/// </remarks>
/// <param name="fromPool">객체 풀에서 복제할지 여부를 지정합니다. 기본값은 true입니다.</param>
/// <returns>현재 인스턴스의 깊은 복사본인 새로운 <see cref="DataArray"/> 인스턴스를 반환합니다.</returns>
public DataArray Copy(bool fromPool = true)
{
// 풀에서 새 DataArray 인스턴스를 가져옵니다.
@@ -540,5 +613,96 @@ namespace UVC.Data.Core
dataArray.FromJArray(jArray);
return dataArray;
}
/// <summary>
/// JToken을 DataObject로 비동기 재질화하여 반환합니다. 이름: ConvertTokenToDataObjectAsync
/// </summary>
public async UniTask<DataObject> ConvertTokenToDataObjectAsync(JToken token, int batchSize =50, CancellationToken cancellationToken = default)
{
if (token.Type == JTokenType.Object)
{
var dataObject = DataObjectPool.Get();
await dataObject.FromJObjectIncrementalAsync((JObject)token, batchSize, cancellationToken);
return dataObject;
}
else
{
var dataObject = DataObjectPool.Get();
var value = await MaterializePrimitiveOrCollectionAsync(token, batchSize, cancellationToken);
dataObject.Add("value", value);
return dataObject;
}
}
private async UniTask<object?> MaterializePrimitiveOrCollectionAsync(JToken token, int batchSize, CancellationToken cancellationToken)
{
switch (token.Type)
{
case JTokenType.String:
return token.ToString();
case JTokenType.Integer:
return token.ToObject<int>();
case JTokenType.Float:
return token.ToObject<float>();
case JTokenType.Boolean:
return token.ToObject<bool>();
case JTokenType.Array:
JArray array = (JArray)token;
var dataArray = DataArrayPool.Get();
await dataArray.FromJArrayIncrementalAsync(array, batchSize, cancellationToken);
return dataArray;
case JTokenType.Object:
var dataObject = DataObjectPool.Get();
await dataObject.FromJObjectIncrementalAsync((JObject)token, batchSize, cancellationToken);
return dataObject;
default:
return token.ToString();
}
}
// Note: existing synchronous ConvertToDataObject remains for compatibility
/// <summary>
/// 비동기 팩토리: JArray로부터 완전히 점진 재질화된 DataArray를 생성합니다.
/// </summary>
public static async UniTask<DataArray> CreateFromJArrayAsync(JArray jArray, int batchSize =50, CancellationToken cancellationToken = default)
{
var arr = new DataArray();
await arr.FromJArrayIncrementalAsync(jArray, batchSize, cancellationToken);
return arr;
}
/// <summary>
/// 비동기 팩토리: JSON 문자열로부터 완전히 점진 재질화된 DataArray를 생성합니다.
/// </summary>
public static async UniTask<DataArray> CreateFromJsonStringAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(jsonString)) return new DataArray();
#if UNITY_WEBGL && !UNITY_EDITOR
var arr = new DataArray();
await arr.FromJsonStringIncrementalAsync(jsonString, batchSize, cancellationToken);
return arr;
#else
var jArr = await UniTask.RunOnThreadPool(() => JArray.Parse(jsonString));
return await CreateFromJArrayAsync(jArr, batchSize, cancellationToken);
#endif
}
/// <summary>
/// 비동기 팩토리: 스트림으로부터 완전히 점진 재질화된 DataArray를 생성합니다.
/// </summary>
public static async UniTask<DataArray> CreateFromStreamAsync(System.IO.Stream jsonStream, int batchSize =50, CancellationToken cancellationToken = default)
{
if (jsonStream == null) throw new ArgumentNullException(nameof(jsonStream));
using (var sr = new System.IO.StreamReader(jsonStream))
using (var reader = new Newtonsoft.Json.JsonTextReader(sr))
{
reader.SupportMultipleContent = true;
var serializer = new Newtonsoft.Json.JsonSerializer();
var jArr = serializer.Deserialize<JArray>(reader);
if (jArr == null) return new DataArray();
return await CreateFromJArrayAsync(jArr, batchSize, cancellationToken);
}
}
}
}

View File

@@ -2,8 +2,11 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using UVC.Extention;
namespace UVC.Data.Core
@@ -26,8 +29,8 @@ namespace UVC.Data.Core
/// // 필드 이름 변환 설정
/// mask.NamesForReplace = new Dictionary<string, string>
/// {
/// { "userName", "name" },
/// { "userEmail", "email" }
/// { "userName", "name" },
/// { "userEmail", "email" }
/// };
///
/// // JObject 속성으로 마스킹 규칙 추가
@@ -46,7 +49,7 @@ namespace UVC.Data.Core
/// // - password는 제외됨
/// </code>
/// </example>
public class DataObject : OrderedDictionary<string, object?>, IDataObject
public partial class DataObject : OrderedDictionary<string, object?>, IDataObject
{
private bool isInPool = false;
/// <summary>
@@ -118,7 +121,7 @@ namespace UVC.Data.Core
}
/// <summary>
/// Id에 해당하는 key 문자열
/// Id에 해당하는 key 문자열
/// </summary>
public string? IdKey { get; set; } = null;
@@ -286,11 +289,142 @@ namespace UVC.Data.Core
return this;
}
/// <summary>
/// UniTask 기반 비차단 방식으로 JObject로부터 데이터를 채웁니다. WebGL 같은 싱글스레드 환경에서
/// UI 블로킹을 피하기 위해 일정 수의 키마다 await로 제어권을 양보합니다.
/// await myDataObject.FromJObjectIncrementalAsync(jObject, 200, cancellationToken);
/// </summary>
/// <param name="other">JObject 소스</param>
/// <param name="batchSize">한 번에 처리할 속성 수</param>
/// <param name="cancellationToken">취소 토큰</param>
public async UniTask FromJObjectIncrementalAsync(JObject other, int batchSize =50, CancellationToken cancellationToken = default)
{
if (other == null) return;
IdKey = null;
Name = string.Empty;
_isInitializing = true;
int count =0;
try
{
foreach (var prop in other.Properties())
{
cancellationToken.ThrowIfCancellationRequested();
#if UNITY_WEBGL && !UNITY_EDITOR
var value = await ConvertJTokenToObjectAsync(prop.Value, batchSize, cancellationToken);
this[prop.Name] = value;
#else
this[prop.Name] = ConvertJTokenToObject(prop.Value);
#endif
count++;
if (count % batchSize ==0)
{
await UniTask.Yield(cancellationToken);
}
}
}
finally
{
_isInitializing = false;
}
}
/// <summary>
/// UniTask 기반 비차단 방식으로 JSON 문자열을 스트리밍하여 파싱합니다.
/// JsonTextReader를 사용해 토큰 단위로 읽고, 일정 수의 항목마다 await로 제어권을 양보합니다.
/// await myDataObject.FromJsonStringIncrementalAsync(jsonString, 200, cancellationToken);
/// </summary>
/// <param name="jsonString">JSON 문자열</param>
/// <param name="batchSize">한 번에 처리할 속성 수</param>
/// <param name="cancellationToken">취소 토큰</param>
public async UniTask FromJsonStringIncrementalAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(jsonString)) return;
using (var sr = new System.IO.StringReader(jsonString))
using (var reader = new Newtonsoft.Json.JsonTextReader(sr))
{
reader.SupportMultipleContent = false;
// 기대: StartObject
if (!reader.Read() || reader.TokenType != Newtonsoft.Json.JsonToken.StartObject) return;
IdKey = null;
Name = string.Empty;
_isInitializing = true;
int count =0;
while (reader.Read())
{
cancellationToken.ThrowIfCancellationRequested();
if (reader.TokenType == Newtonsoft.Json.JsonToken.PropertyName)
{
string propName = (string)reader.Value!;
// 다음 토큰은 값의 시작이어야 함. JToken.ReadFrom를 사용하여 값 전체를 읽는다.
if (!reader.Read()) break;
JToken token = JToken.ReadFrom(reader);
#if UNITY_WEBGL && !UNITY_EDITOR
var value = await ConvertJTokenToObjectAsync(token, batchSize, cancellationToken);
this[propName] = value;
#else
this[propName] = ConvertJTokenToObject(token);
#endif
count++;
if (count % batchSize ==0)
{
await UniTask.Yield(cancellationToken);
}
}
else if (reader.TokenType == Newtonsoft.Json.JsonToken.EndObject)
{
break;
}
}
_isInitializing = false;
}
}
/// <summary>
/// JToken을 적절한 C# 객체 타입으로 변환하는 헬퍼 메서드입니다.
/// </summary>
/// <param name="token">변환할 JToken 객체</param>
/// <returns>변환된 C# 객체</returns>
private async UniTask<object?> MaterializeJTokenAsync(JToken token, int batchSize =50, CancellationToken cancellationToken = default)
{
switch (token.Type)
{
case JTokenType.String:
return token.ToString();
case JTokenType.Integer:
return token.ToObject<int>();
case JTokenType.Float:
return token.ToObject<float>();
case JTokenType.Boolean:
return token.ToObject<bool>();
case JTokenType.Object:
// 점진적으로 DataObject로 채우기
var dataObject = DataObjectPool.Get();
await dataObject.FromJObjectIncrementalAsync((JObject)token, batchSize, cancellationToken);
return dataObject;
case JTokenType.Array:
JArray array = (JArray)token;
var dataArray = DataArrayPool.Get();
await dataArray.FromJArrayIncrementalAsync(array, batchSize, cancellationToken);
return dataArray;
default:
return token.ToString();
}
}
/// <summary>
/// 기존 ConvertJTokenToObject는 동기 API를 유지하되, 호출자가 비동기 전환을 원하면
/// MaterializeJTokenAsync를 직접 호출하도록 합니다. 기본 동작은 기존과 동일합니다.
/// </summary>
private object? ConvertJTokenToObject(JToken token)
{
switch (token.Type)
@@ -315,6 +449,15 @@ namespace UVC.Data.Core
}
}
/// <summary>
/// 새 비동기 전환용 헬퍼: 동기 ConvertJTokenToObject 대신 사용하여 비동기적으로 토큰을 재질화합니다.
/// 메서드 이름: ConvertJTokenToObjectAsync
/// </summary>
public UniTask<object?> ConvertJTokenToObjectAsync(JToken token, int batchSize =50, CancellationToken cancellationToken = default)
{
return MaterializeJTokenAsync(token, batchSize, cancellationToken);
}
/// <summary>
/// 모든 프로퍼티를 변경된 것으로 표시합니다.
/// 전체 데이터가 갱신되었을 때 사용합니다.
@@ -383,14 +526,13 @@ namespace UVC.Data.Core
/// <returns>변환된 정수 값 또는 기본값</returns>
public int? GetInt(string propertyName, int? defaultValue = null)
{
if (!TryGetValue(propertyName, out var v) || v == null) return defaultValue;
return v switch
if (TryGetValue(propertyName, out object? value) && value != null)
{
int i => i,
long l when l >= int.MinValue && l <= int.MaxValue => (int)l,
string s when int.TryParse(s, out var i2) => i2,
_ => defaultValue
};
if (value is int intValue)
return intValue;
return Convert.ToInt32(value);
}
return defaultValue;
}
/// <summary>
@@ -573,7 +715,7 @@ namespace UVC.Data.Core
return null; // 깊이 제한 초과 시 탐색 중단
}
// 1. 현재 객체에서 직접 속성 검색
//1. 현재 객체에서 직접 속성 검색
if (TryGetValue(propertyName, out object? value) && value != null)
{
if (value is DataObject dataObject)
@@ -592,12 +734,12 @@ namespace UVC.Data.Core
}
}
// 2. 중첩된 DataObject에서 재귀적으로 검색
//2. 중첩된 DataObject에서 재귀적으로 검색
foreach (KeyValuePair<string, object?> keyValue in this)
{
if (keyValue.Value is DataObject nestedDataObject)
{
// 재귀 호출 시 깊이를 1 증가
// 재귀 호출 시 깊이를1 증가
DataObject? foundInNested = nestedDataObject.GetDataObjectInternal(propertyName, currentDepth + 1, maxDepth);
if (foundInNested != null)
{
@@ -692,13 +834,7 @@ namespace UVC.Data.Core
/// </summary>
public void RemoveAll()
{
if (Count == 0)
{
changedProperies.Clear();
return;
}
foreach (var value in Values)
foreach (var value in Values.ToList())
{
if (value is DataObject dataObject)
{
@@ -823,8 +959,8 @@ namespace UVC.Data.Core
foreach (var keyValue in otherDataObject)
{
if (!this.ContainsKey(keyValue.Key) || (this[keyValue.Key] == null && keyValue.Value != null)
|| (this[keyValue.Key] != null && keyValue.Value == null)
|| (this[keyValue.Key] != null && keyValue.Value != null && !this[keyValue.Key]!.Equals(keyValue.Value)))
|| (this[keyValue.Key] != null && keyValue.Value == null)
|| (this[keyValue.Key] != null && keyValue.Value != null && !this[keyValue.Key]!.Equals(keyValue.Value)))
{
//참조 타입과 값 타입 구분하여 복사
object? valueToSet;
@@ -866,7 +1002,7 @@ namespace UVC.Data.Core
/// cref="DataArray"/> 유형의 속성은 깊이 복사되고, 다른 유형의 속성은 직접 복사되도록 합니다. <c>IdKey</c>가 없으면
/// 현재 객체의 첫 번째 키-값 쌍이 업데이트된 객체에 포함됩니다.
///</remarks>
/// <param name="fromPool">업데이트된 객체 풀에서 검색해야 하는지 여부를 나타내는 값입니다. <see langword="true"/>인 경우
/// <param name="fromPool">업데이트된 객체 풀에서 검색해야 하는지 여부를 나타내는 값입니다. <see langword="true"/>인 경우
/// <see cref="DataObjectPool"/>에서 객체를 가져옵니다. 그렇지 않으면 새 <see cref="DataObject"/>
/// 인스턴스가 생성됩니다.</param>
/// <returns>현재 객체에서 복사된 속성과 값을 포함하는 업데이트된 <see cref="IDataObject"/> 인스턴스입니다.
@@ -912,7 +1048,6 @@ namespace UVC.Data.Core
public void ReturnToPool()
{
if (IsInPool) return; // 중복 반환 방지
if (CreatedFromPool)
{
Reset();
@@ -926,7 +1061,7 @@ namespace UVC.Data.Core
/// <summary>
/// 업데이트 된 속성의 수.
/// </summary>
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면0을 반환합니다.</returns>
public int UpdatedCount { get => changedProperies.Count; }
private bool isUpdateImmediately = false;
@@ -975,6 +1110,49 @@ namespace UVC.Data.Core
return ToJObject().ToString(Newtonsoft.Json.Formatting.None);
}
/// <summary>
/// 비동기 팩토리: JObject로부터 완전히 점진 재질화된 DataObject를 생성합니다.
/// </summary>
public static async UniTask<DataObject> CreateFromJObjectAsync(JObject other, int batchSize =50, CancellationToken cancellationToken = default)
{
var obj = new DataObject();
await obj.FromJObjectIncrementalAsync(other, batchSize, cancellationToken);
return obj;
}
/// <summary>
/// 비동기 팩토리: JSON 문자열로부터 완전히 점진 재질화된 DataObject를 생성합니다.
/// </summary>
public static async UniTask<DataObject> CreateFromJsonStringAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(jsonString)) return new DataObject();
#if UNITY_WEBGL && !UNITY_EDITOR
var obj = new DataObject();
await obj.FromJsonStringIncrementalAsync(jsonString, batchSize, cancellationToken);
return obj;
// WebGL: 스레드가 없으므로 incremental 파서를 사용해 메인스레드에서 점진 처리
#else
// non-WebGL: 스레드풀에서 전체 파싱 후 비동기 재질화
var jObj = await UniTask.RunOnThreadPool(() => JObject.Parse(jsonString));
return await CreateFromJObjectAsync(jObj, batchSize, cancellationToken);
#endif
}
/// <summary>
/// 비동기 팩토리: 스트림으로부터 완전히 점진 재질화된 DataObject를 생성합니다.
/// </summary>
public static async UniTask<DataObject> CreateFromStreamAsync(System.IO.Stream jsonStream, int batchSize =50, CancellationToken cancellationToken = default)
{
if (jsonStream == null) throw new ArgumentNullException(nameof(jsonStream));
using (var sr = new System.IO.StreamReader(jsonStream))
using (var reader = new Newtonsoft.Json.JsonTextReader(sr))
{
reader.SupportMultipleContent = true;
var serializer = new Newtonsoft.Json.JsonSerializer();
var jObj = serializer.Deserialize<JObject>(reader);
if (jObj == null) return new DataObject();
return await CreateFromJObjectAsync(jObj, batchSize, cancellationToken);
}
}
}
}

View File

@@ -11,6 +11,7 @@ using UnityEngine;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Log;
using UVC.network;
using UVC.Tests;
namespace UVC.Data.Mqtt
@@ -197,7 +198,7 @@ namespace UVC.Data.Mqtt
#if UNITY_WEBGL && !UNITY_EDITOR
if (UseMockup)
{
ULog.Warn("WebGL Mockup 모드는 아직 구현되지 않았습니다.");
Debug.LogWarning("WebGL Mockup 모드는 아직 구현되지 않았습니다.");
return;
}
if (webGlMqttService != null) return;

View File

@@ -169,8 +169,9 @@ namespace UVC.Data
}
}
});
}
#endif
}
public static UserSetting? FromDataObject(DataObject dataObject)
{
if (dataObject == null) return null;