게시글 보니까 Typia쓰면 되는데 이런 거 왜 만들었냐고 하길래 글 써봄


ㅆㅇㅆ 본인의 말에 의하면 FlatBuffers를 대체하기 위해 만들었다고 하는데,

이 소리를 이해하려면 Flatbuffers가 왜 나왔는지를 먼저 이해해야 한다


FlatBuffers가 나온 이유는 직렬화가 느리기 때문이다.

그렇다면 직렬화란 뭘까?



1. 직렬화란 무엇인가


우리가 객체를 만들면 그 객체는 메모리에 저장된다.

문제는 이 객체를 남에게 보낼 때 발생한다.


해당 객체가 담긴 0x8as2 어쩌고 하는 특정 메모리 주소는

해당 컴퓨터에서만 유효하기 때문에 상대 컴에서는 아무 의미가 없다.


즉 내가 데이터를 보내봤자 상대가 알아먹을 수가 없다는 거다.


그래서 나온 게 직렬화다

직렬화에는 여러가지 방법이 있는데 제일 유명한 것 중 하나는 json이다


근데 json에는 심각한 문제가 있다.



2. json


일단 json은 아래와 같이 생겼다.


```

"id":7,

"age":41

}

```

딱 봐도 읽기가 쉽다.

근데 이걸 컴퓨터가 이해하고 사용하려면 아래의 3단계가 필요하다.


- id는 7이네, age는 41이네... 이렇게 파싱작업이 필요하고

- 숫자인지 문자인지 타입판단도 해야되고

- 위 단계들을 통해 종합적으로 새 객체를 만들어내야함


각 단계를 위해 내부적으로 2~3단계를 더 거치기 때문에

실질적으로는 7~10단계 쯤 된다.

딱 봐도 너무 느릴 것 같다.


그래서 더 빠르게 데이터를 주고받고자

바이너리 직렬화란 게 나왔다



3. 바이너리 직렬화


내가 상대에게 8바이트 데이터를 줄 건데,


앞 4바이트는 ID고

뒷 4바이트는 age면


데이터가

01 0a 03 04

02 0c 0d 01


이딴 식으로 갈테니깐

컴퓨터가 엄청난 속도로 읽을 수 있을 것이다.


이것이 바이너리 직렬화다.

근데 이러려면 어디서 어디까지가 id인지 등, 스키마를 미리 정해놓아야겠지?


이것들을 자기네 나름대로 해놓은 게 바이너리 직렬화 라이브러리들이다.

일반적으로 직렬화 라이브러리라고 함은

바이너리 직렬화 라이브러리들을 의미한다.


여기서 ProtoBuf나 FaltBuffers 같은 게 나왔다.



4. FlatBuffers


FlatBuffers 개발자들은 안그래도 빠른 바이너리 직렬화를 더더욱 빠르게 하려고 했다.

그래서 도입한 게 Zero Copy란 개념이다.


이게 뭔 소리냐면 일반적인 바이너리 직렬화는


1. 전체 바이너리를 읽어서

2. 객체로 복원하고

3. 그 객체를 사용한다.


이렇게 작동한다.

직관적으로 생각해봐도 느릴 것 같다.

실제로 피클이 이래서 느리다. (이거만 이유는 아니지만...)


잘 생각해보면

어차피 어디가 뭔지 알고있는데 굳이 객체로 저장하는 작업을 받자마자 해둘 필요가 있을까?

걍 필요할 때마다 보면 되지 않을까?


FlatBuffers가 바로 그렇게 구현된 라이브러리다.

이걸 제로 카피라고 부른다. 객체로 복원(Copy)하는 작업이 없기 때문.


딱 들어도 존나 빠를 것 같다. 실제로 빠르다.

대신 조금 귀찮다.

빌더 패턴을 강제당하고, 스키마도 정해야하는 등 여러 제약사항이 있는 것이다.


하지만 속도가 중요한 프로젝트에서는 어쩔 수 없이 쓴다.

그렇다면 Zeno는 여기서 뭘 더 개선한 걸까?



5. Zeno


간단히 말하자면 Zeno는 FlatBuffers보다 더 빠른 속도를 내기위해 만들어졌다.


FlatBuffers가 속도를 위해 빌더 패턴 등의 귀찮은 것들을 강제했듯이,

Zeno도 속도를 위해 귀찮은 것 하나를 더 강제한다.


스키마 고정 <-- 이게 Zeno의 핵심이다


Flatbuffers는 스키마를 진화시킬 수 있는데,

