# 내 API에 JSON 사용을 중단하고 Protobuf로 바꾼 이유

> Clean Markdown view of GeekNews topic #24841. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=24841](https://news.hada.io/topic?id=24841)
- GeekNews Markdown: [https://news.hada.io/topic/24841.md](https://news.hada.io/topic/24841.md)
- Type: GN+
- Author: [xguru](https://news.hada.io/@xguru)
- Published: 2025-12-05T09:49:50+09:00
- Updated: 2025-12-05T09:49:50+09:00
- Original source: [aloisdeniel.com](https://aloisdeniel.com/blog/better-than-json)
- Points: 38
- Comments: 10

## Summary

많은 개발자가 여전히 **JSON**을 기본처럼 사용하지만, 최근 들어 **Protobuf**로 전환하는 팀이 조용히 늘고 있습니다. **엄격한 타입 정의**와 **자동 코드 생성**, **이진 직렬화의 효율성** 덕분에 API 통신에서 안정성과 속도를 함께 얻을 수 있기 때문입니다. 물론 디버깅은 다소 불편하지만, 서버·클라이언트 간 **스키마 일관성**과 **데이터 크기 절감**이 주는 이점은 분명합니다. 성능 병목이나 타입 불일치를 자주 겪는 팀이라면, JSON 대신 Protobuf를 한번쯤 고민해볼 만한 시점입니다.

## Topic Body

- 웹 API의 표준으로 자리 잡은 **JSON**은 읽기 쉽고 유연하지만, 성능과 안정성 면에서 한계가 있음  
- **Protobuf(Protocol Buffers)** 는 **엄격한 타입 정의**와 **자동 코드 생성**을 통해 데이터 구조를 명확히 보장함  
- **이진 직렬화**를 사용해 JSON보다 약 3배 이상 **데이터 크기를 줄이고 전송 속도를 향상**시킴  
- 서버와 클라이언트가 동일한 **.proto 스키마**를 공유해 타입 불일치나 수동 검증이 필요 없음  
- 디버깅은 어렵지만, **성능·유지보수성·개발 효율성** 측면에서 Protobuf가 현대적 API에 더 적합함  

---
### JSON의 보편성과 한계
- JSON은 **사람이 읽기 쉬운 텍스트 포맷**으로, 단순한 `console.log()`만으로도 데이터를 확인할 수 있음  
- **웹과의 완벽한 통합성** 덕분에 JavaScript와 백엔드 프레임워크 전반에서 널리 채택됨  
- 필드 추가·삭제·타입 변경이 자유로운 **유연성**을 제공하지만, 이로 인해 구조 불일치나 오류 발생 가능성 존재  
- **도구 생태계가 풍부**해 텍스트 편집기나 `curl`만으로도 쉽게 다룰 수 있음  
- 그러나 이러한 장점에도 불구하고, **성능과 타입 안정성** 면에서는 더 나은 대안이 존재함  

### Protobuf의 개요
- **Google**이 2001년에 개발하고 2008년에 공개한 **이진 직렬화 포맷**  
- 내부 시스템과 **마이크로서비스 간 통신**에서 널리 사용됨  
- 종종 **gRPC와 함께 사용해야 한다는 오해**가 있지만, Protobuf는 독립적으로 HTTP API에서도 활용 가능  
- 초기에는 **이진 포맷의 비가시성** 때문에 접근성이 낮았으나, 효율성과 안정성 면에서 강점이 큼  

### 강력한 타입 시스템과 코드 생성
- Protobuf는 `.proto` 파일을 통해 **데이터 구조를 명확히 정의**함  
  - 각 필드는 **엄격한 타입**, **숫자 식별자**, **고정된 이름**을 가짐  
- 예시:
  ```
  message User {
    int32 id = 1;
    string name = 2;
    string email = 3;
    bool isActive = 4;
  }
  ```
- `protoc` 명령으로 Dart, TypeScript, Kotlin, Swift, C#, Go, Rust 등 다양한 언어의 **자동 코드 생성** 지원  
- 생성된 코드로 **직렬화(`writeToBuffer`)와 역직렬화(`fromBuffer`)** 를 수행하며, **수동 검증이나 파싱 불필요**  
- 결과적으로 **시간 절약**과 **유지보수성 향상**을 동시에 달성  

### 이진 직렬화의 효율성
- Protobuf는 **텍스트 대신 이진 데이터**로 직렬화되어 매우 **압축적이고 빠름**  
- 동일한 데이터(`User` 객체)의 크기 비교:
  - JSON: 86바이트 (공백 제거 시 68바이트)  
  - Protobuf: **30바이트**  
- 효율의 원인:
  - 숫자에 **varint 인코딩** 사용  
  - **텍스트 키 대신 숫자 태그** 사용  
  - 공백 및 불필요한 구문 제거  
  - **선택적 필드 최적화**  
- 결과적으로 **대역폭 절감**, **응답 속도 향상**, **모바일 데이터 절약**, **사용자 경험 개선** 효과  

### Dart 기반 Protobuf API 예시
- `shelf` 패키지를 이용해 **간단한 HTTP 서버**를 구성하고, `User` 객체를 Protobuf로 반환  
- 서버 코드 핵심:
  - `User()` 객체를 생성 후 `writeToBuffer()`로 직렬화  
  - 응답 헤더에 `'content-type': 'application/protobuf'` 지정  
- 클라이언트는 `http` 패키지와 `user.pb.dart`를 사용해 **Protobuf 데이터를 직접 디코딩**  
- 서버와 클라이언트가 동일한 `.proto` 스키마를 공유하므로 **데이터 구조 불일치가 발생하지 않음**  
- 동일한 방식이 **Go, Rust, Kotlin, Swift, C#, TypeScript** 등에서도 동일하게 적용 가능  

### JSON의 남은 장점
- Protobuf는 **스키마 없이 의미를 해석하기 어려움**  
  - 필드 이름 대신 숫자 식별자만 표시되어 **사람이 읽기 어려움**  
- 예시 비교:
  - JSON: `{ "id": 42, "name": "Alice" }`  
  - Protobuf: `1: 42, 2: "Alice"`  
- 따라서 Protobuf는:
  - **전용 디코딩 도구** 필요  
  - **스키마 관리 및 버전 관리** 필수  
- 그럼에도 불구하고, **성능과 효율성의 이점이 훨씬 큼**  

### 결론
- Protobuf는 **성숙하고 고성능**의 직렬화 기술로, **공개 API에서도 충분히 활용 가능**  
- gRPC 없이도 **일반 HTTP API**에서 독립적으로 동작  
- **성능, 견고성, 오류 감소, 개발 효율성**을 모두 향상시키는 도구  
- 차세대 프로젝트에서 **Protobuf를 도입할 가치가 충분함**

## Comments



### Comment 47237

- Author: neo
- Created: 2025-12-05T09:49:50+09:00
- Points: 4

###### [Hacker News 의견](https://news.ycombinator.com/item?id=46111469) 
- JSON은 종종 **모호하거나 보장되지 않은 데이터**를 보내게 됨. 필드 누락, 타입 오류, 키 오타, 문서화되지 않은 구조 등 다양한 문제가 생김. 하지만 Protobuf는 `.proto` 파일로 메시지 구조를 명확히 정의함으로써 이런 일이 불가능하다고 주장하는 글이 있었음. 그러나 이는 Protobuf의 철학을 오해한 것임. `proto3`에서는 **required 필드 자체를 지원하지 않음**. 공식 문서([Protobuf Best Practices](https://protobuf.dev/best-practices/dos-donts/))에서도 “required 필드는 해로워서 제거되었다”고 명시되어 있음. 결국 Protobuf 클라이언트도 JSON API처럼 방어적으로 작성해야 함
  - 해당 블로그에는 비슷한 오해가 많음. 예를 들어 SVG 사용을 반대하는 글에서는 **벡터 포맷의 자유로운 스케일링**이라는 장점을 고려하지 않음
  - 문제의 핵심은 언어나 클라이언트/서버 구현 차이일 뿐임. 나는 Go의 **Marshalling 개념**을 활용해 Gooey 프레임워크를 클라이언트에 사용 중임. Go의 한계를 극복하면 매우 타입 세이프하게 쓸 수 있음. 단, `json:"-"`로 private 필드를 막는 게 중요함. 내 프로젝트는 [Gooey](https://github.com/cookiengineer/gooey)에서 볼 수 있음
  - 이 글은 **직렬화 포맷과 계약(Contract)** 개념을 혼동하고 있음
  - 네트워크 시스템에서는 인코딩 방식과 무관하게 **데이터 불일치(Skew)** 문제가 항상 존재함. 다만 Protobuf는 디코딩 후 정적 타입 객체를 제공함. JSON도 검증하면 되지만 대부분 그렇게 하지 않음. 결국 JSON 객체가 이리저리 변형되며 내부 구조를 아무도 확신할 수 없게 됨
  - 아마 원글 작성자는 단순히 Protobuf에서 **누락된 필드가 기본값으로 초기화**된다는 점을 말하고 싶었던 것 같음. 이는 “required” 필드 개념과는 다름

- 압축된 JSON은 충분히 쓸 만하고, **초기 커뮤니케이션 비용이 낮음**. 물론 필드가 빠지거나 타입이 바뀌면 문제가 생기지만, 완벽히 타입화된 구조를 설계하고 버전 동기화를 위한 프로세스를 만드는 사람들은 대부분 실패함. 결국 **인간의 비용이 낮은 쪽이 승리**함. 그래서 JSON은 더 낮은 인간 커뮤니케이션 비용을 가진 대체제가 나오기 전까지는 사라지지 않을 것임
  - 맞음. 대부분의 아키텍트는 **gRPC** 같은 명확한 필요가 없으면 proto를 고려하지 않음. `console.log()`로 바로 디버깅할 수 있는 대안이 나오기 전까지 JSON은 대체되지 않을 것임
  - 디버깅도 JSON의 강점임. 그냥 열어서 읽으면 됨. 반면 Protobuf는 **툴링**이 필요함
  - 맞는 말임. 하지만 사람들은 설계 단계에서 15분 더 투자하기보다, 나중에 3개월 동안 문제를 되짚는 걸 택함
  - JSON이 COBOL처럼 완전히 사라지진 않겠지만, **새 프로젝트**에서는 굳이 쓸 이유가 없음

- Protobuf는 완벽하지 않음. 서버와 클라이언트가 다른 시점에 배포되어 **스펙 버전이 다르면** 안전성이 깨짐. ID 재사용 금지, unknown-field 복사 등으로 완화할 수 있지만, 분산 시스템은 본질적으로 복잡함. 그래도 `protobuf3`는 `protobuf2`의 문제를 많이 해결했음. 예전엔 기본값이 설정된 건지 누락된 건지 구분이 안 됐는데, 이제는 `message` 타입을 쓰면 해결됨
  - JSON이든 Protobuf든 **버전 호환성 테스트를 CI 파이프라인에서 강제**해야 안전함
  - 어떤 타입 시스템도 **네트워크를 통과하면 깨짐**

- 글에서 “초고효율”이라 했지만 gzip 언급이 없음. 대부분의 텍스트 데이터는 이미 **자동 압축되어 전송**됨. 따라서 Protobuf는 **gzip된 JSON**과 비교해야 함
  - 나도 여러 바이너리 포맷을 테스트했지만, 결국 **gzipped JSON이 압도적**으로 효율적이었음
  - JSON의 단점은 직렬화/역직렬화 속도임. 나머지는 점진적으로 해결 가능함
  - **스트리밍 Brotli/zstd JSON/HTML**도 고려할 만함. 연결 유지 중 압축 윈도우를 활용할 수 있음
  - 관련 참고: [Auth0의 Protobuf 성능 비교 글](https://auth0.com/blog/beating-json-performance-with-protobuf/)
  - JSON과 mod_deflate의 조합은 **체감 차이가 매우 큼**

- 더 나은 프로토콜을 옹호하는 건 좋지만, Protobuf가 **효율성과 사용성 모두에서 JSON을 대체**한다고 보긴 어려움. Protobuf는 엄격한 스키마 때문에 JSON이 잘하는 영역을 놓침. 오히려 **CBOR**가 JSON 대체로 더 적합함. CBOR은 JSON처럼 유연하면서도 더 간결한 인코딩을 가짐
  - 하지만 Protobuf의 **엄격한 스키마**가 오히려 장점일 수도 있음. 대부분의 API는 JSON 스키마를 공개하지 않기 때문임. 나는 ajv나 superstruct로 검증했지만, Protobuf는 그럴 필요가 없음
  - 브라우저가 **CBOR API를 직접 지원**하면 좋겠음. 내부 구현은 이미 있으니 어렵지 않을 것임

- 1984년의 **ASN.1**은 이미 Protobuf가 하는 일을 더 유연하게 수행함. DER 인코딩을 쓰면 그렇게 나쁘지 않음. [ASN.1 DER 예시](https://en.wikipedia.org/wiki/ASN.1#Example_encoded_in_DER)를 보면 됨. Protobuf는 달성하는 것에 비해 **너무 복잡함**
  - ASN.1은 기능이 너무 많음. 모든 기능을 지원하면 **과도하게 복잡한 라이브러리**가 되고, 일부만 지원하면 더 이상 표준 ASN.1이 아님
  - 나는 **ASN.1 DER**을 선호함. 직접 C로 구현한 DER 인코더/디코더를 FOSS로 공개했음. 확장형 “ASN.1X”를 만들어 JSON의 데이터 모델을 완전히 포함시킴
  - 하지만 SNMP 같은 시스템에서 ASN.1의 **과도한 유연성**은 오히려 문제였음. 제조사마다 제멋대로 확장함
  - Google 내부에서도 **Protobuf 직렬화/역직렬화에 CPU를 많이 소모**했음
  - ASN.1은 **과설계(overengineered)** 되어 지원이 어려움. 상속 같은 기능은 불필요함

- 나는 프로덕션 시스템 전체를 Protobuf로 구성했는데, **관리 자체가 고통스러웠음**. 기술적으로는 좋아 보이지만 실제로는 JSON이 훨씬 단순함
  - JSON의 **가독성과 디버깅 편의성**은 과소평가할 수 없음. 대부분의 팀은 단기 효율을 위해 JSON을 선택함
  - 어떤 문제가 있었는지 궁금함. 내 경험상 Protobuf의 불편함보다 JSON의 **데이터 손상 위험**이 더 큼. Protobuf는 컴파일 에러로 잡히지만 JSON은 프로덕션에서 터짐

- Protobuf는 훌륭하지만 **zero-copy**를 지원하지 않는 게 아쉬움. Cap’n Proto 같은 포맷은 직렬화/역직렬화 병목을 없애줌
  - 하지만 실제로는 **zero-copy가 오히려 느릴 수도 있음**. 캐시 내 복사는 거의 공짜지만, 동적 구조를 직접 다루면 오버헤드가 생김. 대부분의 경우 한 번 복사(one-copy)만으로 충분함
  - Cap’n Proto의 마케팅에서 나온 주장일 뿐, 실제로는 **성능 차이가 미미함**. 두 포맷 모두 네이티브 타입 ↔ 바이너리 변환이 필요함. 페이로드에 따라 성능은 비슷함
  - 이건 포맷의 문제가 아니라 **라이브러리 구현 문제**일 수도 있음

- 나는 NodeJS 프로젝트에서 `.proto`로 전체 API를 정의하고, **Content-Type에 따라 proto 또는 JSON으로 응답**하는 서버를 만들었음. Swagger보다 훨씬 구조적임. 다만 Google이 이런 기능을 **공식 라이브러리로 제공하지 않은 게 아쉬움**. gRPC는 HTTP/2 의존성 때문에 불편함. 참고로 **Text proto는 최고의 정적 설정 언어**라고 생각함
  - 이런 목적이라면 [Twirp](https://github.com/twitchtv/twirp)가 적합함. Protobuf나 JSON을 단순한 HTTP 위에서 다룸
  - [ConnectRPC](https://connectrpc.com/)도 비슷한 접근을 제공함. 다만 지원 범위는 아직 불분명함

- 내가 꿈꾸는 바이너리 포맷은 **스키마 기반이면서도 메시지 안에 스키마를 포함**하는 형태임. 이렇게 하면 vim 플러그인으로 바로 읽을 수 있음. 수백만 개 객체를 다룰 때 1KB의 스키마를 2GB 메시지에 붙이는 건 큰 부담이 아님
  - Google 내부에는 이미 이런 **스키마 내장형 Protobuf 생태계**가 있음. [Riegeli](https://github.com/google/riegeli)를 참고할 만함
  - [Avro](https://avro.apache.org/)나 [Yardl](https://microsoft.github.io/yardl/)도 비슷한 접근을 제공함
  - 하지만 웹 서비스에서는 오히려 **스키마가 200KB, 메시지가 1KB**인 경우가 많음. 이럴 땐 비효율적임
  - Avro는 여전히 좋은 대안임

### Comment 47449

- Author: tested
- Created: 2025-12-09T14:14:58+09:00
- Points: 1

TypeSpec  
https://typespec.io/

### Comment 47364

- Author: onixboox
- Created: 2025-12-08T09:26:32+09:00
- Points: 1

https://msgpack.org/ 이거는 어떤가요?

### Comment 47389

- Author: cosine20
- Created: 2025-12-08T11:45:12+09:00
- Points: 1
- Parent comment: 47364
- Depth: 1

MessagePack도 좋아요.

### Comment 47302

- Author: savvykang
- Created: 2025-12-06T14:14:41+09:00
- Points: 1

디버깅용 공식 디코더도 없는 포맷을 성숙하다고 주장하는 건 모순이라 생각합니다

### Comment 47299

- Author: vipeen
- Created: 2025-12-06T13:55:52+09:00
- Points: 2

"디버깅은 어렵지만"  
  
탈락

### Comment 47254

- Author: jjw9512151
- Created: 2025-12-05T16:07:00+09:00
- Points: 1

모든 도구가 그렇듯 만능은 없습니다만 Protobuf도 충분히 좋은 도구래 생각합니다.   
특히나 다양한 언어(고객]에 고용량 고주기(초당 20회)데이터를 임베디드환경에서 쏴줘야할때가 있었는데 nanopb로 깔끔하게 한적이 있네요.

### Comment 47253

- Author: ifmkl
- Created: 2025-12-05T14:16:45+09:00
- Points: 1

그렇게 엄격히 하다보면 xml로 오는거 아닌가요 ㅎㅎ

### Comment 47288

- Author: click
- Created: 2025-12-06T09:34:36+09:00
- Points: 1
- Parent comment: 47253
- Depth: 1

스키마도 dtd로 정의해두고 파서쪽에서 캐싱하면 스키마는 한번만 전송하는 효과도 있겠네요

### Comment 47248

- Author: bakyeono
- Created: 2025-12-05T12:12:53+09:00
- Points: 1

- 내가 꿈꾸는 바이너리 포맷은 스키마 기반이면서도 메시지 안에 스키마를 포함하는 형태임. 이렇게 하면 vim 플러그인으로 바로 읽을 수 있음. 수백만 개 객체를 다룰 때 1KB의 스키마를 2GB 메시지에 붙이는 건 큰 부담이 아님  
- 하지만 웹 서비스에서는 오히려 스키마가 200KB, 메시지가 1KB인 경우가 많음. 이럴 땐 비효율적임  
  
=> 스키마는 어차피 반드시 1회는 전송해야 하지 않아요? JSON이라도 스키마가 없는 게 아니라 묵시적으로 데이터에 포함되어 있는 거라서 스키마를 전송 안 하는 건 아닌 것 같습니다. 오히려 항목 하나하나마다 스키마를 중복으로 전송하기 때문에 더 비효율적이죠. "스키마 기반이면서도 메시지 안에 스키마를 포함하는 형태"는 꽤 괜찮을 것 같네요.
