0. 상황
RPC 통신을 들여다보다 Protocol Buffers(이하 protobuf)를 마주했다. "XML보다 3
10배 작고, 20
100배 빠르고, 더 쉽다"는 그 문구.
작고 빠른 건 알겠는데, 대체 왜 그런 건지. 그리고 멀쩡히 잘 쓰던 XML, JSON은 뭐가 부족했던 건지 궁금해서 파봤다.
1. RPC가 뭐길래 직렬화가 필요한가
RPC(Remote Procedure Call)는 남의 서버에 있는 함수를 내 코드의 함수처럼 호출하는 방식이다. getUser(123)을 부르면 네트워크 타고 저쪽에서 실행되고 결과가 돌아온다.
근데 네트워크로는 객체를 그대로 못 보낸다. 메모리 속 데이터를 전송 가능한 바이트 열로 바꿔야 하는데, 이게 직렬화(serialization)다. 받는 쪽은 다시 데이터로 푸는 역직렬화를 한다.
그래서 RPC의 성능과 사용성은 "직렬화를 무슨 방식으로 하느냐"에 좌우된다. 후보가 XML, JSON, protobuf다.
2. 세 방식의 개요
XML — 태그로 감싸는 텍스트.
<person><name>John</name><id>123</id></person> <!-- 약 45 bytes -->
표준이 오래됐고 검증·네임스페이스 등 기능이 풍부하지만 그만큼 무겁다.
JSON — 중괄호로 묶는 텍스트.
{"name":"John","id":123} // 약 24 bytes
웹 API의 사실상 표준. 필드 이름이 데이터 안에 들어있어 스키마 없이도 사람이 바로 읽는다.
Protobuf — 스키마 기반 바이너리. 먼저 .proto로 구조를 정의하고
message Person {
string name = 1;
int32 id = 2;
}
실제로는 사람이 못 읽는 바이트를 보낸다.
0A 04 4A 6F 68 6E 10 7B // 8 bytes
여기 핵심은 "name", "id" 같은 필드 이름이 없다는 점이다. 양쪽이 .proto를 미리 공유하니 "1번 = name"임을 이미 알고, 번호만 보내면 된다.
3. self-describing — 모든 차이의 뿌리
세 방식을 가르는 근본은 "데이터가 자기 자신을 설명하느냐(self-describing)"다.
- JSON/XML: 필드 이름이 데이터 안에 들어있어 받자마자 해석된다. 데이터가 스스로를 설명한다.
- Protobuf: 바이트만 받으면 뭔지 알 수 없다. 스키마가 바깥에 따로 있어야 해석된다. self-describing이 아니다.
JSON/XML = 이름표가 붙은 상자. 열면 바로 안다. 대신 이름표가 공간을 먹는다.
Protobuf = 번호만 매긴 칸 + 따로 가진 안내문(스키마). 안내문 있는 사람만 안다. 작고 가볍지만 안내문 없으면 무용지물.
이 한 가지 차이가 작고/빠르고/쉬운 이유를 전부 만든다.
4. 왜 작고, 빠르고, 쉬운가
작은 이유
- 필드 이름을 안 보낸다.
"name"반복 대신 1바이트 번호 태그. - 숫자를 바이너리로.
123이 JSON은 3바이트, protobuf varint는 1바이트. - 닫는 태그·따옴표·중괄호 같은 구조용 문자가 없다.
빠른 이유
- 텍스트 파싱이 없다. 바이트 위치와 의미가 스키마로 고정돼 메모리에 거의 그대로 매핑된다.
- 컴파일러가 필드 전용 읽기/쓰기 코드를 미리 생성해, 런타임에 문자열 키 탐색이 없다.
쉬운 이유 (작성 간결함이 아니라 워크플로 안정성)
.proto하나가 데이터 구조의 단일 정의(계약) 역할을 한다.- 컴파일하면 타입 있는 클래스가 자동 생성돼 오타·타입 불일치를 컴파일 단계에서 잡는다.
- 스키마 하나로 여러 언어 시스템이 같은 데이터를 주고받는다.
5. .proto는 언제, 어떻게 공유되는가
오해하기 쉬운 부분. .proto는 데이터와 같이 전송되지 않는다. 런타임에 바이트가 갈 때 스키마는 함께 안 간다. 대신 빌드 시점에 양쪽 코드 안에 미리 박힌다.
- 정의(개발):
.proto작성. 필드 번호를 정하는 게 곧 계약. - 컴파일(빌드):
protoc로 각 언어 코드 생성. - 포함: 생성된 코드가 서버/클라이언트에 각각 컴파일돼 들어감.
- 통신(런타임): 오가는 건 압축된 바이트뿐.
통화 전에 똑같은 암호표를 미리 나눠 갖고, 통화 중엔 암호만 주고받는 것과 같다.
실무에선 공유 Git 저장소, 스키마 레지스트리(Buf), 컴파일된 패키지 배포(npm/Maven/PyPI) 등으로 양쪽이 맞춘다.
6. .proto가 바뀌면 동시 배포해야 하나?
규칙만 지키면 동시 배포는 필요 없다. 오히려 그걸 피하려고 설계됐다. 번호 기반 식별이 만드는 두 동작 덕분이다.
- 수신 측이 모르는 번호 → 무시
- 수신 측이 기대한 필드 없음 → 기본값 처리
email = 3을 추가한 경우, 신·구 어느 쪽이 보내든 깨지지 않는다(forward/backward compatibility). 그래서 순차 배포가 가능하다. 단, 규칙을 지킬 때만.
- 해도 됨: 새 번호로 필드 추가, 필드 이름 변경, 제거 후
reserved봉인 - 깨짐: 기존 번호 변경, 번호 재사용, 호환 안 되는 타입 변경(int32→string)
7. 그럼 결국 가독성 지옥 아닌가? (proto rot)
점진적으로 추가만 하다 보면 .proto가 누더기가 된다. 이미 이름이 있다. proto rot.
message User {
reserved 4, 7, 12;
reserved "legacy_id";
string name = 1;
int32 id = 2; // deprecated
string email = 3;
string user_id = 5; // id 대체용
string old_phone = 9 [deprecated = true];
}
번호=역사라 관련 필드가 흩어지고, reserved/deprecated 흔적이 쌓이고, 신·구 필드 공존으로 헷갈린다.
다만 이건 protobuf 탓이 아니라 오래 사는 모든 인터페이스의 노화다. JSON API도 늙는다. 오히려 JSON은 스키마가 문서에 흩어지거나 없어 추적이 더 막막할 때도 많다. protobuf는 적어도 .proto 한 파일에 역사가 명시적으로 박힌다. 지저분한 흔적은 "안 깨지게 진화시킨 대가가 기록된 것"이다.
관리법도 도구가 아니라 운영 규율이다.
- 메시지를 하위 메시지로 쪼개고 중첩
- deprecated를 방치 말고, 트래픽 사라진 거 확인 후 제거 +
reserved봉인 - 린팅/스키마 레지스트리(Buf)로 위생 유지
- 부채가 임계점 넘으면
v2로 끊고 구버전 병행 후 폐기
8. 정리: 언제 뭘 쓰나
- XML: 검증·표준 기능이 풍부하지만 가장 무겁다. 레거시/엔터프라이즈, 복잡한 문서 구조에.
- JSON: 사람이 읽기 쉽고 범용적이며 스키마 없이 동작. 외부 공개 REST API의 사실상 표준.
- Protobuf: 작고 빠르고 타입 안전하지만 스키마 공유·컴파일이 필수고 사람이 못 읽는다. 내부 서비스 간 통신(특히 gRPC 마이크로서비스)에 빛난다.
결국 "데이터에 설명을 넣을 것이냐(JSON/XML), 빼서 스키마로 따로 둘 것이냐(protobuf)"가 근본 갈림길이다. 빼서 얻은 속도·크기에는 스키마 공유와 번호 규율이라는 대가가 따라온다.
'CS > CS' 카테고리의 다른 글
| [algorithm] 정렬(버블정렬, 선택정렬, 삽입정렬, 퀵정렬, 병합정렬, 힙정렬, 기수정렬, 계수정렬) (1) | 2024.05.15 |
|---|---|
| 데이터베이스-동시성 제어 (0) | 2024.02.06 |
| [DB] 트렌젝션과 ACID (0) | 2024.01.30 |
| [DB] Index란? (0) | 2024.01.18 |
| [CS] 엣지 케이스와 코너 케이스는 뭘까?(feat.. 조금 더 안정성 있는 서비스를 향하여..) (0) | 2024.01.16 |
야나의 코딩 일기장 :) #코딩블로그 #기술블로그 #코딩 #조금씩,꾸준히
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!