황현동 블로그 개발, 인생, 유우머

260115 Unity MessagePack-CSharp 직렬화 마스터

Tags:

🚀 Unity MessagePack-CSharp 직렬화 완전정복

“JSON보다 10배 빠르고, 바이너리 크기는 절반 이하!”

Unity 게임 개발에서 고성능 직렬화의 필수 도구, MessagePack-CSharp를 마스터하세요.


📚 목차

  1. MessagePack 개요
  2. 바이너리 포맷 상세 분석
  3. Unity 설치 및 설정
  4. Attribute 기반 직렬화
  5. Code Generation & Source Generator
  6. AOT/IL2CPP 완벽 대응
  7. Custom Formatter 마스터
  8. Resolver 시스템 심화
  9. Unity 특화 타입 처리
  10. LZ4 압축 최적화
  11. 네트워크 통신 아키텍처
  12. 게임 세이브/로드 시스템
  13. JSON vs MessagePack 성능 비교
  14. 멀티플레이어 패킷 설계
  15. TypelessFormatter & 다형성
  16. Union 타입 활용법
  17. 버전 호환성 전략
  18. 디버깅 및 검사 도구
  19. 실전 예제: 인벤토리 시스템
  20. 실전 예제: 랭킹 시스템
  21. 고급 최적화 테크닉
  22. 트러블슈팅 가이드

🎯 1. MessagePack 개요

1.1 MessagePack이란?

MessagePack은 “JSON과 같지만 빠르고 작은” 효율적인 바이너리 직렬화 포맷입니다. 🎁

핵심 특징

  • 10배 빠른 속도: MessagePack-CSharp는 다른 C# 직렬화 라이브러리보다 10배 이상 빠릅니다
  • 💾 작은 크기: JSON 대비 30-50% 수준의 바이너리 크기
  • 🌍 다중 언어 지원: 50개 이상의 프로그래밍 언어 지원
  • 🎮 Unity 최적화: Unity 전용 타입 및 IL2CPP 완벽 지원

실제 크기 비교

숫자 값 JSON 크기 MessagePack 크기
10 2 bytes 1 byte
100 3 bytes 1 byte
-108 4 bytes 1 byte
65535 5 bytes 3 bytes

1.2 왜 Unity 게임에 필요한가? 🎮

사용 시나리오

  1. 🌐 네트워크 멀티플레이어
    • 실시간 플레이어 위치/상태 동기화
    • 채팅 메시지 전송
    • 게임 이벤트 브로드캐스트
  2. 💾 세이브/로드 시스템
    • 플레이어 프로필 저장
    • 게임 진행 상황 저장
    • 설정 데이터 영구 보관
  3. 📦 애셋 번들 데이터
    • 대량의 게임 데이터 패킹
    • 다운로드 가능한 콘텐츠(DLC)
    • 메타데이터 관리
  4. 🔄 서버 통신
    • REST API 요청/응답
    • WebSocket 메시지
    • 랭킹/리더보드 동기화

🔬 2. 바이너리 포맷 상세 분석

2.1 MessagePack 타입 시스템

MessagePack은 다음과 같은 타입들을 지원합니다:

기본 타입

• Integer (int8, int16, int32, int64, uint8, uint16, uint32, uint64)
• Float (float32, float64)
• Boolean
• Nil (null)
• String (UTF-8)
• Binary (byte array)

컬렉션 타입

• Array (순서가 있는 리스트)
• Map (키-값 딕셔너리)

Extension 타입

• Timestamp (날짜/시간)
• Custom Extension Types (사용자 정의 타입)

2.2 바이너리 인코딩 예제 📊

정수 인코딩

Positive FixInt (0~127)

값: 42
바이트: [0x2a]
크기: 1 byte

uint 16

값: 1000
바이트: [0xcd, 0x03, 0xe8]
크기: 3 bytes

문자열 인코딩

FixStr (길이 0~31)

값: "hello"
바이트: [0xa5, 0x68, 0x65, 0x6c, 0x6c, 0x6f]
      [길이5, 'h', 'e', 'l', 'l', 'o']
크기: 6 bytes

배열 인코딩

FixArray (길이 0~15)

값: [1, 2, 3]
바이트: [0x93, 0x01, 0x02, 0x03]
      [길이3, 값1, 값2, 값3]
크기: 4 bytes

2.3 MessagePack vs JSON 바이트 비교 ⚖️

예제 객체

public class Player
{
    public int Id { get; set; } = 12345;
    public string Name { get; set; } = "Warrior";
    public float Health { get; set; } = 100.0f;
    public bool IsAlive { get; set; } = true;
}

JSON 인코딩 결과

{"Id":12345,"Name":"Warrior","Health":100.0,"IsAlive":true}

크기: 60 bytes

MessagePack 인코딩 결과

[0x84, 0xa2, 0x49, 0x64, 0xcd, 0x30, 0x39, ...]

크기: 약 28 bytes (53% 절약!)


💿 3. Unity 설치 및 설정

3.1 시스템 요구사항 🔧

Unity 버전

  • 최소 요구사항: Unity 2022.3.12f1
  • 권장: Unity 2022 LTS 이상
  • 이유: C# Source Generator 지원을 위해 필요

.NET 환경

  • .NET Standard 2.1 이상
  • C# 9.0 이상 권장 (Source Generator 활용)

3.2 설치 방법 (2단계 필수!) 📦

⚠️ 중요: 두 단계 모두 반드시 수행해야 합니다!

Step 1: NuGet에서 MessagePack 설치

  1. NuGetForUnity 설치
    • Unity Package Manager에서 NuGetForUnity 추가
    • 또는 GitHub에서 다운로드: NuGetForUnity
  2. MessagePack 패키지 설치

     1. Unity 메뉴: Window → NuGet → Manage NuGet Packages
     2. "MessagePack" 검색
     3. "MessagePack" 선택 후 Install 버튼 클릭
    

Step 2: Unity 전용 패키지 설치

  1. Package Manager 열기
    • Unity 메뉴: Window → Package Manager
  2. Git URL로 패키지 추가

     1. 좌측 상단 "+" 버튼 클릭
     2. "Add package from git URL" 선택
     3. 다음 URL 입력:
    
https://github.com/MessagePack-CSharp/MessagePack-CSharp.git?path=src/MessagePack.UnityClient/Assets/Scripts/MessagePack

3.3 설치 확인 ✅

테스트 스크립트 작성

using UnityEngine;
using MessagePack;
using System;

public class MessagePackTest : MonoBehaviour
{
    [MessagePackObject]
    public class TestData
    {
        [Key(0)]
        public int Value { get; set; }
        
        [Key(1)]
        public string Name { get; set; }
    }
    
