quot;Failed to serialize {typeof(T).Name}: {ex.Message}"); throw; } } public T Deserialize<T>(byte[] data) where T : IMessage<T>, new() { try { var parser = new MessageParser<T>(() => new T()); return parser.ParseFrom(data); } catch (Exception ex) { _logger.LogError(quot;Failed to deserialize {typeof(T).Name}: {ex.Message}"); throw; } } public async UniTask<bool> SendToServerAsync<T>(T message) where T : IMessage<T> { try { var data = Serialize(message); var content = new ByteArrayContent(data); content.Headers.ContentType = new MediaTypeHeaderValue("application/x-protobuf"); var endpoint = GetEndpointForType<T>(); var response = await _httpClient.PostAsync(quot;{_serverBaseUrl}{endpoint}", content); if (response.IsSuccessStatusCode) { _logger.LogInfo(quot;Successfully sent {typeof(T).Name} to server"); return true; } else { _logger.LogError(quot;Server responded with {response.StatusCode}"); return false; } } catch (Exception ex) { _logger.LogError(quot;Failed to send {typeof(T).Name} to server: {ex.Message}"); return false; } } public async UniTask<TResponse> RequestFromServerAsync<TRequest, TResponse>(TRequest request) where TRequest : IMessage<TRequest> where TResponse : IMessage<TResponse>, new() { try { var requestData = Serialize(request); var content = new ByteArrayContent(requestData); content.Headers.ContentType = new MediaTypeHeaderValue("application/x-protobuf"); var endpoint = GetEndpointForType<TRequest>(); var response = await _httpClient.PostAsync(quot;{_serverBaseUrl}{endpoint}", content); if (response.IsSuccessStatusCode) { var responseData = await response.Content.ReadAsByteArrayAsync(); return Deserialize<TResponse>(responseData); } else { throw new HttpRequestException(quot;Server error: {response.StatusCode}"); } } catch (Exception ex) { _logger.LogError(quot;Request failed: {ex.Message}"); throw; } } public async UniTask SaveToFileAsync<T>(string filePath, T message) where T : IMessage<T> { try { var data = Serialize(message); var fullPath = Path.Combine(Application.persistentDataPath, filePath); // 디렉토리 생성 var directory = Path.GetDirectoryName(fullPath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } await File.WriteAllBytesAsync(fullPath, data); _logger.LogInfo(quot;Saved {typeof(T).Name} to {filePath}"); } catch (Exception ex) { _logger.LogError(quot;Failed to save {typeof(T).Name}: {ex.Message}"); throw; } } public async UniTask<T> LoadFromFileAsync<T>(string filePath) where T : IMessage<T>, new() { try { var fullPath = Path.Combine(Application.persistentDataPath, filePath); if (!File.Exists(fullPath)) { _logger.LogWarning(quot;File not found: {filePath}"); return new T(); } var data = await File.ReadAllBytesAsync(fullPath); var result = Deserialize<T>(data); _logger.LogInfo(quot;Loaded {typeof(T).Name} from {filePath}"); return result; } catch (Exception ex) { _logger.LogError(quot;Failed to load {typeof(T).Name}: {ex.Message}"); throw; } } private string GetEndpointForType<T>() where T : IMessage<T> { return typeof(T).Name.ToLowerInvariant().Replace("request", "").Replace("response", ""); } } ``` ### 데이터 변환 유틸리티 ```csharp using Google.Protobuf.WellKnownTypes; using System; public static class ProtobufConverter { // Unity 타입 → Protobuf 타입 변환 public static Game.Player.PlayerData ToProtobuf(this PlayerModel model) { return new Game.Player.PlayerData { PlayerId = model.PlayerId, PlayerName = model.PlayerName, Level = model.Level.Value, Experience = model.Experience.Value, Stats = model.Stats.ToProtobuf(), CreatedAt = Timestamp.FromDateTime(model.CreatedAt.ToUniversalTime()), LastLogin = Timestamp.FromDateTime(model.LastLoginAt.ToUniversalTime()) }; } public static Game.Player.PlayerStats ToProtobuf(this PlayerStats stats) { return new Game.Player.PlayerStats { MaxHealth = stats.maxHealth, CurrentHealth = stats.currentHealth, AttackPower = stats.attackPower, Defense = stats.defense, MoveSpeed = stats.moveSpeed }; } // Protobuf 타입 → Unity 타입 변환 public static void FromProtobuf(this PlayerModel model, Game.Player.PlayerData proto) { model.PlayerId = proto.PlayerId; model.PlayerName = proto.PlayerName; model.Level.Value = proto.Level; model.Experience.Value = proto.Experience; model.Stats.FromProtobuf(proto.Stats); model.CreatedAt = proto.CreatedAt.ToDateTime(); model.LastLoginAt = proto.LastLogin.ToDateTime(); } public static void FromProtobuf(this PlayerStats stats, Game.Player.PlayerStats proto) { stats.maxHealth = proto.MaxHealth; stats.currentHealth = proto.CurrentHealth; stats.attackPower = proto.AttackPower; stats.defense = proto.Defense; stats.moveSpeed = proto.MoveSpeed; } } ``` ## 🔄 MVVM 패턴과의 통합 ### ViewModel에서의 Protobuf 활용 ```csharp public class PlayerViewModel : IDisposable { [Inject] private IPlayerModel _playerModel; [Inject] private IProtobufService _protobufService; [Inject] private IMessagePublisher<PlayerSyncMessage> _syncPublisher; private readonly CompositeDisposable _disposables = new(); // 서버와 동기화 public async UniTask SyncWithServerAsync() { try { var request = new Game.Network.PlayerSyncRequest { PlayerId = _playerModel.PlayerId, LastSyncTime = Timestamp.FromDateTime(DateTime.UtcNow) }; var response = await _protobufService.RequestFromServerAsync< Game.Network.PlayerSyncRequest, Game.Network.PlayerSyncResponse>(request); if (response.Status == Game.Network.ResponseStatus.Success) { _playerModel.FromProtobuf(response.PlayerData); _syncPublisher.Publish(new PlayerSyncMessage { Success = true }); } else { Debug.LogError(quot;Sync failed: {response.ErrorMessage}"); _syncPublisher.Publish(new PlayerSyncMessage { Success = false, Error = response.ErrorMessage }); } } catch (Exception ex) { Debug.LogError(quot;Sync exception: {ex.Message}"); _syncPublisher.Publish(new PlayerSyncMessage { Success = false, Error = ex.Message }); } } // 로컬 저장 public async UniTask SaveToLocalAsync() { try { var protoData = _playerModel.ToProtobuf(); await _protobufService.SaveToFileAsync("player/save_data.pb", protoData); } catch (Exception ex) { Debug.LogError(quot;Local save failed: {ex.Message}"); } } // 로컬 로드 public async UniTask LoadFromLocalAsync() { try { var protoData = await _protobufService.LoadFromFileAsync<Game.Player.PlayerData>("player/save_data.pb"); if (protoData != null) { _playerModel.FromProtobuf(protoData); } } catch (Exception ex) { Debug.LogError(quot;Local load failed: {ex.Message}"); } } public void Dispose() { _disposables?.Dispose(); } } ``` ## 🚀 성능 최적화 ### 메모리 풀링 ```csharp public class ProtobufMessagePool<T> where T : IMessage<T>, new() { private readonly Queue<T> _pool = new(); private readonly int _maxSize; public ProtobufMessagePool(int maxSize = 100) { _maxSize = maxSize; } public T Rent() { if (_pool.Count > 0) { var message = _pool.Dequeue(); message.Clear(); return message; } return new T(); } public void Return(T message) { if (_pool.Count < _maxSize) { message.Clear(); _pool.Enqueue(message); } } } ``` ### 배치 처리 ```csharp public class ProtobufBatchProcessor { private readonly IProtobufService _protobufService; private readonly List<IMessage> _batchQueue = new(); private readonly float _batchInterval = 1.0f; public void QueueMessage<T>(T message) where T : IMessage<T> { _batchQueue.Add(message); if (_batchQueue.Count >= 10) // 배치 크기 제한 { ProcessBatchAsync().Forget(); } } private async UniTaskVoid ProcessBatchAsync() { var batch = new List<IMessage>(_batchQueue); _batchQueue.Clear(); // 배치 메시지 생성 var batchMessage = new Game.Network.BatchMessage(); foreach (var message in batch) { var any = Any.Pack(message); batchMessage.Messages.Add(any); } await _protobufService.SendToServerAsync(batchMessage); } } ``` ## 🧪 테스트 전략 ### Protobuf 단위 테스트 ```csharp [TestFixture] public class ProtobufServiceTests { private UnityProtobufService _protobufService; private Mock<ILoggingService> _mockLogger; [SetUp] public void SetUp() { _mockLogger = new Mock<ILoggingService>(); _protobufService = new UnityProtobufService(_mockLogger.Object); } [Test] public void Serialize_WithValidMessage_ShouldReturnByteArray() { // Arrange var playerData = new Game.Player.PlayerData { PlayerId = "test-player", PlayerName = "Test Player", Level = 10 }; // Act var result = _protobufService.Serialize(playerData); // Assert Assert.IsNotNull(result); Assert.Greater(result.Length, 0); } [Test] public void Deserialize_WithValidData_ShouldReturnMessage() { // Arrange var originalData = new Game.Player.PlayerData { PlayerId = "test-player", PlayerName = "Test Player", Level = 10 }; var serialized = _protobufService.Serialize(originalData); // Act var result = _protobufService.Deserialize<Game.Player.PlayerData>(serialized); // Assert Assert.AreEqual(originalData.PlayerId, result.PlayerId); Assert.AreEqual(originalData.PlayerName, result.PlayerName); Assert.AreEqual(originalData.Level, result.Level); } } ``` ## 📊 성능 비교 ### JSON vs Protobuf 벤치마크 ```csharp public class SerializationBenchmark { private PlayerData _testData; private Game.Player.PlayerData _protoData; [SetUp] public void Setup() { _testData = new PlayerData { // 테스트 데이터 설정 }; _protoData = _testData.ToProtobuf(); } [Benchmark] public string JsonSerialize() { return JsonUtility.ToJson(_testData); } [Benchmark] public byte[] ProtobufSerialize() { return _protoData.ToByteArray(); } [Benchmark] public PlayerData JsonDeserialize() { var json = JsonUtility.ToJson(_testData); return JsonUtility.FromJson<PlayerData>(json); } [Benchmark] public Game.Player.PlayerData ProtobufDeserialize() { var bytes = _protoData.ToByteArray(); return Game.Player.PlayerData.Parser.ParseFrom(bytes); } } ``` **벤치마크 결과 예상값:** - Protobuf 직렬화: JSON 대비 2-3배 빠름 - Protobuf 역직렬화: JSON 대비 3-5배 빠름 - 데이터 크기: JSON 대비 30-50% 작음 --- *이 Protobuf 통합 가이드는 Unity 프로젝트에서 고성능 데이터 통신과 저장을 위한 완전한 구현 방안을 제시합니다.*