Protocol Buffers ile Veri İletiminin Modern Yolu

Protocol Buffers Nedir?
Modern yazılım dünyasında hız, artık lüks değil; mecburiyet. Bu hız ihtiyacı, veri iletiminden mimariye kadar her alanda kendini hissettiriyor. Geleneksel veri formatları olan JSON ve XML, her ne kadar esnek ve insan tarafından okunabilir yapılarıyla uzun süre tercih edilseler de, yüksek trafikli sistemlerde ve düşük gecikme gerektiren ortamlarda artık yetersiz kalmaya başladılar. Büyük veri hacimleri, mikroservis mimarileri ve mobil uygulamalar gibi senaryolarda bu formatlar, sistem performansını ciddi ölçüde etkileyebiliyor.
Burada özellikle JSON’un rolü dikkat çekici. İnsan tarafından okunabilir olması nedeniyle birçok geliştirici ve sistem tarafından tercih edilen JSON, başlangıçta pratik bir çözüm sunuyor gibi görünse de; veri boyutunun artmasıyla birlikte bu avantaj yerini ciddi dezavantajlara bırakıyor. Metin tabanlı yapısı, taşıma sırasında ekstra yük anlamına geliyor ve bu da performansı doğrudan etkiliyor

İşte bu noktada Protobuf devreye giriyor. Fazla karakter ve etiket yığınları yok. Sadece ihtiyacın olan veri ve hepsi binary formatta, yani mümkün olan en hızlı ve en az yer kaplayan hâliyle sunuluyor.
Protobuf sadece bir serileştirme formatı değil. Aynı zamanda veri tanımı, sistemler arası iletişim ve hatta API tasarımı için de modern sistemlerde standart olarak kullanılan bir yapı sunuyor. gRPC ile birlikte kullanıldığında, uygulamalar arası haberleşmeyi baştan tanımlayan yepyeni bir mimari sunuyor.
gRPC Nedir? – Protobuf’un Gücünü Ağ Katmanına Taşımak
Protocol Buffers sadece veriyi sıkıştırmakla kalmaz; aynı zamanda bu veriyi sistemler arasında nasıl ileteceğimizin de temelini atar. İşte bu noktada, Protobuf’un doğal partneri olan gRPC (gRPC Remote Procedure Call) devreye girer.

İstemci tarafında her şey, bir .proto dosyasının tanımlanmasıyla başlıyor. Bu dosya içerisinde servisler ve mesaj yapıları net şekilde tanımlanıyor. Ardından, bu tanım protoc derleyicisi tarafından işlenerek otomatik olarak kod üretiliyor. Bu kod parçası, yani Stub, istemcinin yapacağı API çağrılarını doğrudan binary formatta oluşturup karşı tarafa iletebilmesini sağlıyor. Kullanıcı tarafından başlatılan bir işlem, stub aracılığıyla ilgili metoda çevrilerek, gRPC istemcisi tarafından işleniyor.
İletişim süreci, HTTP/2 protokolü üzerinden gerçekleşiyor. gRPC’nin bu protokolü tercih etmesinin sebebi oldukça açık: daha hızlı bağlantılar, düşük gecikme süresi ve aynı bağlantı üzerinden birden fazla çağrı yapılabilmesi (multiplexing). İstemciden gelen istek, önce Protobuf formatında encode ediliyor ve bu haliyle sunucuya gönderiliyor. Sunucu ise bu binary veriyi alıp decode ederek, ilgili metodu çalıştırıyor. Ardından oluşan cevap, yine aynı şekilde encode edilip istemciye ulaştırılıyor.
Sunucu tarafında da benzer şekilde .proto dosyası protoc ile derleniyor. Bu sayede gelen isteklerin anlaşılması ve ilgili metodun tetiklenmesi mümkün oluyor. Bu metodlar genellikle bir API Repository içerisinde tanımlı ve organize şekilde tutuluyor. Sunucunun görevi yalnızca cevabı üretmek değil; gelen isteğin doğruluğunu kontrol etmek, işlemek ve hızlıca sonuçlandırmak. Burada gRPC’nin performansı, özellikle yüksek trafikli ve düşük gecikme gerektiren sistemlerde farkını ortaya koyuyor.
Kısacası gRPC; sadece hızlı bir protokol değil, aynı zamanda geliştiricinin omzundaki yükü hafifleten, bakım ve versiyonlama gibi zorlukları ortadan kaldıran modern bir mimari çözüm. REST’in yavaş yavaş hantallaşmaya başladığı yerlerde, gRPC’nin sade, hızlı ve güvenli yapısı sistemlere yeni bir soluk getiriyor. Özellikle mobil uygulamalarda, IoT sistemlerinde ve mikroservis tabanlı yapılarda tercih edilmesi tesadüf değil — gRPC, ihtiyacı karşılayan cevabı en net şekilde veriyor.
Proto Dosyalarıyla Veri Yapıları ve RPC Servislerini Tanımlamak
Protobuf’un merkezinde, sistemler arası veri alışverişinin nasıl tanımlanacağını belirleyen .proto dosyaları yer alır. Bu tanım dosyaları, yalnızca verinin yapısını ifade etmez; aynı zamanda farklı dillerde çalışan sistemlerin ortak bir protokol üzerinden iletişim kurmasını sağlar.
Aşağıdaki örnek, Protobuf’un temel yaklaşımını ve yapısal sadeliğini daha yakından anlamak için iyi bir başlangıç noktası olacaktır.
// Hangi proto sürümünü kullanacağımızı belirtir.
syntax = "proto3";
// Paket adı: Bu paket user ile ilgili protobuf mesajlarını ve servislerini barındırır.
package user;
// Google protobuf kütüphanelerinden zaman damgası ve esnek veri tipi import ediliyor.
import "google/protobuf/timestamp.proto";
import "google/protobuf/any.proto";
// Go dili için otomatik kod üretiminde kullanılacak paket ayarı.
option go_package = "/.;pb";
// Kullanıcının sistemdeki rolünü belirtmek için enum tanımı.
enum Role {
ROLE_ADMIN = 0; // Yönetici rolü
ROLE_USER = 1; // Standart kullanıcı rolü
}
// Kullanıcı bilgisini temsil eden mesaj tipi.
message User {
string user_id = 1; // Kullanıcının benzersiz ID'si
string first_name = 2; // Kullanıcının adı
string last_name = 3; // Kullanıcının soyadı
string email = 4; // Kullanıcının e-posta adresi
string password = 5; // Kullanıcının şifresi
Role role = 6; // Kullanıcının rolü (admin veya user)
bool is_active = 7; // Kullanıcı aktif mi? true/false
google.protobuf.Timestamp created_at = 8; // Kullanıcının oluşturulma zamanı
google.protobuf.Timestamp updated_at = 9; // Son güncelleme zamanı
}
// Yeni kullanıcı oluşturma isteği için mesaj tipi.
message CreateUserRequest {
string first_name = 1; // Yeni kullanıcının adı
string last_name = 2; // Yeni kullanıcının soyadı
string email = 3; // Yeni kullanıcının e-posta adresi
string password = 4; // Yeni kullanıcının şifresi
Role role = 5; // Yeni kullanıcının rolü
bool is_active = 6; // Yeni kullanıcının aktiflik durumu
}
// Genel cevap mesajı: Her türlü servisten dönebilecek ortak cevap formatı.
message GenericResponse {
int64 status_code = 1; // İşlem sonucu durumu, HTTP kodlarına benzer (200, 404, 500 vb.)
string message = 2; // İşlem hakkında açıklayıcı mesaj
oneof data { // Döndürülebilecek veriler için one of yapısı.
User user = 3;
UserList user_list = 4;
}
}
// Kullanıcı işlemlerini yöneten servis arayüzü.
service UserService {
// Yeni kullanıcı oluşturmak için RPC metodu.
// Girdi olarak CreateUserRequest alır, GenericResponse döner.
rpc CreateUser(CreateUserRequest) returns (GenericResponse);
}
Yukarıda .proto dosyamızın temel yapısını ve içeriğini görebilirsiniz. Bu dosya, veri yapılarımızı ve servis tanımlarımızı sade ve anlaşılır bir şekilde ifade ediyor. Özellikle alanların numaralandırılması ve tip belirtilmesi, hem veri boyutunu küçültür hem de sürüm uyumluluğunu sağlar.
Proto Dosyalarından Otomatik Kod Üretimi ve Derleme Süreci
Bu güçlü tanımlama mekanizması gerçek dünyada nasıl devreye giriyor? İşte burada, .proto dosyalarından otomatik kod üretimi süreci sahneye çıkıyor. Protobuf, yazdığınız veri tanımlarına dayanarak hedeflediğiniz programlama dili için — örneğin Python, Go veya Java — otomatik olarak veri yapıları ve iletişim protokollerini içeren kodlar üretir.
Projelerde bu süreci daha da pratik hale getirmek için genellikle Makefile gibi otomasyon araçları tercih edilir. Aşağıda, user.proto dosyasından GoLang için çıktılar üreten basit bir Makefile hedefi görebilirsiniz:
# Protobuf kaynak dosyalarının bulunduğu dizin.
SOURCE=/usr/bin/protobuf-31.0/src
# .proto dosyasının bulunduğu klasör. Proje içindeki tanımlı protobuf dosyalarını burada tutuyoruz.
INPUT_DIRECTORY=./server/internal/proto
# Oluşacak Go dosyalarının çıkış dizini. Buraya otomatik olarak .pb.go dosyaları yazılır.
OUTPUT_DIRECTORY=./server/internal/pb
# İşlenecek hedef .proto dosyası.
INPUT_FILE=./server/internal/proto/user.proto
# Makefile hedefi: `make proto-out` komutu çalıştırıldığında aşağıdaki komut çalışır.
proto-out:
# protoc derleyicisini çalıştırıyoruz. -I flag'leri ile include dizinlerini belirtiyoruz.
# --go_out: Go için protobuf veri yapıları oluşturur.
# --go-grpc_out: Go için gRPC client ve server kodlarını üretir.
protoc -I $(SOURCE) -I $(INPUT_DIRECTORY) --go_out=$(OUTPUT_DIRECTORY) --go grpc_out=$(OUTPUT_DIRECTORY) $(INPUT_FILE)
Yukarıdaki Makefile hedefi, user.proto dosyasını Go diline çevirerek belirtilen dizine gerekli kaynak dosyaları üretir. Bu süreçte .protoc derleyicisi ile .proto dosyasında tanımlı veri yapıları (messages) ve servisler (services) Go programlama diline özgü yapı ve arayüzler hâline getirilir.
Oluşturulan ilk dosya (user.pb.go) User, CreateUserRequest, GenericResponse gibi mesaj tanımlarını Go Struct’larına dönüştürür. Aynı zamanda bu yapılar üzerinde serileştirme (marshal) ve çözümleme (unmarshal) işlemleri için gerekli yardımcı metotları da otomatik olarak içerir. Böylece uygulama, hem veriyi Protobuf formatında dışa aktarabilir hem de dışarıdan gelen veriyi çözüp yorumlayabilir.
İkinci olarak üretilen dosya (user_grpc.pb.go) ise .proto içerisinde tanımlı olan servisleri Go arayüzleri (interface) olarak kodlar. Örneğin UserService içinde tanımlı CreateUser gibi RPC çağrıları, bu dosyada doğrudan kullanılabilecek şekilde metod imzalarına dönüştürülür. Bu yapı sayesinde sunucu tarafı kendi iş mantığını bu arayüzleri implement ederek oluştururken, istemci tarafı ise doğrudan bu arayüz üzerinden uzak çağrılar gerçekleştirebilir.
Bu derleme yapısı, geliştiricinin yalnızca .proto dosyasında değişiklik yaparak uygulama genelinde tutarlı, güvenilir ve merkezi bir güncelleme yapmasına imkân tanır. .proto dosyasında yapılan her güncelleme, Makefile komutu çalıştırılarak Go koduna yeniden yansıtılır. Böylece hem zamandan tasarruf edilir, hem de manuel müdahale kaynaklı hatalar önlenmiş olur. Ayrıca tutarlı ve sürdürülebilir bir geliştirme ortamı elde edilir; kod tekrarı minimize edilirken sistemler arası uyum ve güvenlik üst düzeyde korunur.
Protobuf ve gRPC ile Etkili Kod Entegrasyonu
Oluşturulan user.pb.go ve user_grpc.pb.go dosyaları, sistemimizin temel veri yapıları ve servis arayüzlerini sağlar. Ancak gerçek dünya uygulamalarında, bu otomatik üretilen kodlar tek başına işlevsel değildir. Bu noktada servis katmanı (service layer) devreye girer.
Servis katmanı, Protobuf ile tanımlanan servis arayüzlerini implement eder ve iş mantığını barındırır. Örneğin, UserServiceImpl yapısı, gRPC tarafından oluşturulan UserServiceServer arayüzünü uygulayarak, CreateUser gibi metotları gerçek işlem kodlarıyla doldurur.
Burada önemli olan, servis katmanının doğrudan veritabanı işlemleriyle ilgilenmemesidir. Bunun yerine, veritabanı erişimi için oluşturulan repository katmanına (örneğin UserRepository) bağımlı olur. Bu katman, veri erişim sorumluluğunu üstlenir ve servis katmanını veri kaynağının detaylarından soyutlar.
Bu yapı sayesinde iş mantığı ve veri erişimi net bir biçimde ayrılır; böylece her iki katmanın sorumlulukları belirginleşir ve karmaşıklık azalır. Ayrıca, katmanların ayrılması test edilebilirliği artırır; iş mantığı, veri erişiminden bağımsız olarak kolayca izole edilip test edilebilir.
Aynı zamanda, Protobuf ve gRPC’nin sunduğu güçlü tip denetimi ve standart protokol sayesinde, servis ve istemci arasında iletişim tutarlı ve güvenli olur. Bu sayede, uygulama mimarisi hem katmanlı hem de protokole dayalı iletişimle desteklenmiş olur. Kodun sürdürülebilirliği sağlanırken gelişmiş hata yönetimi ve iş kurallarının uygulanması da mümkün hale gelir. Böylece uygulama, daha sağlam, esnek ve uzun vadede bakımı kolay bir yapıya kavuşur. Bu durumu, aşağıdaki örnekte görebileceğiniz üzere servis ve veri erişim katmanlarının net bir şekilde ayrılmasıyla elde etmek mümkündür.
package service
import (
"context"
"errors"
"net/http"
"github.com/AtahanPoyraz/server/internal/model"
"github.com/AtahanPoyraz/server/internal/model/enum"
"github.com/AtahanPoyraz/server/internal/pb"
"github.com/AtahanPoyraz/server/internal/repository"
"github.com/google/uuid"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
// UserServiceImpl, gRPC üzerinden gelen kullanıcı işlemlerini yöneten servis katmanıdır.
//
// Bu yapı, pb tarafından üretilen UnimplementedUserServiceServer arayüzünü embed eder ve
// UserRepository aracılığıyla veri işlemlerini gerçekleştirir.
// Her bir gRPC fonksiyon çağrısı burada karşılanarak iş mantığı uygulanır.
//
// Alanlar:
// - UnimplementedUserServiceServer: gRPC server arayüzünü uygular.
// - userRepository: Kullanıcı verileriyle ilgili veritabanı işlemlerini gerçekleştiren repository katmanı.
type UserServiceImpl struct {
pb.UnimplementedUserServiceServer
userRepository repository.UserRepository
}
// NewUserService, UserServiceImpl yapısını oluşturur ve pb.UserServiceServer arayüzünü döner.
//
// Bu fonksiyon dependency injection prensibine uygun şekilde UserRepository'yi parametre olarak alır
// ve UserServiceImpl içerisine enjekte eder. Böylece servis katmanı, veri erişim katmanıyla
// gevşek bağlı (loosely coupled) hale gelir.
//
// Parametreler:
// - userRepository: repository.UserRepository — kullanıcıya ait veritabanı işlemlerini gerçekleştiren yapı.
//
// Dönüş Değeri:
// - pb.UserServiceServer: gRPC server'a register edilebilecek UserService arayüzü.
func NewUserService(userRepository repository.UserRepository) pb.UserServiceServer {
return &UserServiceImpl{
userRepository: userRepository,
}
}
// CreateUser, sistemde yeni bir kullanıcı oluşturmak için kullanılır.
//
// Bu fonksiyon, gRPC üzerinden gelen CreateUserRequest isteğini alır ve userRepository üzerinden
// yeni kullanıcıyı veritabanına kaydeder. Eğer işlem başarılı olursa, HTTP 201 Created durum kodu ile
// bir başarı mesajı döner. Hata durumunda ise ilgili hata üst katmana iletilir.
//
// Parametreler:
// - ctx: İşlem sürecini ve iptali kontrol eden context.Context.
// - in: *pb.CreateUserRequest — Kullanıcının adı, soyadı, e-posta, şifresi, rolü ve aktiflik durumunu içerir.
//
// Dönüş Değeri:
// - *pb.GenericResponse: İşlem sonucu hakkında bilgi içeren yanıt (durum kodu, mesaj, veri).
// - error: Herhangi bir hata oluşursa döner.
func (s *UserServiceImpl) CreateUser(ctx context.Context, in *pb.CreateUserRequest) (*pb.GenericResponse, error) {
err := s.userRepository.CreateUser(in.FirstName, in.LastName, in.Email, in.Password, enum.Role(in.Role.String()), in.IsActive)
if err != nil {
return nil, err
}
return &pb.GenericResponse{
StatusCode: http.StatusCreated,
Message: "User created successfully",
Data: nil,
}, nil
}
Uygulama ayağa kalkarken, oluşturduğumuz UserServiceImpl yapısı, gRPC sunucusuna pb.RegisterUserServiceServer fonksiyonu aracılığıyla tanıtılır. Bu adım, servis katmanında tanımlanan iş mantığının, dış dünyadan gelen gRPC istekleriyle buluşmasını sağlar. Böylece servis mantığı ile ağ katmanı arasında net bir köprü kurulmuş olur ve sistem, gelen her bir kullanıcı isteğini doğru biçimde yönlendirebilir. Aşağıdaki kod örneğinde bu süreci açıkça görebilirsiniz:
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"os/signal"
"sync"
"syscall"
"github.com/AtahanPoyraz/server/config"
"github.com/AtahanPoyraz/server/config/database"
"github.com/AtahanPoyraz/server/internal/pb"
"github.com/AtahanPoyraz/server/internal/repository"
"github.com/AtahanPoyraz/server/internal/service"
"google.golang.org/grpc"
)
var (
appContext *config.AppContext
sigChan = make(chan os.Signal, 1)
errChan = make(chan error, 1)
)
func init() {
// Logger oluşturuluyor
logger := log.New(os.Stdout, "<Goobser> ", log.LstdFlags)
// gRPC sunucusu başlatılıyor
grpcServer := grpc.NewServer()
// Konfigürasyon yükleniyor
cfg, err := config.LoadConfig("config.yaml")
if err != nil {
logger.Fatalf("Failed to load configuration: %v", err)
}
// Veritabanı bağlantısı kuruluyor
db, err := database.SetConfiguration(cfg)
if err != nil {
logger.Fatalf("Failed to connect database: %v", err)
}
// Uygulama bağlamı oluşturuluyor
appContext = config.NewAppContext(logger, grpcServer, cfg, db)
// Repository ve Service katmanları kuruluyor
userRepository := repository.NewUserRepository(appContext.Database)
userService := service.NewUserService(userRepository)
// gRPC servisi kaydediliyor
pb.RegisterUserServiceServer(appContext.Server, userService)
// Sistem sinyalleri dinleniyor
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
// Sunucu başlatma işlemi
wg.Add(1)
go func() {
defer wg.Done()
addr := fmt.Sprintf("%s:%d", appContext.Configuration.Server.Host, appContext.Configuration.Server.Port)
listener, err := net.Listen("tcp", addr)
if err != nil {
errChan <- fmt.Errorf("failed to listen on %s: %w", addr, err)
return
}
appContext.Logger.Printf("gRPC server listening on %s", addr)
if err := appContext.Server.Serve(listener); err != nil {
errChan <- fmt.Errorf("gRPC server failed: %w", err)
}
}()
// Kapatma ve hata yönetimi
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-ctx.Done():
appContext.Logger.Println("Context cancelled. Shutting down...")
appContext.Server.GracefulStop()
case sig := <-sigChan:
appContext.Logger.Printf("Received signal: %v. Shutting down gracefully...", sig)
appContext.Server.GracefulStop()
case err := <-errChan:
appContext.Logger.Fatalf("Fatal error: %v", err)
}
}()
wg.Wait()
}
Bu yapı sayesinde, uygulama başlatıldığında servis katmanında tanımlanan fonksiyonlar otomatik olarak gRPC sunucusuna bağlanır. Böylece dışarıdan gelen her CreateUser veya benzeri istek, doğrudan UserServiceImpl içinde tanımlanan iş mantığına yönlendirilir.
Ayrıca bu kurulum, uygulamanın genişletilebilirliğini de destekler. Örneğin, sisteme yeni servisler eklemek ya da mevcut servislerde değişiklik yapmak oldukça kolay ve güvenlidir. Yapılan her değişiklik sadece ilgili katmana dokunur; bu da mimarinin esnekliğini ve sürdürülebilirliğini artırır.
Bu noktada artık sistem, yalnızca gRPC çağrılarını alıp cevaplayabilen bir yapı olmaktan çıkıp, açıkça tanımlanmış sorumluluklara sahip, katmanlı ve test edilebilir bir mimariye dönüşmüş olur.
Sonuç
Protobuf ve gRPC kullanılarak oluşturulan katmanlı mimari, modern uygulamalarda sağlamlık, esneklik ve sürdürülebilirlik sağlayabilir. Protobuf ile tanımlanan açık ve net veri yapıları, gRPC’nin etkin iletişim mekanizması ve iyi yapılandırılmış çok katmanlı yapılar sayesinde, karmaşık sistemler bile kolayca yönetilebilir hale gelir.
gRPC ve Protobuf kombinasyonu, mikroservis mimarilerinde ve yüksek performans gerektiren uygulamalarda güçlü ve esnek bir çözüm sunar. Yüksek verimlilik, tip güvenliği ve çoklu dil desteği gibi avantajları sayesinde projeleriniz daha modüler, ölçeklenebilir ve bakımı kolay olur.
Kendi deneyimlerimizden yola çıkarak, bu teknolojileri öğrenmenin ve uygulamanın özellikle backend servisleri geliştirirken büyük fark yarattığını söyleyebilirim.
Eğer siz de performans, güvenilirlik ve kolay entegrasyon arıyorsanız, gRPC ile Protobuf kesinlikle değerlendirmeniz gereken teknolojiler arasındadır.






Eline sağlık.