    void Start()
    {
        // 직렬화 테스트
        var data = new TestData { Value = 42, Name = "Test" };
        byte[] bytes = MessagePackSerializer.Serialize(data);
        
        Debug.Log($"✅ MessagePack 설치 성공!");
        Debug.Log($"📦 Serialized size: {bytes.Length} bytes");
        
        // 역직렬화 테스트
        var restored = MessagePackSerializer.Deserialize<TestData>(bytes);
        Debug.Log($"🔄 Deserialized: Value={restored.Value}, Name={[restored.Name](http://restored.Name)}");
    }
}

예상 출력

✅ MessagePack 설치 성공!
📦 Serialized size: 8 bytes
🔄 Deserialized: Value=42, Name=Test

🏷️ 4. Attribute 기반 직렬화

4.1 MessagePackObject Attribute 🎯

기본 사용법

using MessagePack;

[MessagePackObject]
public class PlayerData
{
    [Key(0)]
    public int PlayerId { get; set; }
    
    [Key(1)]
    public string PlayerName { get; set; }
    
    [Key(2)]
    public float Experience { get; set; }
    
    [Key(3)]
    public int Level { get; set; }
}

Key Attribute 규칙

DO (권장)

  • 0부터 시작하는 연속된 정수 사용
  • 순서대로 부여 (0, 1, 2, 3…)
  • 삭제된 필드의 Key는 재사용하지 않기

DON’T (비권장)

  • 중간에 Key 건너뛰기 (0, 1, 3, 5…)
  • 음수 Key 사용
  • Key 순서 무작위 배치

4.2 String Key vs Integer Key 🔑

Integer Key (권장) ⚡

[MessagePackObject]
public class FastPlayer
{
    [Key(0)] public int Id { get; set; }
    [Key(1)] public string Name { get; set; }
}

장점

  • ✅ 최소 바이너리 크기
  • ✅ 최고 성능
  • ✅ 네트워크 전송에 최적

단점

  • ⚠️ 가독성 낮음
  • ⚠️ 필드 순서 관리 필요

String Key (선택) 📝

[MessagePackObject(true)]  // keyAsPropertyName: true
public class ReadablePlayer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

장점

  • ✅ 높은 가독성
  • ✅ JSON과 호환 가능
  • ✅ 디버깅 용이

단점

  • ⚠️ 크기 증가 (약 20-30%)
  • ⚠️ 성능 저하 (약 15-20%)

4.3 IgnoreMember Attribute 🚫

[MessagePackObject]
public class GameEntity
{
    [Key(0)]
    public int EntityId { get; set; }
    
    [Key(1)]
    public Vector3 Position { get; set; }
    
    // 직렬화에서 제외
    [IgnoreMember]
    public GameObject CachedGameObject { get; set; }
    
    [IgnoreMember]
    public Transform CachedTransform { get; set; }
}

제외해야 하는 필드 타입

  • 🚫 Unity의 직렬화 불가능 타입 (GameObject, Component 등)
  • 🚫 임시 캐시 데이터
  • 🚫 계산된 속성 (Computed Properties)
  • 🚫 순환 참조가 있는 객체

4.4 생성자 패턴 🏗️

기본 생성자 (권장)

[MessagePackObject]
public class Item
{
    [Key(0)] public int ItemId { get; set; }
    [Key(1)] public string ItemName { get; set; }
    [Key(2)] public int Quantity { get; set; }
    
    // 파라미터 없는 생성자 (필수)
    public Item()
    {
    }
}

SerializationConstructor (고급)

[MessagePackObject]
public class ImmutableItem
{
    [Key(0)] public int ItemId { get; }
    [Key(1)] public string ItemName { get; }
    [Key(2)] public int Quantity { get; }
    
    [SerializationConstructor]
    public ImmutableItem(int itemId, string itemName, int quantity)
    {
        ItemId = itemId;
        ItemName = itemName;
        Quantity = quantity;
    }
}

장점

  • ✅ Immutable 객체 지원
  • ✅ 유효성 검사 로직 포함 가능
  • ✅ 읽기 전용 속성 지원

⚙️ 5. Code Generation & Source Generator

5.1 Source Generator란? 🔧

MessagePack v3부터는 C# Source Generator를 사용하여 컴파일 타임에 Formatter 코드를 자동 생성합니다.

기존 방식 (mpc.exe) vs 새로운 방식 (Source Generator)

항목 mpc.exe (구버전) Source Generator (v3+)
실행 시점 수동 실행 필요 자동 (빌드 시)
Unity 통합 별도 설정 필요 NuGet 설치 시 자동
유지보수 번거로움 편리함
IL2CPP 지원 수동 생성 자동 지원
에러 감지 런타임 컴파일 타임

5.2 Source Generator 동작 원리 🔍

Step 1: Attribute 분석

컴파일러가 [MessagePackObject] 어트리뷰트가 붙은 클래스를 탐지합니다.

[MessagePackObject]  // ← Source Generator가 이것을 감지
public class PlayerStats
{
    [Key(0)] public int Health { get; set; }
    [Key(1)] public int Mana { get; set; }
}

Step 2: Formatter 자동 생성

Source Generator가 다음과 같은 코드를 자동으로 생성합니다:

