#nullable enable using Cysharp.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using UnityEngine; using UVC.Log; using UVC.Network; using UVC.Tests; using UVC.Threading; namespace UVC.Data { /// /// HTTP 요청 파이프라인을 관리하는 클래스입니다. /// /// /// 이 클래스는 HTTP 요청의 실행 및 반복 요청을 관리합니다. /// 등록된 요청(HttpPipeLineInfo)을 키 값으로 관리하며, /// 주기적 데이터 수집을 위한 반복 요청 기능을 제공합니다. /// /// 주요 기능: /// - 단일 및 반복 HTTP 요청 관리 /// - 요청 결과의 JSON 데이터를 IDataObject로 변환 /// - 안전한 요청 취소 및 자원 정리 /// - 테스트를 위한 목업 기능 지원 /// /// 모든 HTTP 요청은 백그라운드 스레드(스레드풀)에서 처리되어 메인 스레드 차단을 방지합니다. /// 요청 결과 처리 시 핸들러(SuccessHandler, FailHandler)는 자동으로 메인 스레드에서 호출됩니다. /// 이를 통해 UI 스레드 차단 없이 효율적인 네트워크 작업을 수행하면서도, /// UI 업데이트는 안전하게 메인 스레드에서 처리할 수 있습니다. /// /// 모든 반복 실행은 CancellationTokenSource를 통해 취소할 수 있으며, /// 취소 후 현재 진행 중인 모든 요청이 안전하게 완료되는 것을 보장합니다. /// /// /// /// // HttpPipeLine 인스턴스 생성 /// var httpPipeline = new HttpPipeLine(); /// /// // 데이터 매퍼 설정 (응답 데이터 변환용) /// var dataMask = new DataMask(); /// dataMask["name"] = "이름"; /// dataMask["value"] = 0; /// var dataMapper = new DataMapper(dataMask); /// /// // 단일 요청 설정 및 등록 /// var singleRequest = new HttpPipeLineInfo("https://api.example.com/data") /// .setDataMapper(dataMapper) /// .setHandler(data => { /// // 데이터 처리 로직 /// ULog.Debug($"데이터 수신: {data?.ToString() ?? "null"}"); /// }); /// httpPipeline.Add("dataRequest", singleRequest); /// /// // 반복 요청 설정 및 등록 /// var repeatingRequest = new HttpPipeLineInfo("https://api.example.com/status") /// .setDataMapper(dataMapper) /// .setHandler(data => { /// // 상태 데이터 처리 /// ULog.Debug($"상태 업데이트: {data?.ToString() ?? "null"}"); /// }) /// .setRepeat(true, 0, 5000); // 5초마다 무한 반복 /// httpPipeline.Add("statusMonitor", repeatingRequest); /// /// // 요청 실행 /// await httpPipeline.Excute("dataRequest"); // 단일 실행 /// await httpPipeline.Excute("statusMonitor"); // 반복 실행 시작 /// /// // 나중에 반복 요청 중지 /// httpPipeline.StopRepeat("statusMonitor"); /// /// // 더 이상 필요없는 요청 제거 /// httpPipeline.Remove("dataRequest"); /// /// public class HttpPipeLine { /// /// 테스트를 위한 목업 모드 활성화 여부를 설정하거나 가져옵니다. /// /// /// true로 설정하면 실제 HTTP 요청 대신 MockHttpRequester를 사용합니다. /// 테스트 환경에서 외부 의존성 없이 HTTP 요청을 시뮬레이션할 때 유용합니다. /// public bool UseMockup { get; internal set; } = false; /// /// 등록된 HTTP 파이프라인 정보를 저장하는 사전 /// private Dictionary infoList = new Dictionary(); /// /// 실행 중인 반복 작업의 취소 토큰을 관리하는 사전 /// private Dictionary repeatTokenSources = new Dictionary(); /// /// 진행 중인 요청의 상태를 추적하는 사전입니다. /// /// /// 키는 요청 식별자이고, 값은 현재 요청이 실행 중인지 여부를 나타냅니다. /// 이 상태 추적은 StopRepeat 메서드가 요청의 완전한 종료를 보장하기 위해 사용됩니다. /// private Dictionary requestInProgress = new Dictionary(); /// /// 새로운 HTTP 요청 정보를 추가하거나 기존 정보를 업데이트합니다. /// /// 요청을 식별하는 키 /// HTTP 요청 정보 /// /// 동일한 키가 이미 존재하는 경우 새로운 정보로 대체됩니다. /// public void Add(string key, HttpPipeLineInfo info) { if (!infoList.ContainsKey(key)) { infoList.Add(key, info); } else { infoList[key] = info; // Update existing entry } } /// /// 지정한 키의 HTTP 요청 정보를 제거합니다. /// /// 제거할 요청의 키 /// /// 실행 중인 반복 작업이 있다면 함께 중지됩니다. /// public async UniTask RemoveAsync(string key) { if (infoList.ContainsKey(key)) { await StopRepeat(key); infoList.Remove(key); } } /// /// 지정한 키의 HTTP 요청을 실행합니다. /// /// 실행할 요청의 키 /// 메인 스레드로 전환할지 여부 /// 비동기 작업 /// /// 요청 정보의 repeat 속성에 따라 단일 실행 또는 반복 실행을 시작합니다. /// 이미 실행 중인 반복 작업이 있다면 먼저 중지하고 완료를 대기한 후 새로운 요청을 시작합니다. /// 단일 실행의 경우 완료될 때까지 대기하지만, 반복 실행은 백그라운드에서 실행됩니다. /// /// 모든 HTTP 요청 처리는 백그라운드 스레드에서 수행되며, 핸들러만 메인 스레드에서 호출됩니다. /// /// 지정된 키가 등록되어 있지 않은 경우 public async UniTask Excute(string key) { if (!infoList.ContainsKey(key)) { throw new KeyNotFoundException($"No HTTP request found with key '{key}'."); } HttpPipeLineInfo info = infoList[key]; // 반복 설정에 관계없이 이전에 실행 중인 반복 작업이 있다면 중지 await StopRepeat(key); // 스레드풀에서 요청 처리 실행 await UniTask.SwitchToThreadPool(); try { if (!info.Repeat) { // 단일 실행 로직 호출 await ExecuteSingle(key, info); await UniTask.SwitchToMainThread(); } else { // 반복 설정이 있는 경우에만 StartRepeat 호출 // Forget()을 호출하지 않고 StartRepeat가 스레드풀에서 계속 실행되도록 함 StartRepeat(key).Forget(); } } catch (Exception ex) { // 예외가 발생한 경우에도 메인 스레드로 복귀 await UniTask.SwitchToMainThread(); throw; // 예외 재발생 } } /// /// 단일 HTTP 요청을 실행하고 결과를 처리합니다. /// /// 요청을 식별하는 키 /// HTTP 요청 정보 /// 요청 취소를 위한 취소 토큰 /// 비동기 작업 /// /// 이 메서드는 백그라운드 스레드에서 HTTP 요청을 보내고, 응답 데이터를 파싱하여 IDataObject로 변환합니다. /// JSON 객체 또는 배열 형식의 응답을 처리할 수 있으며, 취소 토큰을 통해 언제든지 작업을 취소할 수 있습니다. /// 모든 핸들러 호출은 메인 스레드에서 이루어져 UI 업데이트를 안전하게 수행할 수 있습니다. /// /// 작업이 취소된 경우 발생 /// JSON 응답 파싱 중 오류가 발생한 경우 /// HTTP 요청 중 다른 예외가 발생한 경우 private async UniTask ExecuteSingle(string key, HttpPipeLineInfo info, CancellationToken cancellationToken = default) { int retryCount = 0; Exception lastException = null; while (retryCount <= info.MaxRetryCount) { // 취소 요청 확인 if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException("Operation cancelled", cancellationToken); } lock (requestInProgress) { requestInProgress[key] = true; } try { string result = string.Empty; if (UseMockup) { result = await MockHttpRequester.Request(info.Url, info.Method, info.Body, info.Headers); } else { result = await HttpRequester.Request(info.Url, info.Method, info.Body, info.Headers); } // 응답 처리 전에 다시 취소 요청 확인 if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException("Operation cancelled", cancellationToken); } IDataObject? dataObject = null; result = result.Trim(); if (!string.IsNullOrEmpty(result)) { try { HttpResponseResult responseResult = info.ResponseMask.Apply(result); // 응답 마스크 적용 결과가 성공이 아니면 실패 핸들러 호출 후 반환 if (!responseResult.IsSuccess) { if (info.FailHandler != null) { string errorMessage = responseResult.Message!; // UI 스레드에서 실패 핸들러 호출 await UniTask.SwitchToMainThread(); info.FailHandler.Invoke(errorMessage); } return; } else { result = responseResult.Data!.Trim(); } if (result.StartsWith("{")) { if (info.Validator != null) { if (info.Validator.SupportsStreamParsing && result.Length > info.Validator.SupportsStreamLength) { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result))) { if (info.Validator != null && !info.Validator.IsValid(stream)) { if (info.FailHandler != null) { // UI 스레드에서 실패 핸들러 호출 await UniTask.SwitchToMainThread(); info.FailHandler.Invoke("Data is not Valid"); } return; } } } else { JObject source = JObject.Parse(result); if (info.Validator != null && !info.Validator.IsValid(source)) { if (info.FailHandler != null) { // UI 스레드에서 실패 핸들러 호출 await UniTask.SwitchToMainThread(); info.FailHandler.Invoke("Data is not Valid"); } return; } } } if (info.DataMapper != null) { if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength) { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result))) { dataObject = info.DataMapper.MapObjectStream(stream); } } else { JObject source = JObject.Parse(result); dataObject = info.DataMapper.Map(source); } } } else if (result.StartsWith("[")) { if (info.Validator != null) { if (info.Validator.SupportsStreamParsing && result.Length > info.Validator.SupportsStreamLength) { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result))) { JArray? validSource = info.Validator.GetValidData(stream); if (validSource == null) { if (info.FailHandler != null) { // UI 스레드에서 실패 핸들러 호출 await UniTask.SwitchToMainThread(); info.FailHandler.Invoke("Data is not Valid"); } return; } } } else { JArray source = JArray.Parse(result); JArray? validSource = info.Validator.GetValidData(source); if (validSource == null) { if (info.FailHandler != null) { // UI 스레드에서 실패 핸들러 호출 await UniTask.SwitchToMainThread(); info.FailHandler.Invoke("Data is not Valid"); } return; } } } if (info.DataMapper != null) { if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength) { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result))) { if (info.Validator != null) { JArray? validSource = info.Validator.GetValidData(stream); if (validSource != null) { dataObject = info.DataMapper.Map(validSource); } } else { dataObject = info.DataMapper.MapArrayStream(stream); } } } else { JArray source = JArray.Parse(result); if (info.Validator != null) { JArray? validSource = info.Validator.GetValidData(source); if (validSource != null) { dataObject = info.DataMapper.Map(validSource); } } else { dataObject = info.DataMapper.Map(source); } } } } } catch (JsonException ex) { ULog.Error($"JSON parsing error in ExecuteSingle for {key}: {ex.Message}\nResponse: {result}", ex); throw; // 상위에서 처리하도록 다시 throw } } // 핸들러 호출 전에 다시 취소 요청 확인 if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException("Operation cancelled", cancellationToken); } if (dataObject != null) dataObject = DataRepository.Instance.AddData(key, dataObject, info.UpdatedDataOnly); // 갱신 된 데이터가 있는 경우 핸들러 호출 if (info.UpdatedDataOnly) { if (dataObject != null && dataObject.UpdatedCount > 0) { if (info.SuccessHandler != null) { // 로컬 변수로 복사하여 클로저에서 안전하게 사용 var handlerData = dataObject; // UI 스레드에서 성공 핸들러 호출 await UniTask.SwitchToMainThread(); info.SuccessHandler.Invoke(handlerData); } return; } } if (dataObject != null) { if (info.SuccessHandler != null) { // 로컬 변수로 복사하여 클로저에서 안전하게 사용 var handlerData = dataObject; // UI 스레드에서 성공 핸들러 호출 await UniTask.SwitchToMainThread(); info.SuccessHandler.Invoke(handlerData); } } else { if (info.FailHandler != null) { // UI 스레드에서 실패 핸들러 호출 await UniTask.SwitchToMainThread(); info.FailHandler.Invoke("Data is Null"); } } return; } catch (OperationCanceledException) { // 취소 예외는 그대로 전파 throw; } catch (Exception ex) { lastException = ex; retryCount++; if (retryCount <= info.MaxRetryCount) { // 재시도 전에 취소 요청 확인 if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException("Operation cancelled", cancellationToken); } ULog.Warning($"Request failed for '{key}', retry {retryCount}/{info.MaxRetryCount} after {info.RetryDelay}ms: {ex.Message}", ex); await UniTask.Delay(info.RetryDelay); } } finally { lock (requestInProgress) { requestInProgress[key] = false; } } } // 모든 재시도 후에도 실패 ULog.Error($"Request failed for '{key}' after {info.MaxRetryCount} retries: {lastException?.Message}", lastException); throw lastException; } /// /// 반복 실행을 시작합니다. /// /// 반복 실행할 요청의 키 /// 비동기 작업 /// /// 지정된 간격(repeatInterval)으로 HTTP 요청을 백그라운드 스레드에서 반복 실행합니다. /// repeatCount가 0인 경우 무한 반복하며, 0보다 큰 경우 지정된 횟수만큼만 실행합니다. /// 작업 실행 중 예외가 발생하면 로그를 기록하고 다음 실행을 시도합니다. /// 취소 요청이 있거나 최대 실행 횟수에 도달하면 반복이 종료됩니다. /// /// 이 메서드는 백그라운드 스레드에서 실행되며, 모든 핸들러 호출은 메인 스레드에서 이루어집니다. /// /// 지정된 키가 등록되어 있지 않은 경우 private async UniTask StartRepeat(string key) { if (!infoList.ContainsKey(key)) { throw new KeyNotFoundException($"No HTTP request found with key '{key}'."); } HttpPipeLineInfo info = infoList[key]; if (!info.Repeat) return; // 새 취소 토큰 생성 CancellationTokenSource cts = new CancellationTokenSource(); repeatTokenSources[key] = cts; int executionCount = 0; try { while (!cts.IsCancellationRequested) { try { // 단일 실행 로직 호출 await ExecuteSingle(key, info, cts.Token); // 지정된 횟수만큼 반복한 경우 중지 if (info.RepeatCount > 0) { executionCount++; if (executionCount >= info.RepeatCount) { break; } } // 토큰이 취소되지 않은 경우에만 지연 if (!cts.IsCancellationRequested) { // 지정된 간격만큼 대기 await UniTask.Delay(info.RepeatInterval, cancellationToken: cts.Token); } } catch (OperationCanceledException) { // 취소된 경우 루프 종료 break; } catch (Exception ex) { // 다른 예외 처리 ULog.Error($"Error in repeat execution for '{key}': {ex.Message}", ex); if (cts.IsCancellationRequested) { break; } try { await UniTask.Delay(info.RepeatInterval, cancellationToken: cts.Token); } catch (OperationCanceledException) { // 취소된 경우 무시하고 루프 종료 break; } } } } finally { lock (repeatTokenSources) // 스레드 안전성 확보 { if (repeatTokenSources.TryGetValue(key, out var currentCts) && currentCts == cts) { repeatTokenSources.Remove(key); } } cts.Dispose(); // 여기서 최종적으로 Dispose 호출 } } /// /// 반복 실행 중인 요청을 중지합니다. /// /// 중지할 요청의 키 /// 요청 중지 작업을 나타내는 비동기 작업 /// /// 해당 키로 실행 중인 반복 작업이 없는 경우 아무 작업도 수행하지 않습니다. /// 요청이 중지되었더라도 현재 실행 중인 작업이 완전히 종료될 때까지 대기합니다. /// 이를 통해 작업 중단 후 자원이 안전하게 정리되는 것을 보장합니다. /// public async UniTask StopRepeat(string key) { CancellationTokenSource? cts = null; lock (repeatTokenSources) // 스레드 안전성 확보 { if (repeatTokenSources.TryGetValue(key, out cts) && !cts.IsCancellationRequested) { cts.Cancel(); repeatTokenSources.Remove(key); } } // 진행 중인 요청이 완료될 때까지 대기 if (cts != null) { while (true) { lock (requestInProgress) { if (!requestInProgress.ContainsKey(key) || !requestInProgress[key]) { break; } } await UniTask.Delay(10); } } } /// /// 모든 반복 실행 중인 요청을 중지합니다. /// /// /// 애플리케이션 종료 시 또는 모든 반복 작업을 일괄 중지해야 할 때 사용합니다. /// 이 메서드는 비동기적으로 작동하지만 완료를 대기하지 않습니다. /// 모든 작업이 완전히 종료될 때까지 기다려야 하는 경우, 각 키에 대해 개별적으로 StopRepeat를 호출하고 대기해야 합니다. /// public async UniTask StopAllRepeats() { foreach (var key in new List(repeatTokenSources.Keys)) { await StopRepeat(key); } } /// /// 현재 활성화된 요청 목록과 상태 정보를 반환합니다. /// /// 키와 요청 상태 정보를 포함하는 딕셔너리 /// /// 반환되는 딕셔너리는 등록된 모든 HTTP 요청에 대한 상태 정보를 제공합니다. /// 각 요청에 대해 활성 상태, 반복 설정, 반복 횟수, 반복 간격을 확인할 수 있습니다. /// /// /// /// var httpPipeline = new HttpPipeLine(); /// // 파이프라인에 요청 추가 후... /// /// // 모든 활성 요청 확인 /// var activeRequests = httpPipeline.GetActiveRequests(); /// foreach (var request in activeRequests) /// { /// ULog.Debug($"요청 키: {request.Key}, 활성 상태: {request.Value.IsActive}, " + /// $"반복 중: {request.Value.IsRepeating}, 반복 간격: {request.Value.RepeatInterval}ms"); /// } /// /// public Dictionary GetActiveRequests() { var result = new Dictionary(); foreach (var key in infoList.Keys) { bool isRepeating = repeatTokenSources.ContainsKey(key); result[key] = new HttpPipeLineRequestStatus { IsActive = isRepeating, IsRepeating = isRepeating, RepeatCount = isRepeating ? infoList[key].RepeatCount : 0, RepeatInterval = isRepeating ? infoList[key].RepeatInterval : 0 }; } return result; } /// /// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다. /// /// 이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다. /// 를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다. public void Dispose() { // 모든 반복 작업 중지 StopAllRepeats().Forget(); // 요청 상태 초기화 requestInProgress.Clear(); // 등록된 요청 정보 초기화 infoList.Clear(); // 취소 토큰 소스 정리 foreach (var cts in repeatTokenSources.Values) { cts.Dispose(); } repeatTokenSources.Clear(); } } /// /// HTTP 요청의 현재 상태 정보를 나타내는 클래스입니다. /// /// /// 이 클래스는 HTTP 파이프라인에 등록된 요청의 활성 상태, 반복 설정, /// 반복 횟수, 반복 간격에 관한 정보를 제공합니다. /// public class HttpPipeLineRequestStatus { /// /// 요청이 현재 활성 상태인지 여부를 나타냅니다. /// public bool IsActive { get; set; } /// /// 요청이 반복 실행 중인지 여부를 나타냅니다. /// public bool IsRepeating { get; set; } /// /// 반복 설정된 횟수를 나타냅니다. 0은 무한 반복을 의미합니다. /// public int RepeatCount { get; set; } /// /// 반복 요청 간의 간격을 밀리초 단위로 나타냅니다. /// public int RepeatInterval { get; set; } } }