내 API에 JSON 사용을 중단하고 Protobuf로 바꾼 이유
(aloisdeniel.com)- 웹 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"
- JSON:
- 따라서 Protobuf는:
- 전용 디코딩 도구 필요
- 스키마 관리 및 버전 관리 필수
- 그럼에도 불구하고, 성능과 효율성의 이점이 훨씬 큼
결론
- Protobuf는 성숙하고 고성능의 직렬화 기술로, 공개 API에서도 충분히 활용 가능
- gRPC 없이도 일반 HTTP API에서 독립적으로 동작
- 성능, 견고성, 오류 감소, 개발 효율성을 모두 향상시키는 도구
- 차세대 프로젝트에서 Protobuf를 도입할 가치가 충분함
- 내가 꿈꾸는 바이너리 포맷은 스키마 기반이면서도 메시지 안에 스키마를 포함하는 형태임. 이렇게 하면 vim 플러그인으로 바로 읽을 수 있음. 수백만 개 객체를 다룰 때 1KB의 스키마를 2GB 메시지에 붙이는 건 큰 부담이 아님
- 하지만 웹 서비스에서는 오히려 스키마가 200KB, 메시지가 1KB인 경우가 많음. 이럴 땐 비효율적임
=> 스키마는 어차피 반드시 1회는 전송해야 하지 않아요? JSON이라도 스키마가 없는 게 아니라 묵시적으로 데이터에 포함되어 있는 거라서 스키마를 전송 안 하는 건 아닌 것 같습니다. 오히려 항목 하나하나마다 스키마를 중복으로 전송하기 때문에 더 비효율적이죠. "스키마 기반이면서도 메시지 안에 스키마를 포함하는 형태"는 꽤 괜찮을 것 같네요.
Hacker News 의견
-
JSON은 종종 모호하거나 보장되지 않은 데이터를 보내게 됨. 필드 누락, 타입 오류, 키 오타, 문서화되지 않은 구조 등 다양한 문제가 생김. 하지만 Protobuf는
.proto파일로 메시지 구조를 명확히 정의함으로써 이런 일이 불가능하다고 주장하는 글이 있었음. 그러나 이는 Protobuf의 철학을 오해한 것임.proto3에서는 required 필드 자체를 지원하지 않음. 공식 문서(Protobuf Best Practices)에서도 “required 필드는 해로워서 제거되었다”고 명시되어 있음. 결국 Protobuf 클라이언트도 JSON API처럼 방어적으로 작성해야 함- 해당 블로그에는 비슷한 오해가 많음. 예를 들어 SVG 사용을 반대하는 글에서는 벡터 포맷의 자유로운 스케일링이라는 장점을 고려하지 않음
- 문제의 핵심은 언어나 클라이언트/서버 구현 차이일 뿐임. 나는 Go의 Marshalling 개념을 활용해 Gooey 프레임워크를 클라이언트에 사용 중임. Go의 한계를 극복하면 매우 타입 세이프하게 쓸 수 있음. 단,
json:"-"로 private 필드를 막는 게 중요함. 내 프로젝트는 Gooey에서 볼 수 있음 - 이 글은 직렬화 포맷과 계약(Contract) 개념을 혼동하고 있음
- 네트워크 시스템에서는 인코딩 방식과 무관하게 데이터 불일치(Skew) 문제가 항상 존재함. 다만 Protobuf는 디코딩 후 정적 타입 객체를 제공함. JSON도 검증하면 되지만 대부분 그렇게 하지 않음. 결국 JSON 객체가 이리저리 변형되며 내부 구조를 아무도 확신할 수 없게 됨
- 아마 원글 작성자는 단순히 Protobuf에서 누락된 필드가 기본값으로 초기화된다는 점을 말하고 싶었던 것 같음. 이는 “required” 필드 개념과는 다름
-
압축된 JSON은 충분히 쓸 만하고, 초기 커뮤니케이션 비용이 낮음. 물론 필드가 빠지거나 타입이 바뀌면 문제가 생기지만, 완벽히 타입화된 구조를 설계하고 버전 동기화를 위한 프로세스를 만드는 사람들은 대부분 실패함. 결국 인간의 비용이 낮은 쪽이 승리함. 그래서 JSON은 더 낮은 인간 커뮤니케이션 비용을 가진 대체제가 나오기 전까지는 사라지지 않을 것임
- 맞음. 대부분의 아키텍트는 gRPC 같은 명확한 필요가 없으면 proto를 고려하지 않음.
console.log()로 바로 디버깅할 수 있는 대안이 나오기 전까지 JSON은 대체되지 않을 것임 - 디버깅도 JSON의 강점임. 그냥 열어서 읽으면 됨. 반면 Protobuf는 툴링이 필요함
- 맞는 말임. 하지만 사람들은 설계 단계에서 15분 더 투자하기보다, 나중에 3개월 동안 문제를 되짚는 걸 택함
- JSON이 COBOL처럼 완전히 사라지진 않겠지만, 새 프로젝트에서는 굳이 쓸 이유가 없음
- 맞음. 대부분의 아키텍트는 gRPC 같은 명확한 필요가 없으면 proto를 고려하지 않음.
-
Protobuf는 완벽하지 않음. 서버와 클라이언트가 다른 시점에 배포되어 스펙 버전이 다르면 안전성이 깨짐. ID 재사용 금지, unknown-field 복사 등으로 완화할 수 있지만, 분산 시스템은 본질적으로 복잡함. 그래도
protobuf3는protobuf2의 문제를 많이 해결했음. 예전엔 기본값이 설정된 건지 누락된 건지 구분이 안 됐는데, 이제는message타입을 쓰면 해결됨- JSON이든 Protobuf든 버전 호환성 테스트를 CI 파이프라인에서 강제해야 안전함
- 어떤 타입 시스템도 네트워크를 통과하면 깨짐
-
글에서 “초고효율”이라 했지만 gzip 언급이 없음. 대부분의 텍스트 데이터는 이미 자동 압축되어 전송됨. 따라서 Protobuf는 gzip된 JSON과 비교해야 함
- 나도 여러 바이너리 포맷을 테스트했지만, 결국 gzipped JSON이 압도적으로 효율적이었음
- JSON의 단점은 직렬화/역직렬화 속도임. 나머지는 점진적으로 해결 가능함
- 스트리밍 Brotli/zstd JSON/HTML도 고려할 만함. 연결 유지 중 압축 윈도우를 활용할 수 있음
- 관련 참고: Auth0의 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 예시를 보면 됨. 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가 적합함. Protobuf나 JSON을 단순한 HTTP 위에서 다룸
- ConnectRPC도 비슷한 접근을 제공함. 다만 지원 범위는 아직 불분명함
-
내가 꿈꾸는 바이너리 포맷은 스키마 기반이면서도 메시지 안에 스키마를 포함하는 형태임. 이렇게 하면 vim 플러그인으로 바로 읽을 수 있음. 수백만 개 객체를 다룰 때 1KB의 스키마를 2GB 메시지에 붙이는 건 큰 부담이 아님