TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 23 READS

Serde를 다시 설계한다면: Rust 직렬화 라이브러리 Deser 실험

Serde를 다시 설계한다면: Rust 직렬화 라이브러리 Deser 실험
SOURCE IMAGE · HACKER NEWS

Rust 생태계에서 직렬화는 사실상 Serde 하나로 통일되어 있다. 파생 매크로로 Serialize와 Deserialize를 붙이기만 하면 JSON부터 바이너리 포맷까지 다룰 수 있는 이 라이브러리는 오랫동안 Rust 개발 생산성의 상징이었다. 그러나 대량의 신뢰할 수 없는 JSON을 처리해 온 이들에게는 Serde의 편의성 이면에 자리한 구조적 한계가 반복적으로 문제를 일으켜 왔다. Flask와 Jinja의 제작자로 알려진 Armin Ronacher는 2022년 시작했다가 방치해 둔 실험 프로젝트 Deser를 다시 꺼내 들며, Serde를 '더 잘' 만드는 일이 왜 그렇게 어려운지, 그리고 어떤 대가를 치러야 가능한지를 정리했다.

Serde의 한계는 버그가 아니라 설계의 결과다

글이 드는 세 가지 사례는 모두 개별 기능의 결함이 아니라 기능 간 상호작용에서 비롯된다. 첫째, 내부 태그(internally tagged) 열거형과 serde_json의 arbitrary_precision 옵션을 함께 쓰면 문제가 생긴다. Serde의 데이터 모델에는 임의 정밀도 숫자를 담을 자리가 없어 serde_json은 매직 키를 가진 맵으로 우회 신호를 보내는데, 태그를 확인할 때까지 필드를 버퍼링하는 열거형은 이 매직 키를 알지 못한다. 게다가 Cargo의 기능 통합(feature unification) 때문에 의존성 그래프의 어떤 크레이트든 이 옵션을 켜면 전체에 영향을 준다. 둘째, flatten이 값을 버퍼링하는 순간 JSON 키 "42"는 그냥 문자열로 남아 정수 변환이 되지 않으며, 오류 위치도 해당 키가 아니라 문서의 끝을 가리킨다. 셋째, Rust에서는 함수를 타입 파라미터로 넘길 수 없어 Option이나 Vec 내부에 from_hex 같은 변환을 적용하려면 래퍼마다 별도 함수를 써야 하고, from_opt_hex를 만드는 순간 #[serde(default)]를 따로 붙이지 않으면 필드가 더 이상 선택적이지 않게 된다.

이 문제들의 뿌리는 세 가지 설계 결정에 있다. 자기 서술형(JSON, YAML)과 비자기 서술형(bincode, protobuf)을 하나의 트레이트로 모두 지원한다는 점, 버퍼링 시 포맷이 알고 있던 정보를 잃어버리는 고정 데이터 모델, 그리고 중첩 단계마다 호출 스택을 소모하는 재귀 구조다. 문제는 이 결정들이 Serde의 안정성 보장에 의해 보호되고 있어, 고치는 순간 모든 포맷과 손으로 작성한 구현이 깨진다는 데 있다. 관련 이슈들이 수년째 열려 있는 이유다.

이름처럼 방향을 뒤집은 아키텍처

Deser라는 이름은 Serde의 앞뒤를 바꾼 것으로, 실제 동작 방향도 정반대다. Serde에서는 타입이 역직렬화를 주도해 디시리얼라이저에 기대하는 값의 종류를 묻고, 포맷이 비지터로 되돌아 호출하며, 중첩마다 재귀가 발생한다. 반면 Deser는 miniserde에서 빌려온 구조로, 포맷이 다음 값의 타입을 알려주고 이벤트를 싱크(sink)에 밀어 넣는다. 중첩된 값을 만나면 그 안으로 호출해 들어가는 대신 드라이버에 새 싱크를 넘기고, 모든 상태를 힙(실제로는 아레나)에 유지한다. 덕분에 스택이 중첩 깊이만큼 자라지 않으며, 이론적으로는 입력을 기다리는 동안 역직렬화를 멈출 수도 있다. 다만 이 설계는 자기 서술형이 아닌 protobuf 같은 포맷을 의도적으로 배제한다. Serde를 고치려면 어딘가에서 다른 타협을 감수해야 한다는 점을 보여주는 대목이다.

대신 Deser가 통제할 수 있는 영역은 완성도다. JSON은 물론 JSONC, JSON5, HJSON까지 하나의 공유 파서 템플릿에서 생성하고, CBOR와 MessagePack, YAML 1.1과 1.2, TOML, CSV/TSV, urlencoded, 환경 변수, 그리고 Serde가 지원을 거부해 온 XML과 애플의 plist까지 다룬다. 어댑터가 타입이므로 Hex를 Vec 안에, DisplayFromStr을 Option 속 Vec 안에 넣을 수 있고, 검증기도 어댑터라 파싱하면서 값이 비어 있지 않은지 확인할 수 있다. 경로 계층을 켜면 태그가 값 뒤에 오는 버퍼링 상황에서도 오류가 구조상 어디서 났는지 정확히 짚어준다. XML에서는 네임스페이스를 접두사가 아닌 실제 URI로 매칭하는데, Serde 진영의 quick-xml이 접두사를 버리고 네임스페이스를 무시하거나 겹치는 리스트에서 중복 필드 오류를 내는 것과 대비된다. TOML의 네이티브 datetime도 Deser에서는 확장 값으로 유지되어, 이를 아는 포맷은 그대로 보존하고 나머지는 문자열로 쓴다.

공짜가 아닌 대가

장점은 분명하지만 비용도 명확하다. 이 설계는 동적 디스패치와 힙에 사는 싱크·이미터에 의존하기 때문에 런타임 오버헤드가 상당하다. JSON 읽기 성능은 데이터에 따라 serde_json보다 33% 빠르거나 60% 느리며 평균적으로 약 10% 느리다. 쓰기는 세 배 빠른 경우부터 70% 느린 경우까지 편차가 크고 평균으로는 비슷하다. YAML과 TOML에서는 눈에 띄게 빠르지만 이는 아키텍처보다 포맷 구현의 차이에 가깝다. 컴파일 측면에서는 모든 것을 단형화하지 않는 덕에 파생 코드의 릴리스 빌드가 약 2.3배 빠르지만, 대신 바이너리 크기는 꽤 늘어난다. 힙 위 싱크 체인을 유지하기 위해 내부적으로 unsafe도 사용한다.

실무자 입장에서 이 프로젝트의 의미는 두 갈래다. 하나는 Sentry Relay처럼 신뢰할 수 없는 대량 JSON을 다루거나 XML·plist·TOML datetime 같은 포맷 고유 기능이 절실한 환경에서, Deser의 타협이 오히려 잘 맞을 수 있다는 실용적 선택지다. 원칙적으로는 드롭인 대체가 가능한 수준까지 왔다. 다른 하나는 더 근본적인 교훈으로, orphan 규칙과 생태계 장악력 때문에 Serde를 실제로 대체하기는 어렵다는 저자 자신의 인정이다. 결국 Deser는 Serde를 밀어내려는 시도라기보다, '만약 다시 설계한다면 무엇을 포기하고 무엇을 얻을 것인가'라는 질문을 구체적인 코드로 던지는 실험이다. Rust 직렬화의 설계 공간에 관심이 있는 개발자라면, 채택 여부와 무관하게 이 트레이드오프의 지형도를 살펴볼 가치가 있다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://lucumr.pocoo.org/2026/9/29/deser/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...