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

388 lines
20 KiB
C#

#nullable enable
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using UnityEngine;
using UVC.Data.Core;
using UVC.Log;
namespace UVC.Data.Http
{
/// <summary>
/// HTTP 응답 데이터를 처리하는 정적 클래스입니다.
/// </summary>
/// <remarks>
/// 이 클래스는 `HttpDataFetcher`로부터 데이터 처리 로직을 분리하여 재사용성을 높입니다.
/// HTTP 응답(문자열)을 입력받아, 설정된 규칙(`HttpRequestConfig`)에 따라 파싱, 유효성 검사,
/// 데이터 변환(`IDataObject`로 매핑) 및 결과 핸들러 호출까지의 과정을 담당합니다.
/// 모든 메서드는 정적(static)이므로 인스턴스 생성 없이 사용할 수 있습니다.
/// </remarks>
public static class HttpDataProcessor
{
/// <summary>
/// HTTP 응답 문자열을 비동기적으로 처리하고, 결과를 IDataObject로 변환한 후 적절한 핸들러를 호출합니다.
/// </summary>
/// <param name="key">요청을 식별하는 고유 키. 로그 및 데이터 저장소에서 사용됩니다.</param>
/// <param name="info">HTTP 요청에 대한 모든 설정(URL, 매퍼, 핸들러 등)을 담고 있는 객체입니다.</param>
/// <param name="responseContent">HTTP 요청으로부터 받은 원시 응답 문자열입니다.</param>
/// <param name="cancellationToken">비동기 작업을 취소하기 위한 토큰입니다.</param>
/// <remarks>
/// 이 메서드는 다음과 같은 순서로 응답을 처리합니다.
/// 1. 응답 문자열이 비어 있는지 확인합니다.
/// 2. `HttpResponseMask`를 적용하여 응답의 성공 여부를 판단하고 실제 데이터 부분을 추출합니다.
/// 3. 데이터가 분할 처리(Split) 대상인지, 단일 객체(Object)인지, 배열(Array)인지 확인하고 각각의 처리 메서드를 호출합니다.
/// 4. 처리된 데이터(`IDataObject`)를 `DataRepository`에 추가하거나 업데이트합니다.
/// 5. 설정에 따라 성공(`SuccessHandler`) 또는 실패(`FailHandler`) 핸들러를 메인 스레드에서 호출하여 UI 업데이트 등을 안전하게 처리할 수 있도록 합니다.
/// 6. 모든 과정에서 발생하는 예외(JSON 파싱, 작업 취소 등)를 처리하고 로그를 남깁니다.
/// </remarks>
public static void ProcessResponse(string key, HttpRequestConfig info, string responseContent, CancellationToken cancellationToken)
{
// 매핑된 최종 데이터 객체를 담을 변수입니다. try-finally 블록에서 자원 해제를 위해 외부에 선언합니다.
IDataObject? mappedObject = null;
try
{
// 앞뒤 공백 제거
responseContent = responseContent.Trim();
// 응답이 비어있으면 실패로 간주하고 FailHandler를 호출합니다.
if (string.IsNullOrEmpty(responseContent))
{
// 핸들러는 UI와 상호작용할 수 있으므로 메인 스레드에서 호출합니다.
if (info.FailHandler != null)
{
UniTask.Post(() => info.FailHandler.Invoke("Response content is null or empty."));
}
return;
}
// ResponseMask를 사용해 응답이 성공적인지 확인하고, 실제 데이터 부분을 추출합니다.
// 예를 들어, 응답이 {"status":"OK", "data":{...}} 형태일 때, "status"가 "OK"인지 확인하고 "data" 부분만 가져옵니다.
HttpResponseResult responseResult = info.ResponseMask.Apply(responseContent);
if (!responseResult.IsSuccess)
{
if (info.FailHandler != null)
{
string errorMessage = responseResult.Message!;
UniTask.Post(() => info.FailHandler.Invoke(errorMessage));
}
return;
}
// 마스크를 통해 추출된 실제 데이터
responseContent = responseResult.Data!.Trim();
// 응답을 최상위 키를 기준으로 분할 처리해야 하는 경우
if (info.SplitResponseByKey && responseContent.StartsWith("{"))
{
ProcessSplitResponse(info, responseContent, cancellationToken);
return; // 분할 처리가 완료되면 이 메서드의 나머지 로직은 실행하지 않습니다.
}
// 응답이 JSON 객체 형태인 경우 (e.g., {"id": 1, "name": "item"})
else if (responseContent.StartsWith("{"))
{
mappedObject = ProcessObjectResponse(info, responseContent, cancellationToken);
if (mappedObject == null) return;
}
// 응답이 JSON 배열 형태인 경우 (e.g., [{"id": 1}, {"id": 2}])
else if (responseContent.StartsWith("["))
{
mappedObject = ProcessArrayResponse(info, responseContent, cancellationToken);
if (mappedObject == null) return;
}
// 핸들러 호출 전 작업 취소 요청이 있었는지 다시 확인합니다.
cancellationToken.ThrowIfCancellationRequested();
// 매핑된 데이터를 DataRepository에 저장하거나 업데이트합니다.
var repoObject = mappedObject;
if (mappedObject != null)
{
// DataRepository는 모든 데이터를 중앙에서 관리하는 저장소입니다.
repoObject = DataRepository.Instance.AddOrUpdateData(key, mappedObject, info.UpdatedDataOnly);
// 만약 반환된 객체가 원본과 같다면, 핸들러에 전달하기 위해 복제본을 만듭니다.
if (repoObject == mappedObject) repoObject = mappedObject.Clone(fromPool: false);
}
// 'UpdatedDataOnly' 옵션이 켜져 있고, 실제로 데이터가 업데이트되었을 때만 핸들러를 호출합니다.
if (info.UpdatedDataOnly)
{
if (repoObject != null && repoObject.UpdatedCount > 0)
{
if (info.SuccessHandler != null)
{
var handlerData = repoObject;
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
}
return;
}
}
// 최종적으로 처리된 데이터가 있는 경우 성공 핸들러를 호출합니다.
if (repoObject != null)
{
if (info.SuccessHandler != null)
{
var handlerData = repoObject;
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
}
}
// 처리된 데이터가 없는 경우 실패 핸들러를 호출합니다.
else
{
if (info.FailHandler != null)
{
UniTask.Post(() => info.FailHandler.Invoke("Data is Null"));
}
}
}
catch (JsonException ex)
{
// JSON 파싱 중 오류가 발생한 경우
ULog.Error($"JSON parsing error for {key}: {ex.Message}\nResponse: {responseContent}", ex);
if (info.FailHandler != null)
{
UniTask.Post(() => info.FailHandler.Invoke($"JSON parsing error: {ex.Message}"));
}
}
catch (OperationCanceledException)
{
// 작업이 외부에서 취소된 경우, 예외를 다시 던져 상위 호출자에게 알립니다.
throw;
}
catch (Exception ex)
{
// 그 외 모든 예외 처리
ULog.Error($"Error processing response for {key}: {ex.Message}", ex);
if (info.FailHandler != null)
{
UniTask.Post(() => info.FailHandler.Invoke($"Error processing response: {ex.Message}"));
}
}
finally
{
// 매핑 과정에서 생성된 IDataObject 객체는 내부적으로 풀링될 수 있습니다.
// 처리가 끝난 객체는 다시 풀에 반환하여 메모리 할당을 줄입니다.
mappedObject?.ReturnToPool();
}
}
/// <summary>
/// 키-값 형태의 JSON 응답을 각 키별로 분할하여 처리합니다.
/// </summary>
/// <param name="info">HTTP 요청 설정 객체.</param>
/// <param name="jsonResponse">분할할 JSON 객체 문자열.</param>
/// <param name="cancellationToken">비동기 작업을 위한 취소 토큰.</param>
/// <remarks>
/// 예를 들어, `{"AGV": [...], "ALARM": [...]}` 같은 응답을 받으면,
/// "AGV"와 "ALARM"을 각각 별개의 데이터로 간주하고 처리합니다.
/// 각 키에 대해 별도의 DataMapper나 Validator를 `HttpRequestConfig`에 설정할 수 있습니다.
/// </remarks>
public static void ProcessSplitResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken = null)
{
JObject responseObject = JObject.Parse(jsonResponse);
// JSON 객체의 모든 프로퍼티(키-값 쌍)를 순회합니다.
foreach (var property in responseObject.Properties())
{
cancellationToken?.ThrowIfCancellationRequested();
string subKey = property.Name; // "AGV", "ALARM" 등
JToken subToken = property.Value; // `[...]` 또는 `{...}`
IDataObject? subMappedObject = null;
// 현재 키(subKey)에 대한 분할 설정을 가져옵니다.
DataMapperValidator? splitConfig = null;
if (info.SplitConfigs != null) info.SplitConfigs.TryGetValue(subKey, out splitConfig);
// 분할 설정에 지정된 매퍼와 유효성 검사기를 사용합니다. 없으면 null입니다.
var subKeyDataMapper = splitConfig?.DataMapper;
var subKeyValidator = splitConfig?.Validator;
//매퍼가 null인 경우, 해당 키는 처리하지 않습니다.
if (subKeyDataMapper == null) continue;
try
{
// 하위 데이터가 배열 또는 객체 형태인지에 따라 적절한 처리 메서드를 호출합니다.
if (subToken is JArray && subToken.Count() > 0)
{
// 분할 처리 중 개별 항목의 실패가 전체 실패로 이어지지 않도록 `invokeFailHandler`를 false로 설정합니다.
subMappedObject = ProcessArrayResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false);
}
else if (subToken is JObject && subToken.Count() > 0)
{
subMappedObject = ProcessObjectResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false);
}
//Debug.Log($"Processing split response for key: {subKey}, {subToken.Count()}. {subToken.GetType().Name} subMappedObject == null:{subMappedObject == null}");
// 매핑된 결과가 없으면 (예: 유효성 검사 실패) 다음 키로 넘어갑니다.
if (subMappedObject == null)
{
continue;
}
// 분할된 데이터를 `subKey`를 키로 사용하여 DataRepository에 저장합니다.
var repoObject = DataRepository.Instance.AddOrUpdateData(subKey, subMappedObject, info.UpdatedDataOnly);
// 핸들러 호출 조건(데이터가 있거나, 업데이트된 경우)을 확인합니다.
bool shouldInvokeHandler = repoObject != null && (!info.UpdatedDataOnly || repoObject.UpdatedCount > 0);
if (shouldInvokeHandler && info.SuccessHandler != null)
{
var handlerData = repoObject.Clone(fromPool: false);
//await UniTask.SwitchToMainThread();
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
// 다음 키 처리를 위해 다시 스레드 풀로 전환합니다.
//await UniTask.SwitchToThreadPool();
}
}
finally
{
// 사용이 끝난 객체는 풀에 반환합니다.
subMappedObject?.ReturnToPool();
}
}
}
private static IDataObject? ProcessObjectResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true)
{
Action<string>? failHandler = invokeFailHandler ? info.FailHandler : null;
return ProcessObjectResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler);
}
/// <summary>
/// 단일 JSON 객체 응답을 검증하고 매핑합니다.
/// </summary>
/// <param name="jsonResponse">처리할 JSON 객체 문자열.</param>
/// <param name="cancellationToken">취소 토큰.</param>
/// <param name="dataMapper">사용할 데이터 매퍼. null이면 원시 데이터를 반환합니다.</param>
/// <param name="validator">사용할 유효성 검사기. null이면 검사를 생략합니다.</param>
/// <param name="failHandler">실패 시 호출할 핸들러.</param>
/// <returns>매핑된 IDataObject 또는 null(실패 시).</returns>
/// <remarks>
/// 이 메서드는 먼저 `DataValidator`를 통해 데이터의 유효성을 검사하고,
/// 통과하면 `DataMapper`를 사용해 JSON을 `IDataObject`로 변환합니다.
/// 대용량 JSON의 경우 메모리 효율을 위해 스트림 기반 파싱을 지원합니다.
/// </remarks>
public static IDataObject? ProcessObjectResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action<string>? failHandler)
{
// 유효성 검사기가 설정된 경우
if (validator != null)
{
bool isValid;
// 스트림 파싱을 지원하고 데이터가 충분히 큰 경우, 스트림으로 유효성을 검사합니다.
if (validator.SupportsStreamParsing && jsonResponse.Length > validator.SupportsStreamLength)
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse));
isValid = validator.IsValid(stream);
}
else
{
JObject source = JObject.Parse(jsonResponse);
isValid = validator.IsValid(source);
}
// 유효성 검사에 실패하면 실패 핸들러를 호출하고 null을 반환합니다.
if (!isValid)
{
if (failHandler != null)
{
UniTask.Post(() => failHandler.Invoke("Data is not Valid"));
}
return null;
}
}
cancellationToken?.ThrowIfCancellationRequested();
// 데이터 매퍼가 설정된 경우
if (dataMapper != null)
{
// 스트림 파싱 지원 여부에 따라 매핑 방식을 선택합니다.
if (dataMapper.SupportsStreamParsing && jsonResponse.Length > dataMapper.SupportsStreamLength)
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse));
return dataMapper.MapObjectStream(stream);
}
else
{
JObject source = JObject.Parse(jsonResponse);
return dataMapper.Map(source);
}
}
else
{
// 매퍼가 없으면 원시 JSON 문자열을 그대로 `DataObject`로 감싸 반환합니다.
return new DataObject(jsonResponse);
}
}
private static IDataObject? ProcessArrayResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true)
{
Action<string>? failHandler = invokeFailHandler ? info.FailHandler : null;
return ProcessArrayResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler);
}
/// <summary>
/// JSON 배열 응답을 검증하고 매핑합니다.
/// </summary>
/// <param name="jsonResponse">처리할 JSON 배열 문자열.</param>
/// <param name="cancellationToken">취소 토큰.</param>
/// <param name="dataMapper">사용할 데이터 매퍼. null이면 원시 데이터를 반환합니다.</param>
/// <param name="validator">사용할 유효성 검사기. null이면 검사를 생략합니다.</param>
/// <param name="failHandler">실패 시 호출할 핸들러.</param>
/// <returns>매핑된 IDataObject(보통 DataArray) 또는 null(실패 시).</returns>
/// <remarks>
/// `DataValidator`를 통해 배열의 각 항목을 필터링하여 유효한 데이터만 포함하는 새 배열을 만들 수 있습니다.
/// 그 후 `DataMapper`를 통해 배열 전체를 `DataArray` 객체로 변환합니다.
/// </remarks>
public static IDataObject? ProcessArrayResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action<string>? failHandler)
{
JArray? sourceArray;
// 유효성 검사기가 설정된 경우
if (validator != null)
{
// 스트림 파싱 지원 여부에 따라 유효한 데이터만 필터링합니다.
if (validator.SupportsStreamParsing && jsonResponse.Length > validator.SupportsStreamLength)
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse));
sourceArray = validator.GetValidData(stream);
}
else
{
sourceArray = validator.GetValidData(JArray.Parse(jsonResponse));
}
// 유효한 데이터가 하나도 없는 경우
if (sourceArray == null || sourceArray.Count == 0)
{
if (failHandler != null)
{
UniTask.Post(() => failHandler.Invoke("Data is not Valid or empty after validation"));
}
return null;
}
}
else
{
// 검사기가 없으면 원본 배열을 그대로 사용합니다.
sourceArray = JArray.Parse(jsonResponse);
}
cancellationToken?.ThrowIfCancellationRequested();
// 데이터 매퍼가 설정된 경우, 필터링된 배열을 매핑합니다.
if (dataMapper != null)
{
return dataMapper.Map(sourceArray);
}
else
{
// 매퍼가 없으면 필터링된 배열을 `DataArray`로 감싸 반환합니다.
return new DataArray(sourceArray.ToString());
}
}
}
}