Bab sebelumnya kita sudah lihat struktur project dan titik masuk OrderServiceApplication.kt. Sekarang kita bongkar konsep yang jadi jantung Spring — dan yang membuat framework ini terasa "berbeda" dibanding menulis backend manual: dependency injection.
Bean dan IoC Container
Di Spring, sebuah bean adalah object yang siklus hidupnya (pembuatan, penyimpanan, penghubungan dengan object lain) dikelola oleh Spring, bukan oleh kode kita secara manual. Kalau biasanya di kode tanpa framework kamu menulis val service = OrderService(OrderRepository()) sendiri, di Spring kamu cukup mendeklarasikan bahwa OrderService butuh OrderRepository, dan Spring — lewat IoC container (Inversion of Control, kontrol pembuatan object dibalik dari kode kita ke framework) — yang akan membuat instance-nya, menyuntikkan dependency yang dibutuhkan, dan menyimpannya supaya bisa dipakai ulang di tempat lain.
Kenapa ini penting? Karena begitu jumlah kelas yang saling bergantung membesar (controller butuh service, service butuh repository, repository butuh koneksi database, dst), merangkai semuanya secara manual jadi pekerjaan berulang dan rawan salah urutan. IoC container membebaskan kita dari itu — kita cukup deklarasikan "aku butuh X", dan Spring yang mencari cara menyediakannya.
Constructor Injection: Gaya Idiomatis di Kotlin
Ada beberapa cara Spring bisa menyuntikkan dependency: lewat field (@Autowired lateinit var), lewat setter, atau lewat constructor. Di Kotlin, constructor injection adalah pilihan yang jauh lebih idiomatis dibanding dua yang lain, dan bukan cuma soal selera:
- Immutability — dependency yang disuntikkan lewat constructor bisa langsung ditandai
val, bukanvaryang bisa diubah-ubah setelah object dibuat. Field injection (lateinit var) memaksa kita pakaivardanlateinit, yang berarti ada jendela waktu di mana object itu ada tapi dependency-nya belum terisi — potensiNullPointerExceptionkalau ada kode yang mengakses lebih awal dari seharusnya. - Testability — kelas dengan constructor injection bisa dites dengan cara paling sederhana: panggil constructor-nya langsung dengan mock/fake, tanpa perlu menjalankan Spring context sama sekali. Field injection butuh reflection atau helper testing khusus untuk mengisi field private itu.
- Eksplisit — dependency yang dibutuhkan sebuah kelas langsung terlihat di satu tempat: parameter constructor-nya. Tidak perlu scroll ke bawah mencari field mana saja yang dianotasi
@Autowired.
Kita akan lihat pola ini konsisten di seluruh project — termasuk dua kelas kecil yang jadi fokus bab ini.
AppInfoProperties.kt — Configuration Properties yang Type-Safe
package com.indokoding.orderservice.config
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties(prefix = "app.info")
data class AppInfoProperties(
val name: String,
val description: String
)
Kelas ini merepresentasikan sekelompok nilai konfigurasi dari application.yml, dengan prefix app.info. Bandingkan dengan application.yml:
app:
info:
name: Order Service
description: Service pengelolaan order - seri Belajar Kotlin Backend untuk Microservice
Anotasi @ConfigurationProperties(prefix = "app.info") memberi tahu Spring Boot: cari semua key di bawah app.info di file konfigurasi (yaml, properties, environment variable — semuanya kompatibel), lalu petakan ke property data class ini berdasarkan nama. app.info.name masuk ke name, app.info.description masuk ke description. Kita mendapat class yang type-safe — kalau name didefinisikan String, Spring akan gagal start kalau isinya tidak bisa dipetakan ke String, dan IDE bisa memberi autocomplete + type checking saat kita pakai object ini di tempat lain.
Ini alternatif yang jauh lebih rapi dibanding menyebar @Value("\${app.info.name}") satu-satu di setiap kelas yang butuh nilai itu — sekali konfigurasi berkelompok berubah strukturnya, kita cukup ubah satu data class, bukan mencari-cari setiap @Value yang tersebar.
Kenapa Butuh @ConfigurationPropertiesScan?
AppInfoProperties sendiri tidak dianotasi @Component atau sejenisnya — dia tidak otomatis jadi bean hanya karena dianotasi @ConfigurationProperties. Di sinilah @ConfigurationPropertiesScan di OrderServiceApplication.kt berperan:
@SpringBootApplication
@ConfigurationPropertiesScan
class OrderServiceApplication
Anotasi ini memberi tahu Spring Boot: scan seluruh package (dan sub-package) dari kelas ini ke bawah, cari semua kelas yang dianotasi @ConfigurationProperties, lalu daftarkan otomatis sebagai bean. Tanpa baris ini, kita harus mendaftarkan AppInfoProperties secara manual satu-satu lewat @EnableConfigurationProperties(AppInfoProperties::class) di setiap kelas konfigurasi yang memakainya — merepotkan begitu jumlah kelas properties bertambah seiring project membesar.
PingController.kt — Constructor Injection dalam Praktik
package com.indokoding.orderservice.config
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class PingController(private val appInfo: AppInfoProperties) {
@GetMapping("/api/ping")
fun ping(): Map<String, String> = mapOf(
"status" to "ok",
"service" to appInfo.name,
"description" to appInfo.description
)
}
Perhatikan class PingController(private val appInfo: AppInfoProperties) — inilah constructor injection yang kita bahas di atas, dalam bentuk paling ringkas. Tidak ada @Autowired sama sekali di sini; sejak Spring 4.3, kalau sebuah kelas cuma punya satu constructor, Spring otomatis tahu itu constructor yang harus dipakai untuk injection, tanpa anotasi tambahan. appInfo langsung tersedia sebagai val yang tidak akan pernah null atau berubah setelah PingController dibuat — persis manfaat immutability yang kita bahas sebelumnya.
Method ping() di-mapping ke GET /api/ping lewat @GetMapping, mengembalikan Map<String, String> — Spring (lewat Jackson) otomatis mengubahnya jadi response JSON tanpa kita perlu menulis serialisasi manual. Isinya membaca langsung dari appInfo.name dan appInfo.description, yang nilainya berasal dari application.yml lewat alur yang baru saja kita telusuri.
Coba Sendiri
Jalankan aplikasinya (./gradlew bootRun), lalu panggil endpoint ini — ingat, order-service jalan di port 8082, sesuai server.port di application.yml, bukan port default Spring Boot (8080):
curl http://localhost:8082/api/ping
Kalau semua terhubung dengan benar, responsnya berupa JSON berisi status: ok beserta name dan description yang sama persis dengan yang kita tulis di application.yml. Endpoint sesederhana ini sengaja jadi contoh pertama karena merangkum tiga hal sekaligus: dependency injection lewat constructor, binding konfigurasi type-safe, dan controller paling minimal. Bab berikutnya kita perluas ke routing yang lebih kompleks — controller yang benar-benar menangani resource Order.
Bagian dari Series: Kotlin Backend Microservice - Belajar Fundamental sampai Microservice Nyata
Belajar Kotlin + Spring Boot dari fundamental bahasa (null safety, OOP, coroutines) sampai membangun sistem microservice sungguhan - REST API, Postgre...
Lihat Series Lengkap