Zeno는 이걸 못하게 막은 것.


예를 들어 원래 스키마에서 ID를 추가했다고 치자.


User {

  name:string;

  age:int;

  id:int;

}


flatbuffers는

4바이트는 name

4바이트는 age

4바이트는 id

이렇게 기억해두고 그에 따라 view를 한다. (정확히는 table과 vtable이란 걸 쓴다)


즉, 어디서 어디까지의 바이트가 id인지 age인지에 대한 오프셋 데이터를 들고 있는 것이다.

따라서 모르는 바이트가 들어오면 그냥 안보면 그만이다.


근데 zeno는 오프셋 데이터가 없다.


즉... 아래처럼 있다가 중간에 age의 위치를 바꾸면


[id 4바이트][score 8바이트][age 4바이트] ->

[id 4바이트][age 4바이트][score 8바이트]


에러가 난다. 더 심하게는 잘못된 값을 읽을 수도 있다.

score는 1100이고 age는 41인데 

age를 1100으로 읽고 score가 41이 될 수도 있는 것이다.


근데 잘 생각해보면

스키마를 프로젝트 중간에 굳이 바꿀 이유가 별로 없고, 바꾼다 해도 양쪽을 동시에 업뎃하면 된다.


즉... 불필요한 걸 덜어내고 속도를 얻었다고 할 수 있다.

이러한 방식은 특히 대량스캔에 매우 유리하다


FlatBuffers에서의 대량스캔은


n번째 레코드 찾기

-> 그 레코드의 table 위치 찾기

-> vtable에서 age 필드 위치 찾기 (오프셋 탐색)

-> age 읽기


이렇게 되어있는데, Zeno의 대량스캔은 단순히 아래와 같다.


n번째 레코드의 age 위치 = 시작 + n * 레코드크기 + age오프셋

-> age 읽기


엄청나게 차이가 날 수 밖에 없다.


그러나 스키마를 고정한다는 것은 범용기능 상당수를 포기한다는 것이다.

예를 들어 선택필드라던지 필드 ID 기반 접근 등이 Zeno에선 불가능하다.


그리고 개발자의 결벽증스러운 부분들이 라이브러리에 반영되어있어서

TS타입을 그대로 안 받아주고 z.Vector<T>처럼 ABI를 다 명확하게 써놔야한다.


FlatBuffers가 속도를 위해 귀찮음은 것들을 강제했듯,

Zeno도 속도를 위해 귀찮은 것들을 강제한 것이다.



6. 그래서 얼마나 빨라지는데?


오일러 물리 (3d물체 회전 등에 쓰이는 계산, 읽고 쓰기 같이 돌림) 등등

게임에서 많이 쓰이는 연산 몇 개를 밴치마크로 돌려보니까 FlatBuffers보다 최대 22배까지 빨랐음


단순히 22배라고 하면 적어보일 수 있는데

초당 60프레임에서 22배면 1320프레임임 

빠르기는 확실히 빠르다.

그야 걍 데이터 뷰 하는 거랑 동급이니깐 빠를 수 밖에 없음


근데 잘 생각해보면 Three.js 같은 애들은 속도가 중요하니깐

걍 자기네들이 데이터뷰하는 코드를 미리 내부에 구현해둔다.


그래서 FlatBuffers도 안쓰고 그보다 더 빠른 Zeno도 쓸 이유가 없다.

물론 새 프로젝트들은 Zeno를 쓰는 편이 머리가 덜 아플 테니까 좋겠지.

근데 딱 Zeno를 써야할 플젝이 얼마나 있을까?

내가 봤을 땐 게임 쪽은 좀 애매함


오히려 금융 쪽 프로그램에서 실시간성을 높게 확보해주는 용으로 쓰거나


아니면 아래의 그림처럼


24b0d121e09c28a8699fe8b115ef046c60f42a4894



옵시디언의 유사도 검색용 임베딩 플러그인 같은데서는 충분히 쓸만하다.

이 플러그인 존나 개느린데 Zeno쓰면 아주 극적으로 속도를 향상시켜줄 수 있을 것임



7. Typia를 쓰는 것이 더 나은가?


Typia는 JSON제대로 보냈는지, 잘 보냈는지 검증하기 위해서 쓰는 것이고 Ts의 type을 써서 빠른 라이브러리임.

즉 직렬화를 더 잘하기 위한 것이고

바이너리 직렬화와는 아예 결이 다르다



8. 마무리


나 좀 강의글 잘 쓰는 듯?

진짜 강의팔이 마렵네