Aspect Oriented Programming #
Dalam pengembangan aplikasi skala menengah hingga besar, ada satu pola yang hampir pasti kamu temui: logika yang berulang di banyak tempat bukan karena duplikasi yang ceroboh, melainkan karena ia memang dibutuhkan di mana-mana. Logging di setiap endpoint, pengecekan autentikasi di setiap handler, pengukuran latency di setiap method kritis, manajemen transaction di setiap operasi write. Logika ini bukan core business — tapi wajib ada. Jika ditaruh langsung di setiap method, kode bisnis tenggelam di bawah concern teknis. Jika dilupakan di satu tempat, konsistensi rusak. Aspect-Oriented Programming hadir untuk menyelesaikan masalah ini: memisahkan cross-cutting concern ke modul terpisah yang diterapkan secara deklaratif, tanpa mencemari kode bisnis. Artikel ini membahas AOP dari konsep dasarnya, lima istilah inti yang harus dipahami, implementasi di Java Spring dan pola middleware Go, empat use case utama, limitasi yang sering diremehkan, dan panduan kapan AOP adalah solusi yang tepat.
Apa Itu Cross-Cutting Concern? #
Sebelum membahas AOP, penting memahami masalah yang ia selesaikan. Cross-cutting concern adalah logika yang dibutuhkan di banyak bagian sistem tapi bukan bagian dari domain bisnis manapun secara spesifik.
Contoh yang paling mudah: logging. Setiap method di setiap layer memerlukan logging — tapi logging bukan bagian dari logika “buat order” atau “proses payment”. Ia adalah kebutuhan teknis yang memotong melintang semua domain.
// ANTI-PATTERN: cross-cutting concern tersebar di setiap method bisnis
func (s *OrderService) CreateOrder(req CreateOrderRequest) (*Order, error) {
// Logging — bukan bagian dari logika order
log.Info("start CreateOrder", "user_id", req.UserID)
start := time.Now()
// Auth check — bukan bagian dari logika order
if !s.auth.HasPermission(req.UserID, "order:create") {
log.Warn("unauthorized CreateOrder", "user_id", req.UserID)
return nil, ErrUnauthorized
}
// Retry — bukan bagian dari logika order
var order *Order
var err error
for attempt := 1; attempt <= 3; attempt++ {
order, err = s.repo.Save(buildOrder(req))
if err == nil { break }
time.Sleep(time.Duration(attempt) * time.Second)
}
// Metrics — bukan bagian dari logika order
metrics.Histogram("order.create.duration", time.Since(start))
// Logging lagi — masih bukan bagian dari logika order
log.Info("end CreateOrder", "order_id", order.ID, "duration", time.Since(start))
return order, err
}
// Kode bisnis sebenarnya hanya 2 baris — tenggelam di bawah concern teknis
flowchart LR
subgraph Without["❌ Tanpa AOP — Cross-cutting Tersebar"]
O["OrderService\ncreateOrder\n+ logging\n+ auth\n+ metrics\n+ retry"]
P["PaymentService\nprocessPayment\n+ logging\n+ auth\n+ metrics\n+ retry"]
U["UserService\ncreateUser\n+ logging\n+ auth\n+ metrics\n+ retry"]
end
subgraph With["✅ Dengan AOP — Cross-cutting Terpusat"]
LA[LoggingAspect] --> OS["OrderService\ncreateOrder"]
AA[AuthAspect] --> OS
MA[MetricsAspect] --> OS
LA --> PS["PaymentService\nprocessPayment"]
AA --> PS
MA --> PS
LA --> US["UserService\ncreateUser"]
AA --> US
MA --> US
endConcern yang paling umum memerlukan AOP:
| Cross-Cutting Concern | Contoh Konkret |
|---|---|
| Logging | Log method name, parameter, hasil, durasi |
| Authentication | Cek token JWT sebelum method dieksekusi |
| Authorization | Cek permission berdasarkan role |
| Transaction | Buka transaction sebelum, commit/rollback sesudah |
| Caching | Return cache jika ada, simpan ke cache setelah method |
| Retry | Ulangi jika gagal dengan backoff |
| Metrics | Ukur latency, hitung error rate |
| Audit Trail | Catat siapa melakukan apa kapan |
Apa Itu Aspect-Oriented Programming? #
Aspect-Oriented Programming (AOP) adalah paradigma pemrograman yang memisahkan cross-cutting concern ke dalam modul terpisah yang disebut Aspect, lalu menerapkannya secara deklaratif ke titik-titik eksekusi tertentu tanpa mengubah kode bisnis.
Prinsip intinya: kode bisnis hanya berisi logika bisnis. Semua concern tambahan ditangani oleh aspect yang “membungkus” eksekusi dari luar.
// BENAR dengan AOP — kode bisnis murni (di Go: middleware/decorator)
type OrderService struct {
repository OrderRepository
}
func (s *OrderService) CreateOrder(req CreateOrderRequest) (*Order, error) {
// Hanya logika bisnis — tidak ada logging, auth, metrics
order := buildOrder(req)
return s.repository.Save(order)
}
// Logging, auth, metrics ditangani oleh middleware/decorator terpisah
// dan diterapkan secara otomatis tanpa menyentuh kode di atas
Lima Konsep Inti AOP #
Memahami lima istilah ini adalah kunci untuk bisa membaca dan menulis AOP dengan benar.
flowchart TD
AS["Aspect\nModul berisi logic cross-cutting"] --> ADV["Advice\nKode yang dijalankan"]
AS --> PC["Pointcut\nEkspresi pemilih join point"]
PC --> JP["Join Point\nTitik eksekusi\nyang dipilih"]
ADV --> JP
WV["Weaving\nProses penggabungan\nAspect + Business Code"] --> JP1. Aspect #
Modul yang berisi logika cross-cutting. Ini adalah unit yang mendefinisikan apa yang harus dilakukan dan kapan dilakukan.
// Aspect setara di Go: middleware — semua logika logging dikumpulkan di sini
func LoggingMiddleware() fiber.Handler {
// Semua logika logging dikumpulkan di sini
// Handler tidak perlu tahu middleware ini ada
return func(c *fiber.Ctx) error { return c.Next() }
}
2. Join Point #
Titik eksekusi dalam program yang bisa di-intercept — biasanya sebuah method call. Dalam Spring AOP, join point selalu berupa method execution.
Join point yang mungkin:
- Eksekusi method: orderService.createOrder(req)
- Akses field: user.email (hanya di AspectJ, bukan Spring AOP)
- Constructor call: new Order() (hanya di AspectJ)
3. Pointcut #
Ekspresi yang memilih join point mana yang akan di-intercept. Pointcut menentukan di mana advice akan diterapkan.
// Pointcut di Go: predicate pemilih method — dipakai oleh middleware/decorator
// Pointcut: semua method di package service
var serviceLayer = func(method string) bool {
return strings.HasPrefix(method, "service.")
}
// Pointcut: method dengan annotation/tag tertentu
var adminOnly = func(hasTag bool) bool { return hasTag }
// Pointcut: method di struct/type tertentu
var paymentService = func(typeName string) bool {
return typeName == "PaymentService"
}
4. Advice #
Kode yang dijalankan di sekitar join point. Ada lima jenis advice dengan timing eksekusi yang berbeda:
| Tipe Advice | Timing | Cocok Untuk |
|---|---|---|
@Before | Sebelum method dieksekusi | Auth check, parameter logging, validation |
@After | Setelah method selesai (apapun hasilnya) | Resource cleanup, audit trail |
@AfterReturning | Setelah method return sukses | Cache result, success logging |
@AfterThrowing | Setelah method throw exception | Error logging, alert, rollback |
@Around | Sebelum DAN sesudah, bisa intercept result | Timing, retry, caching, transaction |
sequenceDiagram
participant Caller
participant AOP as AOP Proxy
participant Method as Business Method
Caller->>AOP: panggil method
AOP->>AOP: @Before advice
AOP->>Method: proceed()
Method-->>AOP: return result
AOP->>AOP: @AfterReturning advice
AOP->>AOP: @After advice
AOP-->>Caller: return result
Note over AOP: Jika exception:
Note over AOP: @AfterThrowing dipanggil
Note over AOP: @After tetap dipanggil5. Weaving #
Proses menggabungkan aspect dengan kode bisnis. Ada tiga strategi weaving:
| Strategi | Kapan | Kelebihan | Kekurangan |
|---|---|---|---|
| Compile-time | Saat kompilasi (AspectJ) | Performa maksimal, semua join point | Butuh compiler khusus |
| Load-time | Saat class di-load ke JVM | Fleksibel | Konfigurasi lebih kompleks |
| Runtime (Proxy) | Saat aplikasi berjalan (Spring AOP) | Paling mudah setup | Terbatas pada method yang bisa di-proxy |
Spring AOP menggunakan runtime proxy — ia membuat proxy object yang membungkus bean asli. Ini kenapa Spring AOP tidak bisa intercept method private atau final.
Implementasi AOP di Java Spring #
Logging Aspect #
// ANTI-PATTERN: logging manual di setiap method service
func (s *OrderService) createOrder(req CreateOrderRequest) (*Order, error) {
log.Infof("start createOrder userId=%v", req.UserID) // duplikat di setiap method
order, err := s.doCreate(req)
log.Infof("end createOrder orderId=%v", order.ID) // duplikat di setiap method
return order, err
}
// BENAR: logging terpusat di middleware — semua handler otomatis ter-log
func LoggingMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
method := c.Method()
log.Infof("[START] %s", method)
start := time.Now()
err := c.Next() // ← jalankan handler asli
if err != nil {
log.Errorf("[ERROR] %s failed after %v: %v", method, time.Since(start), err)
return err
}
log.Infof("[END] %s took %v", method, time.Since(start))
return nil
}
}
Authorization Aspect dengan Custom Annotation #
// Definisi "annotation" di Go: struct marker sederhana
type RequireRole struct {
Role string // role yang dibutuhkan
}
// Aspect yang menerapkan check — middleware/auth wrapper
func RequireRoleMiddleware(requiredRole string, getCurrentRole func() string, next fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
currentRole := getCurrentRole()
if currentRole != requiredRole {
return c.Status(403).JSON(fiber.Map{
"error": "Role '" + requiredRole + "' diperlukan, tapi user punya '" + currentRole + "'",
})
}
return next(c)
}
}
// Penggunaan — deklaratif, bersih
func (s *AdminService) DeleteUser(userID string) {
// Auth check ditangani middleware — method ini hanya berisi logika bisnis
s.userRepository.Delete(userID)
}
func (s *AdminService) ResetSystem() {
s.systemRepository.ResetAll()
}
Performance Monitoring Aspect #
// BENAR: ukur latency setiap method handler secara otomatis
type MetricsMiddleware struct {
registry *MetricsRegistry
}
func NewMetricsMiddleware(registry *MetricsRegistry) *MetricsMiddleware {
return &MetricsMiddleware{registry: registry}
}
func (m *MetricsMiddleware) Wrap(handler fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
methodName := c.Route().Name
className := "handler"
sample := m.registry.StartTimer("method.latency")
err := handler(c)
status := "success"
if err != nil {
status = "error"
}
sample.Stop(map[string]string{
"class": className,
"method": methodName,
"status": status,
})
return err
}
}
Pola AOP di Go: Middleware dan Decorator #
Go tidak punya framework AOP seperti Spring, tapi pola yang sama bisa dicapai dengan middleware pattern dan decorator pattern — yang justru lebih eksplisit dan lebih mudah dipahami.
HTTP Middleware (Fiber/Echo/Gin) #
// BENAR: cross-cutting concern sebagai middleware — tidak masuk ke handler
func LoggingMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
method := c.Method()
path := c.Path()
// Before: catat request masuk
log.Infof("[START] %s %s", method, path)
err := c.Next() // ← jalankan handler asli
// After: catat hasil dan durasi
log.Infof("[END] %s %s status=%d duration=%v",
method, path, c.Response().StatusCode(), time.Since(start))
return err
}
}
func AuthMiddleware(jwtSecret string) fiber.Handler {
return func(c *fiber.Ctx) error {
token := c.Get("Authorization")
claims, err := validateJWT(token, jwtSecret)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "unauthorized"})
}
c.Locals("user_id", claims.UserID)
return c.Next()
}
}
// Daftarkan sekali, berlaku ke semua route di bawahnya
app := fiber.New()
app.Use(LoggingMiddleware()) // apply ke semua route
app.Use(AuthMiddleware(secret)) // apply ke semua route
app.Get("/orders", getOrders) // otomatis ter-log dan ter-auth
app.Post("/orders", createOrder) // otomatis ter-log dan ter-auth
app.Delete("/orders/:id", deleteOrder)
Service Decorator Pattern #
// Interface bisnis
type OrderRepository interface {
Save(ctx context.Context, order *Order) error
FindByID(ctx context.Context, id string) (*Order, error)
}
// ANTI-PATTERN: logging di dalam implementasi — mencemari repository
type MySQLOrderRepository struct{ db *sql.DB }
func (r *MySQLOrderRepository) Save(ctx context.Context, order *Order) error {
log.Infof("saving order %s", order.ID) // ← tidak seharusnya di sini
_, err := r.db.ExecContext(ctx, "INSERT INTO orders ...", order)
log.Infof("saved order %s err=%v", order.ID, err)
return err
}
// BENAR: logging sebagai decorator — wraps repository tanpa mengubahnya
type LoggingOrderRepository struct {
inner OrderRepository // ← bungkus implementasi asli
logger Logger
}
func NewLoggingOrderRepository(inner OrderRepository, logger Logger) OrderRepository {
return &LoggingOrderRepository{inner: inner, logger: logger}
}
func (r *LoggingOrderRepository) Save(ctx context.Context, order *Order) error {
r.logger.Infof("[REPO] Save order %s", order.ID)
start := time.Now()
err := r.inner.Save(ctx, order) // delegasi ke implementasi asli
r.logger.Infof("[REPO] Save order %s done in %v err=%v",
order.ID, time.Since(start), err)
return err
}
func (r *LoggingOrderRepository) FindByID(ctx context.Context, id string) (*Order, error) {
r.logger.Infof("[REPO] FindByID %s", id)
order, err := r.inner.FindByID(ctx, id)
r.logger.Infof("[REPO] FindByID %s found=%v", id, order != nil)
return order, err
}
// Wiring di main() — transparansi penuh
func main() {
mysqlRepo := repository.NewMySQLOrderRepository(db)
loggingRepo := NewLoggingOrderRepository(mysqlRepo, logger) // decorator
cachingRepo := NewCachingOrderRepository(loggingRepo, redis) // decorator di atas decorator
service := service.NewOrderService(cachingRepo) // service tidak tahu ada logging/caching
}
Empat Use Case Utama AOP #
flowchart TD
AOP["Aspect / Middleware"] --> L["Logging\nLog semua method\nscara konsisten"]
AOP --> A["Auth & AuthZ\nCek token dan role\nsebelum method dieksekusi"]
AOP --> M["Metrics\nUkur latency dan\nhitung error rate"]
AOP --> T["Transaction\nBuka dan tutup\ntransaksi otomatis"]
L --> B["Business Logic\nBersih dan fokus\npada domain"]
A --> B
M --> B
T --> B| Use Case | Approach | Alasan AOP Tepat |
|---|---|---|
| Logging | @Around atau middleware | Berlaku di semua endpoint/method secara konsisten |
| Auth/AuthZ | @Before atau middleware | Harus dilakukan sebelum logika bisnis, tidak boleh lupa |
| Transaction | @Around | Buka sebelum, commit sesudah, rollback jika exception |
| Metrics/Tracing | @Around atau middleware | Perlu akses ke start time, method name, dan hasil |
Limitasi AOP yang Harus Dipahami #
Limitasi Spring AOP #
@Service
public class OrderService {
@Transactional // ← Aspect yang mengelola transaction
public void createOrder(CreateOrderRequest req) {
Order order = repository.save(req);
// ANTI-PATTERN: memanggil method @Transactional dari dalam class yang sama
// Spring AOP menggunakan proxy — this.processOrder() tidak melalui proxy!
this.processOrder(order); // ← @Transactional di sini TIDAK akan bekerja
}
@Transactional(propagation = REQUIRES_NEW)
public void processOrder(Order order) {
// Kira-kira dalam transaction baru — tapi tidak, karena dipanggil via this
}
}
// BENAR: pisahkan ke bean terpisah agar melalui proxy
@Service
public class OrderService {
private final OrderProcessor processor; // ← bean terpisah
@Transactional
public void createOrder(CreateOrderRequest req) {
Order order = repository.save(req);
processor.processOrder(order); // ← melalui proxy, @Transactional bekerja
}
}
flowchart LR
subgraph Direct["this.method() — Tidak Melalui Proxy"]
C1[Caller] --> P1[Spring Proxy]
P1 -->|"apply aspect"| B1["Bean\ncreateOrder"]
B1 -->|"this.processOrder()\nLEWATI proxy"| B1
B1 --> B2["processOrder\naspect TIDAK berlaku"]
end
subgraph External["bean.method() — Melalui Proxy"]
C2[Caller] --> P2["Proxy A\ncreateOrder"]
P2 -->|"apply aspect"| B3[createOrder]
B3 --> P3["Proxy B\nprocessOrder"]
P3 -->|"apply aspect"| B4["processOrder\naspect BERLAKU"]
endHal-hal yang tidak bisa di-intercept oleh Spring AOP (karena berbasis proxy):
| Yang Tidak Bisa | Solusi |
|---|---|
Method private | Pindahkan ke method public atau gunakan AspectJ |
Method final | Hapus final atau gunakan AspectJ |
this.method() internal call | Pisahkan ke bean terpisah atau inject self |
| Constructor | Gunakan AspectJ compile-time weaving |
| Field access | Gunakan AspectJ |
Spring AOP bekerja lewat proxy object. Ini berarti jika sebuah method memanggil method lain dalam class yang sama (this.method()), proxy tidak ikut campur dan semua advice (@Transactional,@Cacheable, custom aspect) di method yang dipanggil tidak akan bekerja. Ini adalah penyebab bug yang sangat sering terjadi dan sangat sulit dilacak.
Best Practice Menggunakan AOP #
Jangan Taruh Business Logic di Aspect #
// ANTI-PATTERN: business logic di dalam middleware — salah tempat
func handleOrder(next fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
// ← ini seharusnya di OrderService, bukan di middleware!
req := c.Locals("req").(CreateOrderRequest)
if req.Amount <= 0 {
return fiber.NewError(400, "Amount must be positive")
}
return next(c)
}
}
// BENAR: business logic di service, aspect hanya concern teknis
func logAndMeasure(next fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
err := next(c) // ← tidak modifikasi args atau result
log.Infof("%v took %v", c.Route().Path, time.Since(start))
return err
}
}
Gunakan Pointcut yang Spesifik #
// ANTI-PATTERN: pointcut terlalu luas — intercept semua method di seluruh aplikasi
var logAll = func(method string) bool { return true } // ← semua method!
// Intercept method internal framework, library pihak ketiga, dsb.
// BENAR: spesifik ke package dan layer yang memang butuh
var logServiceLayer = func(method string) bool {
return strings.HasPrefix(method, "com.example.service.")
}
// Atau gunakan marker-based pointcut — lebih eksplisit
var auditMethod = func(hasTag bool) bool { return hasTag }
Anti-Pattern yang Harus Dihindari #
// ✗ Business logic di Aspect — aspect menjadi "controller" terselubung
func handle(next fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
if !hasEnoughStock() {
return fiber.NewError(400, "out of stock") // ← logika bisnis!
}
return next(c)
}
}
// ✓ Validasi stok di dalam service, bukan di aspect
// ✗ Pointcut terlalu luas — performa terdampak, debugging sulit
var intercept = func(next fiber.Handler) fiber.Handler { return next } // ← semua method
// ✓ Batasi ke layer spesifik: execution(* com.example.service..*(..))
// ✗ Terlalu banyak aspect bertumpuk — alur eksekusi tidak jelas
// LoggingMiddleware → MetricsMiddleware → RetryMiddleware → CachingMiddleware → AuthMiddleware
// Tidak ada yang tahu aspek mana yang jalan duluan
// ✓ Gunakan urutan pendaftaran eksplisit (app.Use urut)
app.Use(AuthMiddleware()) // paling awal
app.Use(LoggingMiddleware())
app.Use(MetricsMiddleware()) // paling akhir
// ✗ Tidak mendokumentasikan aspect — engineer baru tidak tahu kenapa method tiba-tiba ter-log
// ✓ Tambahkan komentar di middleware dan di method yang di-intercept
// ✓ Catat di README atau internal wiki: "semua handler otomatis di-log oleh LoggingMiddleware"
Kapan Menggunakan dan Tidak Menggunakan AOP #
Gunakan AOP Jika: #
| Kondisi | Contoh |
|---|---|
| Concern berulang di 10+ tempat | Logging di semua handler, auth di semua endpoint |
| Concern harus konsisten | Semua transaction harus dibuka/ditutup dengan cara yang sama |
| Perubahan concern tidak boleh menyentuh kode bisnis | Ganti logging library tidak boleh ubah service |
| Concern bersifat deklaratif | @RequireAdmin, @Cacheable, @Transactional |
Jangan Gunakan AOP Jika: #
❌ Concern hanya dibutuhkan di satu atau dua tempat
→ Duplikasi kecil lebih baik dari kompleksitas AOP yang tidak perlu
❌ Business logic perlu masuk ke aspect
→ Tanda desain salah — refactor service, bukan tambah aspect
❌ Tim belum familiar dengan AOP
→ Debugging aspect yang salah sangat membingungkan tanpa pemahaman dasar
❌ Aplikasi sangat kecil
→ Overhead setup tidak sebanding dengan manfaat
❌ Butuh intercept method private atau self-call di Java
→ Spring AOP tidak bisa; butuh AspectJ atau refactor arsitektur
Checklist Implementasi AOP #
DESAIN ASPECT:
□ Aspect hanya berisi logika cross-cutting, bukan logika bisnis
□ Setiap aspect punya satu tanggung jawab (SRP)
□ Aspect didokumentasikan: apa yang ia lakukan, ke method mana ia berlaku
POINTCUT:
□ Pointcut spesifik ke layer atau annotation tertentu
□ Tidak ada pointcut yang terlalu luas (execution(* *.*(..)))
□ Pointcut diuji dengan unit test atau integration test
ORDERING:
□ Urutan eksekusi aspect ditentukan eksplisit dengan @Order
□ Auth aspect dijalankan sebelum logging aspect
LIMITASI (Java Spring AOP):
□ Tidak ada method private atau final yang diharapkan ter-intercept
□ Tidak ada internal self-call (this.method()) yang diharapkan ter-intercept
□ Tim mengetahui proxy mechanism dan limitasinya
TESTING:
□ Ada test yang membuktikan aspect bekerja saat dipanggil dari luar
□ Ada test yang membuktikan business logic tetap berjalan benar dengan aspect
□ Error dari aspect tidak menyembunyikan error bisnis yang sebenarnya
Ringkasan #
- AOP memisahkan cross-cutting concern — logika seperti logging, auth, metrics, dan transaction dipisahkan ke Aspect sehingga kode bisnis tetap bersih dan fokus.
- Lima konsep inti: Aspect (modul cross-cutting), Join Point (titik eksekusi yang bisa di-intercept), Pointcut (ekspresi pemilih join point), Advice (kode yang dijalankan), Weaving (proses penggabungan).
- Lima tipe Advice:
@Before,@After,@AfterReturning,@AfterThrowing,@Around— pilih berdasarkan kapan concern perlu dijalankan.- Spring AOP berbasis proxy — hanya bisa intercept method
publicyang dipanggil dari luar bean;this.method()internal call tidak melalui proxy dan tidak ter-intercept.- Go menggunakan middleware dan decorator — pola yang lebih eksplisit dan lebih mudah di-debug dibanding annotation-based AOP.
- Jangan taruh business logic di Aspect — Aspect hanya untuk concern teknis; keputusan bisnis tetap di service.
- Gunakan Pointcut yang spesifik —
execution(* com.example.service..*(..))jauh lebih aman dariexecution(* *(..)).- Dokumentasikan semua Aspect — aspect yang tidak terdokumentasi adalah sumber kebingungan bagi engineer baru dan debugging yang menyulitkan.
- AOP tepat untuk concern yang berulang di banyak tempat dan harus konsisten — untuk concern yang hanya dibutuhkan di satu dua tempat, cukup tulis langsung.
← Sebelumnya: Dependency Injection Berikutnya: Retry Strategy →