1130 lines
45 KiB
C#
1130 lines
45 KiB
C#
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using UVC.Log;
|
|
|
|
namespace UVC.Data.Core
|
|
{
|
|
/// <summary>
|
|
/// 서로 다른 JSON 데이터 구조 간에 매핑 기능을 제공하는 클래스입니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 클래스는 JSON 데이터를 DataObject와 DataArray 형식으로 변환하며, 중첩된 구조(nested structure)도 처리할 수 있습니다.
|
|
/// 소스 JSON 객체의 속성을 Mask 객체에 정의된 타입에 따라 적절히 변환합니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// 기본 사용 예시:
|
|
/// <code>
|
|
/// // 소스 JSON 데이터
|
|
/// var sourceJson = JObject.Parse(@"{
|
|
/// ""name"": ""김철수"",
|
|
/// ""age"": 30,
|
|
/// ""isActive"": true
|
|
/// }");
|
|
///
|
|
/// // Mask 객체 (타입 지정용)
|
|
/// var maskJson = DataMask.Parse(@"{
|
|
/// ""name"": """",
|
|
/// ""age"": 0,
|
|
/// ""isActive"": false
|
|
/// }");
|
|
/// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정
|
|
///
|
|
/// var mapper = new DataMapper(maskJson);
|
|
/// DataObject result = mapper.Map(sourceJson);
|
|
///
|
|
/// // result는 원본과 동일한 구조이며 각 속성이 Mask에 따라 타입 변환됨
|
|
/// </code>
|
|
/// </example>
|
|
public class DataMapper
|
|
{
|
|
|
|
// 재귀 호출 제한하기 위한 설정 추가
|
|
private int maxRecursionDepth = 10;
|
|
|
|
/// <summary>
|
|
/// 재귀 최대 깊이를 설정하거나 가져옵니다. 이 값을 초과하는 중첩 객체는 간소화된 처리가 적용됩니다.
|
|
/// </summary>
|
|
public int MaxRecursionDepth
|
|
{
|
|
get => maxRecursionDepth;
|
|
set => maxRecursionDepth = Math.Max(1, value); // 최소 1 이상 보장
|
|
}
|
|
|
|
/// <summary>
|
|
/// 타입 변환을 위한 마스크 객체
|
|
/// </summary>
|
|
private DataMask mask;
|
|
public DataMask Mask => mask;
|
|
|
|
|
|
/// <summary>
|
|
/// 병렬 처리를 적용할 배열의 최소 크기
|
|
/// </summary>
|
|
private int parallelProcessingThreshold = 1000;
|
|
|
|
/// <summary>
|
|
/// 병렬 처리를 적용할 배열의 최소 크기를 설정하거나 가져옵니다.
|
|
/// </summary>
|
|
public int ParallelProcessingThreshold
|
|
{
|
|
get => parallelProcessingThreshold;
|
|
set => parallelProcessingThreshold = Math.Max(10, value); // 최소 10 이상 보장
|
|
}
|
|
|
|
/// <summary>
|
|
/// 매핑 중 발생한 변환 오류를 추적하는 딕셔너리
|
|
/// </summary>
|
|
private Dictionary<string, Exception> conversionErrors;
|
|
|
|
/// <summary>
|
|
/// 매핑 중 발생한 모든 변환 오류에 접근할 수 있는 읽기 전용 속성
|
|
/// </summary>
|
|
public IReadOnlyDictionary<string, Exception> ConversionErrors => conversionErrors;
|
|
|
|
/// <summary>
|
|
/// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다.
|
|
/// </summary>
|
|
public bool SupportsStreamParsing { get; internal set; } = true;
|
|
|
|
/// <summary>
|
|
/// 대용량 JSON 스트림을 판단 할때 스트림 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다.
|
|
/// </summary>
|
|
public int SupportsStreamLength { get; internal set; } = 10000;
|
|
|
|
/// <summary>
|
|
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
|
|
/// </summary>
|
|
/// <param name="mask">타입 변환을 위한 Mask JSON 객체</param>
|
|
/// <remarks>
|
|
/// Mask 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만,
|
|
/// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다.
|
|
/// </remarks>
|
|
public DataMapper(DataMask mask)
|
|
{
|
|
this.mask = mask ?? throw new ArgumentNullException(nameof(mask));
|
|
this.conversionErrors = new Dictionary<string, Exception>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 소스 객체를 Mask 객체를 기반으로 매핑하여 새로운 DataObject를 생성합니다.
|
|
/// </summary>
|
|
/// <param name="source">매핑할 원본 JSON 객체</param>
|
|
/// <returns>매핑된 DataObject 객체</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// var mapper = new DataMapper(maskJson);
|
|
/// DataObject result = mapper.Map(sourceJson);
|
|
/// ULog.Debug(result["name"].ToString()); // "김철수"
|
|
/// ULog.Debug(result["age"].ToObject<int>()); // 30
|
|
/// </code>
|
|
/// </example>
|
|
public DataObject Map(JObject source)
|
|
{
|
|
return MapObject(source, mask);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 소스 배열을 Mask 객체를 기반으로 매핑하여 새로운 DataArray를 생성합니다.
|
|
/// </summary>
|
|
/// <param name="source">매핑할 원본 JSON 배열</param>
|
|
/// <returns>매핑된 DataArray 객체</returns>
|
|
/// <remarks>
|
|
/// 이 메서드는 Mask 객체를 JArray로 변환하여 소스 배열의 각 항목을 매핑합니다.
|
|
/// Mask 배열이 비어있으면 원본 배열의 각 항목을 그대로 변환하고,
|
|
/// 그렇지 않으면 Mask 배열이 하나인 경우 첫 번째 항목을 템플릿으로 사용하고
|
|
/// 하나 이상인 경우 Mask 배열 개수에 맞춰 템플릿으로 사용(배열 길이가 3이고, 소스 배열 길이가 5일때, 소스배열 3까지만 매핑) 합니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// <code>
|
|
/// var sourceArray = JArray.Parse(@"[
|
|
/// { ""name"": ""김철수"", ""age"": 30 },
|
|
/// { ""name"": ""이영희"", ""age"": 25 }
|
|
/// ]");
|
|
///
|
|
/// var maskJson = DataMask.Parse(@"{
|
|
/// ""name"": """",
|
|
/// ""age"": 0
|
|
/// }");
|
|
/// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정
|
|
///
|
|
/// var mapper = new DataMapper(maskJson);
|
|
/// DataArray result = mapper.Map(sourceArray);
|
|
/// // result는 원본 배열과 동일한 구조의 DataArray
|
|
/// </code>
|
|
/// </example>
|
|
public DataArray Map(JArray source)
|
|
{
|
|
List<DataMask> arr = new List<DataMask>() { mask };
|
|
return MapArray(source, arr);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다.
|
|
/// </summary>
|
|
/// <param name="jsonStream">JSON 데이터 스트림</param>
|
|
/// <returns>매핑된 DataObject</returns>
|
|
public DataObject MapObjectStream(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<JObject>(reader);
|
|
|
|
return Map(sourceObject);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다.
|
|
/// </summary>
|
|
/// <param name="jsonStream">JSON 데이터 스트림</param>
|
|
/// <returns>매핑된 DataObject</returns>
|
|
public DataArray MapArrayStream(System.IO.Stream jsonStream)
|
|
{
|
|
if (jsonStream == null)
|
|
throw new ArgumentNullException(nameof(jsonStream));
|
|
|
|
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
|
{
|
|
// 청크 읽기 설정 - 메모리 사용량 최적화
|
|
reader.SupportMultipleContent = true;
|
|
|
|
var serializer = new Newtonsoft.Json.JsonSerializer();
|
|
var sourceArray = serializer.Deserialize<JArray>(reader);
|
|
|
|
return Map(sourceArray);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 객체를 재귀적으로 매핑합니다.
|
|
/// </summary>
|
|
/// <param name="sourceObject">원본 JSON 객체</param>
|
|
/// <param name="maskObject">Mask JSON 객체</param>
|
|
/// <param name="depth">현재 재귀 깊이</param>
|
|
/// <param name="path">현재 속성 경로(오류 추적용)</param>
|
|
/// <returns>매핑된 DataObject 객체</returns>
|
|
/// <remarks>
|
|
/// 이 메서드는 중첩된 객체와 배열을 포함하여 JSON 구조를 재귀적으로 처리합니다.
|
|
/// Mask 객체에 포함되지 않은 속성은 원본 값을 그대로 사용합니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// 중첩 객체 매핑 예시:
|
|
/// <code>
|
|
/// var sourceJson = JObject.Parse(@"{
|
|
/// ""user"": {
|
|
/// ""name"": ""김철수"",
|
|
/// ""address"": {
|
|
/// ""city"": ""서울"",
|
|
/// ""zipcode"": ""12345""
|
|
/// }
|
|
/// }
|
|
/// }");
|
|
///
|
|
/// var maskJson = DataMask.Parse(@"{
|
|
/// ""user"": {
|
|
/// ""name"": """",
|
|
/// ""address"": {
|
|
/// ""city"": """",
|
|
/// ""zipcode"": """"
|
|
/// }
|
|
/// }
|
|
/// }");
|
|
/// maskJson.ObjectIdKey = "user"; // DataObject의 Id로 사용할 속성 지정
|
|
///
|
|
/// var mapper = new DataMapper(maskJson);
|
|
/// var result = mapper.Map(sourceJson);
|
|
/// // result는 sourceJson과 동일한 중첩 구조를 유지
|
|
/// </code>
|
|
/// </example>
|
|
private DataObject MapObject(JObject sourceObject, DataMask maskObject, int depth = 0, string path = "")
|
|
{
|
|
if (maskObject == null)
|
|
{
|
|
var dataObj = DataObjectPool.Get();
|
|
dataObj.FromJObject(sourceObject);
|
|
return dataObj;
|
|
}
|
|
|
|
// Mask가 비어있으면 원본 객체를 그대로 사용, 깊이 제한에 도달하면 간소화된 처리
|
|
if (maskObject.Count == 0 || depth >= maxRecursionDepth)
|
|
{
|
|
DataObject dObj = DataObjectPool.Get();
|
|
dObj.IdKey = maskObject.ObjectIdKey;
|
|
dObj.Name = maskObject.ObjectName;
|
|
|
|
|
|
// 속성 이름 변환 처리
|
|
foreach (var property in sourceObject.Properties())
|
|
{
|
|
string propertyName = property.Name;
|
|
if (maskObject.NamesForReplace != null && maskObject.NamesForReplace.ContainsKey(propertyName))
|
|
{
|
|
propertyName = maskObject.NamesForReplace[propertyName];
|
|
}
|
|
dObj[propertyName] = ConvertJTokenToObject(property.Value);
|
|
}
|
|
return dObj;
|
|
}
|
|
|
|
DataObject target = DataObjectPool.Get();
|
|
target.IdKey = maskObject.ObjectIdKey;
|
|
target.Name = maskObject.ObjectName;
|
|
|
|
foreach (var property in sourceObject.Properties())
|
|
{
|
|
string propertyName = property.Name;
|
|
string currentPath = string.IsNullOrEmpty(path) ? propertyName : $"{path}.{propertyName}";
|
|
|
|
if (maskObject.ContainsKey(propertyName))
|
|
{
|
|
object maskValue = maskObject[propertyName];
|
|
JToken sourceValue = property.Value;
|
|
|
|
// 속성 이름 변환 처리
|
|
if (maskObject.NamesForReplace != null && maskObject.NamesForReplace.ContainsKey(propertyName))
|
|
{
|
|
propertyName = maskObject.NamesForReplace[propertyName];
|
|
}
|
|
|
|
try
|
|
{
|
|
// 중첩된 객체 처리
|
|
if (sourceValue.Type == JTokenType.Object && maskValue is DataMask maskSubObject)
|
|
{
|
|
target[propertyName] = MapObject((JObject)sourceValue, maskSubObject, depth + 1, currentPath);
|
|
}
|
|
// 중첩된 배열 처리
|
|
else if (sourceValue.Type == JTokenType.Array && maskValue is List<DataMask> maskArrayTemplate)
|
|
{
|
|
var arr = MapArray((JArray)sourceValue, maskArrayTemplate, currentPath);
|
|
target[propertyName] = arr;
|
|
}
|
|
// 일반 속성 처리
|
|
else
|
|
{
|
|
MapProperty(propertyName, sourceValue, maskValue, target, currentPath);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 예외 정보 저장 후 기본값 설정
|
|
conversionErrors[currentPath] = ex;
|
|
ULog.Warning($"매핑 중 오류 발생 - 경로: {currentPath}", ex);
|
|
|
|
// 마스크 값 타입에 맞는 기본값 설정
|
|
SetDefaultValueForProperty(propertyName, maskValue, target);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Mask에 없는 속성은 무시 (필요시 설정을 통해 포함 가능)
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 예외 발생 시 마스크 값 타입에 맞는 기본값을 설정합니다.
|
|
/// </summary>
|
|
private void SetDefaultValueForProperty(string propertyName, object maskValue, DataObject target)
|
|
{
|
|
if (maskValue is string)
|
|
{
|
|
target[propertyName] = string.Empty;
|
|
}
|
|
else if (maskValue is int || maskValue is long || maskValue is short || maskValue is byte)
|
|
{
|
|
target[propertyName] = 0;
|
|
}
|
|
else if (maskValue is double || maskValue is float || maskValue is decimal)
|
|
{
|
|
target[propertyName] = 0.0;
|
|
}
|
|
else if (maskValue is bool)
|
|
{
|
|
target[propertyName] = false;
|
|
}
|
|
else if (maskValue is DateTime)
|
|
{
|
|
target[propertyName] = null;
|
|
}
|
|
else if (maskValue is DataMask)
|
|
{
|
|
target[propertyName] = DataObjectPool.Get();
|
|
}
|
|
else if (maskValue is List<DataMask>)
|
|
{
|
|
target[propertyName] = DataArrayPool.Get();
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// JToken을 실제 객체로 변환하는 헬퍼 메서드
|
|
/// </summary>
|
|
/// <param name="token">변환할 JToken 객체</param>
|
|
/// <returns>변환된 .NET 객체</returns>
|
|
/// <remarks>
|
|
/// 이 메서드는 JToken의 타입에 따라 적절한 .NET 타입의 객체로 변환합니다.
|
|
/// 객체는 DataObject로, 배열은 DataArray로 변환되며, 기본 타입은 해당하는 .NET 타입으로 변환됩니다.
|
|
/// </remarks>
|
|
private object ConvertJTokenToObject(JToken token)
|
|
{
|
|
if (token == null) return null;
|
|
|
|
switch (token.Type)
|
|
{
|
|
case JTokenType.Object:
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.FromJObject((JObject)token);
|
|
return dataObject;
|
|
case JTokenType.Array:
|
|
JArray array = (JArray)token;
|
|
if (array.All(item => item.Type == JTokenType.Object))
|
|
{
|
|
return DataArrayPool.Get().FromJArray(array);
|
|
}
|
|
return array.ToObject<object[]>();
|
|
case JTokenType.Integer:
|
|
return token.ToObject<int>();
|
|
case JTokenType.Float:
|
|
return token.ToObject<double>();
|
|
case JTokenType.String:
|
|
return token.ToObject<string>();
|
|
case JTokenType.Boolean:
|
|
return token.ToObject<bool>();
|
|
case JTokenType.Date:
|
|
return token.ToObject<DateTime>();
|
|
case JTokenType.Null:
|
|
return null;
|
|
default:
|
|
return token.ToObject<object>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 배열을 재귀적으로 매핑합니다.
|
|
/// </summary>
|
|
/// <param name="sourceArray">원본 JSON 배열</param>
|
|
/// <param name="maskArray">Mask JSON 배열</param>
|
|
/// <param name="path">현재 속성 경로(오류 추적용)</param>
|
|
/// <returns>매핑된 DataArray 객체</returns>
|
|
/// <remarks>
|
|
/// Mask 배열이 비어있으면, 원본 배열의 각 항목을 DataObject로 변환하여 복사합니다.
|
|
/// 단순 값(문자열, 숫자 등)은 "value" 키를 가진 DataObject로 래핑됩니다.
|
|
///
|
|
/// Mask 배열이 비어있지 않으면, Mask 배열의 첫 번째 항목을 템플릿으로 사용하고
|
|
/// 하나 이상인 경우 Mask 배열 개수에 맞춰 템플릿으로 사용(배열 길이가 3이고, 소스 배열 길이가 5일때, 소스배열 3까지만 매핑) 합니다.
|
|
/// 원본 배열의 각 항목을 매핑합니다. 중첩 배열은 "items" 키를 가진 DataObject로 래핑됩니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// 배열 매핑 예시:
|
|
/// <code>
|
|
/// var sourceJson = JObject.Parse(@"{
|
|
/// ""contacts"": [
|
|
/// { ""type"": ""mobile"", ""number"": ""010-1234-5678"" },
|
|
/// { ""type"": ""home"", ""number"": ""02-123-4567"" }
|
|
/// ]
|
|
/// }");
|
|
///
|
|
/// var maskJson = DataMask.Parse(@"{
|
|
/// ""contacts"": [
|
|
/// { ""type"": """", ""number"": """" }
|
|
/// ]
|
|
/// }");
|
|
/// maskJson.ObjectIdKey = "contacts"; // DataObject의 Id로 사용할 속성 지정
|
|
///
|
|
/// var mapper = new DataMapper(maskJson);
|
|
/// var result = mapper.Map(sourceJson);
|
|
/// // result.contacts는 원본 배열과 동일한 구조의 DataArray
|
|
/// </code>
|
|
/// </example>
|
|
private DataArray MapArray(JArray sourceArray, List<DataMask> maskTemplates, string path = "")
|
|
{
|
|
// 빠른 초기 크기 할당으로 재할당 방지
|
|
DataArray targetArray = DataArrayPool.Get().FromCapacity(sourceArray.Count);
|
|
|
|
// 특정 크기 이상일 경우 병렬 처리 적용
|
|
if (sourceArray.Count > parallelProcessingThreshold && maskTemplates.Count <= 1)
|
|
{
|
|
return MapArrayInParallel(sourceArray, maskTemplates, path);
|
|
}
|
|
|
|
// Mask 배열이 비어있으면 원본 배열을 그대로 사용
|
|
if (maskTemplates == null || maskTemplates.Count == 0)
|
|
{
|
|
for (int i = 0; i < sourceArray.Count; i++)
|
|
{
|
|
JToken sourceItem = sourceArray[i];
|
|
string itemPath = $"{path}[{i}]";
|
|
|
|
try
|
|
{
|
|
if (sourceItem.Type == JTokenType.Object)
|
|
{
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.FromJObject((JObject)sourceItem);
|
|
targetArray.Add(dataObject);
|
|
}
|
|
else
|
|
{
|
|
// DataObject가 아닌 경우, 새 DataObject를 만들고 값을 넣어줍니다
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.Add("value", ConvertJTokenToObject(sourceItem));
|
|
targetArray.Add(dataObject);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
conversionErrors[itemPath] = ex;
|
|
ULog.Warning($"배열 항목 변환 오류 - 경로: {itemPath}", ex);
|
|
|
|
// 오류 발생 시 빈 객체 추가
|
|
targetArray.Add(DataObjectPool.Get());
|
|
}
|
|
}
|
|
return targetArray;
|
|
}
|
|
|
|
// Mask 템플릿 사용 - 반복 사용 혹은 일치 사용
|
|
for (int i = 0; i < sourceArray.Count; i++)
|
|
{
|
|
JToken sourceItem = sourceArray[i];
|
|
string itemPath = $"{path}[{i}]";
|
|
|
|
// 현재 인덱스에 맞는 마스크 템플릿 선택 (또는 첫 번째 템플릿 재사용)
|
|
DataMask maskTemplate;
|
|
if (maskTemplates.Count > 1)
|
|
{
|
|
// maskTemplates.Count > 1이면서 소스 항목 개수 보다 적은 경우
|
|
// maskTemplates.Count 보다 큰 인덱스는 무시
|
|
// 예: maskTemplates.Count가 3이고, sourceArray.Count가 5인 경우
|
|
// 0, 1, 2 인덱스는 maskTemplates[0], maskTemplates[1], maskTemplates[2] 사용
|
|
// 3, 4 인덱스는 저장 않함
|
|
if (i >= maskTemplates.Count)
|
|
continue;
|
|
int templateIndex = Math.Min(i, maskTemplates.Count - 1);
|
|
maskTemplate = maskTemplates[templateIndex];
|
|
}
|
|
else
|
|
{
|
|
// 단일 템플릿 반복 사용
|
|
maskTemplate = maskTemplates[0];
|
|
}
|
|
|
|
try
|
|
{
|
|
if (sourceItem.Type == JTokenType.Object)
|
|
{
|
|
JObject sourceItem2 = (JObject)sourceItem;
|
|
targetArray.Add(MapObject(sourceItem2, maskTemplate, 0, itemPath));
|
|
}
|
|
else if (sourceItem.Type == JTokenType.Array)
|
|
{
|
|
// DataArray는 DataObject만 담을 수 있으므로, 배열을 처리하는 방식을 변경해야 함
|
|
var nestedArray = MapArray((JArray)sourceItem, maskTemplates, itemPath);
|
|
var container = DataObjectPool.Get();
|
|
container.Add("items", nestedArray);
|
|
targetArray.Add(container);
|
|
}
|
|
else
|
|
{
|
|
// 단순 값을 DataObject로 래핑
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.Add("value", ConvertJTokenToObject(sourceItem));
|
|
targetArray.Add(dataObject);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
conversionErrors[itemPath] = ex;
|
|
ULog.Warning($"배열 항목 변환 오류 - 경로: {itemPath}", ex);
|
|
|
|
// 오류 발생 시 빈 객체 추가
|
|
targetArray.Add(DataObjectPool.Get());
|
|
}
|
|
}
|
|
|
|
return targetArray;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 대형 배열을 병렬로 처리하는 배열 매핑 메서드
|
|
/// </summary>
|
|
private DataArray MapArrayInParallel(JArray sourceArray, List<DataMask> maskTemplates, string path)
|
|
{
|
|
DataArray targetArray = DataArrayPool.Get().FromCapacity(sourceArray.Count);
|
|
DataMask maskTemplate = maskTemplates.Count > 0 ? maskTemplates[0] : new DataMask();
|
|
|
|
var parallelOptions = new ParallelOptions
|
|
{
|
|
MaxDegreeOfParallelism = Environment.ProcessorCount
|
|
};
|
|
|
|
// 결과를 담을 배열 (순서 보존을 위해)
|
|
var results = new DataObject[sourceArray.Count];
|
|
|
|
try
|
|
{
|
|
Parallel.For(0, sourceArray.Count, parallelOptions, i =>
|
|
{
|
|
var sourceItem = sourceArray[i];
|
|
string itemPath = $"{path}[{i}]";
|
|
|
|
try
|
|
{
|
|
if (sourceItem.Type == JTokenType.Object && maskTemplate != null)
|
|
{
|
|
results[i] = MapObject((JObject)sourceItem, maskTemplate, 0, itemPath);
|
|
}
|
|
else
|
|
{
|
|
// 단순 값 처리
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.Add("value", ConvertJTokenToObject(sourceItem));
|
|
results[i] = dataObject;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lock (conversionErrors)
|
|
{
|
|
conversionErrors[itemPath] = ex;
|
|
}
|
|
ULog.Warning($"병렬 배열 처리 오류 - 경로: {itemPath}", ex);
|
|
|
|
// 오류 발생 시 빈 객체 생성
|
|
results[i] = DataObjectPool.Get();
|
|
}
|
|
});
|
|
|
|
// 결과를 순서대로 추가
|
|
targetArray.AddRange(results);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ULog.Error($"병렬 배열 매핑 중 치명적 오류 발생: {ex.Message}", ex);
|
|
|
|
// 병렬 처리 실패 시 일반 순차 처리로 대체
|
|
return MapArraySequential(sourceArray, maskTemplates, path);
|
|
}
|
|
|
|
return targetArray;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 병렬 처리가 실패할 경우 대체 사용할 순차적 배열 처리 메서드
|
|
/// </summary>
|
|
private DataArray MapArraySequential(JArray sourceArray, List<DataMask> maskTemplates, string path)
|
|
{
|
|
DataArray targetArray = DataArrayPool.Get().FromCapacity(sourceArray.Count);
|
|
DataMask maskTemplate = maskTemplates.Count > 0 ? maskTemplates[0] : new DataMask();
|
|
|
|
for (int i = 0; i < sourceArray.Count; i++)
|
|
{
|
|
JToken sourceItem = sourceArray[i];
|
|
string itemPath = $"{path}[{i}]";
|
|
|
|
try
|
|
{
|
|
if (sourceItem.Type == JTokenType.Object)
|
|
{
|
|
targetArray.Add(MapObject((JObject)sourceItem, maskTemplate, 0, itemPath));
|
|
}
|
|
else
|
|
{
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.Add("value", ConvertJTokenToObject(sourceItem));
|
|
targetArray.Add(dataObject);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
conversionErrors[itemPath] = ex;
|
|
ULog.Warning($"순차 배열 처리 오류 - 경로: {itemPath}", ex);
|
|
targetArray.Add(DataObjectPool.Get());
|
|
}
|
|
}
|
|
|
|
return targetArray;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 개별 속성을 매핑합니다.
|
|
/// </summary>
|
|
/// <param name="propertyName">속성 이름</param>
|
|
/// <param name="sourceValue">매핑할 원본 값</param>
|
|
/// <param name="maskValue">타입을 결정하는 Mask 값</param>
|
|
/// <param name="target">값을 추가할 대상 DataObject</param>
|
|
/// <param name="path">현재 속성 경로(오류 추적용)</param>
|
|
/// <remarks>
|
|
/// 이 메서드는 Mask 값의 타입에 따라 원본 값을 적절한 타입으로 변환합니다.
|
|
///
|
|
/// 지원되는 타입:
|
|
/// - 문자열 (string)
|
|
/// - 정수 (int)
|
|
/// - 실수 (double, float)
|
|
/// - 불리언 (bool)
|
|
/// - 날짜/시간 (DateTime)
|
|
/// - 열거형 (Enum)
|
|
/// - DataMap (문자열 매핑 딕셔너리)
|
|
/// - DataMask (중첩된 마스크 객체)
|
|
/// - List<DataMask> (배열 마스크)
|
|
///
|
|
/// 타입 변환이 불가능한 경우 원본 값이 그대로 사용됩니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// 다양한 타입 매핑 예시:
|
|
/// <code>
|
|
/// var sourceJson = JObject.Parse(@"{
|
|
/// ""name"": ""김철수"",
|
|
/// ""age"": 30,
|
|
/// ""height"": 175.5,
|
|
/// ""isActive"": true,
|
|
/// ""birthDate"": ""1990-01-01T00:00:00"",
|
|
/// ""status"": ""Active""
|
|
/// }");
|
|
///
|
|
/// // Mask 객체 설정 (열거형 포함)
|
|
/// var maskJson = new DataMask();
|
|
/// maskJson["name"] = "";
|
|
/// maskJson["age"] = 0;
|
|
/// maskJson["height"] = 0.0;
|
|
/// maskJson["isActive"] = false;
|
|
/// maskJson["birthDate"] = JToken.FromObject(DateTime.Now);
|
|
/// maskJson["status"] = JToken.FromObject(UserStatus.Inactive);
|
|
///
|
|
/// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정
|
|
///
|
|
/// var mapper = new DataMapper(maskJson);
|
|
/// var result = mapper.Map(sourceJson);
|
|
/// // result에는 모든 속성이 적절한 타입으로 변환됨
|
|
/// </code>
|
|
/// </example>
|
|
private void MapProperty(string propertyName, JToken sourceValue, object maskValue, DataObject target, string path)
|
|
{
|
|
|
|
// 소스 값이 널이면 바로 널 설정
|
|
if (sourceValue == null || sourceValue.Type == JTokenType.Null)
|
|
{
|
|
target[propertyName] = null;
|
|
return;
|
|
}
|
|
|
|
// 타입별 매핑 처리를 분리된 메서드로 호출
|
|
if (maskValue is string)
|
|
{
|
|
MapStringProperty(propertyName, sourceValue, target);
|
|
}
|
|
else if (maskValue is int || maskValue is long || maskValue is short || maskValue is byte)
|
|
{
|
|
MapIntegerProperty(propertyName, sourceValue, target);
|
|
}
|
|
else if (maskValue is double || maskValue is float || maskValue is decimal)
|
|
{
|
|
MapFloatingPointProperty(propertyName, sourceValue, target);
|
|
}
|
|
else if (maskValue is bool)
|
|
{
|
|
MapBooleanProperty(propertyName, sourceValue, target);
|
|
}
|
|
else if (maskValue is DateTime)
|
|
{
|
|
MapDateTimeProperty(propertyName, sourceValue, target);
|
|
}
|
|
else if (maskValue is Enum || (maskValue?.GetType()?.IsEnum ?? false))
|
|
{
|
|
MapEnumProperty(propertyName, sourceValue, maskValue, target);
|
|
}
|
|
else if (maskValue is Dictionary<string, object> dicObj && dicObj.GetType() == typeof(DataMask))
|
|
{
|
|
MapNestedObjectProperty(propertyName, sourceValue, (DataMask)maskValue, target, path);
|
|
}
|
|
else if (maskValue is List<DataMask>)
|
|
{
|
|
MapNestedArrayProperty(propertyName, sourceValue, (List<DataMask>)maskValue, target, path);
|
|
}
|
|
else if (maskValue is Dictionary<string, string> || maskValue is DataMap)
|
|
{
|
|
MapDataMapProperty(propertyName, sourceValue, maskValue, target);
|
|
}
|
|
else if (maskValue is JToken jToken)
|
|
{
|
|
MapJTokenProperty(propertyName, sourceValue, jToken, target);
|
|
}
|
|
else
|
|
{
|
|
// 기타 타입은 그대로 객체로 변환
|
|
target[propertyName] = ConvertJTokenToObject(sourceValue);
|
|
}
|
|
}
|
|
|
|
#region 타입별 속성 매핑 메서드
|
|
|
|
/// <summary>
|
|
/// 문자열 타입 속성 매핑
|
|
/// </summary>
|
|
private void MapStringProperty(string propertyName, JToken sourceValue, DataObject target)
|
|
{
|
|
if (sourceValue.Type == JTokenType.String || sourceValue.Type == JTokenType.Integer ||
|
|
sourceValue.Type == JTokenType.Float || sourceValue.Type == JTokenType.Boolean)
|
|
{
|
|
target[propertyName] = sourceValue.ToObject<string>();
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = sourceValue.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 정수 타입 속성 매핑
|
|
/// </summary>
|
|
private void MapIntegerProperty(string propertyName, JToken sourceValue, DataObject target)
|
|
{
|
|
if (sourceValue.Type == JTokenType.Integer || sourceValue.Type == JTokenType.Float ||
|
|
sourceValue.Type == JTokenType.String)
|
|
{
|
|
try
|
|
{
|
|
// 명시적으로 값 범위 검사를 통해 오버플로우 방지
|
|
var doubleValue = sourceValue.ToObject<double>();
|
|
if (doubleValue > int.MaxValue || doubleValue < int.MinValue)
|
|
{
|
|
throw new OverflowException($"값 {doubleValue}가 Int32 범위를 벗어남");
|
|
}
|
|
|
|
target[propertyName] = Convert.ToInt32(doubleValue);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
target[propertyName] = 0; // 변환 실패시 기본값
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 부동소수점 타입 속성 매핑
|
|
/// </summary>
|
|
private void MapFloatingPointProperty(string propertyName, JToken sourceValue, DataObject target)
|
|
{
|
|
if (sourceValue.Type == JTokenType.Float || sourceValue.Type == JTokenType.Integer ||
|
|
sourceValue.Type == JTokenType.String)
|
|
{
|
|
try
|
|
{
|
|
target[propertyName] = sourceValue.ToObject<double>();
|
|
}
|
|
catch
|
|
{
|
|
target[propertyName] = 0.0; // 변환 실패시 기본값
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = 0.0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 불리언 타입 속성 매핑
|
|
/// </summary>
|
|
private void MapBooleanProperty(string propertyName, JToken sourceValue, DataObject target)
|
|
{
|
|
if (sourceValue.Type == JTokenType.Boolean)
|
|
{
|
|
target[propertyName] = sourceValue.ToObject<bool>();
|
|
}
|
|
else if (sourceValue.Type == JTokenType.Integer)
|
|
{
|
|
// 0은 false, 나머지는 true로 처리
|
|
target[propertyName] = sourceValue.ToObject<int>() != 0;
|
|
}
|
|
else if (sourceValue.Type == JTokenType.String)
|
|
{
|
|
string strValue = sourceValue.ToString().ToLower();
|
|
// 일반적인 참/거짓 문자열 처리
|
|
if (strValue == "true" || strValue == "yes" || strValue == "y" || strValue == "1")
|
|
{
|
|
target[propertyName] = true;
|
|
}
|
|
else if (strValue == "false" || strValue == "no" || strValue == "n" || strValue == "0")
|
|
{
|
|
target[propertyName] = false;
|
|
}
|
|
else
|
|
{
|
|
// 나머지는 bool.Parse 시도
|
|
try
|
|
{
|
|
target[propertyName] = bool.Parse(strValue);
|
|
}
|
|
catch
|
|
{
|
|
target[propertyName] = false; // 변환 실패시 기본값
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// DateTime 타입 속성 매핑
|
|
/// </summary>
|
|
private void MapDateTimeProperty(string propertyName, JToken sourceValue, DataObject target)
|
|
{
|
|
if (sourceValue.Type == JTokenType.Date)
|
|
{
|
|
target[propertyName] = sourceValue.ToObject<DateTime>();
|
|
}
|
|
else if (sourceValue.Type == JTokenType.String)
|
|
{
|
|
string dateStr = sourceValue.ToString();
|
|
if (DateTime.TryParse(dateStr, out DateTime dateValue))
|
|
{
|
|
target[propertyName] = dateValue;
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 열거형 타입 속성 매핑
|
|
/// </summary>
|
|
private void MapEnumProperty(string propertyName, JToken sourceValue, object maskValue, DataObject target)
|
|
{
|
|
try
|
|
{
|
|
Type enumType = maskValue.GetType();
|
|
if (sourceValue.Type == JTokenType.String)
|
|
{
|
|
target[propertyName] = Enum.Parse(enumType, sourceValue.ToString(), true);
|
|
}
|
|
else if (sourceValue.Type == JTokenType.Integer)
|
|
{
|
|
target[propertyName] = Enum.ToObject(enumType, sourceValue.ToObject<int>());
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = maskValue; // 변환 불가능하면 마스크 기본값 사용
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
target[propertyName] = maskValue; // 변환 실패시 마스크 기본값 사용
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 중첩된 객체 타입 속성 매핑
|
|
/// </summary>
|
|
private void MapNestedObjectProperty(string propertyName, JToken sourceValue, DataMask nestedMask, DataObject target, string path)
|
|
{
|
|
if (sourceValue.Type == JTokenType.Object)
|
|
{
|
|
target[propertyName] = MapObject((JObject)sourceValue, nestedMask, 0, $"{path}.{propertyName}");
|
|
}
|
|
else if (sourceValue.Type == JTokenType.String)
|
|
{
|
|
// 문자열을 JSON으로 파싱 시도
|
|
try
|
|
{
|
|
JObject jObj = JObject.Parse(sourceValue.ToString());
|
|
target[propertyName] = MapObject(jObj, nestedMask, 0, $"{path}.{propertyName}");
|
|
}
|
|
catch
|
|
{
|
|
target[propertyName] = sourceValue.ToString();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = ConvertJTokenToObject(sourceValue);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 중첩된 배열 타입 속성 매핑
|
|
/// </summary>
|
|
private void MapNestedArrayProperty(string propertyName, JToken sourceValue, List<DataMask> maskTemplates, DataObject target, string path)
|
|
{
|
|
if (sourceValue.Type == JTokenType.Array)
|
|
{
|
|
target[propertyName] = MapArray((JArray)sourceValue, maskTemplates, $"{path}.{propertyName}");
|
|
}
|
|
else if (sourceValue.Type == JTokenType.String)
|
|
{
|
|
// 문자열을 JSON 배열로 파싱 시도
|
|
try
|
|
{
|
|
JArray jArr = JArray.Parse(sourceValue.ToString());
|
|
target[propertyName] = MapArray(jArr, maskTemplates, $"{path}.{propertyName}");
|
|
}
|
|
catch
|
|
{
|
|
// 배열 파싱 실패시 단일 항목 배열로 처리
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.Add("value", sourceValue.ToString());
|
|
var array = DataArrayPool.Get().FromCapacity(1);
|
|
array.Add(dataObject);
|
|
target[propertyName] = array;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 다른 타입은 단일 항목 배열로 변환
|
|
var dataObject = DataObjectPool.Get();
|
|
dataObject.Add("value", ConvertJTokenToObject(sourceValue));
|
|
var array = DataArrayPool.Get().FromCapacity(1);
|
|
array.Add(dataObject);
|
|
target[propertyName] = array;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// DataMap 타입 속성 매핑 (문자열 매핑)
|
|
/// </summary>
|
|
private void MapDataMapProperty(string propertyName, JToken sourceValue, object maskValue, DataObject target)
|
|
{
|
|
Dictionary<string, string> dataMap;
|
|
|
|
if (maskValue is DataMap dm)
|
|
{
|
|
dataMap = dm;
|
|
}
|
|
else
|
|
{
|
|
dataMap = (Dictionary<string, string>)maskValue;
|
|
}
|
|
|
|
if (sourceValue.Type == JTokenType.String)
|
|
{
|
|
string strValue = sourceValue.ToString();
|
|
if (dataMap.ContainsKey(strValue))
|
|
{
|
|
target[propertyName] = dataMap[strValue];
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = strValue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = sourceValue.ToObject<object>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// JToken 타입 속성 매핑 (이전 코드와의 호환성)
|
|
/// </summary>
|
|
private void MapJTokenProperty(string propertyName, JToken sourceValue, JToken jToken, DataObject target)
|
|
{
|
|
if (sourceValue.Type == jToken.Type)
|
|
{
|
|
switch (sourceValue.Type)
|
|
{
|
|
case JTokenType.String:
|
|
target[propertyName] = sourceValue.ToObject<string>();
|
|
return;
|
|
case JTokenType.Integer:
|
|
target[propertyName] = sourceValue.ToObject<int>();
|
|
return;
|
|
case JTokenType.Float:
|
|
target[propertyName] = sourceValue.ToObject<double>();
|
|
return;
|
|
case JTokenType.Boolean:
|
|
target[propertyName] = sourceValue.ToObject<bool>();
|
|
return;
|
|
case JTokenType.Date:
|
|
target[propertyName] = sourceValue.ToObject<DateTime>();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (jToken.Type == JTokenType.Date && sourceValue.Type == JTokenType.String)
|
|
{
|
|
string dateStr = sourceValue.ToObject<string>();
|
|
if (DateTime.TryParse(dateStr, out DateTime dateValue))
|
|
{
|
|
target[propertyName] = dateValue;
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = null;
|
|
}
|
|
}
|
|
else if (jToken.Type == JTokenType.Object && sourceValue.Type == JTokenType.String)
|
|
{
|
|
try
|
|
{
|
|
// 먼저 DataMap로 변환 시도
|
|
var dataMap = jToken.ToObject<DataMap>();
|
|
if (dataMap != null)
|
|
{
|
|
string strValue = sourceValue.ToObject<string>();
|
|
if (dataMap.ContainsKey(strValue))
|
|
{
|
|
target[propertyName] = dataMap[strValue];
|
|
}
|
|
else
|
|
{
|
|
target[propertyName] = strValue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// DataMap가 아니면 소스 값 그대로 사용
|
|
target[propertyName] = sourceValue.ToObject<object>();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 변환 실패 시 소스 값 그대로 사용
|
|
target[propertyName] = sourceValue.ToObject<object>();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 기타 타입은 그대로 객체로 변환
|
|
target[propertyName] = ConvertJTokenToObject(sourceValue);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|
|
|