Design Patterns dengan Go — Part 12: Proxy

Pahami design pattern Proxy di Golang. Pelajari cara mengontrol akses, mengimplementasikan rate limiting, caching, dan lazy initialization menggunakan proxy.

Design Patterns dengan Go — Part 12: Proxy

Dalam arsitektur aplikasi modern, mengekspos sistem inti atau sumber daya berat secara langsung ke konsumen luar sering kali tidak direkomendasikan. Kita mungkin perlu menerapkan kontrol akses, melakukan caching, mencatat riwayat log, atau membatasi jumlah permintaan (rate limit) sebelum semuanya mencapai layanan utama kita.

Proxy Design Pattern adalah structural design pattern yang bertindak sebagai pengganti (surrogate) atau tempat penampung (placeholder) bagi objek lain. Proxy bertugas mengontrol akses ke objek aslinya, memungkinkan Anda menjalankan suatu logika sebelum atau sesudah permintaan diteruskan ke objek utama.


Analogi Konseptual: Gerbang Keamanan VIP

Bayangkan pintu masuk ke brankas bank atau klub VIP. Pengunjung tidak bisa langsung melenggang masuk untuk mengambil uang atau menemui tamu VIP. Mereka harus berinteraksi terlebih dahulu dengan teller bank atau penjaga keamanan di pintu depan.

Penjaga keamanan tersebut akan:

  • Memeriksa identitas atau kartu akses pengunjung (authorization).
  • Menolak masuk jika kuota ruangan sudah penuh (rate-limiting).
  • Mencatat siapa saja yang masuk dan keluar (logging/auditing).
  • Membagikan brosur informasi instan alih-alih memanggil manajer (caching).

Hanya setelah melewati gerbang pemeriksaan inilah pengunjung diperbolehkan mengakses brankas atau layanan utama. Di sini, penjaga keamanan bertindak sebagai Proxy bagi objek/layanan aslinya.


Diagram Konseptual

Berikut adalah diagram Mermaid untuk Proxy pattern, menunjukkan bagaimana Client berinteraksi dengan Proxy yang memiliki interface yang sama dengan Real Subject:

classDiagram
    class Server {
        <<interface>>
        +HandleRequest(url string, method string) (int, string)
    }

    class RealServer {
        +HandleRequest(url string, method string) (int, string)
    }

    class ProxyServer {
        -realServer RealServer
        -rateLimiter map[string]int
        -maxAllowedRequests int
        +HandleRequest(url string, method string) (int, string)
        -checkRateLimit(clientIP string) bool
    }

    Server <|.. RealServer : implements
    Server <|.. ProxyServer : implements
    ProxyServer --> RealServer : delegates to

Skenario Masalah & Use Case

Bayangkan kita sedang membangun sebuah backend API di Go. Kita memiliki struct bernama RealServer yang menangani operasi-operasi berat (seperti memproses gambar atau menggabungkan tabel database yang besar).

Jika kita membiarkan server ini diakses langsung oleh publik tanpa pelindung, klien yang bermasalah atau serangan siber dapat membanjiri server dengan jutaan request hingga menyebabkan server lumpuh (outage). Dibandingkan mengotori kode logika bisnis di RealServer dengan baris kode keamanan dan pembatasan request, kita dapat membuat ProxyServer dengan interface yang sama. Proxy ini akan menyaring request yang berlebihan dan meneruskan request yang sah ke real server.


Contoh Kode Golang

Berikut adalah contoh kode Go yang lengkap dan dapat dikompilasi, mendemonstrasikan implementasi Proxy untuk membatasi jumlah request (rate-limiting).

package main

import (
	"fmt"
)

// ---------------------------------------------------------
// 1. Subject Interface
// ---------------------------------------------------------

// Server mendefinisikan kontrak yang harus dipenuhi oleh real server dan proxy.
type Server interface {
	HandleRequest(url, method string) (int, string)
}

// ---------------------------------------------------------
// 2. Real Subject
// ---------------------------------------------------------

// RealServer merepresentasikan layanan inti yang menangani operasi berat.
type RealServer struct{}

