Selama lebih dari dua dekade, nama Spring hampir tidak bisa dipisahkan dari backend Java. Framework ini lahir pada tahun 2002 dari buku Expert One-on-One J2EE Design and Development yang ditulis Rod Johnson, yang membedah kompleksitas berlebihan standar J2EE kala itu — terutama Enterprise JavaBeans (EJB) — lalu menawarkan alternatif yang jauh lebih ringan. Dari benih itu tumbuh Spring Framework yang kini menjadi fondasi mayoritas aplikasi perusahaan Java di dunia. Pada Chapter ini kita akan memetakan masalah yang diselesaikan Spring, mengenal anggota ekosistemnya, dan melihat konsep inti yang membuat semuanya bekerja: Inversion of Control.

Masalah yang Dibereskan Spring #

Pada era J2EE sekitar tahun 2000-an awal, membangun satu komponen bisnis saja menuntut berkas deployment XML panjang, container aplikasi berat, dan kode yang menempel erat pada spesifikasi vendor. Tiga keluhan utama muncul dari para pengembang. Pertama, tight coupling: kelas merakit dependensinya sendiri dengan new, sehingga komponen sulit diganti. Kedua, pengujian nyaris mustahil karena kelas hanya bisa hidup di dalam container penuh. Ketiga, kebutuhan lintas sekat seperti transaction management dan logging menyebar ke mana-mana sebagai kode duplikat.

Spring menjawab ketiganya dengan satu gagasan: serahkan perakitan objek kepada container. Kelas cukup mendeklarasikan dependensi yang dibutuhkan, container Spring yang menyambungkannya — pola yang kita kenal sebagai dependency injection. Kelengkapan Spring juga menyediakan AOP (Aspect-Oriented Programming) untuk kebutuhan lintas sekat, serta dukungan pengujian kelas pertama. Hasilnya: kode bisnis ramping, mudah diuji, dan longgar kopelnya.

Peta Ekosistem Spring #

Kata “Spring” sesungguhnya adalah payung bagi keluarga proyek. Tabel berikut memetakan anggota yang paling sering kita temui:

ProyekPeran singkat
Spring FrameworkFondasi: container IoC, MVC, transaksi, validator
Spring BootAuto-configuration, starter, server tertanam untuk startup cepat
Spring DataAbstraksi repository di atas JPA, JDBC, Redis, MongoDB
Spring SecurityAuthentication, authorization, proteksi eksploitasi umum
Spring CloudPola untuk sistem terdistribusi: config, discovery, gateway

Spring Framework adalah intinya; proyek lain membangun di atas fondasi itu. Kita akan menyusuri kelimanya secara berurutan, dengan porsi terbesar pada Spring Boot karena merepresentasikan cara kerja Spring masa kini.

Kenapa Spring Dominan #

Dominasi Spring bukan kebetulan. Framework ini datang lebih dulu dengan solusi nyata untuk masalah nyata, lalu menjaga kompatibilitas mundur selama dua dekade — aplikasi bank yang ditulis 2010 masih bisa dimigrasikan bertahap ke Spring Boot 3. Komunitasnya raksasa: dokumentasi resmi, panduan langkah demi langkah di spring.io/guides, dan jawaban Stack Overflow hampir tak pernah kehabisan. Lowongan kerja backend Java secara konsisten menuntut Spring, dan ekosistem tooling — build tool, profiler, APM — semuanya menganggap Spring sebagai warga kelas pertama. Terakhir, Spring bergerak cepat menambatkan diri pada Java versi modern; Spring Boot 3 memindahkan baseline ke Java 17 dan namespace jakarta.*.

Konsep Inti Secara Nyata #

Teori paling melekat bila kita lihat perbedaannya secara langsung. Program berikut membandingkan kelas yang merakit dependensinya sendiri melawan kelas yang menerima dependensi dari luar — persis masalah yang diselesaikan container Spring:

public class KeterikatanVsInjeksi {

    public static void main(String[] args) {
        System.out.println("== Perakitan manual di dalam kelas (keterikatan erat) ==");
        LayananRapatKaku kaku = new LayananRapatKaku();
        kaku.kirim("Rapat sprint besok pukul 09.00");

        System.out.println();
        System.out.println("== Constructor injection: dependensi disuntikkan dari luar ==");
        LayananRapat viaEmail = new LayananRapat(new PengirimEmail());
        viaEmail.kirim("Rapat sprint besok pukul 09.00");

        LayananRapat viaSms = new LayananRapat(new PengirimSms());
        viaSms.kirim("Rapat sprint dibatalkan");

        System.out.println();
        System.out.println("== Ganti implementasi tanpa menyentuh LayananRapat ==");
        LayananRapat viaLog = new LayananRapat(new PengirimLog());
        viaLog.kirim("Notifikasi hanya untuk log internal");
    }
}

// Versi terikat erat: kelas merakit dependensinya sendiri dengan new,
// sehingga PengirimEmail tertanam dan sulit diganti atau diuji terpisah.
class LayananRapatKaku {

    void kirim(String pesan) {
        PengirimEmail pengirim = new PengirimEmail();
        pengirim.kirim(pesan);
    }
}

interface PengirimPesan {
    void kirim(String pesan);
}

class PengirimEmail implements PengirimPesan {
    @Override
    public void kirim(String pesan) {
        System.out.println("[Email] " + pesan);
    }
}