// 자동 생성된 코드 (MessagePackGenerated.cs)
namespace MessagePack.Formatters
{
    public sealed class PlayerStatsFormatter : IMessagePackFormatter<PlayerStats>
    {
        public void Serialize(
            ref MessagePackWriter writer, 
            PlayerStats value, 
            MessagePackSerializerOptions options)
        {
            if (value == null)
            {
                writer.WriteNil();
                return;
            }
            
            writer.WriteArrayHeader(2);
            writer.Write([value.Health](http://value.Health));
            writer.Write(value.Mana);
        }
        
        public PlayerStats Deserialize(
            ref MessagePackReader reader, 
            MessagePackSerializerOptions options)
        {
            if (reader.TryReadNil())
                return null;
            
            var length = reader.ReadArrayHeader();
            var result = new PlayerStats();
            
            for (int i = 0; i < length; i++)
            {
                switch (i)
                {
                    case 0:
                        [result.Health](http://result.Health) = reader.ReadInt32();
                        break;
                    case 1:
                        result.Mana = reader.ReadInt32();
                        break;
                    default:
                        reader.Skip();
                        break;
                }
            }
            
            return result;
        }
    }
}

Step 3: Resolver 등록

생성된 Formatter들은 자동으로 Resolver에 등록됩니다.

// 자동 생성된 Resolver
public sealed class GeneratedResolver : IFormatterResolver
{
    public static readonly GeneratedResolver Instance = new GeneratedResolver();
    
    public IMessagePackFormatter<T> GetFormatter<T>()
    {
        return FormatterCache<T>.Formatter;
    }
    
    private static class FormatterCache<T>
    {
        public static readonly IMessagePackFormatter<T> Formatter;
        
        static FormatterCache()
        {
            // 타입별 Formatter 매핑
            if (typeof(T) == typeof(PlayerStats))
            {
                Formatter = (IMessagePackFormatter<T>)(object)new PlayerStatsFormatter();
            }
            // ... 다른 타입들
        }
    }
}

5.3 Source Generator 설정 확인 ✅

Unity에서 확인하기

  1. 생성된 파일 위치

     Library/Bee/artifacts/[config]/MessagePackGenerated/
    
  2. Visual Studio에서 확인
    • Solution Explorer에서 Dependencies → Analyzers → MessagePack 확장
    • 생성된 소스 파일 확인 가능
  3. 로그 확인

     Debug.Log($"Resolver 타입: {MessagePackSerializer.DefaultOptions.Resolver.GetType()}");
    

🎮 6. AOT/IL2CPP 완벽 대응

6.1 AOT란? 🔍

AOT (Ahead-Of-Time) Compilation은 런타임이 아닌 빌드 타임에 코드를 컴파일하는 방식입니다.

Unity에서 AOT를 사용하는 플랫폼

  • 📱 iOS (Apple 정책에 의해 강제)
  • 🎮 Nintendo Switch
  • 🎯 PlayStation
  • 📦 Xbox
  • 🌐 WebGL (일부 제약)

6.2 IL2CPP 문제점과 해결 ⚠️

문제 1: 동적 코드 생성 불가

문제 상황

// ❌ IL2CPP에서 실패하는 코드
var options = MessagePackSerializerOptions.Standard
    .WithResolver(ContractlessStandardResolver.Instance);  // 런타임 코드 생성 시도

에러 메시지

ExecutionEngineException: Attempting to call method 
'MessagePack.Formatters.DynamicFormatter<T>::Serialize' 
for which no ahead of time (AOT) code was generated.

해결책 1: Source Generator 사용 (v3+)

// ✅ v3에서는 자동으로 AOT 대응 코드 생성
[MessagePackObject]
public class GameData
{
    [Key(0)] public int Score { get; set; }
}

// NuGet 설치 시 Source Generator가 자동으로 AOT 코드 생성

해결책 2: StaticCompositeResolver 사용

using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

public class MessagePackInitializer
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Initialize()
    {
        // IL2CPP 대응 Resolver 설정
        StaticCompositeResolver.Instance.Register(
            // 1. 자동 생성된 Resolver (가장 우선)
            GeneratedResolver.Instance,
            
            // 2. Unity 전용 Resolver
            MessagePack.Unity.UnityResolver.Instance,
            
            // 3. Unity Blit Resolver (Vector3[] 등 고속 직렬화)
            MessagePack.Unity.Extension.UnityBlitWithPrimitiveArrayResolver.Instance,
            
            // 4. 기본 타입 Resolver
            StandardResolver.Instance
        );
        
        var options = MessagePackSerializerOptions.Standard
            .WithResolver(StaticCompositeResolver.Instance);
        
        MessagePackSerializer.DefaultOptions = options;
        
        Debug.Log("✅ MessagePack IL2CPP 설정 완료!");
    }
}

6.3 Unity Build 설정 🔧

Android IL2CPP 설정

1. File → Build Settings → Android
2. Player Settings → Other Settings
3. Scripting Backend: IL2CPP
4. Target Architectures: ARM64 ✅
5. API Compatibility Level: .NET Standard 2.1

iOS 설정

1. File → Build Settings → iOS
2. Player Settings → Other Settings
3. Scripting Backend: IL2CPP (기본값)
4. Target SDK: Device SDK
5. Architecture: ARM64

6.4 IL2CPP 테스트 스크립트 🧪

using UnityEngine;
using MessagePack;
using System.Collections.Generic;

public class IL2CPPTest : MonoBehaviour
{
    [MessagePackObject]
    public class TestData
    {
        [Key(0)] public int Value { get; set; }
        [Key(1)] public string Name { get; set; }
        [Key(2)] public List<int> Numbers { get; set; }
        [Key(3)] public Vector3 Position { get; set; }
    }
    
    void Start()
    {
        try
        {
            // 복잡한 데이터 직렬화 테스트
            var data = new TestData
            {
                Value = 12345,
                Name = "IL2CPP Test",
                Numbers = new List<int> { 1, 2, 3, 4, 5 },
                Position = new Vector3(10f, 20f, 30f)
            };
            
            byte[] bytes = MessagePackSerializer.Serialize(data);
            var restored = MessagePackSerializer.Deserialize<TestData>(bytes);
            
            Debug.Log($"✅ IL2CPP 직렬화 성공!");
            Debug.Log($"📦 크기: {bytes.Length} bytes");
            Debug.Log($"🔄 복원: Value={restored.Value}, Position={restored.Position}");
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"❌ IL2CPP 오류: {ex.Message}");
        }
    }
}

🎨 7. Custom Formatter 마스터

7.1 Custom Formatter란? 🖌️

Custom Formatter는 제3자 라이브러리 타입이나 특수한 직렬화 로직이 필요한 경우에 사용합니다.

사용 시나리오

  • 🔒 암호화된 직렬화
  • 🗜️ 압축 알고리즘 적용
  • 🎯 특정 필드만 선택적 직렬화
  • 🔄 레거시 포맷 호환
  • 🎮 Unity 커스텀 컴포넌트

7.2 IMessagePackFormatter 인터페이스 📋

public interface IMessagePackFormatter<T>
{
    void Serialize(
        ref MessagePackWriter writer, 
        T value, 
        MessagePackSerializerOptions options);
    
    T Deserialize(
        ref MessagePackReader reader, 
        MessagePackSerializerOptions options);
}

7.3 예제 1: Color32 Formatter 🎨

using MessagePack;
using MessagePack.Formatters;
using UnityEngine;

public sealed class Color32Formatter : IMessagePackFormatter<Color32>
{
    // 싱글톤 인스턴스
    public static readonly Color32Formatter Instance = new Color32Formatter();
    
    private Color32Formatter() { }
    
    public void Serialize(
        ref MessagePackWriter writer, 
        Color32 value, 
        MessagePackSerializerOptions options)
    {
        // 4개의 byte를 uint로 패킹 (4 bytes → 5 bytes)
        writer.WriteArrayHeader(4);
        writer.Write(value.r);
        writer.Write(value.g);
        writer.Write(value.b);
        writer.Write(value.a);
    }
    
