quot;player_{playerId}"; var cachedPlayer = _cacheService.Get<PlayerData>(cacheKey); if (cachedPlayer != null) { return cachedPlayer; } // Load from storage var playerData = await _dataService.LoadAsync<PlayerData>( quot;{PLAYER_DATA_KEY}_{playerId}"); // Cache the result if (playerData != null) { _cacheService.Set(cacheKey, playerData, TimeSpan.FromMinutes(30)); } return playerData; } catch (Exception ex) { _logger.LogError(quot;Failed to get player {playerId}: {ex.Message}"); throw; } } public async UniTask<PlayerData> CreateAsync(PlayerData player) { try { // Validate player data ValidatePlayerData(player); // Save to storage await _dataService.SaveAsync(quot;{PLAYER_DATA_KEY}_{player.playerId}", player); // Update cache var cacheKey = quot;player_{player.playerId}"; _cacheService.Set(cacheKey, player, TimeSpan.FromMinutes(30)); // Invalidate related caches _cacheService.Remove(PLAYERS_CACHE_KEY); _logger.LogInfo(quot;Player created: {player.playerId}"); return player; } catch (Exception ex) { _logger.LogError(quot;Failed to create player: {ex.Message}"); throw; } } public async UniTask<PlayerData> UpdateAsync(PlayerData player) { try { ValidatePlayerData(player); // Update last modified time player.lastLoginAt = DateTime.UtcNow; // Save to storage await _dataService.SaveAsync(quot;{PLAYER_DATA_KEY}_{player.playerId}", player); // Update cache var cacheKey = quot;player_{player.playerId}"; _cacheService.Set(cacheKey, player, TimeSpan.FromMinutes(30)); return player; } catch (Exception ex) { _logger.LogError(quot;Failed to update player {player.playerId}: {ex.Message}"); throw; } } private void ValidatePlayerData(PlayerData player) { if (player == null) throw new ArgumentNullException(nameof(player)); if (string.IsNullOrEmpty(player.playerId)) throw new ArgumentException("Player ID cannot be empty"); if (string.IsNullOrEmpty(player.playerName)) throw new ArgumentException("Player name cannot be empty"); } } ``` ## 💨 캐싱 전략 ### 캐시 계층 설계 ```csharp public interface ICacheService { T Get<T>(string key); void Set<T>(string key, T value, TimeSpan? expiry = null); void Remove(string key); void Clear(); bool Exists(string key); } public class MemoryCacheService : ICacheService { private readonly Dictionary<string, CacheItem> _cache = new(); private readonly object _lock = new object(); private class CacheItem { public object Value { get; set; } public DateTime ExpiryTime { get; set; } public bool IsExpired => DateTime.UtcNow > ExpiryTime; } public T Get<T>(string key) { lock (_lock) { if (_cache.TryGetValue(key, out var item)) { if (item.IsExpired) { _cache.Remove(key); return default(T); } return (T)item.Value; } return default(T); } } public void Set<T>(string key, T value, TimeSpan? expiry = null) { lock (_lock) { var expiryTime = expiry.HasValue ? DateTime.UtcNow.Add(expiry.Value) : DateTime.UtcNow.AddHours(1); // Default 1 hour _cache[key] = new CacheItem { Value = value, ExpiryTime = expiryTime }; } } public void Remove(string key) { lock (_lock) { _cache.Remove(key); } } public void Clear() { lock (_lock) { _cache.Clear(); } } public bool Exists(string key) { lock (_lock) { if (_cache.TryGetValue(key, out var item)) { if (item.IsExpired) { _cache.Remove(key); return false; } return true; } return false; } } } ``` ### 캐싱 정책 ```csharp public static class CachePolicy { // Static data that rarely changes public static readonly TimeSpan GameConfig = TimeSpan.FromHours(24); public static readonly TimeSpan ItemDatabase = TimeSpan.FromHours(12); // Player data public static readonly TimeSpan PlayerProfile = TimeSpan.FromMinutes(30); public static readonly TimeSpan PlayerStats = TimeSpan.FromMinutes(15); // Session data public static readonly TimeSpan UserPreferences = TimeSpan.FromHours(2); public static readonly TimeSpan GameSettings = TimeSpan.FromHours(1); // Dynamic data public static readonly TimeSpan LeaderBoard = TimeSpan.FromMinutes(5); public static readonly TimeSpan OnlineStatus = TimeSpan.FromMinutes(2); } ``` ## 🔄 데이터 동기화 ### 동기화 매니저 ```csharp public interface IDataSyncService { UniTask<bool> SyncToCloudAsync(); UniTask<bool> SyncFromCloudAsync(); UniTask<bool> IsCloudDataNewerAsync(); event Action<SyncStatus> SyncStatusChanged; } public enum SyncStatus { Idle, Syncing, Success, Failed, Conflict } public class DataSyncService : IDataSyncService { private readonly IDataService _localDataService; private readonly ICloudDataService _cloudDataService; private readonly ILoggingService _logger; public event Action<SyncStatus> SyncStatusChanged; public DataSyncService( IDataService localDataService, ICloudDataService cloudDataService, ILoggingService logger) { _localDataService = localDataService; _cloudDataService = cloudDataService; _logger = logger; } public async UniTask<bool> SyncToCloudAsync() { try { OnSyncStatusChanged(SyncStatus.Syncing); // Get local player data var localData = await _localDataService.LoadAsync<PlayerData>("current_player"); if (localData == null) { _logger.LogWarning("No local data to sync"); return false; } // Check for conflicts var cloudData = await _cloudDataService.GetPlayerDataAsync(localData.playerId); if (cloudData != null) { var conflictResult = await HandleDataConflict(localData, cloudData); if (!conflictResult) { OnSyncStatusChanged(SyncStatus.Conflict); return false; } } // Upload to cloud await _cloudDataService.SavePlayerDataAsync(localData); OnSyncStatusChanged(SyncStatus.Success); _logger.LogInfo("Data synced to cloud successfully"); return true; } catch (Exception ex) { _logger.LogError(quot;Cloud sync failed: {ex.Message}"); OnSyncStatusChanged(SyncStatus.Failed); return false; } } private async UniTask<bool> HandleDataConflict(PlayerData local, PlayerData cloud) { // Simple last-modified-wins strategy if (local.lastLoginAt > cloud.lastLoginAt) { _logger.LogInfo("Local data is newer, proceeding with upload"); return true; } else if (cloud.lastLoginAt > local.lastLoginAt) { _logger.LogInfo("Cloud data is newer, downloading cloud data"); await _localDataService.SaveAsync("current_player", cloud); return false; // Don't upload, local was updated } else { // Same timestamp, merge data var merged = MergePlayerData(local, cloud); await _localDataService.SaveAsync("current_player", merged); return true; // Upload merged data } } private PlayerData MergePlayerData(PlayerData local, PlayerData cloud) { // Merge strategy: take the best of both return new PlayerData { playerId = local.playerId, playerName = local.playerName, createdAt = local.createdAt < cloud.createdAt ? local.createdAt : cloud.createdAt, lastLoginAt = DateTime.UtcNow, level = Math.Max(local.level, cloud.level), experience = Math.Max(local.experience, cloud.experience), currentStage = Math.Max(local.currentStage, cloud.currentStage), // Merge achievements unlockedAchievements = local.unlockedAchievements .Union(cloud.unlockedAchievements).ToList(), stats = MergePlayerStats(local.stats, cloud.stats), inventory = MergeInventory(local.inventory, cloud.inventory), settings = local.settings // Prefer local settings }; } private void OnSyncStatusChanged(SyncStatus status) { SyncStatusChanged?.Invoke(status); } } ``` ## 📊 데이터 검증 및 무결성 ### 데이터 유효성 검사 ```csharp public interface IDataValidator<T> { ValidationResult Validate(T data); } public class ValidationResult { public bool IsValid { get; set; } public List<string> Errors { get; set; } = new(); public void AddError(string error) { IsValid = false; Errors.Add(error); } } public class PlayerDataValidator : IDataValidator<PlayerData> { public ValidationResult Validate(PlayerData player) { var result = new ValidationResult { IsValid = true }; // Basic validation if (string.IsNullOrEmpty(player.playerId)) result.AddError("Player ID is required"); if (string.IsNullOrEmpty(player.playerName)) result.AddError("Player name is required"); if (player.playerName.Length > 50) result.AddError("Player name too long"); // Stats validation if (player.stats.maxHealth <= 0) result.AddError("Max health must be positive"); if (player.stats.currentHealth > player.stats.maxHealth) result.AddError("Current health cannot exceed max health"); if (player.level < 1 || player.level > 100) result.AddError("Level must be between 1 and 100"); // Inventory validation if (player.inventory.items.Count > player.inventory.maxSlots) result.AddError("Too many items in inventory"); return result; } } ``` ## 🔐 데이터 보안 ### 암호화 서비스 ```csharp public interface IEncryptionService { string Encrypt(string plainText); string Decrypt(string cipherText); string GenerateHash(string input); bool VerifyHash(string input, string hash); } public class AESEncryptionService : IEncryptionService { private readonly byte[] _key; private readonly byte[] _iv; public AESEncryptionService() { // In production, these should come from secure configuration _key = Encoding.UTF8.GetBytes("YourSecretKeyHere1234567890123456"); _iv = Encoding.UTF8.GetBytes("YourIVHere123456"); } public string Encrypt(string plainText) { using (var aes = Aes.Create()) { aes.Key = _key; aes.IV = _iv; var encryptor = aes.CreateEncryptor(); var plainBytes = Encoding.UTF8.GetBytes(plainText); var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); return Convert.ToBase64String(cipherBytes); } } public string Decrypt(string cipherText) { using (var aes = Aes.Create()) { aes.Key = _key; aes.IV = _iv; var decryptor = aes.CreateDecryptor(); var cipherBytes = Convert.FromBase64String(cipherText); var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); return Encoding.UTF8.GetString(plainBytes); } } public string GenerateHash(string input) { using (var sha256 = SHA256.Create()) { var inputBytes = Encoding.UTF8.GetBytes(input); var hashBytes = sha256.ComputeHash(inputBytes); return Convert.ToBase64String(hashBytes); } } public bool VerifyHash(string input, string hash) { var inputHash = GenerateHash(input); return string.Equals(inputHash, hash, StringComparison.Ordinal); } } ``` --- ## 📈 성능 최적화 ### 데이터 로딩 최적화 ```csharp public class OptimizedDataLoader { private readonly Dictionary<string, UniTask> _loadingTasks = new(); public async UniTask<T> LoadWithDedupingAsync<T>(string key) { // Prevent multiple simultaneous loads of same data if (_loadingTasks.ContainsKey(key)) { await _loadingTasks[key]; } var loadTask = LoadDataInternal<T>(key); _loadingTasks[key] = loadTask; try { return await loadTask; } finally { _loadingTasks.Remove(key); } } private async UniTask<T> LoadDataInternal<T>(string key) { // Actual loading logic await UniTask.Delay(100); // Simulate loading return default(T); } } ``` ## 🧪 데이터 계층 테스트 ### Repository 테스트 ```csharp [TestFixture] public class PlayerRepositoryTests { private PlayerRepository _repository; private Mock<IDataService> _mockDataService; private Mock<ICacheService> _mockCacheService; [SetUp] public void SetUp() { _mockDataService = new Mock<IDataService>(); _mockCacheService = new Mock<ICacheService>(); _repository = new PlayerRepository(_mockDataService.Object, _mockCacheService.Object); } [Test] public async UniTask GetByIdAsync_WhenCacheHit_ShouldReturnCachedData() { // Arrange var playerId = "test-player"; var cachedPlayer = new PlayerData { playerId = playerId }; _mockCacheService.Setup(c => c.Get<PlayerData>(quot;player_{playerId}")) .Returns(cachedPlayer); // Act var result = await _repository.GetByIdAsync(playerId); // Assert Assert.AreEqual(cachedPlayer, result); _mockDataService.Verify(d => d.LoadAsync<PlayerData>(It.IsAny<string>()), Times.Never); } } ``` --- *이 데이터 아키텍처 문서는 Unity 프로젝트에서 효율적이고 안전한 데이터 관리를 위한 종합적인 가이드를 제공합니다.*