# Google Protobuf 통합 가이드 ## 📋 기본 정보 - **프로젝트명**: Unity MVVM 게임 프로젝트 - **작성자**: 박프로토 (네트워크 개발자) - **작성일**: 2024-01-03 - **버전**: v1.0 - **상태**: Approved ## 🎯 Protobuf 도입 목적 ### 기존 JSON 대비 장점 - **성능**: 직렬화/역직렬화 속도 3-5배 향상 - **용량**: 데이터 크기 20-40% 절약 - **타입 안정성**: 컴파일 타임 타입 체크 - **하위 호환성**: 스키마 진화 지원 ### 사용 목적 - **네트워크 통신**: 서버와의 실시간 데이터 교환 - **데이터 저장**: 대용량 게임 데이터 압축 저장 - **로그 수집**: 분석용 이벤트 로그 효율적 전송 ## 🔧 Unity Protobuf 설정 ### Package Manager 설정 ```bash # Package Manager를 통한 설치 1. Window → Package Manager 열기 2. "Add package from git URL" 클릭 3. https://github.com/protocolbuffers/protobuf.git#csharp 입력 4. 또는 Packages/manifest.json에 추가: ``` ```json { "dependencies": { "com.google.protobuf": "3.21.12" } } ``` ### 프로토 파일 구조 ``` Assets/ ├── Proto/ # .proto 스키마 파일들 │ ├── Game/ │ │ ├── player.proto # 플레이어 관련 │ │ ├── inventory.proto # 인벤토리 관련 │ │ └── battle.proto # 전투 관련 │ ├── Network/ │ │ ├── messages.proto # 네트워크 메시지 │ │ └── events.proto # 이벤트 정의 │ └── Generated/ # 자동 생성된 C# 파일들 │ ├── PlayerProto.cs │ ├── InventoryProto.cs │ └── ... ``` ## 📄 Protobuf 스키마 정의 ### 플레이어 데이터 스키마 ```protobuf syntax = "proto3"; package game.player; import "google/protobuf/timestamp.proto"; message PlayerData { string player_id = 1; string player_name = 2; int32 level = 3; float experience = 4; PlayerStats stats = 5; google.protobuf.Timestamp created_at = 6; google.protobuf.Timestamp last_login = 7; } message PlayerStats { float max_health = 1; float current_health = 2; float attack_power = 3; float defense = 4; float move_speed = 5; } ``` ### 인벤토리 데이터 스키마 ```protobuf syntax = "proto3"; package game.inventory; message InventoryData { repeated InventorySlot slots = 1; int32 max_slots = 2; int64 last_modified = 3; } message InventorySlot { string item_id = 1; int32 count = 2; int32 slot_index = 3; ItemRarity rarity = 4; } enum ItemRarity { COMMON = 0; UNCOMMON = 1; RARE = 2; EPIC = 3; LEGENDARY = 4; } ``` ### 네트워크 메시지 스키마 ```protobuf syntax = "proto3"; package game.network; // 클라이언트 → 서버 요청 message ClientRequest { string request_id = 1; int64 timestamp = 2; oneof request_type { PlayerSyncRequest player_sync = 10; InventorySyncRequest inventory_sync = 11; GameActionRequest game_action = 12; } } // 서버 → 클라이언트 응답 message ServerResponse { string request_id = 1; ResponseStatus status = 2; string error_message = 3; oneof response_type { PlayerSyncResponse player_sync = 10; InventorySyncResponse inventory_sync = 11; GameActionResponse game_action = 12; } } enum ResponseStatus { SUCCESS = 0; ERROR = 1; INVALID_REQUEST = 2; UNAUTHORIZED = 3; } ``` ## 💻 Unity C# 구현 ### Protobuf 서비스 인터페이스 ```csharp using Google.Protobuf; using Cysharp.Threading.Tasks; using System; public interface IProtobufService { // 직렬화/역직렬화 byte[] Serialize<T>(T message) where T : IMessage<T>; T Deserialize<T>(byte[] data) where T : IMessage<T>, new(); // 네트워크 통신 UniTask<bool> SendToServerAsync<T>(T message) where T : IMessage<T>; UniTask<TResponse> RequestFromServerAsync<TRequest, TResponse>(TRequest request) where TRequest : IMessage<TRequest> where TResponse : IMessage<TResponse>, new(); // 파일 저장/로드 UniTask SaveToFileAsync<T>(string filePath, T message) where T : IMessage<T>; UniTask<T> LoadFromFileAsync<T>(string filePath) where T : IMessage<T>, new(); } ``` ### Protobuf 서비스 구현 ```csharp using Google.Protobuf; using System.Net.Http; using System.Net.Http.Headers; using UnityEngine; using System.IO; public class UnityProtobufService : IProtobufService { private readonly HttpClient _httpClient; private readonly ILoggingService _logger; private readonly string _serverBaseUrl; public UnityProtobufService(ILoggingService logger) { _logger = logger; _httpClient = new HttpClient(); _httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/x-protobuf")); _serverBaseUrl = "https://api.yourgame.com/pb/"; } public byte[] Serialize<T>(T message) where T : IMessage<T> { try { return message.ToByteArray(); } catch (Exception ex) { _logger.LogError(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 프로젝트에서 고성능 데이터 통신과 저장을 위한 완전한 구현 방안을 제시합니다.*