Tampilan
Kenapa penting
Di bab sebelumnya kamu merancang REST antar-service. REST/JSON mudah dibaca manusia, tetapi untuk panggilan internal yang terjadi ribuan kali per detik ada biaya yang terasa: payload teks yang besar, parsing JSON, dan kontrak yang sering hanya hidup di dokumentasi. gRPC menawarkan kontrak yang ditulis sebagai kode (.proto), format biner yang ringkas, HTTP/2, dan streaming. Banyak perusahaan memakai REST di tepi (untuk klien dan mitra) dan gRPC di dalam (antar-service). Memahami keduanya membuatmu bisa memilih dengan alasan, bukan ikut-ikutan.
Konsep inti
gRPC vs REST
| Aspek | REST/JSON | gRPC |
|---|---|---|
| Transport | HTTP/1.1 atau HTTP/2 | HTTP/2 (multiplexing banyak call di satu koneksi) |
| Format | JSON (teks) | Protocol Buffers (biner) |
| Kontrak | Opsional (OpenAPI) | Wajib (.proto), kode client/server di-generate |
| Gaya | Resource + HTTP method | Method RPC (GetStock, ReserveStock) |
| Streaming | Terbatas (SSE, WebSocket terpisah) | Bawaan: server, client, dan dua arah |
| Browser | Langsung | Butuh proxy gRPC-Web |
| Debug | curl, mudah dibaca | Butuh tool khusus seperti grpcurl |
gRPC punya empat jenis method: unary (satu request, satu response — paling umum), server streaming (satu request, banyak response), client streaming (banyak request, satu response), dan bidirectional streaming.
Menulis file .proto
File .proto adalah sumber kebenaran kontrak. Dari file ini, compiler protoc membuat class message dan stub untuk Java (dan bahasa lain).
text
// src/main/proto/inventory.proto
syntax = "proto3";
package toko.inventory.v1;
option java_multiple_files = true;
option java_package = "id.toko.inventory.grpc";
service InventoryService {
rpc GetStock (StockRequest) returns (StockReply);
rpc WatchStock (StockRequest) returns (stream StockReply);
}
message StockRequest {
string sku = 1;
}
message StockReply {
string sku = 1;
int32 available = 2;
reserved 3; // dulu: string location — jangan dipakai ulang
reserved "location";
string warehouse_code = 4;
}Angka = 1, = 2 adalah field number, bukan nilai default. Di proto3, field yang tidak dikirim bernilai default ("", 0, false) — jadi kamu tidak bisa membedakan "tidak dikirim" dan "nol" kecuali memakai kata kunci optional.
Kenapa field number sakral: wire format
Protobuf tidak mengirim nama field. Setiap field diawali tag = (fieldNumber << 3) | wireType, lalu nilainya. Program Java murni berikut meng-encode StockReply secara manual supaya kamu melihat byte-nya:
java
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
public class WireFormatDemo {
static final int VARINT = 0;
static final int LENGTH_DELIMITED = 2;
static void writeVarint(ByteArrayOutputStream out, long value) {
while ((value & ~0x7FL) != 0) {
out.write((int) ((value & 0x7F) | 0x80));
value >>>= 7;
}
out.write((int) value);
}
static void writeTag(ByteArrayOutputStream out, int fieldNumber, int wireType) {
writeVarint(out, ((long) fieldNumber << 3) | wireType);
}
public static void main(String[] args) {
// message StockReply { string sku = 1; int32 available = 2; }
var out = new ByteArrayOutputStream();
byte[] sku = "KOPI-250G".getBytes(StandardCharsets.UTF_8);
writeTag(out, 1, LENGTH_DELIMITED);
writeVarint(out, sku.length);
out.writeBytes(sku);
writeTag(out, 2, VARINT);
writeVarint(out, 150);
byte[] proto = out.toByteArray();
String json = "{\"sku\":\"KOPI-250G\",\"available\":150}";
System.out.println("Protobuf (hex): " + HexFormat.of().formatHex(proto));
System.out.println("Protobuf : " + proto.length + " byte");
System.out.println("JSON : " + json.getBytes(StandardCharsets.UTF_8).length + " byte");
}
}text
Protobuf (hex): 0a094b4f50492d32353047109601
Protobuf : 14 byte
JSON : 35 byte0a = field 1 bertipe length-delimited, 09 = panjang 9 byte, lalu teks KOPI-250G; 10 = field 2 bertipe varint, 9601 = angka 150 dalam varint. Payload 14 byte versus 35 byte JSON. Karena yang dikirim hanya nomor, aturan evolusinya ketat:
| Boleh | Dilarang |
|---|---|
| Menambah field baru dengan nomor baru | Mengubah nomor field yang sudah ada |
Menghapus field lalu menandainya reserved | Memakai ulang nomor atau nama field yang dihapus |
| Mengganti nama field (nomor tetap) | Mengubah tipe ke tipe yang tidak kompatibel |
| Menambah method RPC baru | Mengubah tipe request/response method yang ada |
Client lama mengabaikan field yang tidak dikenalnya — mirip tolerant reader di REST. Untuk perubahan yang mematahkan, buat package baru (toko.inventory.v2).
Generate kode Java dengan Maven
Kompilasi .proto adalah bagian standar gRPC, bukan fitur Spring. Plugin yang umum dipakai adalah protobuf-maven-plugin (org.xolstice) bersama os-maven-plugin untuk memilih binary protoc sesuai sistem operasi.
xml
<!-- Potongan pom.xml; versi grpc & protobuf ikuti BOM yang kamu pakai -->
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>Hasilnya ada di target/generated-sources/protobuf/java (message: StockRequest, StockReply) dan target/generated-sources/protobuf/grpc-java (stub: InventoryServiceGrpc). Jangan mengedit file hasil generate — ubah .proto, lalu build ulang.
Server dan client dengan Spring gRPC
Spring gRPC (proyek org.springframework.grpc) menyediakan auto-configuration server dan client. Untuk Spring Boot 4.1, pakai lini Spring gRPC 1.0.x beserta starter-nya; mulai Spring Boot 4.2 auto-configuration gRPC dipindah ke Spring Boot sendiri (spring-boot-starter-grpc-server), sementara lini Spring gRPC 1.1.x hanya berisi bagian inti.
xml
<!-- Potongan pom.xml — Spring Boot 4.1 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-dependencies</artifactId>
<version>1.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-server-spring-boot-starter</artifactId>
</dependency>
</dependencies>Implementasi server cukup berupa bean yang meng-extend base class hasil generate (bean bertipe BindableService otomatis didaftarkan ke server di port default 9090):
java
// Potongan — inventory-service
@Service
class InventoryGrpcService extends InventoryServiceGrpc.InventoryServiceImplBase {
private final StockRepository stocks;
InventoryGrpcService(StockRepository stocks) {
this.stocks = stocks;
}
@Override
public void getStock(StockRequest request, StreamObserver<StockReply> responseObserver) {
stocks.findBySku(request.getSku()).ifPresentOrElse(
stock -> {
responseObserver.onNext(StockReply.newBuilder()
.setSku(stock.sku())
.setAvailable(stock.available())
.build());
responseObserver.onCompleted();
},
() -> responseObserver.onError(Status.NOT_FOUND
.withDescription("SKU tidak dikenal: " + request.getSku())
.asRuntimeException()));
}
}Di sisi client, tambahkan spring-grpc-client-spring-boot-starter, konfigurasikan channel bernama, lalu buat stub dari GrpcChannelFactory:
yaml
# application.yml order-service
spring:
grpc:
client:
channels:
inventory:
address: inventory-service:9090java
// Potongan — order-service
@Configuration
class InventoryGrpcClientConfig {
@Bean
InventoryServiceGrpc.InventoryServiceBlockingStub inventoryStub(GrpcChannelFactory channels) {
return InventoryServiceGrpc.newBlockingStub(channels.createChannel("inventory"));
}
}Tanpa Spring
Spring gRPC hanya membungkus grpc-java. Tanpa Spring, server dibuat dengan ServerBuilder.forPort(9090).addService(new InventoryGrpcService(repo)).build().start(), dan client dengan ManagedChannelBuilder.forAddress("inventory-service", 9090).usePlaintext().build(). Kode service dan stub-nya sama.
Deadline dan status code
gRPC memakai deadline (batas waktu absolut) alih-alih read timeout. Deadline ikut dikirim ke server, sehingga server tahu kapan client sudah tidak menunggu — konsep timeout budget dari bab sebelumnya menjadi bawaan protokol. Tanpa deadline, call gRPC bisa menunggu tanpa batas.
java
// Potongan — order-service memanggil inventory dengan deadline
StockReply reply;
try {
reply = inventoryStub
.withDeadlineAfter(300, TimeUnit.MILLISECONDS)
.getStock(StockRequest.newBuilder().setSku(sku).build());
} catch (StatusRuntimeException e) {
switch (e.getStatus().getCode()) {
case NOT_FOUND -> throw new UnknownSkuException(sku);
case DEADLINE_EXCEEDED, UNAVAILABLE -> throw new InventoryUnavailableException(sku, e);
default -> throw e;
}
}Status code yang paling sering kamu temui:
| Kode | Arti | Padanan HTTP kasar | Aman di-retry? |
|---|---|---|---|
OK | Sukses | 200 | — |
INVALID_ARGUMENT | Input tidak valid | 400 | Tidak |
NOT_FOUND | Data tidak ada | 404 | Tidak |
ALREADY_EXISTS | Duplikat | 409 | Tidak |
PERMISSION_DENIED | Tidak berhak | 403 | Tidak |
UNAUTHENTICATED | Kredensial tidak ada/salah | 401 | Tidak |
RESOURCE_EXHAUSTED | Kuota/rate limit habis | 429 | Dengan backoff |
DEADLINE_EXCEEDED | Deadline lewat | 504 | Hati-hati |
UNAVAILABLE | Server tidak bisa dihubungi sementara | 503 | Ya (idempotent) |
INTERNAL, UNKNOWN | Error tak terduga di server | 500 | Tidak |
Exception biasa yang lolos dari method service sampai ke client sebagai UNKNOWN — selalu terjemahkan error domain ke status yang bermakna.
Kapan memilih gRPC vs REST
Pilih gRPC untuk komunikasi internal antar-service yang sering dan sensitif latency, kontrak ketat lintas bahasa, atau kebutuhan streaming. Pilih REST untuk API publik, klien browser, integrasi dengan mitra, dan bila kemudahan debug lebih penting daripada efisiensi. Keduanya tetap komunikasi sinkron: semua aturan bab sebelumnya — timeout, latency berantai, chatty API — tetap berlaku.
Contoh di dunia backend
Order-service memakai gRPC ke inventory untuk memesan stok saat checkout. Kontrak ReserveStock membawa idempotency_key agar retry pada UNAVAILABLE tidak memotong stok dua kali.
text
// inventory.proto — tambahan aditif, field number baru
service InventoryService {
rpc GetStock (StockRequest) returns (StockReply);
rpc ReserveStock (ReserveRequest) returns (ReserveReply);
}
message ReserveRequest {
string idempotency_key = 1;
string order_id = 2;
repeated ReserveLine lines = 3;
}
message ReserveLine {
string sku = 1;
int32 quantity = 2;
}
message ReserveReply {
string reservation_id = 1;
}java
// Potongan — server: stok kurang dilaporkan sebagai FAILED_PRECONDITION
@Override
public void reserveStock(ReserveRequest request, StreamObserver<ReserveReply> responseObserver) {
try {
String reservationId = reservationService.reserve(
request.getIdempotencyKey(), request.getOrderId(), request.getLinesList());
responseObserver.onNext(ReserveReply.newBuilder().setReservationId(reservationId).build());
responseObserver.onCompleted();
} catch (InsufficientStockException e) {
responseObserver.onError(Status.FAILED_PRECONDITION
.withDescription(e.getMessage())
.asRuntimeException());
}
}FAILED_PRECONDITION menandakan sistem tidak dalam kondisi yang dibutuhkan (stok kurang) — client tidak boleh me-retry begitu saja, berbeda dengan UNAVAILABLE.
Pertanyaan interview
1. Kenapa gRPC biasanya lebih efisien daripada REST/JSON?
Payload protobuf berbentuk biner dan hanya membawa field number serta nilai, jadi lebih kecil dan lebih cepat di-parse daripada JSON. HTTP/2 juga memungkinkan banyak call berjalan bersamaan di satu koneksi. Namun keuntungan ini paling terasa pada panggilan internal yang sangat sering; untuk API publik, kemudahan REST sering lebih bernilai.
2. Bagaimana cara aman menghapus field dari message protobuf?
Hapus field-nya lalu tambahkan nomor dan namanya ke reserved agar tidak pernah dipakai ulang. Client lama yang masih mengirim atau membaca field itu tetap berjalan: field yang tidak dikenal diabaikan, dan field yang tidak dikirim bernilai default.
3. Apa beda deadline gRPC dengan read timeout biasa?
Deadline adalah titik waktu absolut yang ikut dikirim ke server dan bisa diteruskan ke panggilan berikutnya, sehingga seluruh rantai tahu kapan harus berhenti. Read timeout hanya berlaku lokal di satu client dan tidak diketahui server.
Jebakan umum
- Memakai ulang field number setelah field dihapus. Selalu
reserved, dan jangan pernah mengubah nomor field. - Tidak memasang deadline. Call bisa menggantung selamanya dan menghabiskan resource.
- Melempar exception biasa dari service. Client hanya melihat
UNKNOWN; pakaiStatusyang tepat. - Me-retry semua status. Hanya
UNAVAILABLE(dan kadangRESOURCE_EXHAUSTEDdengan backoff) yang layak di-retry, itu pun untuk operasi idempotent. - Mengira nilai
0atau""berarti "tidak dikirim". Di proto3 itu nilai default; pakaioptionalbila perlu membedakan. - Memakai gRPC untuk API publik browser tanpa gRPC-Web atau gateway — klien tidak bisa memanggilnya langsung.
Ringkasan
- gRPC memakai HTTP/2, protobuf biner, dan kontrak
.protoyang menghasilkan kode client dan server; mendukung streaming. - Protobuf hanya mengirim field number: tambah field dengan nomor baru,
reservedfield yang dihapus, jangan ubah nomor. protobuf-maven-pluginmen-generate message dan stub; Spring gRPC menyediakan auto-configuration server/client di atas grpc-java.- Selalu pasang deadline dan terjemahkan error ke status code gRPC yang bermakna.
- gRPC cocok untuk jalur internal yang intens; REST tetap pilihan utama untuk API publik dan browser.