gRPC untuk komunikasi antar layanan
Kalau kamu memang memisahkan layanan, gRPC memberi kontrak yang dijaga compiler di kedua sisi. Untuk API yang dipanggil browser, REST atau GraphQL hampir selalu pilihan yang lebih tepat.
Intisari
- Kontrak ditulis di
.proto, kode klien dan server digenerate โ kesalahan kontrak jadi error kompilasi. - Protobuf biner di atas HTTP/2: lebih kecil dan lebih cepat daripada JSON.
- Browser tidak bisa memanggil gRPC langsung โ butuh gRPC-Web atau gateway.
- Streaming dua arah bawaan; berguna untuk umpan dan pemrosesan besar.
- Untuk layanan internal antar tim, kontrak bertipe menghapus seluruh kategori kesalahpahaman.
Kontraknya
syntax = "proto3";
package katalog.v1;
option go_package = "contoh.com/toko/gen/katalog/v1;katalogv1";
service Katalog {
rpc AmbilProduk(AmbilProdukRequest) returns (Produk);
rpc DaftarProduk(DaftarProdukRequest) returns (DaftarProdukResponse);
rpc PantauStok(PantauStokRequest) returns (stream PerubahanStok);
}
message Produk {
int64 id = 1;
string nama = 2;
int64 harga = 3;
// Nomor field adalah kontrak sesungguhnya โ JANGAN pernah diubah
// atau dipakai ulang. Nama boleh berubah; nomor tidak.
}
message AmbilProdukRequest {
int64 id = 1;
}
go get -tool google.golang.org/protobuf/cmd/protoc-gen-go
go get -tool google.golang.org/grpc/cmd/protoc-gen-go-grpc
buf generate # atau protoc dengan daftar flag yang panjang
Server
type katalogServer struct {
katalogv1.UnimplementedKatalogServer // WAJIB di-embed: menjaga
svc *katalog.Layanan // kompatibilitas saat proto bertambah
}
func (s *katalogServer) AmbilProduk(ctx context.Context,
req *katalogv1.AmbilProdukRequest) (*katalogv1.Produk, error) {
p, err := s.svc.Ambil(ctx, req.GetId())
switch {
case errors.Is(err, katalog.ErrTidakDitemukan):
return nil, status.Errorf(codes.NotFound, "produk %d tidak ada", req.GetId())
case err != nil:
// Jangan bocorkan detail internal ke pemanggil (Fase 1).
s.log.ErrorContext(ctx, "ambil produk gagal", "err", err)
return nil, status.Error(codes.Internal, "terjadi kesalahan")
}
return &katalogv1.Produk{
Id: p.ID, Nama: p.Nama, Harga: int64(p.Harga),
}, nil
}
func main() {
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
pulihkanInterceptor(log), // pemulih panic (Fase 1)
logInterceptor(log),
authInterceptor(auth),
),
grpc.StatsHandler(otelgrpc.NewServerHandler()), // trace (Fase 9)
)
katalogv1.RegisterKatalogServer(srv, &katalogServer{svc: svc})
lis, _ := net.Listen("tcp", ":9090")
srv.Serve(lis)
}
Interceptor adalah middleware versi gRPC, dan urutannya sama pentingnya dengan di HTTP (Fase 3): pemulih panic terluar, lalu log, lalu autentikasi. Ekosistemnya juga sudah menyediakan instrumentasi OpenTelemetry, sehingga trace tetap menyambung melintasi batas layanan.
Klien
conn, err := grpc.NewClient("katalog.internal:9090",
grpc.WithTransportCredentials(insecure.NewCredentials()), // di dalam VPC
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
grpc.WithDefaultServiceConfig(`{
"methodConfig": [{
"name": [{"service": "katalog.v1.Katalog"}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
}
}]
}`))
if err != nil {
return err
}
defer conn.Close()
klien := katalogv1.NewKatalogClient(conn)
ctx, batal := context.WithTimeout(ctx, 2*time.Second)
defer batal()
p, err := klien.AmbilProduk(ctx, &katalogv1.AmbilProdukRequest{Id: 42})
Kebijakan retry ada di konfigurasi, bukan di kodemu โ termasuk backoff dan daftar kode status yang layak diulang. Ini salah satu keuntungan nyata gRPC dibanding klien HTTP buatan sendiri (Fase 3), di mana semua itu harus ditulis dan diuji sendiri.
gRPC versus REST
| gRPC | REST + JSON | |
|---|---|---|
| Kontrak | Dijaga compiler | OpenAPI (Fase 7), atau tidak sama sekali |
| Ukuran payload | Lebih kecil (biner) | Lebih besar |
| Bisa dipanggil browser | Tidak langsung | Ya |
| Debug dengan curl | Butuh grpcurl | Mudah |
| Bisa di-cache CDN | Tidak | Ya (Fase 9) |
| Streaming | Bawaan, dua arah | SSE / WebSocket |
| Melalui ALB | Ya (target group gRPC) | Ya |
| Cocok untuk | Antar layanan internal | API publik, browser |
Untuk situs bertrafik baca tinggi โ sasaran roadmap ini โ poin "bisa di-cache CDN" biasanya
menentukan. API publik berbasis HTTP dengan header Cache-Control yang benar bisa
dilayani dari edge; gRPC tidak. Pakai gRPC di belakang, HTTP di depan.
Menjaga kompatibilitas proto
| Perubahan | Aman? |
|---|---|
| Menambah field dengan nomor baru | โ |
| Menambah method baru | โ |
| Mengganti nama field (nomor tetap) | โ di kabel; โ untuk kode yang memakainya |
| Mengubah nomor field | โ merusak semuanya |
| Mengubah tipe field | โ |
| Menghapus field | โ ๏ธ tandai reserved supaya nomornya tidak dipakai ulang |
| Mengganti nama paket/service | โ โ buat versi baru: katalog.v2 |
buf breaking memeriksa perubahan yang merusak kompatibilitas di CI, dengan
membandingkan proto-mu terhadap cabang utama. Untuk kontrak yang dipakai tim lain, ini gerbang yang
sama pentingnya dengan tes.
Latihan: definisikan satu service gRPC dengan dua method, generate kodenya, dan panggil dari
klien Go. Lalu ubah nomor sebuah field di .proto, generate ulang hanya di sisi server, dan
amati apa yang diterima klien lama โ itu alasan nomor field disebut kontrak sesungguhnya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.