Aspect Oriented Programming #

Ada sebuah pattern masalah yang hampir universal di codebase yang sudah berkembang: logging muncul di setiap service method, pengecekan autentikasi diulang di setiap endpoint, pengukuran durasi eksekusi tersebar di mana-mana, dan penanganan transaction ditulis ulang di setiap use case. Semua ini bukan business logic — tapi semuanya wajib ada, dan semuanya identik. Setiap kali ada kebutuhan untuk mengubah format log, developer harus menyentuh puluhan file. Setiap kali ada pembaruan logic autentikasi, ada risiko ada satu endpoint yang terlewat. Aspect-Oriented Programming (AOP) adalah paradigma yang lahir untuk menyelesaikan masalah ini: memisahkan cross-cutting concerns dari business logic menjadi modul tersendiri yang bisa diterapkan secara deklaratif ke seluruh codebase. Panduan ini membahas AOP dari konsep dasarnya, lima istilah kunci yang harus dipahami, implementasi konkret di Java Spring AOP beserta padanannya di Go dan Dart, hingga batas-batas yang harus dijaga agar AOP tidak menjadi sumber kompleksitas tersembunyi.

Apa Itu Cross-Cutting Concern? #

Sebelum membahas AOP, penting dipahami masalah yang ia selesaikan. Cross-cutting concern adalah fungsionalitas yang dibutuhkan di banyak bagian sistem, tidak spesifik ke satu domain bisnis, dan cenderung tersebar secara horizontal melintasi banyak class dan layer.

Tanpa AOP — cross-cutting concern tersebar di mana-mana:

OrderService.createOrder():
    log.Info("start createOrder")          ← logging
    if !auth.IsAuthorized(user) { panic }   ← authorization
    start := time.Now()                     ← metrics
    // ... 3 baris business logic
    log.Info("duration:", time.Since(start)) ← metrics
    log.Info("end createOrder")             ← logging

PaymentService.processPayment():
    log.Info("start processPayment")        ← logging (duplikasi)
    if !auth.IsAuthorized(user) { panic }   ← authorization (duplikasi)
    start := time.Now()                     ← metrics (duplikasi)
    // ... 5 baris business logic
    log.Info("duration:", time.Since(start)) ← metrics (duplikasi)
    log.Info("end processPayment")          ← logging (duplikasi)

Dari setiap 10 baris kode, mungkin hanya 3 yang benar-benar business logic. Sisanya adalah concern yang sama diulang-ulang. Masalahnya bukan hanya duplikasi — tapi fakta bahwa business logic menjadi sulit dibaca, dan setiap perubahan pada concern tersebut (misalnya: tambahkan request ID ke semua log) harus dilakukan di banyak tempat.

Contoh cross-cutting concern yang paling umum ditemui: logging dan tracing, authentication dan authorization, transaction management, performance monitoring, caching, retry dan circuit breaker, serta audit trail.


Lima Konsep Inti AOP #

AOP memiliki terminologi spesifik yang perlu dipahami sebelum membaca kode implementasinya.

1. Aspect #

Modul yang berisi logic cross-cutting. Ini adalah “concern” yang dipindahkan keluar dari business class. Satu aspect bisa diterapkan ke banyak titik eksekusi secara bersamaan.

// Aspect = class khusus yang berisi cross-cutting logic
@Aspect
@Component
public class LoggingAspect {
    // Semua logic logging ada di sini, bukan di business class
}

2. Join Point #

Titik eksekusi di dalam program di mana sebuah aspect bisa dimasukkan — misalnya eksekusi method, akses field, atau constructor call. Di Spring AOP, join point hampir selalu berarti eksekusi method.

// Join point = setiap eksekusi method ini adalah kandidat join point
public Order createOrder(String userId, List<Item> items) {
    // ← join point: saat method ini dieksekusi
    return orderRepository.save(buildOrder(userId, items));
}

3. Pointcut #

Ekspresi yang menentukan join point mana yang akan di-intercept oleh aspect. Ini adalah “filter” yang menentukan scope penerapan aspect.

// Pointcut expression — pilih semua method di package service
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayerMethods() {}

// Pointcut expression — hanya method yang punya annotation @Audited
@Pointcut("@annotation(com.example.annotation.Audited)")
public void auditedMethods() {}

