# 데이터 아키텍처 문서 ## 📋 기본 정보 - **프로젝트명**: Unity MVVM 게임 프로젝트 - **작성자**: 김데이터 (데이터 아키텍트) - **작성일**: 2024-01-03 - **버전**: v1.0 - **상태**: Approved ## 🗃️ 데이터 아키텍처 개요 ### 데이터 계층 구조 ``` ┌─────────────────────────────────────────┐ │ Application Layer │ │ ┌─────────────┐ ┌─────────────────┐ │ │ │ ViewModels │ │ Controllers │ │ │ └─────────────┘ └─────────────────┘ │ └─────────────┬───────────────────────────┘ │ ┌─────────────▼───────────────────────────┐ │ Domain Layer │ │ ┌─────────────┐ ┌─────────────────┐ │ │ │ Models │ │ Domain Services│ │ │ └─────────────┘ └─────────────────┘ │ └─────────────┬───────────────────────────┘ │ ┌─────────────▼───────────────────────────┐ │ Infrastructure Layer │ │ ┌─────────────┐ ┌─────────────────┐ │ │ │Data Services│ │ Repositories │ │ │ │(JSON/Protobuf)││ │ │ │ └─────────────┘ └─────────────────┘ │ └─────────────┬───────────────────────────┘ │ ┌─────────────▼───────────────────────────┐ │ Storage Layer │ │ ┌─────────────┐ ┌─────────────────┐ │ │ │Local Storage│ │ Cloud Storage │ │ │ │(JSON/Binary)│ │ (Protobuf) │ │ │ └─────────────┘ └─────────────────┘ │ └─────────────────────────────────────────┘ ``` ## 💾 저장소 계층 설계 ### 로컬 저장소 구조 ``` Local Storage/ ├── 📱 PlayerPrefs (간단 설정) │ ├── Volume_Master: 0.8 │ ├── Volume_SFX: 1.0 │ ├── Graphics_Quality: High │ └── Language: Korean │ ├── 📄 JSON Files (구조화된 데이터 - 로컬용) │ ├── SaveData/ │ │ ├── player_profile.json │ │ ├── game_progress.json │ │ ├── inventory_data.json │ │ └── achievements.json │ ├── Settings/ │ │ └── user_preferences.json │ └── Cache/ │ ├── api_cache.json │ └── asset_manifest.json │ ├── 🗂️ Protobuf Files (네트워크 통신용) │ ├── Network/ │ │ ├── player_sync.pb │ │ ├── inventory_sync.pb │ │ └── game_events.pb │ └── Cache/ │ └── server_responses.pb │ ├── 🗄️ SQLite Database (관계형 데이터) │ ├── Tables: │ │ ├── Items │ │ ├── PlayerStats │ │ ├── QuestProgress │ │ └── GameLogs │ └── Indexes for performance │ └── 📦 Binary Files (대용량/성능) ├── compressed_save.dat ├── texture_cache.bin └── audio_cache.bin ``` ### 클라우드 저장소 연동 ``` Cloud Storage (Firebase/AWS)/ ├── 👤 User Profile │ ├── /users/{userId}/profile.json │ ├── /users/{userId}/settings.json │ └── /users/{userId}/achievements.json │ ├── 🎮 Game Data │ ├── /saves/{userId}/current_save.json │ ├── /saves/{userId}/backup_saves/ │ └── /saves/{userId}/cloud_sync.json │ ├── 📊 Analytics Data │ ├── /analytics/events/ │ ├── /analytics/sessions/ │ └── /analytics/metrics/ │ └── 🏆 Leaderboards ├── /leaderboards/global/ ├── /leaderboards/friends/ └── /leaderboards/weekly/ ``` ## 🔄 데이터 플로우 아키텍처 ### 읽기 플로우 (Read Flow) ```mermaid graph TD A[ViewModel Request] --> B[Domain Service] B --> C[Repository] C --> D{Cache Hit?} D -->|Yes| E[Return Cached Data] D -->|No| F[Data Service] F --> G{Local Available?} G -->|Yes| H[Load from Local] G -->|No| I[Load from Cloud] H --> J[Update Cache] I --> J J --> K[Return to ViewModel] K --> L[Update UI] ``` ### 쓰기 플로우 (Write Flow) ```mermaid graph TD A[ViewModel Change] --> B[Domain Service] B --> C[Validate Data] C --> D{Valid?} D -->|No| E[Return Error] D -->|Yes| F[Update Model] F --> G[Repository Save] G --> H[Save to Local] H --> I[Update Cache] I --> J{Cloud Sync Enabled?} J -->|Yes| K[Queue Cloud Sync] J -->|No| L[Complete] K --> M[Background Sync] M --> L ``` ## 🎯 데이터 모델 설계 ### 플레이어 데이터 모델 ```csharp [Serializable] public class PlayerData { [Header("Basic Info")] public string playerId; public string playerName; public DateTime createdAt; public DateTime lastLoginAt; [Header("Game Progress")] public int level; public float experience; public int currentStage; public List<string> unlockedAchievements; [Header("Stats")] public PlayerStats stats; public PlayerInventory inventory; public PlayerSettings settings; public PlayerData() { playerId = Guid.NewGuid().ToString(); createdAt = DateTime.UtcNow; level = 1; experience = 0f; stats = new PlayerStats(); inventory = new PlayerInventory(); settings = new PlayerSettings(); } } [Serializable] public class PlayerStats { public float maxHealth = 100f; public float currentHealth = 100f; public float attackPower = 10f; public float defense = 5f; public float moveSpeed = 5f; // Calculated properties public float HealthPercentage => maxHealth > 0 ? currentHealth / maxHealth : 0f; public bool IsAlive => currentHealth > 0f; } [Serializable] public class PlayerInventory { public List<InventoryItem> items = new(); public int maxSlots = 48; public Dictionary<string, int> itemCounts = new(); public bool HasSpace => items.Count < maxSlots; public int UsedSlots => items.Count; public int FreeSlots => maxSlots - items.Count; } ``` ### 게임 설정 데이터 ```csharp [Serializable] public class GameSettings { [Header("Audio Settings")] public float masterVolume = 0.8f; public float musicVolume = 0.7f; public float sfxVolume = 1.0f; public bool isMuted = false; [Header("Graphics Settings")] public GraphicsQuality graphicsQuality = GraphicsQuality.High; public int targetFrameRate = 60; public bool vSyncEnabled = true; public bool particleEffects = true; [Header("Gameplay Settings")] public DifficultyLevel difficulty = DifficultyLevel.Normal; public bool autoSave = true; public bool hapticFeedback = true; [Header("Localization")] public SystemLanguage language = SystemLanguage.Korean; public string localizationVersion = "1.0"; } public enum GraphicsQuality { Low = 0, Medium = 1, High = 2, Ultra = 3 } public enum DifficultyLevel { Easy = 0, Normal = 1, Hard = 2, Expert = 3 } ``` ## 🏪 Repository 패턴 구현 ### 기본 Repository 인터페이스 ```csharp public interface IRepository<TEntity, TKey> { UniTask<TEntity> GetByIdAsync(TKey id); UniTask<IEnumerable<TEntity>> GetAllAsync(); UniTask<TEntity> CreateAsync(TEntity entity); UniTask<TEntity> UpdateAsync(TEntity entity); UniTask<bool> DeleteAsync(TKey id); UniTask<bool> ExistsAsync(TKey id); } public interface IPlayerRepository : IRepository<PlayerData, string> { UniTask<PlayerData> GetByNameAsync(string playerName); UniTask<IEnumerable<PlayerData>> GetTopPlayersAsync(int count); UniTask<bool> UpdateStatsAsync(string playerId, PlayerStats stats); } ``` ### Player Repository 구현 ```csharp public class PlayerRepository : IPlayerRepository { private readonly IDataService _dataService; private readonly ICacheService _cacheService; private readonly ILoggingService _logger; private const string PLAYER_DATA_KEY = "player_data"; private const string PLAYERS_CACHE_KEY = "all_players"; public PlayerRepository( IDataService dataService, ICacheService cacheService, ILoggingService logger) { _dataService = dataService; _cacheService = cacheService; _logger = logger; } public async UniTask<PlayerData> GetByIdAsync(string playerId) { try { // Check cache first var cacheKey = 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 프로젝트에서 효율적이고 안전한 데이터 관리를 위한 종합적인 가이드를 제공합니다.*