Proxy Pattern #
Ada sebuah ReportService yang menghasilkan laporan keuangan — setiap kali dipanggil, ia membaca ribuan baris data dari database, melakukan agregasi kompleks, dan menghasilkan PDF besar. Prosesnya memakan waktu 8 detik. Tiga masalah muncul sekaligus: pertama, tidak semua user seharusnya bisa mengakses laporan ini — hanya role finance_admin. Kedua, laporan yang sama diminta berulang kali dalam satu sesi sehingga seharusnya bisa di-cache. Ketiga, setiap akses perlu di-log untuk keperluan audit regulasi. Mengubah ReportService langsung untuk menangani semua concern ini akan mencampur logika bisnis dengan logika infrastruktur. Proxy Pattern memberikan solusi yang lebih bersih: bungkus ReportService dalam satu atau beberapa proxy — masing-masing menangani satu concern — sementara ReportService tetap fokus pada tugasnya: menghasilkan laporan.
Apa itu Proxy Pattern? #
Proxy Pattern adalah structural design pattern yang menyediakan objek pengganti yang mengontrol akses ke objek asli. Proxy mengimplementasikan interface yang sama dengan objek yang diwakilnya, sehingga client tidak perlu tahu apakah ia sedang berbicara dengan objek asli atau proxy.
Perbedaan kunci Proxy dari pattern lain yang juga “membungkus” objek: Proxy berfokus pada kontrol akses dan manajemen lifecycle, bukan pada penambahan behavior fungsional. Decorator menambahkan fitur baru; Proxy mengontrol, melindungi, atau mengoptimalkan akses ke fitur yang sudah ada.
Lima alasan utama menggunakan Proxy:
- Virtual Proxy — menunda pembuatan objek mahal sampai benar-benar dibutuhkan (lazy initialization)
- Protection Proxy — mengontrol siapa yang boleh mengakses objek berdasarkan otorisasi
- Caching Proxy — menyimpan hasil operasi mahal agar tidak diulang untuk input yang sama
- Remote Proxy — merepresentasikan objek yang berada di sistem lain (network, microservice)
- Logging/Monitoring Proxy — mencatat semua akses untuk audit, debugging, atau observability
flowchart LR
C[Client] -->|interface yang sama| P[Proxy]
P -->|setelah kontrol & validasi| R[Real Subject]
subgraph "Yang Proxy Lakukan Sebelum/Sesudah"
direction TB
P1["✓ Cek otorisasi"]
P2["✓ Periksa cache"]
P3["✓ Log request"]
P4["✓ Lazy-init real subject"]
P5["✓ Rate limiting"]
end
Lima Jenis Proxy dan Use Case-nya #
Setiap jenis proxy menyelesaikan masalah yang berbeda. Memahami keempatnya membantu kamu memilih jenis yang tepat untuk situasi yang dihadapi.
flowchart TD
Q{"Masalah apa\nyang ingin\ndipecahkan?"}
Q -->|"Objek mahal dibuat,\ntapi mungkin tidak dipakai"| VP[Virtual Proxy\nLazy initialization]
Q -->|"Tidak semua user\nboleh akses"| PP[Protection Proxy\nOtorisasi & auth]
Q -->|"Hasil komputasi\nmahal dan berulang"| CP[Caching Proxy\nMemoization]
Q -->|"Objek ada di\nsistem lain"| RP[Remote Proxy\nNetwork abstraction]
Q -->|"Perlu tahu siapa\nmengakses apa & kapan"| LP[Logging Proxy\nAudit & observability]
Virtual Proxy: Lazy Initialization #
Objek seperti koneksi database, image editor, atau PDF renderer mahal untuk dibuat. Jika ada kemungkinan objek tidak akan digunakan dalam sesi tersebut, menundanya sampai pertama kali dibutuhkan bisa menghemat resource secara signifikan.
// ANTI-PATTERN: selalu buat objek mahal saat startup, meski mungkin tidak dipakai
type App struct {
reportGenerator *HeavyReportGenerator // ~3 detik untuk diinisialisasi
pdfRenderer *PDFRenderer // ~2 detik untuk diinisialisasi
// keduanya selalu dibuat, meski user mungkin tidak pernah generate report
}
// BENAR: Virtual Proxy — buat hanya saat pertama kali dibutuhkan
type LazyReportProxy struct {
real *HeavyReportGenerator
mu sync.Mutex
config ReportConfig
}
func (p *LazyReportProxy) Generate(params ReportParams) (*Report, error) {
p.mu.Lock()
if p.real == nil {
// Inisialisasi dilakukan di sini, bukan di startup
var err error
p.real, err = NewHeavyReportGenerator(p.config)
if err != nil {
p.mu.Unlock()
return nil, fmt.Errorf("failed to initialize report generator: %w", err)
}
}
p.mu.Unlock()
return p.real.Generate(params)
}
Protection Proxy: Otorisasi #
Protection Proxy mengecek apakah pemanggil memiliki izin yang cukup sebelum mendelegasikan ke objek asli. Ini memisahkan logika otorisasi dari logika bisnis.
// Protection Proxy memastikan hanya role yang tepat yang bisa mengakses
type AuthorizedReportProxy struct {
real ReportService
authSvc AuthorizationService
}
func (p *AuthorizedReportProxy) Generate(ctx context.Context, params ReportParams) (*Report, error) {
userID := getUserIDFromContext(ctx)
if err := p.authSvc.CheckPermission(ctx, userID, "report:generate"); err != nil {
return nil, fmt.Errorf("access denied: %w", err)
}
return p.real.Generate(ctx, params)
}
Implementasi Lengkap: Report Service dengan Stack Proxy #
Mari bangun ReportService yang dilindungi oleh tiga lapis proxy: otorisasi, caching, dan logging — masing-masing sebagai proxy terpisah yang bisa dikombinasikan.
Subject Interface #
package report
import (
"context"
"time"
)
// Report merepresentasikan laporan yang dihasilkan.
type Report struct {
ID string
Title string
GeneratedAt time.Time
DataPoints int
Content []byte // isi PDF atau HTML
CacheKey string
}
// ReportParams mendefinisikan parameter untuk menghasilkan laporan.
type ReportParams struct {
Type string // "financial", "inventory", "user_activity"
StartDate time.Time
EndDate time.Time
Format string // "pdf", "xlsx", "html"
Filters map[string]string
}
// ReportService adalah Subject interface — kontrak yang diimplementasikan
// oleh real service dan semua proxy-nya.
type ReportService interface {
Generate(ctx context.Context, params ReportParams) (*Report, error)
GetByID(ctx context.Context, reportID string) (*Report, error)
ListAvailable(ctx context.Context, userID string) ([]ReportMeta, error)
}
// ReportMeta menyimpan metadata laporan tanpa isi penuh.
type ReportMeta struct {
ID string
Title string
Type string
GeneratedAt time.Time
SizeBytes int64
}
Real Subject: ReportGenerator #
package report
import (
"context"
"database/sql"
"fmt"
"time"
)
// ReportGenerator adalah real subject — menghasilkan laporan sesungguhnya.
// Tidak ada logging, caching, atau otorisasi di sini.
type ReportGenerator struct {
db *sql.DB
template TemplateEngine
}
func NewReportGenerator(db *sql.DB, template TemplateEngine) ReportService {
return &ReportGenerator{db: db, template: template}
}
func (g *ReportGenerator) Generate(ctx context.Context, params ReportParams) (*Report, error) {
// Simulasi proses mahal: query data, agregasi, render
rows, err := g.db.QueryContext(ctx, buildQuery(params))
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
data, err := aggregateRows(rows)
if err != nil {
return nil, fmt.Errorf("aggregation failed: %w", err)
}
content, err := g.template.Render(params.Format, data)
if err != nil {
return nil, fmt.Errorf("render failed: %w", err)
}
return &Report{
ID: generateReportID(),
Title: fmt.Sprintf("%s Report (%s)", params.Type, params.StartDate.Format("2006-01")),
GeneratedAt: time.Now(),
DataPoints: len(data),
Content: content,
CacheKey: buildCacheKey(params),
}, nil
}
func (g *ReportGenerator) GetByID(ctx context.Context, reportID string) (*Report, error) {
// Query ke database untuk mengambil laporan yang sudah tersimpan
return nil, nil // simplified
}
func (g *ReportGenerator) ListAvailable(ctx context.Context, userID string) ([]ReportMeta, error) {
return nil, nil // simplified
}
func buildQuery(params ReportParams) string {
return fmt.Sprintf("SELECT * FROM transactions WHERE date BETWEEN '%s' AND '%s'",
params.StartDate.Format("2006-01-02"), params.EndDate.Format("2006-01-02"))
}
func buildCacheKey(params ReportParams) string {
return fmt.Sprintf("%s:%s:%s:%s",
params.Type,
params.StartDate.Format("20060102"),
params.EndDate.Format("20060102"),
params.Format)
}
func generateReportID() string {
return fmt.Sprintf("RPT-%d", time.Now().UnixNano())
}
Proxy 1: Protection Proxy #
package report
import (
"context"
"fmt"
)
// AuthorizationService mendefinisikan kontrak untuk pengecekan otorisasi.
type AuthorizationService interface {
CheckPermission(ctx context.Context, userID, permission string) error
GetUserRoles(ctx context.Context, userID string) ([]string, error)
}
// ProtectionProxy mengontrol akses berdasarkan otorisasi user.
// Selalu menjadi lapisan terluar — tidak ada gunanya caching atau logging
// jika user tidak punya akses.
type ProtectionProxy struct {
real ReportService
authSvc AuthorizationService
}
func NewProtectionProxy(real ReportService, authSvc AuthorizationService) ReportService {
return &ProtectionProxy{real: real, authSvc: authSvc}
}
func (p *ProtectionProxy) Generate(ctx context.Context, params ReportParams) (*Report, error) {
userID := getUserIDFromContext(ctx)
// Cek permission umum untuk generate report
if err := p.authSvc.CheckPermission(ctx, userID, "report:generate"); err != nil {
return nil, fmt.Errorf("permission denied: %w", err)
}
// Cek permission spesifik per tipe laporan
specificPerm := fmt.Sprintf("report:generate:%s", params.Type)
if err := p.authSvc.CheckPermission(ctx, userID, specificPerm); err != nil {
return nil, fmt.Errorf("permission denied for %s report: %w", params.Type, err)
}
return p.real.Generate(ctx, params)
}
func (p *ProtectionProxy) GetByID(ctx context.Context, reportID string) (*Report, error) {
userID := getUserIDFromContext(ctx)
if err := p.authSvc.CheckPermission(ctx, userID, "report:read"); err != nil {
return nil, fmt.Errorf("permission denied: %w", err)
}
return p.real.GetByID(ctx, reportID)
}
func (p *ProtectionProxy) ListAvailable(ctx context.Context, userID string) ([]ReportMeta, error) {
// Untuk list, filter berdasarkan role user
roles, err := p.authSvc.GetUserRoles(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to get user roles: %w", err)
}
allReports, err := p.real.ListAvailable(ctx, userID)
if err != nil {
return nil, err
}
return filterByRoles(allReports, roles), nil
}
func filterByRoles(reports []ReportMeta, roles []string) []ReportMeta {
// Filter laporan yang boleh dilihat berdasarkan role
return reports // simplified — implementasi nyata akan filter
}
func getUserIDFromContext(ctx context.Context) string {
if id, ok := ctx.Value("user_id").(string); ok {
return id
}
return ""
}
Proxy 2: Caching Proxy #
package report
import (
"context"
"fmt"
"sync"
"time"
)
// CacheEntry menyimpan laporan yang di-cache beserta waktu kadaluarsanya.
type CacheEntry struct {
report *Report
expiresAt time.Time
}
func (e *CacheEntry) isExpired() bool {
return time.Now().After(e.expiresAt)
}
// CachingProxy menyimpan hasil Generate agar tidak diulang untuk parameter yang sama.
// Laporan yang sama (tipe + periode + format) tidak perlu di-generate dua kali.
type CachingProxy struct {
real ReportService
mu sync.RWMutex
cache map[string]*CacheEntry
ttl time.Duration
stats struct {
hits int64
misses int64
}
}
func NewCachingProxy(real ReportService, ttl time.Duration) ReportService {
return &CachingProxy{
real: real,
cache: make(map[string]*CacheEntry),
ttl: ttl,
}
}
func (p *CachingProxy) Generate(ctx context.Context, params ReportParams) (*Report, error) {
cacheKey := buildCacheKey(params)
// Check cache — optimistic read dengan RLock
p.mu.RLock()
if entry, ok := p.cache[cacheKey]; ok && !entry.isExpired() {
p.mu.RUnlock()
p.stats.hits++
fmt.Printf("[CachingProxy] Cache HIT for key: %s\n", cacheKey)
return entry.report, nil
}
p.mu.RUnlock()
// Cache miss — generate laporan baru
p.stats.misses++
fmt.Printf("[CachingProxy] Cache MISS for key: %s — generating...\n", cacheKey)
report, err := p.real.Generate(ctx, params)
if err != nil {
return nil, err
}
// Simpan ke cache
p.mu.Lock()
p.cache[cacheKey] = &CacheEntry{
report: report,
expiresAt: time.Now().Add(p.ttl),
}
p.mu.Unlock()
return report, nil
}
func (p *CachingProxy) GetByID(ctx context.Context, reportID string) (*Report, error) {
// GetByID tidak di-cache karena report bisa diupdate
return p.real.GetByID(ctx, reportID)
}
func (p *CachingProxy) ListAvailable(ctx context.Context, userID string) ([]ReportMeta, error) {
// ListAvailable di-cache per user dengan TTL pendek
cacheKey := fmt.Sprintf("list:%s", userID)
p.mu.RLock()
if entry, ok := p.cache[cacheKey]; ok && !entry.isExpired() {
p.mu.RUnlock()
// Simplified: dalam nyatanya CacheEntry perlu menyimpan []ReportMeta juga
return nil, nil
}
p.mu.RUnlock()
return p.real.ListAvailable(ctx, userID)
}
// Invalidate menghapus entri cache tertentu.
// Dipanggil ketika data yang mempengaruhi laporan berubah.
func (p *CachingProxy) Invalidate(pattern string) {
p.mu.Lock()
defer p.mu.Unlock()
for key := range p.cache {
if matchesPattern(key, pattern) {
delete(p.cache, key)
}
}
}
// CacheStats mengembalikan statistik cache untuk monitoring.
func (p *CachingProxy) CacheStats() (hits, misses int64, size int) {
p.mu.RLock()
defer p.mu.RUnlock()
return p.stats.hits, p.stats.misses, len(p.cache)
}
func matchesPattern(key, pattern string) bool {
return len(pattern) == 0 || key[:min(len(pattern), len(key))] == pattern
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
Proxy 3: Logging Proxy #
package report
import (
"context"
"fmt"
"log/slog"
"time"
)
// AuditEvent merepresentasikan satu kejadian akses untuk keperluan audit.
type AuditEvent struct {
UserID string
Action string
ReportType string
Success bool
Duration time.Duration
Error string
Timestamp time.Time
}
// AuditStore menyimpan audit events.
type AuditStore interface {
Record(event AuditEvent) error
}
// LoggingProxy mencatat semua akses ke ReportService untuk keperluan audit.
// Wajib ada di sistem yang tunduk pada regulasi (GDPR, SOX, PCI-DSS).
type LoggingProxy struct {
real ReportService
logger *slog.Logger
auditStore AuditStore
}
func NewLoggingProxy(real ReportService, logger *slog.Logger, audit AuditStore) ReportService {
return &LoggingProxy{real: real, logger: logger, auditStore: audit}
}
func (p *LoggingProxy) Generate(ctx context.Context, params ReportParams) (*Report, error) {
userID := getUserIDFromContext(ctx)
start := time.Now()
p.logger.InfoContext(ctx, "report generation requested",
"user_id", userID,
"report_type", params.Type,
"period", fmt.Sprintf("%s to %s",
params.StartDate.Format("2006-01-02"),
params.EndDate.Format("2006-01-02")),
"format", params.Format,
)
report, err := p.real.Generate(ctx, params)
duration := time.Since(start)
event := AuditEvent{
UserID: userID,
Action: "report:generate",
ReportType: params.Type,
Success: err == nil,
Duration: duration,
Timestamp: time.Now(),
}
if err != nil {
event.Error = err.Error()
p.logger.ErrorContext(ctx, "report generation failed",
"user_id", userID,
"report_type", params.Type,
"duration_ms", duration.Milliseconds(),
"error", err,
)
} else {
p.logger.InfoContext(ctx, "report generation succeeded",
"user_id", userID,
"report_id", report.ID,
"report_type", params.Type,
"data_points", report.DataPoints,
"duration_ms", duration.Milliseconds(),
)
}
// Audit log — non-fatal jika gagal
if auditErr := p.auditStore.Record(event); auditErr != nil {
p.logger.WarnContext(ctx, "failed to record audit event", "error", auditErr)
}
return report, err
}
func (p *LoggingProxy) GetByID(ctx context.Context, reportID string) (*Report, error) {
userID := getUserIDFromContext(ctx)
p.logger.InfoContext(ctx, "report access", "user_id", userID, "report_id", reportID)
report, err := p.real.GetByID(ctx, reportID)
_ = p.auditStore.Record(AuditEvent{
UserID: userID,
Action: "report:read",
Success: err == nil,
Timestamp: time.Now(),
})
return report, err
}
func (p *LoggingProxy) ListAvailable(ctx context.Context, userID string) ([]ReportMeta, error) {
return p.real.ListAvailable(ctx, userID)
}
Perakitan: Stack Proxy #
func NewReportServiceStack(
db *sql.DB,
template TemplateEngine,
authSvc AuthorizationService,
logger *slog.Logger,
auditStore AuditStore,
cacheTTL time.Duration,
) ReportService {
// Lapisan terdalam: real implementation
real := NewReportGenerator(db, template)
// Lapisan caching — langsung di atas real service
cached := NewCachingProxy(real, cacheTTL)
// Lapisan logging — di atas caching
// (log akan menunjukkan apakah ini cache hit atau miss)
logged := NewLoggingProxy(cached, logger, auditStore)
// Lapisan otorisasi — paling luar
// (tidak perlu cache, log, atau hit real service jika user tidak berhak)
authorized := NewProtectionProxy(logged, authSvc)
return authorized
}
// Penggunaan
func main() {
svc := NewReportServiceStack(db, tmpl, authSvc, logger, audit, 30*time.Minute)
ctx := context.WithValue(context.Background(), "user_id", "user-123")
// Client hanya tahu interface ReportService — tidak tahu ada 3 proxy di baliknya
report, err := svc.Generate(ctx, ReportParams{
Type: "financial",
StartDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
EndDate: time.Date(2024, 3, 31, 0, 0, 0, 0, time.UTC),
Format: "pdf",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Report generated: %s\n", report.ID)
}
Remote Proxy: Mengabstraksi Network #
Remote Proxy merepresentasikan objek yang ada di service lain — client berinteraksi seolah objek ada di lokal, padahal semua operasi melewati network.
// UserServiceClient adalah Remote Proxy untuk UserService yang ada di microservice lain.
// Client code menggunakan interface yang sama seolah service ada di lokal.
type UserServiceClient struct {
baseURL string
httpClient *http.Client
timeout time.Duration
}
func NewUserServiceClient(baseURL string, timeout time.Duration) UserService {
return &UserServiceClient{
baseURL: baseURL,
httpClient: &http.Client{Timeout: timeout},
timeout: timeout,
}
}
func (c *UserServiceClient) FindByID(ctx context.Context, id int) (*User, error) {
url := fmt.Sprintf("%s/users/%d", c.baseURL, id)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("remote call failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("user %d not found", id)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &user, nil
}
func (c *UserServiceClient) FindByEmail(ctx context.Context, email string) (*User, error) {
url := fmt.Sprintf("%s/users?email=%s", c.baseURL, email)
// ... implementasi serupa
return nil, nil
}
// Client tidak tahu apakah UserService ini lokal atau remote
func processUser(svc UserService, id int) {
user, err := svc.FindByID(context.Background(), id)
// sama persis untuk lokal atau remote
}
Virtual Proxy dengan sync.Once #
Untuk lazy initialization yang thread-safe, kombinasi Proxy dengan sync.Once adalah idiom yang sangat umum di Golang.
// LazyPDFRenderer adalah Virtual Proxy untuk PDF renderer yang mahal.
// Renderer hanya dibuat saat pertama kali Generate dipanggil.
type LazyPDFRenderer struct {
once sync.Once
renderer PDFRenderer
config PDFConfig
initErr error
}
func NewLazyPDFRenderer(config PDFConfig) PDFRenderer {
return &LazyPDFRenderer{config: config}
}
func (l *LazyPDFRenderer) Generate(content []byte, options RenderOptions) ([]byte, error) {
l.once.Do(func() {
// Inisialisasi mahal — hanya terjadi satu kali
l.renderer, l.initErr = initializePDFEngine(l.config)
if l.initErr != nil {
return
}
l.renderer.LoadFonts(l.config.FontPaths)
l.renderer.SetQuality(l.config.Quality)
})
if l.initErr != nil {
return nil, fmt.Errorf("PDF engine initialization failed: %w", l.initErr)
}
return l.renderer.Generate(content, options)
}
func (l *LazyPDFRenderer) Close() error {
if l.renderer != nil {
return l.renderer.Close()
}
return nil
}
Proxy di Ekosistem Golang #
Proxy Pattern muncul secara natural di banyak bagian ekosistem Golang, meski jarang disebut secara eksplisit.
gRPC Unary Interceptor sebagai Proxy #
// gRPC interceptor adalah Proxy untuk semua RPC call
func AuthInterceptor(authSvc AuthService) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Protection Proxy: cek token sebelum forward ke handler
token := extractToken(ctx)
if err := authSvc.ValidateToken(ctx, token); err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
return handler(ctx, req)
}
}
func LoggingInterceptor(logger *slog.Logger) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Logging Proxy: log sebelum dan sesudah handler
start := time.Now()
resp, err := handler(ctx, req)
logger.InfoContext(ctx, "gRPC call",
"method", info.FullMethod,
"duration_ms", time.Since(start).Milliseconds(),
"error", err,
)
return resp, err
}
}
// Rangkai interceptor — sama seperti merangkai proxy
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
LoggingInterceptor(logger), // outer: log dulu
AuthInterceptor(authSvc), // inner: cek auth
),
)
http.RoundTripper sebagai Proxy
#
// RetryTransport adalah Proxy untuk http.RoundTripper dengan retry logic
type RetryTransport struct {
wrapped http.RoundTripper
maxRetries int
retryDelay time.Duration
}
func NewRetryTransport(wrapped http.RoundTripper, maxRetries int) *RetryTransport {
return &RetryTransport{
wrapped: wrapped,
maxRetries: maxRetries,
retryDelay: 500 * time.Millisecond,
}
}
func (t *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var (
resp *http.Response
err error
)
for attempt := 0; attempt <= t.maxRetries; attempt++ {
resp, err = t.wrapped.RoundTrip(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil // sukses atau client error — tidak perlu retry
}
if attempt < t.maxRetries {
time.Sleep(t.retryDelay * time.Duration(attempt+1))
}
}
return resp, err
}
// Penggunaan
client := &http.Client{
Transport: NewRetryTransport(http.DefaultTransport, 3),
}
Testing Proxy #
// MockReportService untuk testing proxy secara terisolasi
type MockReportService struct {
GenerateFn func(ctx context.Context, params ReportParams) (*Report, error)
GetByIDFn func(ctx context.Context, id string) (*Report, error)
GenerateCount int
}
func (m *MockReportService) Generate(ctx context.Context, params ReportParams) (*Report, error) {
m.GenerateCount++
if m.GenerateFn != nil {
return m.GenerateFn(ctx, params)
}
return &Report{ID: "mock-001", Title: "Mock Report", CacheKey: buildCacheKey(params)}, nil
}
func (m *MockReportService) GetByID(ctx context.Context, id string) (*Report, error) {
if m.GetByIDFn != nil {
return m.GetByIDFn(ctx, id)
}
return &Report{ID: id}, nil
}
func (m *MockReportService) ListAvailable(ctx context.Context, userID string) ([]ReportMeta, error) {
return nil, nil
}
func TestCachingProxy_DoesNotCallRealServiceOnCacheHit(t *testing.T) {
mock := &MockReportService{}
proxy := NewCachingProxy(mock, 5*time.Minute)
ctx := context.Background()
params := ReportParams{Type: "financial", Format: "pdf",
StartDate: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
EndDate: time.Date(2024, 3, 31, 0, 0, 0, 0, time.UTC),
}
// Panggil pertama — cache miss
_, err := proxy.Generate(ctx, params)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mock.GenerateCount != 1 {
t.Errorf("expected 1 call on first request, got %d", mock.GenerateCount)
}
// Panggil kedua — harus cache hit, real service tidak dipanggil lagi
_, err = proxy.Generate(ctx, params)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mock.GenerateCount != 1 {
t.Errorf("expected real service not called on cache hit, got %d calls", mock.GenerateCount)
}
}
func TestProtectionProxy_DeniesUnauthorizedAccess(t *testing.T) {
mock := &MockReportService{}
mockAuth := &MockAuthService{
CheckPermissionFn: func(ctx context.Context, userID, perm string) error {
return fmt.Errorf("permission denied")
},
}
proxy := NewProtectionProxy(mock, mockAuth)
ctx := context.WithValue(context.Background(), "user_id", "unauthorized-user")
_, err := proxy.Generate(ctx, ReportParams{Type: "financial"})
if err == nil {
t.Error("expected error for unauthorized access, got nil")
}
if mock.GenerateCount != 0 {
t.Errorf("real service should not be called when access is denied, got %d calls", mock.GenerateCount)
}
}
func TestLoggingProxy_LogsOnSuccess(t *testing.T) {
mock := &MockReportService{}
var logBuf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&logBuf, nil))
auditStore := &MockAuditStore{}
proxy := NewLoggingProxy(mock, logger, auditStore)
ctx := context.WithValue(context.Background(), "user_id", "user-123")
_, _ = proxy.Generate(ctx, ReportParams{Type: "financial", Format: "pdf"})
if !strings.Contains(logBuf.String(), "report generation succeeded") {
t.Errorf("expected success log, got: %s", logBuf.String())
}
if len(auditStore.Events) != 1 {
t.Errorf("expected 1 audit event, got %d", len(auditStore.Events))
}
}
Proxy vs Decorator: Kapan Mana #
Keduanya mengimplementasikan interface yang sama dan membungkus objek lain — perbedaannya ada di niat desain dan cara penggunaan.
| Aspek | Proxy | Decorator |
|---|---|---|
| Niat utama | Kontrol akses, lifecycle, representasi | Tambah behavior fungsional |
| Dibuat oleh | System/infrastructure — client tidak memilih | Client — memilih decorator yang diinginkan |
| Jumlah lapisan | Biasanya satu proxy per concern | Bisa banyak layer bertingkat bebas |
| Tahu wrapped object? | Proxy mengelola lifecycle wrapped object | Decorator menerima wrapped object dari luar |
| Contoh | Auth proxy, cache proxy, lazy proxy | Logging decorator, retry decorator |
// Proxy: sistem yang menentukan apakah objek asli dibuat atau tidak
type LazyProxy struct {
real *HeavyService // proxy mengelola lifecycle ini
once sync.Once
}
// Client tidak bisa bypass — proxy mengontrol akses ke real object
// Decorator: client memilih dan merangkai
type LoggingDecorator struct {
next HeavyService // disuplai dari luar oleh client
}
// Client bisa pakai real object langsung tanpa decorator
Kapan Menggunakan dan Kapan Tidak #
GUNAKAN Proxy jika:
✓ Perlu lazy initialization — objek mahal yang mungkin tidak digunakan
✓ Perlu otorisasi sebelum mengizinkan akses ke objek
✓ Ingin caching hasil operasi mahal yang deterministik
✓ Perlu merepresentasikan objek di sistem remote (microservice, API)
✓ Perlu audit log untuk setiap akses tanpa mengubah objek asli
HINDARI Proxy jika:
✗ Hanya butuh menambah fitur fungsional — gunakan Decorator
✗ Satu proxy menangani terlalu banyak concern — pecah menjadi proxy terpisah
✗ Objek yang diproksikan sangat sederhana — overhead tidak sepadan
✗ Proxy berisi business logic — proxy seharusnya tipis dan fokus
Checklist Review Proxy #
DESAIN:
□ Proxy mengimplementasikan interface yang sama persis dengan real subject
□ Setiap proxy menangani satu concern (Single Responsibility)
□ Urutan proxy dalam stack sudah dipertimbangkan: auth → log → cache → real
□ Proxy tidak berisi business logic
IMPLEMENTASI:
□ Caching proxy menggunakan mutex untuk akses concurrent yang aman
□ Protection proxy selalu memeriksa izin sebelum mendelegasikan
□ Logging proxy tidak menggagalkan operasi meski logging gagal
□ Virtual proxy menggunakan sync.Once untuk thread-safe lazy initialization
□ Semua proxy meneruskan context ke real subject
TESTING:
□ Setiap proxy di-test secara terpisah dengan mock real subject
□ Test cache hit dan cache miss untuk caching proxy
□ Test akses ditolak dan diizinkan untuk protection proxy
□ Test bahwa real subject tidak dipanggil ketika akses ditolak
□ Test audit log dicatat meski operasi gagal (untuk logging proxy)
Ringkasan #
- Proxy menyediakan pengganti yang mengontrol akses ke objek asli — client menggunakan interface yang sama, tidak tahu ada proxy di antara mereka.
- Lima jenis proxy: Virtual (lazy init), Protection (otorisasi), Caching (memoization), Remote (network abstraction), dan Logging (audit/observability) — masing-masing untuk masalah yang berbeda.
- Urutan proxy penting: Protection harus paling luar (tidak ada gunanya caching jika user tidak berhak), diikuti Logging, kemudian Caching, lalu Real Subject.
- Bedakan dari Decorator: Proxy untuk kontrol dan manajemen lifecycle; Decorator untuk penambahan behavior fungsional. Niatnya berbeda meski struktur kodenya mirip.
- gRPC interceptor dan http.RoundTripper adalah implementasi Proxy Pattern yang sudah built-in di Golang — kamu sudah menggunakannya tanpa menyadarinya.
- Caching proxy harus thread-safe — gunakan
sync.RWMutexdengan double-check untuk mencegah race condition.- Virtual proxy dengan
sync.Onceadalah kombinasi idiomatik Golang untuk lazy initialization yang aman dari race condition.- Proxy harus tipis — jika proxy mulai berisi business logic, itu sinyal bahwa logika tersebut seharusnya ada di real subject atau di service layer.