// Pointcut expression — method di class tertentu dengan return type non-void
@Pointcut("execution(!void com.example.service.OrderService.*(..))")
public void orderServiceNonVoidMethods() {}

4. Advice #

Kode yang dieksekusi di sekitar join point yang cocok dengan pointcut. Ada lima jenis advice yang menentukan kapan kode dieksekusi relatif terhadap method target.

Jenis Advice dan Kapan Dieksekusi:

@Before       → sebelum method dieksekusi
              → use case: logging start, authorization check, input validation

@After        → setelah method selesai (sukses ATAU error)
              → use case: logging end, release resource

@AfterReturning → setelah method selesai dengan sukses (tidak error)
               → use case: logging result, cache update, audit sukses

@AfterThrowing → setelah method melempar exception
               → use case: error logging, alert, rollback tambahan

@Around       → membungkus eksekusi method sepenuhnya
               → use case: performance timing, retry, transaction, caching
               → paling powerful — bisa mengubah input, output, bahkan mencegah eksekusi

5. Weaving #

Proses menggabungkan aspect dengan business code di titik-titik yang ditentukan pointcut. Weaving bisa terjadi di compile-time (AspectJ), load-time, atau runtime.

Compile-time weaving (AspectJ):
    Source code → [compiler + weaver] → bytecode yang sudah "disisipi" aspect
    Keunggulan: tidak ada runtime overhead, bisa intercept method private
    Kekurangan: butuh compiler khusus

Runtime weaving (Spring AOP default):
    Spring membuat proxy object yang membungkus bean asli
    Saat method dipanggil → proxy intercept → jalankan advice → panggil method asli
    Keunggulan: mudah setup, tidak butuh compiler khusus
    Kekurangan: tidak bisa intercept method private atau internal call (this.method())

Implementasi di Java Spring AOP #

Spring AOP menggunakan proxy-based weaving — Spring membuat objek proxy yang membungkus bean asli dan menyisipkan advice di sekitar method call.

Logging Aspect — @Before dan @After #

// Business class — bersih dari concern logging
@Service
public class OrderService {
    private final OrderRepository repo;

    public OrderService(OrderRepository repo) {
        this.repo = repo;
    }

    public Order createOrder(String userId, List<Item> items) {
        // Hanya business logic di sini — tidak ada satu baris logging pun
        return repo.save(buildOrder(userId, items));
    }

    public void cancelOrder(String orderId) {
        repo.updateStatus(orderId, "CANCELLED");
    }
}

// Logging aspect — semua logging terpusat di sini
@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    // Pointcut: semua method di package service
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}

    @Before("serviceLayer()")
    public void logMethodEntry(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("[START] {}.{}() args={}", 
            joinPoint.getTarget().getClass().getSimpleName(),
            methodName, Arrays.toString(args));
    }

    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logMethodSuccess(JoinPoint joinPoint, Object result) {
        log.info("[SUCCESS] {}.{}() result={}", 
            joinPoint.getTarget().getClass().getSimpleName(),
            joinPoint.getSignature().getName(), result);
    }

    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void logMethodError(JoinPoint joinPoint, Exception ex) {
        log.error("[ERROR] {}.{}() threw: {}",
            joinPoint.getTarget().getClass().getSimpleName(),
            joinPoint.getSignature().getName(), ex.getMessage());
    }
}

Dengan aspect ini, setiap method di seluruh package service otomatis ter-log — tanpa satu pun baris log di business class.

Performance Monitoring — @Around #

@Around adalah advice yang paling powerful karena ia membungkus eksekusi method sepenuhnya — bisa mengukur waktu, memodifikasi argumen, bahkan mencegah method dieksekusi sama sekali.

@Aspect
@Component
public class PerformanceAspect {

    private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);

    // Intercept semua method yang memakan waktu signifikan (service layer)
    @Around("execution(* com.example.service.*.*(..))")
    public Object measureExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().toShortString();
        long startTime = System.currentTimeMillis();

        try {
            Object result = pjp.proceed(); // ← eksekusi method asli
            long duration = System.currentTimeMillis() - startTime;

            if (duration > 1000) {
                // Alert jika method lambat
                log.warn("[SLOW] {} took {}ms — investigate!", methodName, duration);
            } else {
                log.debug("[PERF] {} completed in {}ms", methodName, duration);
            }

            // Kirim ke metrics system
            Metrics.timer("service.method.duration")
                .tag("method", methodName)
                .record(duration, TimeUnit.MILLISECONDS);

            return result;

        } catch (Throwable ex) {
            long duration = System.currentTimeMillis() - startTime;
            log.error("[PERF-ERROR] {} failed after {}ms", methodName, duration);
            throw ex; // rethrow — jangan menelan exception
        }
    }
}