class PengirimSms implements PengirimPesan {
    @Override
    public void kirim(String pesan) {
        System.out.println("[SMS] " + pesan);
    }
}

class PengirimLog implements PengirimPesan {
    @Override
    public void kirim(String pesan) {
        System.out.println("[Log] " + pesan);
    }
}

// Versi lentur: dependensi masuk lewat konstruktor (injection point)
// dan disimpan final agar tidak bisa ditukar di tengah jalan.
class LayananRapat {

    private final PengirimPesan pengirim;

    LayananRapat(PengirimPesan pengirim) {
        this.pengirim = pengirim;
    }

    void kirim(String pesan) {
        pengirim.kirim(pesan);
    }
}

Output yang Dihasilkan:

text
== Perakitan manual di dalam kelas (keterikatan erat) ==
[Email] Rapat sprint besok pukul 09.00

== Constructor injection: dependensi disuntikkan dari luar ==
[Email] Rapat sprint besok pukul 09.00
[SMS] Rapat sprint dibatalkan

== Ganti implementasi tanpa menyentuh LayananRapat ==
[Log] Notifikasi hanya untuk log internal

Mari kita bedah bagian pentingnya:

  1. class LayananRapatKaku: kelas ini membuat PengirimEmail sendiri lewat new di dalam method. Konsekuensinya, mengirim lewat SMS menuntut perubahan kode, dan menguji LayananRapatKaku terpisah dari email tidak mungkin dilakukan.
  2. interface PengirimPesan: kontrak pengiriman ditarik keluar sebagai abstraction, sehingga LayananRapat hanya mengenal antarmuka, bukan implementasi konkret.
  3. LayananRapat(PengirimPesan pengirim): constructor menjadi injection point. Siapa pun — kode utama di sini, container Spring di aplikasi nyata — menyuntikkan implementasi yang diinginkan.
  4. private final PengirimPesan pengirim: kata kunci final menjamin dependensi tidak ditukar setelah objek selesai dibangun, satu praktik yang juga dianjurkan Spring lewat constructor injection.

Bentuk nyatanya di Spring cukup ringkas — berikut potongan yang ditampilkan apa adanya:

java
@Service
public class NotifikasiService {

    private final PengirimPesan pengirim;

    public NotifikasiService(PengirimPesan pengirim) {
        this.pengirim = pengirim;
    }

    public void pesananBaru(String pembeli, String produk) {
        pengirim.kirim("Pesanan " + pembeli + ": " + produk);
    }
}

@Component
public class PengirimEmail implements PengirimPesan {
    @Override
    public void kirim(String pesan) {
        // kirim email melalui SMTP
    }
}

Anotasi @Component dan turunannya @Service menandai kelas sebagai kandidat bean; container Spring memindai paket, menemukan PengirimEmail, lalu menyuntikkannya ke konstruktor NotifikasiService tanpa satu baris perakitan manual. Detail mekanisme penyambungan ini kita bahas tuntas pada Chapter Dependency Injection dengan Spring berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

KeterikatanVsInjeksi.java

Unduh berkas
java
public class KeterikatanVsInjeksi {

    public static void main(String[] args) {
        System.out.println("== Perakitan manual di dalam kelas (keterikatan erat) ==");
        LayananRapatKaku kaku = new LayananRapatKaku();
        kaku.kirim("Rapat sprint besok pukul 09.00");

        System.out.println();
        System.out.println("== Constructor injection: dependensi disuntikkan dari luar ==");
        LayananRapat viaEmail = new LayananRapat(new PengirimEmail());
        viaEmail.kirim("Rapat sprint besok pukul 09.00");

        LayananRapat viaSms = new LayananRapat(new PengirimSms());
        viaSms.kirim("Rapat sprint dibatalkan");

        System.out.println();
        System.out.println("== Ganti implementasi tanpa menyentuh LayananRapat ==");
        LayananRapat viaLog = new LayananRapat(new PengirimLog());
        viaLog.kirim("Notifikasi hanya untuk log internal");
    }
}

// Versi terikat erat: kelas merakit dependensinya sendiri dengan new,
// sehingga PengirimEmail tertanam dan sulit diganti atau diuji terpisah.
class LayananRapatKaku {

    void kirim(String pesan) {
        PengirimEmail pengirim = new PengirimEmail();
        pengirim.kirim(pesan);
    }
}

interface PengirimPesan {
    void kirim(String pesan);
}

class PengirimEmail implements PengirimPesan {
    @Override
    public void kirim(String pesan) {
        System.out.println("[Email] " + pesan);
    }
}

class PengirimSms implements PengirimPesan {
    @Override
    public void kirim(String pesan) {
        System.out.println("[SMS] " + pesan);
    }
}

class PengirimLog implements PengirimPesan {
    @Override
    public void kirim(String pesan) {
        System.out.println("[Log] " + pesan);
    }
}

// Versi lentur: dependensi masuk lewat konstruktor (injection point)
// dan disimpan final agar tidak bisa ditukar di tengah jalan.
class LayananRapat {

    private final PengirimPesan pengirim;

    LayananRapat(PengirimPesan pengirim) {
        this.pengirim = pengirim;
    }

    void kirim(String pesan) {
        pengirim.kirim(pesan);
    }
}