tutorial

46 Kompatibilitas dan Evolusi Schema Protobuf

Pendahuluan

Transmisi data antar microservices kini menjadi pilar arsitektur backend modern. Salah satu teknologi yang populer digunakan adalah Protocol Buffers (Protobuf). Keunggulan utamanya antara lain serialisasi cepat, sekecil mungkin, lintas bahasa, dan—yang tak kalah penting—fleksibilitas dalam schema evolution. Namun, menjaga kompatibilitas saat schema berevolusi bisa jadi rumit dan fragile jika kita tidak jeli. Artikel ini mengupas tuntas aspek penting tersebut, dari teori, praktik, hingga contoh kode dan simulasi.


Apa Itu Kompatibilitas Schema?

Secara sederhana, schema compatibility berarti aplikasi versi lama tetap mampu membaca atau menulis data menggunakan schema versi baru (atau sebaliknya) tanpa error yang merusak integritas data.

Dalam Protobuf, terdapat dua istilah penting:

  • Backward Compatibility: Aplikasi lama bisa membaca data yang diproduksi aplikasi baru.
  • Forward Compatibility: Aplikasi baru bisa membaca data yang dibentuk aplikasi lama.

Kunci utama di Protobuf adalah penggunaan field tags yang eksplisit pada setiap field. Ini membedakan Protobuf dengan format seperti JSON atau XML.

Contoh Schema Sederhana

// v1: Pesan Awal
message UserProfile {
  string name = 1;
  int32 age = 2;
}

Evolusi Schema: Studi Kasus

Bayangkan Anda perlu menambahkan field baru email. Apa yang harus diperhatikan agar tetap kompatibel?

// v2: email ditambahkan
message UserProfile {
  string name = 1;
  int32 age = 2;
  string email = 3; // tambahkan field baru dengan tag unik
}

Penjelasan:

  • Field lama (name, age) tetap pada tag lama (1, 2).
  • Field baru (email) memakai tag baru (3).

Aplikasi lama yang hanya tahu v1 akan mengabaikan field tag 3 secara otomatis dan membuangnya, sehingga garis besar backward dan forward compatibility tetap terjaga.


Aturan Emas Evolusi Schema Protobuf

Berikut tabel singkat evolusi schema beserta dampaknya pada kompatibilitas:

AksiBackward CompatibleForward CompatibleAman Dipraktikkan?
Menambah Field OptionalYesYesYes
Menghapus Field (tanpa reuse tag)YesYesYes
Menghapus Field (dengan reuse tag)NoNoJangan!
Mengubah Type FieldNoNoJangan!
Mengubah Tag NumberNoNoJangan!
Mengubah Nama FieldYesYesAman
Menjadikan Field Wajib (from optional)NoNoJangan!


Diagram Alur: Evolusi Schema yang Direkomendasikan

flowchart TD
    A[Mulai Evolusi Schema]
    B{Perlu Tambah Field?}
    C{Perlu Hapus Field?}
    D[Re-use tag lama?]
    E[Aman: tambahkan dengan tag baru]
    F[Aman: biarkan tag menganggur]
    G[Berbahaya: jangan reuse tag!]
    H[Perlu Ubah Type Field?]
    I[Berbahaya: akan korup data!]
    J[Selesai]

    A --> B
    B -- ya --> E
    B -- tidak --> C
    C -- ya --> D
    D -- ya --> G
    D -- tidak --> F
    C -- tidak --> H
    H -- ya --> I
    H -- tidak --> J
    E --> J
    F --> J
    G --> J
    I --> J

Simulasi Evolusi: Studi Kode

Berikut adalah versi Golang dari contoh dua arah forward compatibility dan backward compatibility pada Protocol Buffers:

user_v1.proto

syntax = "proto3";
package userpb;

message UserProfile {
  string name = 1;
  int32 age = 2;
}

user_v2.proto

syntax = "proto3";
package userpb;

message UserProfile {
  string name = 1;
  int32 age = 2;
  string email = 3;
}

⚙️ Generate File Go

protoc --go_out=. user_v1.proto
protoc --go_out=. user_v2.proto