Security / Authorization Aspect — Custom Annotation #

Pola yang sangat umum: gunakan custom annotation sebagai “marker”, lalu buat aspect yang intercept semua method dengan annotation tersebut.

// Step 1: definisikan custom annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
    String[] value(); // role yang diizinkan: "ADMIN", "MANAGER", dll
}

// Step 2: buat aspect yang merespons annotation
@Aspect
@Component
public class SecurityAspect {

    @Before("@annotation(requireRole)")
    public void checkAuthorization(JoinPoint joinPoint, RequireRole requireRole) {
        String currentUserRole = SecurityContext.getCurrentUserRole();
        String[] allowedRoles = requireRole.value();

        boolean isAllowed = Arrays.stream(allowedRoles)
            .anyMatch(role -> role.equals(currentUserRole));

        if (!isAllowed) {
            throw new AccessDeniedException(
                "Role '" + currentUserRole + "' is not allowed. " +
                "Required: " + Arrays.toString(allowedRoles)
            );
        }
    }
}

// Step 3: gunakan annotation di business method — deklaratif, bersih
@Service
public class UserManagementService {

    @RequireRole({"ADMIN"})
    public void deleteUser(String userId) {
        userRepository.delete(userId);
    }

    @RequireRole({"ADMIN", "MANAGER"})
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    // Method tanpa annotation = tidak ada restriction
    public User getMyProfile(String userId) {
        return userRepository.findById(userId);
    }
}

Transaction Management — AOP yang Paling Sering Digunakan Diam-Diam #

@Transactional di Spring adalah AOP yang sudah built-in dan paling sering digunakan tanpa disadari sebagai AOP:

@Service
public class TransferService {

    @Transactional  // ← ini adalah @Around advice yang inject transaction management
    public void transfer(String fromId, String toId, BigDecimal amount) {
        accountRepository.debit(fromId, amount);
        accountRepository.credit(toId, amount);
        // Jika method ini throw exception → transaction otomatis rollback
        // Jika sukses → transaction otomatis commit
        // Tanpa @Transactional, kamu harus tulis ini manual di setiap method
    }
}

Pola Padanan AOP di Go dan Dart #

Go dan Dart tidak punya framework AOP seperti Spring, tapi pola yang sama bisa dicapai dengan middleware, wrapper, dan decorator pattern.

Go — Middleware sebagai AOP #

// Middleware adalah padanan "advice" di Go
// Ini adalah @Around equivalent untuk HTTP handler

// "Aspect" logging untuk semua HTTP endpoint
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        correlationID := r.Header.Get("X-Correlation-ID")
        if correlationID == "" {
            correlationID = uuid.New().String()
        }

        log.Infof("[REQ] %s %s correlation_id=%s", r.Method, r.URL.Path, correlationID)

        // Inject correlation ID ke context — tersedia di handler dan downstream
        ctx := context.WithValue(r.Context(), "correlation_id", correlationID)
        next.ServeHTTP(w, r.WithContext(ctx))

        log.Infof("[RES] %s %s duration=%v correlation_id=%s",
            r.Method, r.URL.Path, time.Since(start), correlationID)
    })
}

// "Aspect" authorization untuk route tertentu
func RequireAuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return // ← equivalent @Before yang menghentikan eksekusi
        }
        claims, err := validateJWT(token)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), "user_claims", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Wiring — terapkan "aspect" secara deklaratif ke route
router := mux.NewRouter()
router.Use(LoggingMiddleware)    // berlaku untuk semua route
router.Use(MetricsMiddleware)    // berlaku untuk semua route

apiRouter := router.PathPrefix("/api").Subrouter()
apiRouter.Use(RequireAuthMiddleware) // berlaku hanya untuk /api/*
// Untuk service layer di Go — decorator pattern sebagai AOP
type OrderRepository interface {
    Save(ctx context.Context, order *Order) error
    FindByID(ctx context.Context, id string) (*Order, error)
}

// Decorator yang menambahkan logging ke repository mana pun
type LoggingOrderRepository struct {
    inner  OrderRepository // ← wrap implementasi asli
    logger Logger
}