    public Color32 Deserialize(
        ref MessagePackReader reader, 
        MessagePackSerializerOptions options)
    {
        var count = reader.ReadArrayHeader();
        
        byte r = reader.ReadByte();
        byte g = reader.ReadByte();
        byte b = reader.ReadByte();
        byte a = reader.ReadByte();
        
        return new Color32(r, g, b, a);
    }
}

7.4 예제 2: 압축 String Formatter 🗜️

using MessagePack;
using MessagePack.Formatters;
using [System.IO](http://System.IO);
using [System.IO](http://System.IO).Compression;
using System.Text;

public sealed class CompressedStringFormatter : IMessagePackFormatter<string>
{
    public static readonly CompressedStringFormatter Instance = new CompressedStringFormatter();
    
    // 이 크기 이상일 때만 압축
    private const int CompressionThreshold = 128;
    
    public void Serialize(
        ref MessagePackWriter writer, 
        string value, 
        MessagePackSerializerOptions options)
    {
        if (value == null)
        {
            writer.WriteNil();
            return;
        }
        
        byte[] bytes = Encoding.UTF8.GetBytes(value);
        
        // 작은 문자열은 압축하지 않음
        if (bytes.Length < CompressionThreshold)
        {
            writer.WriteArrayHeader(2);
            writer.Write(false);  // 압축 안함
            writer.Write(bytes);
            return;
        }
        
        // GZip 압축
        using (var output = new MemoryStream())
        {
            using (var gzip = new GZipStream(output, CompressionMode.Compress))
            {
                gzip.Write(bytes, 0, bytes.Length);
            }
            
            byte[] compressed = output.ToArray();
            
            writer.WriteArrayHeader(2);
            writer.Write(true);  // 압축됨
            writer.Write(compressed);
        }
    }
    
    public string Deserialize(
        ref MessagePackReader reader, 
        MessagePackSerializerOptions options)
    {
        if (reader.TryReadNil())
            return null;
        
        var count = reader.ReadArrayHeader();
        bool isCompressed = reader.ReadBoolean();
        byte[] bytes = reader.ReadBytes().Value.ToArray();
        
        if (!isCompressed)
        {
            return Encoding.UTF8.GetString(bytes);
        }
        
        // GZip 압축 해제
        using (var input = new MemoryStream(bytes))
        using (var gzip = new GZipStream(input, CompressionMode.Decompress))
        using (var output = new MemoryStream())
        {
            gzip.CopyTo(output);
            return Encoding.UTF8.GetString(output.ToArray());
        }
    }
}

7.5 예제 3: 암호화 Formatter 🔒

using MessagePack;
using MessagePack.Formatters;
using [System.Security](http://System.Security).Cryptography;
using [System.IO](http://System.IO);

public sealed class EncryptedDataFormatter<T> : IMessagePackFormatter<T>
{
    private readonly byte[] _key;
    private readonly byte[] _iv;
    
    public EncryptedDataFormatter(byte[] key, byte[] iv)
    {
        _key = key;
        _iv = iv;
    }
    
    public void Serialize(
        ref MessagePackWriter writer, 
        T value, 
        MessagePackSerializerOptions options)
    {
        // 1. 원본 데이터 직렬화
        byte[] plainBytes = MessagePackSerializer.Serialize(value, options);
        
        // 2. AES 암호화
        using (var aes = Aes.Create())
        {
            aes.Key = _key;
            aes.IV = _iv;
            
            using (var encryptor = aes.CreateEncryptor())
            using (var ms = new MemoryStream())
            {
                using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                {
                    cs.Write(plainBytes, 0, plainBytes.Length);
                }
                
                byte[] encrypted = ms.ToArray();
                writer.Write(encrypted);
            }
        }
    }
    
    public T Deserialize(
        ref MessagePackReader reader, 
        MessagePackSerializerOptions options)
    {
        byte[] encrypted = reader.ReadBytes().Value.ToArray();
        
        // 1. AES 복호화
        using (var aes = Aes.Create())
        {
            aes.Key = _key;
            aes.IV = _iv;
            
            using (var decryptor = aes.CreateDecryptor())
            using (var ms = new MemoryStream(encrypted))
            using (var cs = new CryptoStream(ms, decryptor, [CryptoStreamMode.Read](http://CryptoStreamMode.Read)))
            using (var output = new MemoryStream())
            {
                cs.CopyTo(output);
                byte[] plainBytes = output.ToArray();
                
                // 2. 원본 데이터 역직렬화
                return MessagePackSerializer.Deserialize<T>(plainBytes, options);
            }
        }
    }
}

7.6 Custom Formatter 등록 📝

using MessagePack;
using MessagePack.Resolvers;
using UnityEngine;

public class CustomFormatterInitializer
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Initialize()
    {
        // Custom Resolver 생성
        var resolver = CompositeResolver.Create(
            // Custom Formatters
            new IMessagePackFormatter[]
            {
                Color32Formatter.Instance,
                CompressedStringFormatter.Instance,
            },
            // Standard Resolvers
            new IFormatterResolver[]
            {
                GeneratedResolver.Instance,
                StandardResolver.Instance
            }
        );
        
        var options = MessagePackSerializerOptions.Standard.WithResolver(resolver);
        MessagePackSerializer.DefaultOptions = options;
        
        Debug.Log("✅ Custom Formatter 등록 완료!");
    }
}

🔧 8. Resolver 시스템 심화

8.1 Resolver란? 🔍

Resolver는 타입에 맞는 Formatter를 찾아주는 역할을 합니다.

타입 T → Resolver → IMessagePackFormatter<T> → Serialize/Deserialize

8.2 Built-in Resolvers 📚

StandardResolver

StandardResolver.Instance

지원 타입

  • Primitive types (int, float, string, bool 등)
  • Collection types (List, Dictionary, Array 등)
  • Nullable types
  • DateTime, TimeSpan, Guid
  • BigInteger, Uri

ContractlessStandardResolver

ContractlessStandardResolver.Instance

특징

  • ✅ [MessagePackObject] Attribute 불필요
  • ✅ 자동으로 모든 public 속성 직렬화
  • ❌ IL2CPP에서 사용 불가 (동적 코드 생성)

UnityResolver

MessagePack.Unity.UnityResolver.Instance

지원 타입

 Vector2, Vector3, Vector4
 Quaternion
 Color, Color32
 Bounds
 Rect
 Matrix4x4
 AnimationCurve, Keyframe
 Gradient
 RectOffset
 LayerMask
 Vector2Int, Vector3Int
 RangeInt, RectInt, BoundsInt

UnityBlitResolver

MessagePack.Unity.Extension.UnityBlitWithPrimitiveArrayResolver.Instance

특징

  • 극한의 성능: 메모리 직접 복사 (Blitting)
  • 🎯 배열 특화: Vector3[], Quaternion[], Color[] 등
  • 📦 Extension Format: typecode 30~39 사용

성능 비교

JsonUtility.FromJson<Vector3[]>():  100ms
MessagePack (일반):                  15ms  (6.7배 빠름)
MessagePack (Blit):                   5ms  (20배 빠름!)

8.3 Resolver 우선순위 체계 📊

StaticCompositeResolver.Instance.Register(
    // 1순위: Custom Formatters
    CustomResolver.Instance,
    
    // 2순위: 프로젝트 Generated Resolver
    GeneratedResolver.Instance,
    
    // 3순위: Unity 특화 Resolver
    UnityBlitResolver.Instance,
    UnityResolver.Instance,
    
    // 4순위: 표준 Resolver
    StandardResolver.Instance
);

동작 원리

  1. 위에서 아래로 순서대로 탐색
  2. 첫 번째로 Formatter를 찾으면 즉시 반환
  3. 없으면 다음 Resolver로 이동
  4. 모두 실패하면 예외 발생

8.4 Custom Resolver 구현 🛠️

using MessagePack;
using MessagePack.Formatters;

public sealed class GameDataResolver : IFormatterResolver
{
    public static readonly GameDataResolver Instance = new GameDataResolver();
    
    private GameDataResolver() { }
    
    public IMessagePackFormatter<T> GetFormatter<T>()
    {
        return FormatterCache<T>.Formatter;
    }
    
    private static class FormatterCache<T>
    {
        public static readonly IMessagePackFormatter<T> Formatter;
        
        static FormatterCache()
        {
            // 타입별 Formatter 매핑
            if (typeof(T) == typeof(Color32))
            {
                Formatter = (IMessagePackFormatter<T>)(object)Color32Formatter.Instance;
            }
            else if (typeof(T) == typeof(string))
            {
                Formatter = (IMessagePackFormatter<T>)(object)CompressedStringFormatter.Instance;
            }
            // ... 다른 타입들
        }
    }
}

🎮 9. Unity 특화 타입 처리

9.1 Vector3 직렬화 📍

기본 사용법

using UnityEngine;
using MessagePack;

[MessagePackObject]
public class TransformData
{
    [Key(0)] public Vector3 Position { get; set; }
    [Key(1)] public Vector3 Rotation { get; set; }
    [Key(2)] public Vector3 Scale { get; set; }
}

// 사용 예제
var data = new TransformData
{
    Position = new Vector3(10f, 20f, 30f),
    Rotation = new Vector3(0f, 90f, 0f),
    Scale = [Vector3.one](http://Vector3.one)
};

byte[] bytes = MessagePackSerializer.Serialize(data);
var restored = MessagePackSerializer.Deserialize<TransformData>(bytes);

Debug.Log($"Position: {restored.Position}");  // (10.0, 20.0, 30.0)

Vector3 배열 고속 직렬화 ⚡

using MessagePack;
using UnityEngine;

public class MeshDataOptimized
{
    // UnityBlitResolver를 사용하면 20배 빠름!
    public Vector3[] Vertices { get; set; }
    public Vector3[] Normals { get; set; }
    public Vector2[] UVs { get; set; }
    public int[] Triangles { get; set; }
}

// 초기화 코드에서 UnityBlitResolver 등록 필수
StaticCompositeResolver.Instance.Register(
    MessagePack.Unity.Extension.UnityBlitWithPrimitiveArrayResolver.Instance,
    // ... other resolvers
);

성능 비교 (10,000 Vertices)

JsonUtility:        2500ms
MessagePack:         200ms  (12.5배)
MessagePack (Blit):  125ms  (20배!)

9.2 Quaternion 직렬화 🔄

[MessagePackObject]
public class RotationData
{
    [Key(0)] public Quaternion Rotation { get; set; }
    [Key(1)] public Quaternion[] BoneRotations { get; set; }  // 애니메이션 본 회전
}

// 사용 예제
var data = new RotationData
{
    Rotation = Quaternion.Euler(45f, 90f, 180f),
    BoneRotations = new Quaternion[]
    {
        Quaternion.identity,
        Quaternion.Euler(10f, 0f, 0f),
        Quaternion.Euler(0f, 20f, 0f)
    }
};

byte[] bytes = MessagePackSerializer.Serialize(data);

9.3 Color & Color32 🎨

Color (float)

[MessagePackObject]
public class MaterialData
{
    [Key(0)] public Color MainColor { get; set; }        // 16 bytes (RGBA float)
    [Key(1)] public Color EmissionColor { get; set; }
}

var data = new MaterialData
{
    MainColor = new Color(1f, 0.5f, 0.25f, 1f),
    EmissionColor = [Color.red](http://Color.red)
};

Color32 (byte) - 메모리 효율적

[MessagePackObject]
public class SpriteData
{
    [Key(0)] public Color32 TintColor { get; set; }      // 4 bytes (RGBA byte)
    [Key(1)] public Color32[] Pixels { get; set; }       // 텍스처 데이터
}

var data = new SpriteData
{
    TintColor = new Color32(255, 128, 64, 255),
    Pixels = new Color32[1024 * 1024]  // 1MB
};

크기 비교

Color:   16 bytes per color
Color32:  4 bytes per color  (75% 절약)

9.4 Unity 커스텀 타입 🛠️

Transform Snapshot

using UnityEngine;
using MessagePack;

[MessagePackObject]
public struct TransformSnapshot
{
    [Key(0)] public Vector3 Position { get; set; }
    [Key(1)] public Quaternion Rotation { get; set; }
    [Key(2)] public Vector3 LocalScale { get; set; }
    
    // Transform에서 스냅샷 생성
    public static TransformSnapshot Capture(Transform transform)
    {
        return new TransformSnapshot
        {
            Position = transform.position,
            Rotation = transform.rotation,
            LocalScale = transform.localScale
        };
    }
    
    // Transform에 적용
    public void ApplyTo(Transform transform)
    {
        transform.position = Position;
        transform.rotation = Rotation;
        transform.localScale = LocalScale;
    }
}

Bounds & Rect

[MessagePackObject]
public class ColliderData
{
    [Key(0)] public Bounds Bounds { get; set; }
    [Key(1)] public Rect ScreenRect { get; set; }
    [Key(2)] public RectInt GridRect { get; set; }
}

var data = new ColliderData
{
    Bounds = new Bounds([Vector3.zero](http://Vector3.zero), [Vector3.one](http://Vector3.one) * 10f),
    ScreenRect = new Rect(0, 0, 1920, 1080),
    GridRect = new RectInt(0, 0, 64, 64)
};

AnimationCurve

[MessagePackObject]
public class AnimationData
{
    [Key(0)] public AnimationCurve SpeedCurve { get; set; }
    [Key(1)] public Keyframe[] CustomKeyframes { get; set; }
}

var data = new AnimationData
{
    SpeedCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f),
    CustomKeyframes = new Keyframe[]
    {
        new Keyframe(0f, 0f),
        new Keyframe(0.5f, 1f),
        new Keyframe(1f, 0f)
    }
};

🗜️ 10. LZ4 압축 최적화

10.1 LZ4 압축이란? 📦

LZ4는 극히 빠른 압축 알고리즘입니다.

특징 비교

항목 LZ4 GZip Zstd
압축 속도 ★★★★★ ★★ ★★★
압축률 ★★★ ★★★★ ★★★★★
해제 속도 ★★★★★ ★★★ ★★★★
사용 예 실시간 게임 파일 저장 근최 DLC

실제 성능 데이터

원본 크기:    5000 bytes
LZ4 압축:     2700 bytes  (46% 감소)
압축 시간:    0.15ms
해제 시간:    0.08ms

10.2 LZ4 사용법 🛠️

기본 사용

using MessagePack;
using UnityEngine;

public class LZ4Example : MonoBehaviour
{
    [MessagePackObject]
    public class LargeData
    {
        [Key(0)] public string[] Messages { get; set; }
        [Key(1)] public int[] Numbers { get; set; }
    }
    
    void TestLZ4Compression()
    {
        var data = new LargeData
        {
            Messages = new string[1000],
            Numbers = new int[1000]
        };
        
        // LZ4 옵션 설정
        var options = MessagePackSerializerOptions.Standard
            .WithCompression(MessagePackCompression.Lz4BlockArray);
        
        // 압축 직렬화
        byte[] compressed = MessagePackSerializer.Serialize(data, options);
        
        // 일반 직렬화 비교
        byte[] normal = MessagePackSerializer.Serialize(data);
        
        Debug.Log($"일반: {normal.Length} bytes");
        Debug.Log($"LZ4: {compressed.Length} bytes");
        Debug.Log($"압축률: {(1f - (float)compressed.Length / normal.Length) * 100f:F1}%");
        
        // 자동 해제되어 복원
        var restored = MessagePackSerializer.Deserialize<LargeData>(compressed, options);
    }
}

10.3 LZ4 압축 모드 📊

Lz4Block vs Lz4BlockArray

Lz4Block - 단일 블록

var options = MessagePackSerializerOptions.Standard
    .WithCompression(MessagePackCompression.Lz4Block);
  • ✅ 단순한 구조
  • ❌ 큰 데이터에 비효율적

Lz4BlockArray - 여러 블록 (권장)

var options = MessagePackSerializerOptions.Standard
    .WithCompression(MessagePackCompression.Lz4BlockArray);
  • ✅ 큰 데이터에 최적화
  • ✅ 스트리밍 가능
  • ✅ 부분 압축 해제 가능

10.4 압축 적용 기준 📝

언제 LZ4를 사용할까?

사용 권장

  • 네트워크 전송 데이터
  • 1KB 이상의 데이터
  • 반복적인 패턴이 있는 데이터
  • 큰 JSON 유사 구조

사용 비권장

  • 이미 작은 데이터 (<100 bytes)
  • 랜덤 바이너리 데이터
  • 실시간 60FPS 게임 루프 내부
  • 이미 압축된 데이터 (PNG, MP3 등)

성능 테스트 스크립트

using System.Diagnostics;
using UnityEngine;
using MessagePack;

public class CompressionBenchmark : MonoBehaviour
{
    [MessagePackObject]
    public class TestData
    {
        [Key(0)] public string LongText { get; set; }
        [Key(1)] public int[] Numbers { get; set; }
    }
    
    void Start()
    {
        var data = new TestData
        {
            LongText = new string('A', 10000),  // 10KB 문자열
            Numbers = new int[1000]
        };
        
        // 테스트 1: 압축 없음
        var sw1 = Stopwatch.StartNew();
        byte[] normal = MessagePackSerializer.Serialize(data);
        sw1.Stop();
        
        // 테스트 2: LZ4 압축
        var options = MessagePackSerializerOptions.Standard
            .WithCompression(MessagePackCompression.Lz4BlockArray);
        
        var sw2 = Stopwatch.StartNew();
        byte[] compressed = MessagePackSerializer.Serialize(data, options);
        sw2.Stop();
        
        // 결과 출력
        Debug.Log($"=== 압축 비교 ===");
        Debug.Log($"일반: {normal.Length:N0} bytes, {sw1.Elapsed.TotalMilliseconds:F3}ms");
        Debug.Log($"LZ4:  {compressed.Length:N0} bytes, {sw2.Elapsed.TotalMilliseconds:F3}ms");
        Debug.Log($"크기 감소: {(1f - (float)compressed.Length / normal.Length) * 100f:F1}%");
        
        // 해제 성능 테스트
        var sw3 = Stopwatch.StartNew();
        var restored = MessagePackSerializer.Deserialize<TestData>(compressed, options);
        sw3.Stop();
        
        Debug.Log($"해제: {sw3.Elapsed.TotalMilliseconds:F3}ms");
    }
}

🌐 11. 네트워크 통신 아키텍처

11.1 패킷 설계 원칙 📦

기본 원칙

  1. 크기 최소화: 한 패킷은 64 bytes 이하 권장
  2. 타입 식별자: 첫 바이트는 패킷 타입 ID
  3. 고정 크기: 동적 할당 피하기
  4. 부호 범위: ushort 사용 (0~65535)
  5. 비트 패킹: bool 플래그들은 하나의 byte로

11.2 패킷 타입 설계 🏷️

Packet Type Enum

public enum PacketType : byte
{
    // 플레이어 이동 (1~10)
    PlayerMove = 1,
    PlayerJump = 2,
    PlayerAttack = 3,
    
    // 채팅 (11~20)
    ChatMessage = 11,
    
    // 게임 상태 (21~30)
    GameStart = 21,
    GameEnd = 22,
    
    // 동기화 (31~40)
    SyncTransform = 31,
    SyncHealth = 32,
    
    // 인벤토리 (41~50)
    ItemAdd = 41,
    ItemRemove = 42,
    ItemUse = 43,
}

11.3 기본 패킷 구조 📦

Packet Base Class

using MessagePack;

[Union(0, typeof(PlayerMovePacket))]
[Union(1, typeof(PlayerAttackPacket))]
[Union(2, typeof(ChatMessagePacket))]
[MessagePackObject]
public abstract class Packet
{
    [Key(0)]
    public byte PacketType { get; set; }
    
    [Key(1)]
    public uint Timestamp { get; set; }  // 밀리초 타임스탬프
}

플레이어 이동 패킷

[MessagePackObject]
public class PlayerMovePacket : Packet
{
    [Key(2)] public ushort PlayerId { get; set; }      // 2 bytes
    [Key(3)] public Vector3 Position { get; set; }     // 12 bytes (3 * float)
    [Key(4)] public byte Rotation { get; set; }        // 1 byte (0~255 = 0~360도)
    [Key(5)] public byte Flags { get; set; }           // 1 byte (비트 플래그)
    
    // 총 크기: 약 18 bytes
    
    // Flags 비트 마스크
    public bool IsRunning
    {
        get => (Flags & 0x01) != 0;
        set => Flags = value ? (byte)(Flags | 0x01) : (byte)(Flags & ~0x01);
    }
    
    public bool IsJumping
    {
        get => (Flags & 0x02) != 0;
        set => Flags = value ? (byte)(Flags | 0x02) : (byte)(Flags & ~0x02);
    }
    
    public bool IsCrouching
    {
        get => (Flags & 0x04) != 0;
        set => Flags = value ? (byte)(Flags | 0x04) : (byte)(Flags & ~0x04);
    }
}

공격 패킷

[MessagePackObject]
public class PlayerAttackPacket : Packet
{
    [Key(2)] public ushort AttackerId { get; set; }    // 2 bytes
    [Key(3)] public ushort TargetId { get; set; }      // 2 bytes
    [Key(4)] public byte SkillId { get; set; }         // 1 byte
    [Key(5)] public ushort Damage { get; set; }        // 2 bytes
    [Key(6)] public byte CriticalFlag { get; set; }    // 1 byte (0=normal, 1=crit)
    
    // 총 크기: 약 10 bytes
}

11.4 Network Manager 구현 🌐

using UnityEngine;
using MessagePack;
using System;
using [System.Net](http://System.Net).Sockets;

public class NetworkManager : MonoBehaviour
{
    private TcpClient _client;
    private NetworkStream _stream;
    
    // LZ4 압축 옵션
    private MessagePackSerializerOptions _options;
    
    void Start()
    {
        // MessagePack 설정
        _options = MessagePackSerializerOptions.Standard
            .WithCompression(MessagePackCompression.Lz4BlockArray);
        
        ConnectToServer("127.0.0.1", 7777);
    }
    
    void ConnectToServer(string host, int port)
    {
        _client = new TcpClient();
        _client.Connect(host, port);
        _stream = _client.GetStream();
        
        Debug.Log($"✅ 서버 연결: {host}:{port}");
    }
    
    // 패킷 전송
    public void SendPacket<T>(T packet) where T : Packet
    {
        try
        {
            // 1. 직렬화
            byte[] data = MessagePackSerializer.Serialize(packet, _options);
            
            // 2. 크기 전송 (4 bytes)
            byte[] lengthPrefix = BitConverter.GetBytes(data.Length);
            _stream.Write(lengthPrefix, 0, 4);
            
            // 3. 데이터 전송
            _stream.Write(data, 0, data.Length);
            
            Debug.Log($"📤 패킷 전송: {typeof(T).Name} ({data.Length} bytes)");
        }
        catch (Exception ex)
        {
            Debug.LogError($"❌ 전송 실패: {ex.Message}");
        }
    }
    
    // 패킷 수신
    public T ReceivePacket<T>() where T : Packet
    {
        try
        {
            // 1. 크기 수신 (4 bytes)
            byte[] lengthBuffer = new byte[4];
            _[stream.Read](http://stream.Read)(lengthBuffer, 0, 4);
            int length = BitConverter.ToInt32(lengthBuffer, 0);
            
            // 2. 데이터 수신
            byte[] dataBuffer = new byte[length];
            int totalRead = 0;
            while (totalRead < length)
            {
                int read = _[stream.Read](http://stream.Read)(dataBuffer, totalRead, length - totalRead);
                totalRead += read;
            }
            
            // 3. 역직렬화
            T packet = MessagePackSerializer.Deserialize<T>(dataBuffer, _options);
            
            Debug.Log($"📥 패킷 수신: {typeof(T).Name} ({length} bytes)");
            return packet;
        }
        catch (Exception ex)
        {
            Debug.LogError($"❌ 수신 실패: {ex.Message}");
            return null;
        }
    }
}

11.5 패킷 최적화 테크닉 ⚡

1. Delta Compression

[MessagePackObject]
public class PlayerStateDelta
{
    [Key(0)] public ushort PlayerId { get; set; }
    
    // 번경된 값만 전송 (비트 플래그로 판별)
    [Key(1)] public byte ChangedFlags { get; set; }  // 어떤 필드가 변경되었는지
    [Key(2)] public Vector3? Position { get; set; }  // ChangedFlags & 0x01
    [Key(3)] public byte? Health { get; set; }       // ChangedFlags & 0x02
    [Key(4)] public ushort? Score { get; set; }      // ChangedFlags & 0x04
}

2. Quantization (양자화)

public static class QuantizationHelper
{
    // float (4 bytes) → ushort (2 bytes)
    public static ushort QuantizeFloat(float value, float min, float max)
    {
        float normalized = Mathf.Clamp01((value - min) / (max - min));
        return (ushort)(normalized * 65535f);
    }
    
    public static float DequantizeFloat(ushort value, float min, float max)
    {
        float normalized = value / 65535f;
        return min + normalized * (max - min);
    }
    
    // Vector3 양자화 (12 bytes → 6 bytes)
    public static void QuantizeVector3(Vector3 v, out ushort x, out ushort y, out ushort z)
    {
        x = QuantizeFloat(v.x, -1000f, 1000f);
        y = QuantizeFloat(v.y, 0f, 500f);
        z = QuantizeFloat(v.z, -1000f, 1000f);
    }
}


💾 12. 게임 세이브/로드 시스템

12.1 세이브 데이터 구조 🏗️

참고: Unity Save System Documentation

[MessagePackObject]
public class SaveData
{
    [Key(0)] public string PlayerId { get; set; }
    [Key(1)] public int Level { get; set; }
    [Key(2)] public Vector3 Position { get; set; }
    [Key(3)] public List<Item> Inventory { get; set; }
}

📈 13. JSON vs MessagePack 성능

참고:

Vector3[] 10,000개 직렬화

  • JsonUtility: 2500ms
  • MessagePack: 200ms (12.5배)
  • MessagePack Blit: 125ms (20배!)

🎮 14. 멀티플레이어 패킷

참고: Unity Multiplayer Networking

베스트 프랙티스

  • 패킷 크기: 64 bytes 이하
  • 첨 바이트는 패킷 ID
  • 고정 크기 구조 선호
  • ushort 사용 (0-65535)

🔄 15. TypelessFormatter

참고: MessagePack Polymorphism

// 다형성 지원
var data = MessagePackSerializer.Typeless.Serialize(obj);
var restored = MessagePackSerializer.Typeless.Deserialize(data);

⚠️ 보안 주의: 신뢰할 수 없는 데이터에는 사용 금지


🏷️ 16. Union 타입

참고: MessagePack Union Types

[Union(0, typeof(DamageEvent))]
[Union(1, typeof(HealEvent))]
public interface IGameEvent { }

[MessagePackObject]
public class DamageEvent : IGameEvent
{
    [Key(0)] public int Damage { get; set; }
}

중요: 직렬화 시 명시적 타입 지정 필수

MessagePackSerializer.Serialize<IGameEvent>(evt);

🔄 17. 버전 호환성

참고: MessagePack Versioning

호환성 전략

  1. Array 방식 (권장)
    • 새 필드는 항상 끝에 추가
    • 구 버전은 추가 필드 무시
    • 신 버전은 기본값 사용
  2. Map 방식
    • String Key 사용
    • 유연하지만 크기 증가
[MessagePackObject]
public class VersionedData
{
    [Key(0)] public int Version { get; set; } = 2;
    [Key(1)] public string Name { get; set; }
    [Key(2)] public int? NewField { get; set; }  // v2에서 추가
}

🔍 18. 디버깅 도구

참고: MessagePack Debugging

JSON 변환으로 디버깅

// MessagePack → JSON 변환
var json = MessagePackSerializer.ConvertToJson(msgpackBytes);
Debug.Log(json);  // 가독한 JSON 출력

직렬화 데이터 검사

public static void InspectMessagePack(byte[] data)
{
    var json = MessagePackSerializer.ConvertToJson(data);
    Debug.Log($"Size: {data.Length} bytes");
    Debug.Log($"JSON: {json}");
}

🎮 19. 실전: 인벤토리 시스템

using MessagePack;
using System.Collections.Generic;
using UnityEngine;

[MessagePackObject]
public class Inventory
{
    [Key(0)] public int MaxSlots { get; set; } = 50;
    [Key(1)] public List<InventoryItem> Items { get; set; } = new();
    
    public bool AddItem(InventoryItem item)
    {
        if (Items.Count >= MaxSlots) return false;
        Items.Add(item);
        return true;
    }
    
    public bool RemoveItem(int itemId)
    {
        var item = Items.Find(x => x.ItemId == itemId);
        if (item != null)
        {
            Items.Remove(item);
            return true;
        }
        return false;
    }
    
    public byte[] Serialize()
    {
        return MessagePackSerializer.Serialize(this);
    }
    
    public static Inventory Deserialize(byte[] data)
    {
        return MessagePackSerializer.Deserialize<Inventory>(data);
    }
}

[MessagePackObject]
public class InventoryItem
{
    [Key(0)] public int ItemId { get; set; }
    [Key(1)] public string ItemName { get; set; }
    [Key(2)] public int Quantity { get; set; }
    [Key(3)] public ItemRarity Rarity { get; set; }
    [Key(4)] public ItemStats Stats { get; set; }
}

[MessagePackObject]
public class ItemStats
{
    [Key(0)] public int Attack { get; set; }
    [Key(1)] public int Defense { get; set; }
    [Key(2)] public float CritChance { get; set; }
}

public enum ItemRarity : byte
{
    Common = 0,
    Uncommon = 1,
    Rare = 2,
    Epic = 3,
    Legendary = 4
}

🏆 20. 실전: 랭킹 시스템

using MessagePack;
using System.Collections.Generic;
using System.Linq;

[MessagePackObject]
public class LeaderboardEntry
{
    [Key(0)] public string PlayerId { get; set; }
    [Key(1)] public string PlayerName { get; set; }
    [Key(2)] public int Score { get; set; }
    [Key(3)] public int Rank { get; set; }
    [Key(4)] public long Timestamp { get; set; }
}

[MessagePackObject]
public class Leaderboard
{
    [Key(0)] public string LeaderboardId { get; set; }
    [Key(1)] public List<LeaderboardEntry> Entries { get; set; } = new();
    [Key(2)] public int MaxEntries { get; set; } = 100;
    
    public void AddOrUpdateEntry(LeaderboardEntry entry)
    {
        var existing = Entries.Find(x => x.PlayerId == entry.PlayerId);
        if (existing != null)
        {
            existing.Score = entry.Score;
            existing.Timestamp = entry.Timestamp;
        }
        else
        {
            Entries.Add(entry);
        }
        
        // 점수 정렬 및 순위 업데이트
        Entries = Entries.OrderByDescending(x => x.Score)
                         .Take(MaxEntries)
                         .ToList();
        
        for (int i = 0; i < Entries.Count; i++)
        {
            Entries[i].Rank = i + 1;
        }
    }
    
    public byte[] SerializeCompressed()
    {
        var options = MessagePackSerializerOptions.Standard
            .WithCompression(MessagePackCompression.Lz4BlockArray);
        return MessagePackSerializer.Serialize(this, options);
    }
}

⚡ 21. 고급 최적화

메모리 풀링

public class PacketPool<T> where T : class, new()
{
    private Queue<T> _pool = new();
    private int _maxSize = 100;
    
    public T Rent()
    {
        return _pool.Count > 0 ? _pool.Dequeue() : new T();
    }
    
    public void Return(T obj)
    {
        if (_pool.Count < _maxSize)
        {
            _pool.Enqueue(obj);
        }
    }
}

배치 직렬화

public class BatchSerializer
{
    public byte[] SerializeBatch<T>(List<T> items)
    {
        using var stream = new MemoryStream();
        foreach (var item in items)
        {
            var data = MessagePackSerializer.Serialize(item);
            stream.Write(BitConverter.GetBytes(data.Length), 0, 4);
            stream.Write(data, 0, data.Length);
        }
        return stream.ToArray();
    }
}

🔧 22. 트러블슈팅

자주 발생하는 오류

1. FormatterNotFoundException

원인: Resolver가 타입을 찾을 수 없음

해결:

// MessagePackObject Attribute 추가
[MessagePackObject]
public class YourClass { }

// Resolver 등록
StaticCompositeResolver.Instance.Register(
    GeneratedResolver.Instance,
    StandardResolver.Instance
);

2. IL2CPP AOT 오류

원인: 동적 코드 생성 불가

해결:

  • Source Generator 사용 (v3+)
  • StaticCompositeResolver 사용
  • Unity 2022.3.12f1 이상 사용

3. Key 순서 오류

원인: Key가 연속적이지 않거나 중복됨

해결:

// ✅ 올바른 예제
[Key(0)] public int Id { get; set; }
[Key(1)] public string Name { get; set; }
[Key(2)] public float Score { get; set; }

// ❌ 잘못된 예제
[Key(0)] public int Id { get; set; }
[Key(2)] public string Name { get; set; }  // 1을 건너뛰
[Key(0)] public float Score { get; set; }  // 중복!

📚 참고 자료

공식 문서

Unity 통합

성능 분석

네트워킹

고급 기능


🎉 결론

MessagePack-CSharp는 Unity 게임 개발에서 필수적인 고성능 직렬화 라이브러리입니다.

핵심 장점

  • ⚡ JSON 대비 10배 빠른 속도
  • 📦 50% 이상 작은 바이너리 크기
  • 🎮 Unity 전용 타입 완벽 지원
  • 🚀 IL2CPP/AOT 완전 대응
  • 🗜️ LZ4 압축 내장

추천 사용 시나리오

  1. 네트워크 멀티플레이어 게임
  2. 대용량 세이브/로드 시스템
  3. 서버 API 통신
  4. 애셋 번들 데이터

시작하기

// 1. NuGet에서 MessagePack 설치
// 2. Unity 패키지 설치
// 3. MessagePackObject Attribute 추가
[MessagePackObject]
public class YourData
{
    [Key(0)] public int Value { get; set; }
}

// 4. 직렬화/역직렬화
var bytes = MessagePackSerializer.Serialize(data);
var restored = MessagePackSerializer.Deserialize<YourData>(bytes);

행복한 코딩 되세요! 🚀