260215 LiteDB ORM byte변환 메시저장 예제
15 Feb 2026
🗂️ 260215 LiteDB ORM byte[] 변환 메시 저장 예제
💡 이전 문서 LiteDB 성능 체크 - Unity 환경에서
float[]→byte[]변환 저장을 권장하였다. 본 문서에서는 LiteDB의 ORM 매핑 기능을 활용하여 비즈니스 코드에서는float[]로 사용하되, DB에는byte[]로 저장/복원되는 구체적인 예제를 다룬다.
🎯 1. 핵심 개념: 왜 byte[]로 변환하는가?
float[]을 LiteDB에 그대로 저장하면 BSON BsonArray로 직렬화된다. 이때 각 float 요소마다 타입 바이트 + 값 8bytes(double 변환)로 저장되어 엄청난 오버헤드가 발생한다.
float[] 직접 저장 (BsonArray):
요소 1개 = 타입(1B) + double값(8B) = 9 bytes
float 1,000개 → 9,000 bytes (원본 4,000 bytes의 2.25배)
byte[] 변환 저장 (BsonBinary):
헤더(5B) + 원본 바이트 그대로
float 1,000개 → 4,005 bytes (원본과 거의 동일)
| 방식 | 1,000 floats | 10,000 floats | 오버헤드 |
|---|---|---|---|
| float[] 직접 | 9 KB | 90 KB | ~2.25x |
| byte[] 변환 | 4 KB | 40 KB | ~1.0x |
💡 참고: LiteDB BSON 문서 모델
📌 2. 방법 A: BsonIgnore + 수동 변환 프로퍼티
가장 직관적인 방법. ORM 클래스에 실제 저장용 byte[] 프로퍼티와 비즈니스용 float[] 프로퍼티를 분리한다.
🔹 2.1 ORM 클래스 정의
using LiteDB;
using System;
using System.Runtime.InteropServices;
public class MyMesh
{
// ── DB 저장 필드 ──
[BsonId(true)]
public long Id { get; set; }
public string ModelItemUid { get; set; } // char[64]
// DB에 byte[]로 저장되는 실제 컬럼
[BsonField("vertex")]
public byte[] VertexRaw { get; set; }
[BsonField("index")]
public byte[] IndexRaw { get; set; }
[BsonField("triIndex")]
public byte[] TriangleIndexRaw { get; set; }
// ── 비즈니스 코드용 (DB 저장 제외) ──
[BsonIgnore]
public float[] VertexList
{
get => BytesToFloats(VertexRaw);
set => VertexRaw = FloatsToBytes(value);
}
[BsonIgnore]
public int[] IndexList
{
get => BytesToInts(IndexRaw);
set => IndexRaw = IntsToBytes(value);
}
[BsonIgnore]
public int[] TriangleIndexList
{
get => BytesToInts(TriangleIndexRaw);
set => TriangleIndexRaw = IntsToBytes(value);
}
// ── 변환 유틸 (static) ──
static byte[] FloatsToBytes(float[] arr)
{
if (arr == null || arr.Length == 0) return Array.Empty<byte>();
var bytes = new byte[arr.Length * sizeof(float)];
Buffer.BlockCopy(arr, 0, bytes, 0, bytes.Length);
return bytes;
}
static float[] BytesToFloats(byte[] bytes)
{
if (bytes == null || bytes.Length == 0) return Array.Empty<float>();
var arr = new float[bytes.Length / sizeof(float)];
Buffer.BlockCopy(bytes, 0, arr, 0, bytes.Length);
return arr;
}
static byte[] IntsToBytes(int[] arr)
{
if (arr == null || arr.Length == 0) return Array.Empty<byte>();
var bytes = new byte[arr.Length * sizeof(int)];
Buffer.BlockCopy(arr, 0, bytes, 0, bytes.Length);
return bytes;
}
static int[] BytesToInts(byte[] bytes)
{
if (bytes == null || bytes.Length == 0) return Array.Empty<int>();
var arr = new int[bytes.Length / sizeof(int)];
Buffer.BlockCopy(bytes, 0, arr, 0, bytes.Length);
return arr;
}
}
🧪 2.2 CRUD 사용 예제
using LiteDB;
using System.Collections.Generic;
using System.Linq;
public class MeshRepository
{
private readonly LiteDatabase _db;
private readonly ILiteCollection<MyMesh> _col;
public MeshRepository(string dbPath)
{
// Direct 모드 (Unity 권장)
_db = new LiteDatabase($"Filename={dbPath};Connection=direct");
_col = _db.GetCollection<MyMesh>("meshes");
// modelItemUid 인덱스 생성
_col.EnsureIndex(x => x.ModelItemUid);
}
// ── INSERT ──
public void Insert(MyMesh mesh)
{
_col.Insert(mesh);
}
// ── BULK INSERT ──
public void InsertBulk(List<MyMesh> meshes)
{
// 반드시 List<T>로 전달 (IEnumerable 대비 13배 빠름)
_col.InsertBulk(meshes);
}
// ── READ (단건, 인덱스 쿼리) ──
public MyMesh FindByUid(string uid)
{
return _col.FindOne(x => x.ModelItemUid == uid);
}
// ── READ (전체) ──
public IEnumerable<MyMesh> FindAll()
{
// ToList() 하지 않고 IEnumerable로 반환 → 메모리 효율
return _col.FindAll();
}
// ── UPDATE ──
public bool Update(MyMesh mesh)
{
return _col.Update(mesh);
}
// ── DELETE ──
public bool Delete(long id)
{
return _col.Delete(id);
}
public void Dispose() => _db?.Dispose();
}
🔹 2.3 Unity에서 사용
using UnityEngine;
using System.Collections.Generic;
public class MeshStorageExample : MonoBehaviour
{
private MeshRepository _repo;
void Start()
{
// Application.persistentDataPath = Unity 앱 데이터 경로
string dbPath = System.IO.Path.Combine(
Application.persistentDataPath, "meshdata.db");
_repo = new MeshRepository(dbPath);
}
// ── 저장 예시 ──
void SaveMesh(Mesh unityMesh, string modelItemUid)
{
var myMesh = new MyMesh
{
ModelItemUid = modelItemUid,
// float[], int[] 그대로 할당 → 내부에서 byte[] 자동 변환
VertexList = ConvertVector3ToFloats(unityMesh.vertices),
IndexList = unityMesh.triangles,
TriangleIndexList = unityMesh.triangles
};
_repo.Insert(myMesh);
Debug.Log($"저장 완료: {modelItemUid}");
}
// ── 조회 예시 ──
void LoadMesh(string modelItemUid)
{
var myMesh = _repo.FindByUid(modelItemUid);
if (myMesh == null) return;
// byte[]에서 float[], int[]로 자동 복원
float[] vertices = myMesh.VertexList;
int[] indices = myMesh.IndexList;
Debug.Log($"정점 수: {vertices.Length / 3}, 인덱스 수: {indices.Length}");
}
// ── 대량 저장 예시 ──
void BulkSave(List<MyMesh> meshes)
{
_repo.InsertBulk(meshes);
Debug.Log($"Bulk 저장 완료: {meshes.Count}건");
}
// ── Vector3[] → float[] 변환 유틸 ──
float[] ConvertVector3ToFloats(Vector3[] vectors)
{
var floats = new float[vectors.Length * 3];
for (int i = 0; i < vectors.Length; i++)
{
floats[i * 3] = vectors[i].x;
floats[i * 3 + 1] = vectors[i].y;
floats[i * 3 + 2] = vectors[i].z;
}
return floats;
}
void OnDestroy() => _repo?.Dispose();
}
💡 장점: 코드가 직관적이고, 비즈니스 로직에서
float[]/int[]를 자연스럽게 사용
⚠️ 단점: getter/setter 호출 시마다 변환 발생 → 빈번한 접근 시 성능 주의
📌 3. 방법 B: RegisterType으로 전역 커스텀 직렬화
BsonMapper.Global.RegisterType<T>를 사용하면, ORM 클래스에서 float[]를 그대로 프로퍼티로 사용하면서도 DB 저장 시 자동으로 byte[]로 변환된다.
🔹 3.1 전역 매퍼 등록
using LiteDB;
using System;
/// <summary>
/// 앱 시작 시 한 번만 호출
/// </summary>
public static class LiteDbMapperConfig
{
public static void Initialize()
{
// float[] → BsonBinary 자동 변환 등록
BsonMapper.Global.RegisterType<float[]>(
serialize: floats =>
{
if (floats == null || floats.Length == 0)
return new BsonValue(Array.Empty<byte>());
var bytes = new byte[floats.Length * sizeof(float)];
Buffer.BlockCopy(floats, 0, bytes, 0, bytes.Length);
return new BsonValue(bytes);
},
deserialize: bson =>
{
var bytes = bson.AsBinary;
if (bytes == null || bytes.Length == 0)
return Array.Empty<float>();
var floats = new float[bytes.Length / sizeof(float)];
Buffer.BlockCopy(bytes, 0, floats, 0, bytes.Length);
return floats;
}
);
// int[] → BsonBinary 자동 변환 등록
BsonMapper.Global.RegisterType<int[]>(
serialize: ints =>
{
if (ints == null || ints.Length == 0)
return new BsonValue(Array.Empty<byte>());
var bytes = new byte[ints.Length * sizeof(int)];
Buffer.BlockCopy(ints, 0, bytes, 0, bytes.Length);
return new BsonValue(bytes);
},
deserialize: bson =>
{
var bytes = bson.AsBinary;
if (bytes == null || bytes.Length == 0)
return Array.Empty<int>();
var ints = new int[bytes.Length / sizeof(int)];
Buffer.BlockCopy(bytes, 0, ints, 0, bytes.Length);
return ints;
}
);
}
}
🔹 3.2 ORM 클래스 (깔끔한 버전)
using LiteDB;
public class MyMesh
{
[BsonId(true)]
public long Id { get; set; }
public string ModelItemUid { get; set; }
// float[]로 선언하지만, RegisterType에 의해 byte[]로 저장됨
public float[] VertexList { get; set; }
public int[] IndexList { get; set; }
public int[] TriangleIndexList { get; set; }
}
public class MyProp
{
[BsonId(true)]
public long Id { get; set; }
public string ModelItemUid { get; set; }
public string Name { get; set; }
public string Value { get; set; }
}
🔹 3.3 Unity 초기화 및 사용
using UnityEngine;
public class AppBootstrap : MonoBehaviour
{
void Awake()
{
// 앱 시작 시 한 번만 호출
LiteDbMapperConfig.Initialize();
}
}
using LiteDB;
using UnityEngine;
public class MeshService : MonoBehaviour
{
private LiteDatabase _db;
private ILiteCollection<MyMesh> _meshCol;
private ILiteCollection<MyProp> _propCol;
void Start()
{
string dbPath = System.IO.Path.Combine(
Application.persistentDataPath, "project.db");
_db = new LiteDatabase($"Filename={dbPath};Connection=direct");
_meshCol = _db.GetCollection<MyMesh>("meshes");
_propCol = _db.GetCollection<MyProp>("props");
_meshCol.EnsureIndex(x => x.ModelItemUid);
_propCol.EnsureIndex(x => x.ModelItemUid);
_propCol.EnsureIndex(x => x.Name);
}
// ── 저장: float[]를 그대로 넣으면 byte[]로 자동 변환 ──
public void SaveMesh(string uid, float[] vertices, int[] indices, int[] triIndices)
{
var mesh = new MyMesh
{
ModelItemUid = uid,
VertexList = vertices, // ← float[] 그대로
IndexList = indices, // ← int[] 그대로
TriangleIndexList = triIndices // ← int[] 그대로
};
_meshCol.Insert(mesh);
}
// ── 조회: byte[]에서 float[]로 자동 복원 ──
public MyMesh LoadMesh(string uid)
{
var mesh = _meshCol.FindOne(x => x.ModelItemUid == uid);
// mesh.VertexList → float[] 자동 복원
// mesh.IndexList → int[] 자동 복원
return mesh;
}
// ── Bulk 저장 ──
public void SaveMeshBulk(System.Collections.Generic.List<MyMesh> meshes)
{
_meshCol.InsertBulk(meshes);
}
// ── 스트리밍 전체 조회 (메모리 효율) ──
public void ProcessAllMeshes(System.Action<MyMesh> processor)
{
foreach (var mesh in _meshCol.FindAll())
{
processor(mesh);
// 처리 후 GC 가능
}
}
void OnDestroy() => _db?.Dispose();
}
💡 장점: ORM 클래스가 깔끔, 비즈니스 코드에서 변환 코드 불필요
💡 단점:int[]의 전역 RegisterType이 다른 컬렉션에도 영향 → 의도치 않은 부작용 가능
⚖️ 4. 방법 A vs B 비교
| 항목 | 방법 A (BsonIgnore + 수동) | 방법 B (RegisterType 전역) |
|---|---|---|
| ORM 클래스 | 프로퍼티 쌍 필요 (Raw + 비즈니스) | 깔끔한 단일 프로퍼티 |
| 변환 코드 위치 | 클래스 내부 (명시적) | 전역 매퍼 (암묵적) |
| 영향 범위 | 해당 클래스만 | 모든 float[], int[] 프로퍼티 |
| 부작용 위험 | 없음 | 다른 컬렉션의 int[] 배열도 byte[]로 저장됨 |
| IL2CPP 호환 | Buffer.BlockCopy → 안전 | Buffer.BlockCopy → 안전 |
| 디버깅 | Raw 필드 직접 확인 가능 | LiteDB Studio에서 binary blob으로 보임 |
| 권장 상황 | 메시 전용 클래스가 있을 때 | 프로젝트 전체에서 배열을 byte로 저장할 때 |
💡 권장: 대부분의 경우 방법 A가 안전하다. 방법 B는 전역 영향이므로
int[]가 다른 곳에서도 사용된다면 의도치 않게 byte[]로 저장될 수 있다.
🔗 5. IL2CPP 호환성 참고
⚖️ Buffer.BlockCopy vs MemoryMarshal
| API | IL2CPP | Mono | Burst | 비고 |
|---|---|---|---|---|
| Buffer.BlockCopy | 안전 | 안전 | N/A | .NET 1.0부터 존재, 모든 환경 호환 |
| MemoryMarshal.Cast | 주의 | 안전 | 미지원 | IL2CPP에서 Span/ReadOnlySpan 관련 이슈 보고 |
| BitConverter.GetBytes | 안전 | 안전 | N/A | 단건 변환만 가능, 배열에 비효율 |
| unsafe fixed | 안전 | 안전 | 안전 | 가장 빠르나 unsafe 컨텍스트 필요 |
💡 결론: Unity IL2CPP 환경에서는
Buffer.BlockCopy가 가장 안전하고 효율적인 선택이다.MemoryMarshal은 IL2CPP에서ReadOnlySpan관련 이슈가 보고된 바 있다.
💡 참고: Unity IL2CPP ReadOnlySpan 이슈, MemoryMarshal.Cast Unity 지원
📌 6. 전체 흐름 요약
[Unity 비즈니스 코드]
mesh.VertexList = new float[] { 1.0f, 2.0f, 3.0f, ... };
│
▼
┌─────────────────────────────────┐
│ 방법 A: setter에서 변환 │
│ VertexRaw = FloatsToBytes(v) │
│ │
│ 방법 B: RegisterType가 가로챔 │
│ serialize(floats) → bytes │
└─────────────────────────────────┘
│
▼
[LiteDB 내부]
BsonValue(byte[]) → BSON Binary 타입으로 디스크 저장
헤더 5bytes + 원본 바이트 그대로 (오버헤드 거의 없음)
│
▼
[조회 시]
BSON Binary → byte[] → Buffer.BlockCopy → float[]
mesh.VertexList 접근 시 자동 복원