func (r *LoggingOrderRepository) Save(ctx context.Context, order *Order) error {
    r.logger.Infof("saving order %s", order.ID)
    err := r.inner.Save(ctx, order) // ← panggil implementasi asli
    if err != nil {
        r.logger.Errorf("save order %s failed: %v", order.ID, err)
    } else {
        r.logger.Infof("order %s saved successfully", order.ID)
    }
    return err
}

// main.go — wrap dengan decorator saat wiring
repo      := repository.NewPostgresOrderRepository(db)
loggedRepo := &LoggingOrderRepository{inner: repo, logger: logger}
service   := service.NewOrderService(loggedRepo) // inject yang sudah di-wrap

Dart/Flutter — Interceptor dan Decorator #

// Dart — interceptor sebagai padanan @Around advice untuk HTTP
class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // @Before equivalent
    print('[REQ] ${options.method} ${options.path}');
    print('[REQ] headers: ${options.headers}');
    super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // @AfterReturning equivalent
    print('[RES] ${response.statusCode} ${response.requestOptions.path}');
    super.onResponse(response, handler);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // @AfterThrowing equivalent
    print('[ERR] ${err.response?.statusCode} ${err.requestOptions.path}: ${err.message}');
    super.onError(err, handler);
  }
}

// Wiring — terapkan interceptor ke semua request Dio
final dio = Dio();
dio.interceptors.add(LoggingInterceptor());
dio.interceptors.add(AuthInterceptor());   // inject token ke semua request
dio.interceptors.add(RetryInterceptor());  // retry otomatis untuk 5xx

Limitasi Spring AOP yang Wajib Dipahami #

Spring AOP menggunakan proxy berbasis interface atau CGLIB subclass. Ini membawa beberapa batasan penting:

// ANTI-PATTERN 1: internal call tidak di-intercept
@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        // method ini di-intercept oleh proxy
    }

    public void createBulkOrders(List<Order> orders) {
        for (Order order : orders) {
            this.createOrder(order); // ← TIDAK di-intercept!
            // this.createOrder adalah internal call yang melewati proxy
            // @Transactional di createOrder TIDAK berlaku
        }
    }
}
// ✓ Solusi: inject self-reference, atau pindahkan ke service terpisah

// ANTI-PATTERN 2: method private tidak di-intercept
@Service
public class UserService {

    @RequireRole("ADMIN")
    private void deleteUserInternal(String id) { // ← @RequireRole diabaikan!
        userRepo.delete(id);
    }
}
// ✓ Solusi: jadikan method public, atau gunakan AspectJ untuk full weaving

// ANTI-PATTERN 3: pointcut terlalu umum — intercept semua termasuk infrastruktur
@Around("execution(* *.*(..))")    // ← BERBAHAYA: intercept semua method
public Object logAll(ProceedingJoinPoint pjp) throws Throwable { ... }
// ✓ Solusi: selalu batasi pointcut ke package yang relevan
@Around("execution(* com.example.service..*(..))")
Jika kamu membutuhkan AOP yang bekerja pada method private, internal call, atau constructor, Spring AOP tidak cukup. Pertimbangkan AspectJ dengan compile-time atau load-time weaving. Tapi ingat: ini menambahkan kompleksitas build pipeline yang signifikan dan jarang benar-benar diperlukan dalam aplikasi enterprise biasa.

Aspect Ordering — Ketika Banyak Aspect Bertumpuk #

Ketika beberapa aspect diterapkan ke method yang sama, urutan eksekusi menjadi penting. Logging sebelum authorization? Atau authorization sebelum logging?

// Gunakan @Order untuk mengontrol urutan eksekusi
// Nilai lebih kecil = prioritas lebih tinggi (dieksekusi lebih dulu)

@Aspect
@Component
@Order(1)  // Paling awal — logging harus terjadi sebelum apapun
public class LoggingAspect { ... }

@Aspect
@Component
@Order(2)  // Setelah logging — authorization check
public class SecurityAspect { ... }

@Aspect
@Component
@Order(3)  // Setelah authorization — performance monitoring
public class PerformanceAspect { ... }

@Aspect
@Component
@Order(4)  // Paling akhir sebelum method — transaction management
public class TransactionAspect { ... }
Urutan eksekusi @Around dengan 3 aspect:

Aspect 1 @Around (before)
    Aspect 2 @Around (before)
        Aspect 3 @Around (before)
            ← method dieksekusi di sini →
        Aspect 3 @Around (after)
    Aspect 2 @Around (after)
Aspect 1 @Around (after)

Kapan AOP Layak dan Tidak Layak #

GUNAKAN AOP jika:
  ✓ Concern yang sama diulang di 5+ tempat
  ✓ Concern tidak berisi business logic (logging, auth, metrics, tracing)
  ✓ Perubahan concern harus konsisten di seluruh codebase
  ✓ Tim cukup familiar untuk tidak kebingungan saat debugging
  ✓ Framework mendukung AOP dengan baik (Spring, NestJS)

JANGAN gunakan AOP jika:
  ✗ Logic yang akan dipindahkan berisi business rule atau keputusan domain
  ✗ Aplikasi kecil di mana manual lebih mudah dibaca
  ✗ Tim belum familiar — debugging "magic behavior" bisa sangat menyakitkan
  ✗ Perlu intercept method private atau internal call (Spring AOP tidak bisa)
  ✗ Pointcut-nya sangat spesifik dan hanya berlaku satu tempat — lebih baik inline

Anti-Pattern yang Harus Dihindari #

// ✗ Business logic di dalam aspect — merusak tujuan AOP
@Aspect
@Component
public class OrderAspect {
    @Before("execution(* createOrder(..))")
    public void beforeCreateOrder(JoinPoint jp) {
        Order order = (Order) jp.getArgs()[0];
        if (order.getTotal() > 10_000_000) {  // ← business rule
            applyDiscount(order);               // ← business logic di aspect!
        }
    }
}
// ✓ Business logic tetap di service/domain layer

// ✗ Pointcut yang terlalu umum — performa dan debugging nightmare
@Around("execution(* *.*(..))")
// ✓ Batasi ke package dan layer yang relevan
@Around("execution(* com.example.service..*.*(..))")

// ✗ Aspect yang menelan exception — mengubah error handling secara diam-diam
@Around("serviceLayer()")
public Object wrapping(ProceedingJoinPoint pjp) throws Throwable {
    try {
        return pjp.proceed();
    } catch (Exception e) {
        log.error(e);
        return null; // ← menelan exception! caller tidak tahu ada error
    }
}
// ✓ Selalu rethrow exception kecuali ada alasan sangat kuat

// ✗ Tidak ada dokumentasi untuk aspect — engineer baru kebingungan
@Aspect
@Component
public class MysteryAspect { // ← apa ini? mengapa ada? siapa yang buat?
    @Around("execution(* *Service.*(..))")
    public Object doSomething(ProceedingJoinPoint pjp) throws Throwable {
        // kode tanpa komentar apapun
    }
}
// ✓ Setiap aspect harus punya Javadoc yang menjelaskan tujuan, scope, dan behavior

Ringkasan #

  • AOP memisahkan cross-cutting concerns (logging, auth, metrics, transaction) dari business logic, membuat keduanya bersih dan fokus pada tanggung jawabnya masing-masing.
  • Lima konsep kunci: Aspect (modul concern), Join Point (titik eksekusi yang bisa di-intercept), Pointcut (filter join point mana yang berlaku), Advice (kode yang dijalankan di sekitar join point), dan Weaving (proses penyisipan).
  • Lima jenis advice: @Before (sebelum), @After (selalu setelah), @AfterReturning (setelah sukses), @AfterThrowing (setelah error), @Around (membungkus penuh — paling powerful).
  • Spring AOP berbasis proxy — tidak bisa intercept method private atau internal call (this.method()); gunakan AspectJ jika butuh kemampuan lebih dalam.
  • @Transactional adalah AOP — salah satu penggunaan AOP yang paling sering dipakai tanpa disadari.
  • Custom annotation sebagai pointcut adalah pola paling bersih untuk authorization dan audit — deklaratif, tidak invasif.
  • Gunakan @Order untuk mengontrol urutan eksekusi saat beberapa aspect diterapkan ke method yang sama.
  • Jangan taruh business logic di aspect — aspect hanya untuk infrastruktur concern; domain rule tetap di service/domain layer.
  • Di Go dan Dart: middleware pattern (HTTP layer) dan decorator pattern (service layer) adalah padanan AOP yang idiomatis.

← Sebelumnya: Dependency Injection   Berikutnya: Retry Strategy →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact