2025-07-30 20:16:21 +09:00
|
|
|
using Newtonsoft.Json.Linq;
|
2025-06-05 20:09:28 +09:00
|
|
|
using System;
|
2025-11-10 20:32:10 +09:00
|
|
|
using System.Collections;
|
2025-06-05 20:09:28 +09:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
using System.Linq;
|
2025-11-10 20:32:10 +09:00
|
|
|
using System.Threading;
|
|
|
|
|
using Cysharp.Threading.Tasks;
|
2025-06-05 20:09:28 +09:00
|
|
|
|
2025-07-15 15:25:17 +09:00
|
|
|
namespace UVC.Data.Core
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// DataObject 객체 컬렉션의 변경사항을 추적하는 데이터 배열 클래스
|
|
|
|
|
/// </summary>
|
2025-11-10 20:32:10 +09:00
|
|
|
public partial class DataArray : List<DataObject>, IDataObject
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
2025-06-27 17:50:23 +09:00
|
|
|
private bool isInPool = false;
|
2025-06-26 19:46:13 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 이 객체가 객체 풀에 있는지 여부를 나타냅니다.
|
|
|
|
|
/// 중복 반환을 방지하기 위해 DataArrayPool에서 내부적으로 사용됩니다.
|
|
|
|
|
/// </summary>
|
2025-07-30 20:16:21 +09:00
|
|
|
internal bool IsInPool
|
|
|
|
|
{
|
|
|
|
|
get => isInPool;
|
2025-06-27 17:50:23 +09:00
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
isInPool = value;
|
|
|
|
|
foreach (var item in this)
|
|
|
|
|
{
|
|
|
|
|
item.IsInPool = value; // 내부 DataObject도 풀에 있다고 표시합니다.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-26 19:46:13 +09:00
|
|
|
|
2025-07-22 19:58:14 +09:00
|
|
|
private bool createdFromPool = false;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 객체가 풀에서 생성되었는지 여부를 나타냅니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal bool CreatedFromPool
|
|
|
|
|
{
|
|
|
|
|
get => createdFromPool;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
createdFromPool = value;
|
|
|
|
|
foreach (var item in this)
|
|
|
|
|
{
|
|
|
|
|
item.CreatedFromPool = value; // 내부 DataObject도 풀에서 생성되었음을 표시합니다.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-05 20:09:28 +09:00
|
|
|
// 추가 된 항목 목록
|
|
|
|
|
protected List<DataObject> addedList = new List<DataObject>();
|
|
|
|
|
// 제거 된 항목 목록
|
|
|
|
|
protected List<DataObject> removedList = new List<DataObject>();
|
|
|
|
|
// 수정 된 항목 목록
|
|
|
|
|
protected List<DataObject> modifiedList = new List<DataObject>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 추가된 항목에 접근할 수 있는 읽기 전용 컬렉션
|
|
|
|
|
public ReadOnlyCollection<DataObject> AddedItems => addedList.AsReadOnly();
|
|
|
|
|
|
|
|
|
|
// 제거된 항목에 접근할 수 있는 읽기 전용 컬렉션
|
|
|
|
|
public ReadOnlyCollection<DataObject> RemovedItems => removedList.AsReadOnly();
|
|
|
|
|
|
|
|
|
|
// 제거된 항목에 접근할 수 있는 읽기 전용 컬렉션
|
|
|
|
|
public ReadOnlyCollection<DataObject> ModifiedList => modifiedList.AsReadOnly();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 기본 생성자
|
|
|
|
|
/// </summary>
|
|
|
|
|
public DataArray() : base()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 초기 용량을 지정하는 생성자
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="capacity">초기 용량</param>
|
|
|
|
|
public DataArray(int capacity) : base(capacity)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 기존 컬렉션으로부터 생성하는 생성자
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="collection">초기 항목을 포함하는 컬렉션</param>
|
|
|
|
|
public DataArray(IEnumerable<DataObject> collection) : base(collection)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public DataArray(string jsonString) : base()
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrEmpty(jsonString))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
JArray jArray = JArray.Parse(jsonString);
|
|
|
|
|
foreach (var item in jArray)
|
|
|
|
|
{
|
|
|
|
|
Add(ConvertToDataObject(item));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("Invalid JSON string format.", nameof(jsonString), ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 17:50:23 +09:00
|
|
|
public DataArray(System.IO.Stream jsonStream)
|
|
|
|
|
{
|
|
|
|
|
if (jsonStream == null)
|
|
|
|
|
throw new ArgumentNullException(nameof(jsonStream));
|
|
|
|
|
|
|
|
|
|
// 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만,
|
|
|
|
|
// 현재는 Newtonsoft.Json의 기본 역직렬화 사용
|
|
|
|
|
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
|
|
|
|
{
|
|
|
|
|
// 청크 읽기 설정 - 메모리 사용량 최적화
|
|
|
|
|
reader.SupportMultipleContent = true;
|
|
|
|
|
|
|
|
|
|
var serializer = new Newtonsoft.Json.JsonSerializer();
|
|
|
|
|
var sourceObject = serializer.Deserialize<JArray>(reader);
|
|
|
|
|
|
|
|
|
|
// 수정된 코드: 생성자를 호출하는 대신 JArray 메서드를 사용
|
|
|
|
|
if (sourceObject != null) FromJArray(sourceObject);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-05 20:09:28 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// JArray로부터 DataArray를 생성하는 생성자
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="jArray">JSON 배열</param>
|
|
|
|
|
public DataArray(JArray jArray) : base()
|
|
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
FromJArray(jArray);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-15 15:25:17 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 배열의 JSON 문자열 표현을 사용하여 현재 <see cref="DataArray"/> 인스턴스를 채웁니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>이 메서드는 제공된 JSON 문자열을 구문 분석하고 각 요소를 데이터 객체로 변환한 다음
|
|
|
|
|
/// 현재 <see cref="DataArray"/>에 추가합니다. <paramref name="jsonString"/>이 null이거나
|
|
|
|
|
/// 비어 있는 경우, 메서드는 수정 없이 현재 인스턴스를 반환합니다.</remarks>
|
|
|
|
|
/// <param name="jsonString">구문 분석하여 데이터 객체로 변환할 JSON 문자열입니다. 유효한 JSON 배열을 나타내야 합니다.</param>
|
|
|
|
|
/// <returns>JSON 문자열에서 파싱된 데이터 객체로 채워진 현재 <see cref="DataArray"/> 인스턴스입니다.</returns>
|
|
|
|
|
/// <exception cref="ArgumentException"> <paramref name="jsonString"/>이 유효한 JSON 배열 형식이 아닌 경우 발생합니다.</exception>
|
|
|
|
|
public DataArray FromJsonString(string jsonString)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrEmpty(jsonString))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
JArray jArray = JArray.Parse(jsonString);
|
|
|
|
|
foreach (var item in jArray)
|
|
|
|
|
{
|
|
|
|
|
Add(ConvertToDataObject(item));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("Invalid JSON string format.", nameof(jsonString), ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-10 20:32:10 +09:00
|
|
|
/// <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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
public DataArray FromCapacity(int capacity)
|
|
|
|
|
{
|
|
|
|
|
Capacity = capacity;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public DataArray FromCollection(IEnumerable<DataObject> collection)
|
|
|
|
|
{
|
|
|
|
|
if (collection == null) return this;
|
|
|
|
|
|
|
|
|
|
foreach (var item in collection)
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
// base.Add를 사용하여 추적 로직을 우회하고 직접 추가합니다.
|
|
|
|
|
base.Add(item);
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
2025-06-26 19:46:13 +09:00
|
|
|
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// JArray로부터 DataArray의 내용을 채웁니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="jArray">JSON 배열</param>
|
|
|
|
|
public DataArray FromJArray(JArray jArray)
|
|
|
|
|
{
|
|
|
|
|
if (jArray == null) return this;
|
|
|
|
|
|
|
|
|
|
foreach (var item in jArray)
|
|
|
|
|
{
|
|
|
|
|
// base.Add를 사용하여 추적 로직을 우회하고 직접 추가합니다.
|
|
|
|
|
base.Add(ConvertToDataObject(item));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this;
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// JToken을 DataObject로 변환합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private DataObject ConvertToDataObject(JToken token)
|
|
|
|
|
{
|
|
|
|
|
if (token.Type == JTokenType.Object)
|
|
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
// 풀에서 객체를 가져와 JObject로 초기화합니다.
|
|
|
|
|
var dataObject = DataObjectPool.Get();
|
|
|
|
|
dataObject.FromJObject((JObject)token);
|
|
|
|
|
return dataObject;
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
// JObject가 아닌 경우, 풀에서 새 DataObject를 가져와 값을 넣어줍니다.
|
|
|
|
|
var dataObject = DataObjectPool.Get();
|
2025-06-05 20:09:28 +09:00
|
|
|
dataObject.Add("value", ConvertJTokenToObject(token));
|
|
|
|
|
return dataObject;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private object ConvertJTokenToObject(JToken token)
|
|
|
|
|
{
|
|
|
|
|
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:
|
2025-06-26 19:46:13 +09:00
|
|
|
var dataObject = DataObjectPool.Get();
|
|
|
|
|
dataObject.FromJObject((JObject)token);
|
|
|
|
|
return dataObject;
|
2025-06-05 20:09:28 +09:00
|
|
|
case JTokenType.Array:
|
|
|
|
|
JArray array = (JArray)token;
|
2025-06-26 19:46:13 +09:00
|
|
|
var dataArray = DataArrayPool.Get();
|
|
|
|
|
dataArray.FromJArray(array);
|
|
|
|
|
return dataArray;
|
2025-06-05 20:09:28 +09:00
|
|
|
default:
|
|
|
|
|
return token.ToString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 02:17:54 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 모든 아이템이 추가 된것으로 표시합니다.
|
|
|
|
|
/// 전체 데이터가 갱신되었을 때 사용합니다.
|
|
|
|
|
/// </summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
public void MarkAllAsUpdated()
|
2025-06-06 02:17:54 +09:00
|
|
|
{
|
|
|
|
|
addedList.Clear();
|
|
|
|
|
addedList.AddRange(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다.
|
|
|
|
|
/// 변경된 키는 자동으로 추적됩니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="other">비교할 DataObject</param>
|
2025-09-23 20:40:31 +09:00
|
|
|
/// <param name="updatedDataOnly">true로 설정하면, 다른 속성만 업데이트합니다. false로 설정하면, 모든 속성을 덮어씁니다.</param>
|
|
|
|
|
public void UpdateDifferent(IDataObject other, bool updatedDataOnly)
|
2025-06-06 02:17:54 +09:00
|
|
|
{
|
|
|
|
|
if (other == null) return;
|
2025-06-26 19:46:13 +09:00
|
|
|
if (!(other is DataArray otherArray)) return;
|
2025-06-06 02:17:54 +09:00
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
// 기존 변경 추적 목록을 초기화합니다.
|
|
|
|
|
ClearTrackedChanges();
|
2025-07-30 20:16:21 +09:00
|
|
|
|
2025-08-14 20:05:04 +09:00
|
|
|
//Port에 서도 동일한 Id를 사용하는 경우가 있기 때문에
|
|
|
|
|
//GroupBy를 사용하여 Id로 그룹화한 후, 각 그룹의 첫 번째 항목을 선택하여 딕셔너리를 생성
|
|
|
|
|
var thisDict = this.GroupBy(item => item.Id).ToDictionary(g => g.Key, g => g.First());
|
|
|
|
|
var otherDict = otherArray.GroupBy(item => item.Id).ToDictionary(g => g.Key, g => g.First());
|
2025-06-07 01:53:51 +09:00
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
var thisIds = new HashSet<string>(thisDict.Keys);
|
|
|
|
|
var otherIds = new HashSet<string>(otherDict.Keys);
|
2025-06-07 01:53:51 +09:00
|
|
|
|
2025-09-23 20:40:31 +09:00
|
|
|
if (!updatedDataOnly)
|
2025-06-26 19:46:13 +09:00
|
|
|
{
|
2025-09-23 20:40:31 +09:00
|
|
|
// 제거된 항목 확인 (현재 배열에는 있지만 다른 배열에는 없는 항목)
|
|
|
|
|
foreach (var id in thisIds.Where(id => !otherIds.Contains(id)))
|
|
|
|
|
{
|
|
|
|
|
removedList.Add(thisDict[id]);
|
|
|
|
|
}
|
2025-06-26 19:46:13 +09:00
|
|
|
}
|
2025-06-07 01:53:51 +09:00
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
// 추가된 항목 확인 (다른 배열에는 있지만 현재 배열에는 없는 항목)
|
|
|
|
|
foreach (var id in otherIds.Where(id => !thisIds.Contains(id)))
|
|
|
|
|
{
|
2025-06-27 17:50:23 +09:00
|
|
|
addedList.Add(otherDict[id].Copy(fromPool: false));
|
2025-06-26 19:46:13 +09:00
|
|
|
}
|
2025-06-07 01:53:51 +09:00
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
// 수정된 항목 확인 (양쪽 모두에 있지만 내용이 다른 항목)
|
|
|
|
|
foreach (var id in thisIds.Where(id => otherIds.Contains(id)))
|
|
|
|
|
{
|
|
|
|
|
var thisItem = thisDict[id];
|
|
|
|
|
var otherItem = otherDict[id];
|
|
|
|
|
|
|
|
|
|
// [성능 개선] ToString() 비교는 매우 비효율적입니다.
|
|
|
|
|
// DataObject.UpdateDifferent를 직접 호출하여 변경 사항을 적용하고,
|
|
|
|
|
// UpdatedCount를 통해 실제 변경 여부를 확인합니다.
|
2025-09-23 20:40:31 +09:00
|
|
|
thisItem.UpdateDifferent(otherItem, updatedDataOnly);
|
2025-06-26 19:46:13 +09:00
|
|
|
if (thisItem.UpdatedCount > 0)
|
2025-06-07 01:53:51 +09:00
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
modifiedList.Add(thisItem);
|
2025-06-06 02:17:54 +09:00
|
|
|
}
|
2025-06-26 19:46:13 +09:00
|
|
|
}
|
2025-06-07 01:53:51 +09:00
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
// [성능 개선] RemoveAll과 HashSet을 사용하여 제거 작업의 효율성을 높입니다.
|
|
|
|
|
if (removedList.Count > 0)
|
|
|
|
|
{
|
2025-06-27 17:50:23 +09:00
|
|
|
// 제거될 객체들을 먼저 풀에 반환합니다.
|
|
|
|
|
foreach (var item in removedList)
|
|
|
|
|
{
|
|
|
|
|
if (item.IsInPool) item.ReturnToPool();
|
|
|
|
|
}
|
|
|
|
|
// 그 다음 리스트에서 제거합니다.
|
2025-06-26 19:46:13 +09:00
|
|
|
var removedItemIds = new HashSet<string>(removedList.Select(i => i.Id));
|
|
|
|
|
this.RemoveAll(item => removedItemIds.Contains(item.Id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// [성능 개선] base.AddRange를 사용하여 추적 로직을 우회하고 효율적으로 추가합니다.
|
|
|
|
|
if (addedList.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
base.AddRange(addedList);
|
2025-06-06 02:17:54 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-07-01 20:10:15 +09:00
|
|
|
/// 현재 데이터 객체의 업데이트된 버전을 생성하고 반환합니다. 선택적으로 메모리 할당 최적화를 위해 풀을 사용합니다.
|
2025-11-10 20:32:10 +09:00
|
|
|
/// </summary>
|
2025-07-01 20:10:15 +09:00
|
|
|
/// <remarks>이 메서드는 현재 데이터 객체와 관련 요소의 깊은 복사를 수행합니다.
|
|
|
|
|
/// <paramref name="fromPool"/>이 <see langword="true"/>이면 메서드는 메모리 사용을 최적화하기 위해 객체 풀에서 새 인스턴스를 검색합니다. 그렇지 않으면 새 인스턴스가 직접 생성됩니다. 반환된
|
|
|
|
|
/// 객체는 추가, 제거 및 수정을 포함하여 현재 객체의 상태에 대한 업데이트를 반영합니다.
|
|
|
|
|
/// 변경 사항은 추적되어 업데이트된 객체의 해당 목록에 적용됩니다.</remarks>
|
|
|
|
|
/// <param name="fromPool">객체 풀에서 새 인스턴스를 검색할지 여부를 나타내는 부울 값입니다. <see langword="true"/>
|
|
|
|
|
/// 객체 풀을 사용합니다. <see langword="false"/>를 사용하여 새 인스턴스를 직접 생성합니다.</param>
|
|
|
|
|
/// <returns>추가, 제거 또는 수정된 목록에서 추적된 모든 변경 사항을 포함하여 현재 객체 데이터의 깊은 복사본을 포함하는 업데이트된 <see cref="IDataObject"/> 인스턴스입니다.
|
|
|
|
|
///</returns>
|
|
|
|
|
public IDataObject GetUpdatedObject(bool fromPool = true)
|
2025-06-06 02:17:54 +09:00
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
// 풀에서 새 DataArray 인스턴스를 가져옵니다.
|
2025-07-01 20:10:15 +09:00
|
|
|
var clone = fromPool ? DataArrayPool.Get() : new DataArray();
|
2025-06-26 19:46:13 +09:00
|
|
|
clone.FromCapacity(this.Count);
|
2025-06-05 20:09:28 +09:00
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
// 배열의 모든 DataObject를 순회하며 각각을 복제합니다.
|
|
|
|
|
foreach (var item in this)
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
// DataObject의 Clone 메서드를 호출하여 깊은 복사를 수행하고,
|
|
|
|
|
// base.Add를 사용해 추적 로직 없이 직접 추가합니다.
|
2025-07-01 20:10:15 +09:00
|
|
|
if (item.GetUpdatedObject(fromPool) is DataObject updatedObject)
|
2025-06-26 19:46:13 +09:00
|
|
|
{
|
|
|
|
|
clone.Add(updatedObject);
|
|
|
|
|
if (addedList.Contains(item))
|
|
|
|
|
{
|
|
|
|
|
clone.addedList.Add(updatedObject);
|
|
|
|
|
}
|
|
|
|
|
else if (removedList.Contains(item))
|
|
|
|
|
{
|
|
|
|
|
clone.removedList.Add(updatedObject);
|
|
|
|
|
}
|
|
|
|
|
else if (modifiedList.Contains(item))
|
|
|
|
|
{
|
|
|
|
|
clone.modifiedList.Add(updatedObject);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
return clone;
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
/// 업데이트 된 속성의 수.
|
2025-06-05 20:09:28 +09:00
|
|
|
/// </summary>
|
2025-11-10 20:32:10 +09:00
|
|
|
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면0을 반환합니다.</returns>
|
2025-06-26 19:46:13 +09:00
|
|
|
public int UpdatedCount { get => addedList.Count + modifiedList.Count + removedList.Count; }
|
2025-06-05 20:09:28 +09:00
|
|
|
|
2025-08-18 13:11:39 +09:00
|
|
|
private bool isUpdateImmediately = false;
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 업데이트가 즉시 적용되어야 하는지 여부를 나타냅니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public bool IsUpdateImmediately
|
|
|
|
|
{
|
|
|
|
|
get => isUpdateImmediately;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (isUpdateImmediately == value) return;
|
|
|
|
|
isUpdateImmediately = value;
|
|
|
|
|
// 내부 DataObject에도 동일한 값을 설정합니다.
|
|
|
|
|
foreach (var item in this)
|
|
|
|
|
{
|
|
|
|
|
item.IsUpdateImmediately = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-05 20:09:28 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 컬렉션의 모든 항목을 제거합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public new void Clear()
|
|
|
|
|
{
|
2025-08-18 13:11:39 +09:00
|
|
|
isUpdateImmediately = false;
|
2025-06-27 17:50:23 +09:00
|
|
|
// 내부 리스트와 변경 추적 리스트를 모두 비웁니다.
|
|
|
|
|
ReturnToDataObjectPool();
|
|
|
|
|
base.Clear();
|
|
|
|
|
ClearTrackedChanges();
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
/// 객체를 풀에 반환하기 전에 초기 상태로 리셋합니다.
|
2025-06-05 20:09:28 +09:00
|
|
|
/// </summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
public void Reset()
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
2025-06-27 17:50:23 +09:00
|
|
|
Clear();
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
public void ReturnToPool()
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
2025-07-22 19:58:14 +09:00
|
|
|
if (CreatedFromPool)
|
|
|
|
|
{
|
|
|
|
|
Reset();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 포함된 DataObject들을 먼저 풀에 반환합니다.
|
|
|
|
|
DataArrayPool.Return(this);
|
|
|
|
|
}
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
/// 이 DataArray에 포함된 모든 DataObject를 풀에 반환하고 리스트를 비웁니다.
|
|
|
|
|
/// DataArray의 사용이 끝났을 때 호출해야 합니다.
|
2025-06-05 20:09:28 +09:00
|
|
|
/// </summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
private void ReturnToDataObjectPool()
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
foreach (var item in this)
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
2025-06-27 17:50:23 +09:00
|
|
|
item.ReturnToPool();
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 변경된 인덱스 목록을 초기화합니다.
|
|
|
|
|
/// </summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
public void ClearTrackedChanges()
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
|
|
|
|
addedList.Clear();
|
|
|
|
|
removedList.Clear();
|
|
|
|
|
modifiedList.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
/// 동일한 상태와 값을 가진 현재 데이터 객체의 새 인스턴스를 생성합니다.
|
2025-06-05 20:09:28 +09:00
|
|
|
/// </summary>
|
2025-06-26 19:46:13 +09:00
|
|
|
/// <remarks>복제된 객체는 원본 객체와 독립적이므로, 한 객체를 변경해도 다른 객체에는 영향을 미치지 않습니다.
|
2025-06-27 17:50:23 +09:00
|
|
|
/// </remarks>
|
|
|
|
|
/// <param name="fromPool">객체 풀에서 복제할지 여부를 지정합니다. 기본값은 true입니다.</param>
|
2025-06-26 19:46:13 +09:00
|
|
|
/// <returns>현재 객체의 복사본인 새 <see cref="IDataObject"/> 인스턴스를 반환합니다.</returns>
|
2025-06-27 17:50:23 +09:00
|
|
|
public IDataObject Clone(bool fromPool = true)
|
|
|
|
|
{
|
|
|
|
|
return Copy(fromPool);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 현재 <see cref="DataArray"/> 인스턴스의 요소 및 관련 상태를 포함한 깊은 복사본을 생성합니다.
|
|
|
|
|
///
|
|
|
|
|
/// </summary>
|
2025-11-10 20:32:10 +09:00
|
|
|
/// <remarks>이 메서드는 현재 배열에 있는 모든 <see cref="DataObject"/> 요소의 깊은 복사본을 포함하는 새로운 <see cref=\"DataArray\"/> 인스턴스를 반환합니다.
|
2025-06-27 17:50:23 +09:00
|
|
|
/// 복사된 인스턴스는 내부 목록에서 추적하는 추가, 제거 또는 수정된 요소를 포함하여 원본 인스턴스의 상태도 복제합니다.
|
|
|
|
|
/// </remarks>
|
|
|
|
|
/// <param name="fromPool">객체 풀에서 복제할지 여부를 지정합니다. 기본값은 true입니다.</param>
|
|
|
|
|
/// <returns>현재 인스턴스의 깊은 복사본인 새로운 <see cref="DataArray"/> 인스턴스를 반환합니다.</returns>
|
|
|
|
|
public DataArray Copy(bool fromPool = true)
|
2025-06-05 20:09:28 +09:00
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
// 풀에서 새 DataArray 인스턴스를 가져옵니다.
|
2025-06-27 17:50:23 +09:00
|
|
|
DataArray clone;
|
2025-07-30 20:16:21 +09:00
|
|
|
if (fromPool) clone = DataArrayPool.Get();
|
2025-06-27 17:50:23 +09:00
|
|
|
else clone = new DataArray();
|
2025-06-26 19:46:13 +09:00
|
|
|
|
|
|
|
|
// 배열의 모든 DataObject를 순회하며 각각을 복제합니다.
|
|
|
|
|
foreach (var item in this)
|
|
|
|
|
{
|
|
|
|
|
// DataObject의 Clone 메서드를 호출하여 깊은 복사를 수행하고,
|
|
|
|
|
// base.Add를 사용해 추적 로직 없이 직접 추가합니다.
|
2025-06-27 17:50:23 +09:00
|
|
|
DataObject clonedItem = item.Copy(fromPool);
|
|
|
|
|
clone.Add(clonedItem);
|
|
|
|
|
if (addedList.Contains(item))
|
2025-06-26 19:46:13 +09:00
|
|
|
{
|
2025-06-27 17:50:23 +09:00
|
|
|
clone.addedList.Add(clonedItem);
|
2025-06-26 19:46:13 +09:00
|
|
|
}
|
2025-06-27 17:50:23 +09:00
|
|
|
else if (removedList.Contains(item))
|
|
|
|
|
{
|
|
|
|
|
clone.removedList.Add(clonedItem);
|
|
|
|
|
}
|
|
|
|
|
else if (modifiedList.Contains(item))
|
|
|
|
|
{
|
|
|
|
|
clone.modifiedList.Add(clonedItem);
|
|
|
|
|
}
|
2025-07-30 20:16:21 +09:00
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
}
|
2025-06-27 17:50:23 +09:00
|
|
|
|
2025-06-26 19:46:13 +09:00
|
|
|
return clone;
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// DataArray를 JArray로 변환합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public JArray ToJArray()
|
|
|
|
|
{
|
|
|
|
|
JArray array = new JArray();
|
|
|
|
|
foreach (var item in this)
|
|
|
|
|
{
|
|
|
|
|
array.Add(item);
|
|
|
|
|
}
|
|
|
|
|
return array;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// DataArray를 JArray로 암시적 변환합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static implicit operator JArray(DataArray dataArray)
|
|
|
|
|
{
|
|
|
|
|
return dataArray.ToJArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// JArray를 DataArray로 암시적 변환합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static implicit operator DataArray(JArray jArray)
|
|
|
|
|
{
|
2025-06-26 19:46:13 +09:00
|
|
|
var dataArray = DataArrayPool.Get();
|
|
|
|
|
dataArray.FromJArray(jArray);
|
|
|
|
|
return dataArray;
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
2025-11-10 20:32:10 +09:00
|
|
|
|
|
|
|
|
/// <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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-05 20:09:28 +09:00
|
|
|
}
|
|
|
|
|
}
|