Akan menghasilkan:

  • user_v1.pb.go
  • user_v2.pb.go

Kode Go: Simulasi Dua Arah Kompatibilitas

package main

import (
	"fmt"
	"log"

	"google.golang.org/protobuf/proto"

	userpbv1 "path/to/user_v1"
	userpbv2 "path/to/user_v2"
)

func main() {
	fmt.Println("=== 1. Versi Lama Membaca Data Versi Baru ===")

	// V2 menulis data dengan field tambahan: email
	msgV2 := &userpbv2.UserProfile{
		Name:  "Budi",
		Age:   30,
		Email: "budi@example.com",
	}
	data, err := proto.Marshal(msgV2)
	if err != nil {
		log.Fatalf("Gagal serialisasi v2: %v", err)
	}

	// Dibaca oleh versi lama (v1) yang tidak tahu field email
	msgV1 := &userpbv1.UserProfile{}
	if err := proto.Unmarshal(data, msgV1); err != nil {
		log.Fatalf("Gagal deserialisasi v1: %v", err)
	}
	fmt.Println("Deserialized (v1):", msgV1) // email diabaikan tanpa error

	fmt.Println("\n=== 2. Versi Baru Membaca Data Versi Lama ===")

	// V1 menulis data tanpa field email
	msgV1Old := &userpbv1.UserProfile{
		Name: "Rina",
		Age:  20,
	}
	dataOld, err := proto.Marshal(msgV1Old)
	if err != nil {
		log.Fatalf("Gagal serialisasi v1: %v", err)
	}

	// Dibaca oleh versi baru (v2) yang mengharapkan email
	msgV2Read := &userpbv2.UserProfile{}
	if err := proto.Unmarshal(dataOld, msgV2Read); err != nil {
		log.Fatalf("Gagal deserialisasi v2: %v", err)
	}
	fmt.Println("Deserialized (v2):", msgV2Read) // email == "", default
}

Output:

=== 1. Versi Lama Membaca Data Versi Baru ===
Deserialized (v1): name:"Budi" age:30

=== 2. Versi Baru Membaca Data Versi Lama ===
Deserialized (v2): name:"Rina" age:20 email:""

Hasil:

CaseStatusOutput
Old code baca pesan newBackward compatibleemail diabaikan
New code baca pesan oldForward compatibleemail kosong/default


Praktik Terburuk: Reuse Tag Number

Skenario yang harus diavoid:

// v2 - SALAH, reuse tag!
message UserProfile {
  string name = 1;
  int32 age = 2;
  string phone = 3; // tadinya 'email', sekarang 'phone', tag sama!
}

Parse data lama dapat menyebabkan field corruption. Data dengan tag 3 mungkin berisi email, tapi diinterpretasi sebagai phone number.


Tips Engineer: Evolusi Aman

  1. Jangan Pernah Mengubah/Merubah Tipe Field
    Perubahan tipe field (misal dari string ke int32) berpotensi mengakibatkan parsing error dan data korup.

  2. Jangan Gunakan Ulang Tag Number
    Setelah tag dihapus, biarkan tag itu tidak terpakai selamanya.

  3. Hindari Perubahan Optional jadi Required
    Pesan lama yang tidak punya field tersebut akan gagal diparse jika mandatory.

  4. Pertimbangkan Versioning
    Untuk perubahan besar, gunakan pesan baru: UserProfileV2 dst, dan buat prosedur migrasi data.

  5. Baca Panduan Resmi Evolusi Protobuf


Kesimpulan

Kompatibilitas schema adalah aspek vital dalam integrasi sistem berbasis Protobuf. Dengan mengikuti aturan sederhana (jangan pernah mengubah/merubah tag number, tipe field, dan selalu tambahkan field secara additive), kita dapat memastikan evolusi schema berjalan tanpa drama. Simulasi di atas memperlihatkan bagaimana backward dan forward compatibility bekerja secara nyata.

Selalu uji kompatibilitas schema Anda saat mengalami perubahan dan, jika memungkinkan, bangun pipeline CI agar integritas schema ini selalu terjaga. Evolving fast, but without breaking things.


Rujukan:

Happy engineering! 🚀

comments powered by Disqus