// HandleRequest memproses request pada layanan inti.
func (r *RealServer) HandleRequest(url, method string) (int, string) {
	if url == "/app/status" && method == "GET" {
		return 200, "Ok"
	}
	if url == "/app/user" && method == "POST" {
		return 201, "User Created"
	}
	return 404, "Not Found"
}

// ---------------------------------------------------------
// 3. Proxy Subject
// ---------------------------------------------------------

// ProxyServer mengontrol akses ke RealServer dengan membatasi jumlah request.
type ProxyServer struct {
	realServer       *RealServer
	maxRequests      int
	clientRequestMap map[string]int // Memetakan IP Client ke jumlah request
}

// NewProxyServer membuat instans baru dari ProxyServer.
func NewProxyServer(realServer *RealServer, maxRequests int) *ProxyServer {
	return &ProxyServer{
		realServer:       realServer,
		maxRequests:      maxRequests,
		clientRequestMap: make(map[string]int),
	}
}

// HandleRequest memotong request untuk memeriksa pembatasan akses.
func (p *ProxyServer) HandleRequest(url, method string) (int, string) {
	// Simulasi: Kita berasumsi IP Client didapatkan dari jaringan (misal: "192.168.1.50")
	clientIP := "192.168.1.50"

	allowed := p.checkRateLimit(clientIP)
	if !allowed {
		return 429, "Too Many Requests (Batas request terlampaui oleh Proxy)"
	}

	// Teruskan request ke Real Subject jika batas belum terlampaui
	return p.realServer.HandleRequest(url, method)
}

// checkRateLimit adalah metode pembantu untuk memeriksa kuota akses klien.
func (p *ProxyServer) checkRateLimit(ip string) bool {
	p.clientRequestMap[ip]++
	if p.clientRequestMap[ip] > p.maxRequests {
		return false
	}
	return true
}

// ---------------------------------------------------------
// 4. Client Code / Simulasi
// ---------------------------------------------------------

func main() {
	realServer := &RealServer{}
	// Proxy memperbolehkan maksimal 2 request dari IP yang sama sebelum memblokir
	proxy := NewProxyServer(realServer, 2)

	// Simulasi: Client mengirimkan beberapa request berturut-turut
	urls := []string{"/app/status", "/app/status", "/app/status", "/app/user"}
	methods := []string{"GET", "GET", "GET", "POST"}

	for i := 0; i < len(urls); i++ {
		fmt.Printf("Client: Mengirim request %s %s...\n", methods[i], urls[i])
		statusCode, response := proxy.HandleRequest(urls[i], methods[i])
		fmt.Printf("Server Response: [Status Code: %d] %s\n\n", statusCode, response)
	}
}

Ringkasan

Keuntungan

  • Keamanan (Access Control): Melindungi subject utama dari panggilan langsung yang berbahaya atau tidak berizin.
  • Separation of Concerns: Memisahkan logika pendukung (rate-limiting, logging, caching) dari logika bisnis inti.
  • Lazy Initialization: Virtual Proxy dapat menunda proses instansiasi objek berat hingga objek tersebut benar-benar dibutuhkan oleh sistem.
  • Open/Closed Principle: Anda dapat menambahkan proxy baru tanpa perlu mengubah kode klien maupun kode layanan utama.

Kerugian

  • Respons Terlambat (Latency): Penggunaan perantara (middleman) dapat menambahkan sedikit waktu pemrosesan untuk setiap request yang masuk.
  • Duplikasi Kode: Baik Proxy maupun Real Subject harus mengimplementasikan interface yang sama, sehingga perubahan signature method harus selalu disinkronisasikan.

Kapan Harus Digunakan

  • Virtual Proxy (Lazy Loading): Ketika Anda memiliki objek yang berat (seperti koneksi database besar) yang sebaiknya dimuat hanya saat dibutuhkan.
  • Protection Proxy (Access Control): Ketika Anda perlu melakukan otentikasi/otorisasi request sebelum meneruskannya ke fungsi inti.
  • Remote Proxy: Ketika objek asli berada di server eksternal atau di ruang memori yang berbeda.
  • Smart Reference (Caching/Logging): Ketika Anda ingin menyimpan hasil kalkulasi secara lokal (cache) atau mencatat log riwayat pemanggilan method secara transparan.