Lompat ke konten
dokumentasi.id
Commit 43aad26
v1
Star1,1 rb

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

3

Konfigurasi dan Bootstrap

Seluruh nilai yang dapat diubah tanpa menyentuh kode berada di satu berkas, config.json. Paket internal/config membacanya lewat Viper, lalu mengubah setiap bagian menjadi objek yang siap dipakai: logger, koneksi database, validator, instance Fiber, dan Kafka producer. Setelah itu config.Bootstrap merakit objek-objek tersebut menjadi satu graph dependency yang utuh. Halaman ini membahas kedua tahap itu dan urutannya; isi tiap lapisan yang dirakit dijelaskan di Lapisan Repository, Lapisan Usecase, Model, dan Converter, dan Delivery HTTP dan Middleware.

Membaca config.json lewat Viper

NewViper membuat instance Viper baru, menetapkan nama berkas config dengan tipe json, lalu mendaftarkan dua direktori pencarian: ./../ dan ./. Berkas dibaca lewat config.ReadInConfig(). Jika pembacaan gagal, fungsi ini tidak mengembalikan error melainkan memanggil panic(fmt.Errorf("Fatal error config file: %w \n", err)) internal/config/viper.go:11-24.

Berkas config.json yang dibaca memuat empat kelompok nilai: app.name, web (prefork dan port), log.level, database (kredensial, nama database, dan pool), serta kafka (bootstrap.servers, group.id, auto.offset.reset, producer.enabled) config.json:1-39. Viper mengaksesnya dengan key bertitik, misalnya database.pool.idle atau kafka.auto.offset.reset, sehingga struktur bertingkat di JSON tidak perlu diratakan. Tidak ada pemanggilan BindEnv maupun AutomaticEnv di NewViper, jadi satu-satunya sumber konfigurasi adalah berkas tersebut.

⚠️ NewViper mencari config.json secara relatif terhadap direktori kerja proses, bukan terhadap lokasi binary. Menjalankan binary dari luar akar repository akan gagal membaca berkas dan langsung memicu panic dari internal/config/viper.go:20-22. Kemungkinan penggantian sumber konfigurasi (env file, Consul, etcd) memang sudah dicatat sebagai komentar di berkas itu.

Urutan pembuatan komponen di entrypoint

cmd/web/main.go membuat komponen infrastruktur satu per satu sebelum merakit apa pun. Urutannya penting karena dua constructor menerima hasil constructor lain: NewDatabase menerima log, dan semua constructor menerima viperConfig cmd/web/main.go:8-30.

graph LR
  cfg["config.json"] --> v["NewViper"]
  v -->|"log.level"| l["NewLogger"]
  v -->|"database.*"| d["NewDatabase"]
  v -->|"app.name, web.prefork"| f["NewFiber"]
  v -->|"kafka.*"| p["NewKafkaProducer"]
  v -->|"tanpa key"| val["NewValidator"]
  l --> boot["Bootstrap"]
  d --> boot
  f --> boot
  p --> boot
  val --> boot
  style cfg fill:#f59e0b,color:#000
  style v fill:#f59e0b,color:#000
  style d fill:#336791,color:#fff
  style p fill:#8b5cf6,color:#fff
  style l fill:#f59e0b,color:#000
  style f fill:#f59e0b,color:#000
  style val fill:#f59e0b,color:#000
  style boot fill:#2d7d46,color:#fff
%% fig: Konfigurasi menuju constructor infrastruktur
%% desc: Satu instance Viper memberi key ke setiap constructor, lalu kelima objek hasilnya diserahkan ke Bootstrap.
Fig. 1: Konfigurasi menuju constructor infrastrukturSatu instance Viper memberi key ke setiap constructor, lalu kelima objek hasilnya diserahkan ke Bootstrap.

Apa yang dihasilkan setiap constructor

Setiap berkas di internal/config menangani satu jenis dependency. Tabel berikut merangkum key yang dibaca, objek yang dihasilkan, dan perilaku saat gagal, sesuai kode masing-masing fungsi.

FungsiKey config yang dibacaHasilPerilaku saat gagal
NewViperberkas config.json (SetConfigName("config"), SetConfigType("json"))*viper.Viperpanic dengan pesan Fatal error config file
NewLoggerlog.level*logrus.Logger dengan level dari config dan logrus.JSONFormattertidak ada jalur gagal
NewDatabasedatabase.username, password, host, port, name, pool.idle, pool.max, pool.lifetime*gorm.DBlog.Fatalf("failed to connect database: %v", err)
NewValidatortidak ada (parameter viper tidak dipakai)*validator.Validate dari validator.New()tidak ada jalur gagal
NewFiberapp.name, web.prefork*fiber.App dengan NewErrorHandler()tidak ada jalur gagal
NewKafkaProducerkafka.producer.enabled, kafka.bootstrap.serverssarama.SyncProducer, atau nil bila fitur dimatikanlog.Fatalf("Failed to create producer: %v", err)
NewKafkaConsumerGroupkafka.auto.offset.reset, kafka.bootstrap.servers, kafka.group.idsarama.ConsumerGrouplog.Fatalf("Failed to create consumer group: %v", err)

NewLogger memakai logrus.Level(viper.GetInt32("log.level")) sehingga nilai 6 di config.json langsung menjadi level logrus, dan seluruh keluaran log berbentuk JSON internal/config/logrus.go:8-15. NewValidator tetap menerima *viper.Viper demi keseragaman signature, tetapi tidak membaca satu key pun internal/config/validator.go:8-10.

NewDatabase menyusun DSN username:password@tcp(host:port)/name?charset=utf8mb4&parseTime=True&loc=Local dari key database.*, lalu membuka koneksi MySQL lewat GORM internal/config/gorm.go:14-49. Logger GORM diarahkan ke logrusWriter yang memanggil l.Logger.Tracef(message, args...), sehingga query hanya tampil ketika level log mencapai trace internal/config/gorm.go:51-57. Konfigurasi logger GORM-nya memakai SlowThreshold: time.Second * 5, IgnoreRecordNotFoundError: true, dan ParameterizedQueries: true internal/config/gorm.go:26-34. Setelah koneksi terbuka, tiga angka dari database.pool diterapkan: SetMaxIdleConns(10), SetMaxOpenConns(100), dan SetConnMaxLifetime(time.Second * 300) — nilainya berasal dari database.pool.idle, database.pool.max, dan database.pool.lifetime config.json:18-22.

NewFiber hanya mengambil dua nilai: AppName dari app.name dan Prefork dari web.prefork. ErrorHandler diisi NewErrorHandler(), yang mengubah error menjadi response JSON berbentuk {"errors": err.Error()} dengan status 500 kecuali error itu bertipe *fiber.Error internal/config/fiber.go:8-16, internal/config/fiber.go:18-29. Port tidak dibaca di sini; web.port baru dipakai saat app.Listen dipanggil di entrypoint cmd/web/main.go:25-29.

NewKafkaConsumerGroup menerjemahkan kafka.auto.offset.reset: nilai earliest menjadi sarama.OffsetOldest, nilai lain menjadi sarama.OffsetNewest. Daftar broker dipecah dengan strings.Split(..., ",") sehingga satu key dapat memuat beberapa alamat internal/config/kafka.go:11-30. NewKafkaProducer lebih dulu memeriksa kafka.producer.enabled; bila false, ia menulis log Kafka producer is disabled dan mengembalikan nil tanpa mencoba koneksi. Bila aktif, producer dibuat dengan Producer.Return.Successes = true, Producer.RequiredAcks = sarama.WaitForAll, dan Producer.Retry.Max = 3 internal/config/kafka.go:32-50.

⚠️ Pada config.json di repository ini, kafka.producer.enabled bernilai false dan kafka.producer hanya berisi key itu config.json:36-38. Akibatnya NewKafkaProducer mengembalikan nil, Bootstrap melewati seluruh pembuatan producer, dan use case menerima producer nil internal/config/app.go:39-43. Rangkaian pengiriman event yang bergantung pada producer dijelaskan di Messaging Kafka.

Bootstrap merakit seluruh dependency

BootstrapConfig adalah satu struct berisi enam field yang harus sudah siap sebelum perakitan: DB, App, Log, Validate, Config, dan Producer internal/config/app.go:19-26. Bootstrap kemudian berjalan dalam empat langkah berurutan, dan setiap langkah memakai hasil langkah sebelumnya internal/config/app.go:28-66.

graph TD
  boot["Bootstrap"] --> repo["repository.NewUserRepository / NewContactRepository / NewAddressRepository"]
  boot --> prod["gateway/messaging.NewUserProducer / NewContactProducer / NewAddressProducer"]
  repo --> uc["usecase.NewUserUseCase / NewContactUseCase / NewAddressUseCase"]
  prod --> uc
  uc --> ctrl["http.NewUserController / NewContactController / NewAddressController"]
  uc --> mw["middleware.NewAuth"]
  ctrl --> rc["route.RouteConfig"]
  mw --> rc
  rc --> setup["routeConfig.Setup()"]
  style boot fill:#2d7d46,color:#fff
  style repo fill:#336791,color:#fff
  style prod fill:#8b5cf6,color:#fff
  style uc fill:#2d7d46,color:#fff
  style ctrl fill:#2d7d46,color:#fff
  style mw fill:#f59e0b,color:#000
  style rc fill:#f59e0b,color:#000
  style setup fill:#2d7d46,color:#fff
%% fig: Perakitan dependency di dalam Bootstrap
%% desc: Bootstrap membuat repository, producer, use case, controller, dan middleware, lalu RouteConfig.Setup mendaftarkan seluruh route.
Fig. 2: Perakitan dependency di dalam BootstrapBootstrap membuat repository, producer, use case, controller, dan middleware, lalu RouteConfig.Setup mendaftarkan seluruh route.

Urutan node di diagram itu mengikuti urutan pemanggilan di kode. Repository dibuat lebih dulu karena hanya membutuhkan config.Log. Producer dibuat di dalam satu blok if config.Producer != nil, sehingga ketiga variabel producer tetap nil bila objek dari NewKafkaProducer bernilai nil. Use case menerima kombinasi config.DB, config.Log, config.Validate, repository, dan producer; NewAddressUseCase menerima dua repository sekaligus, yaitu contactRepository dan addressRepository. Controller dibuat dari use case dan logger, middleware NewAuth dibuat dari userUseCase karena middleware itu membutuhkan pemeriksaan token, lalu seluruhnya dimasukkan ke route.RouteConfig bersama config.App sebelum routeConfig.Setup() dipanggil internal/config/app.go:28-66. Rincian isi controller dan route ada di Delivery HTTP dan Middleware.

Satu catatan kecil: field Config *viper.Viper tersedia di BootstrapConfig dan selalu diisi pemanggil, tetapi Bootstrap sendiri tidak pernah membacanya; semua kebutuhan konfigurasi sudah terwakili oleh objek yang dirakit sebelumnya internal/config/app.go:19-26, internal/config/app.go:28-66.

Dua entrypoint, satu Bootstrap

cmd/web/main.go dan helper test/init.go memanggil urutan yang sama persis: Viper, logger, validator, Fiber, database, producer, lalu config.Bootstrap dengan enam field yang identik. Perbedaannya hanya posisi pemanggilan NewValidator/NewFiber terhadap NewDatabase, yang tidak mengubah hasil karena antar fungsi itu tidak saling bergantung cmd/web/main.go:8-30, test/init.go:23-38. Karena test memakai Bootstrap yang sama, seluruh use case dan route pada test identik dengan yang dipakai server sungguhan. Cara menjalankan server dan test dijelaskan di Menjalankan Aplikasi, Migrasi, dan Test.

cmd/worker/main.go tidak memakai Bootstrap sama sekali. Entrypoint itu hanya membuat viperConfig dan logger, lalu menjalankan tiga goroutine consumer; setiap goroutine membuat sarama.ConsumerGroup sendiri lewat NewKafkaConsumerGroup cmd/worker/main.go:16-25. Artinya konfigurasi yang sama (kafka.bootstrap.servers, kafka.group.id) dibaca ulang tiga kali, satu kali per topik.

⚠️ NewDatabase dan NewKafkaProducer sama-sama memakai log.Fatalf saat koneksi gagal internal/config/gorm.go:35-37, internal/config/kafka.go:45-48. Kegagalan database atau broker langsung menghentikan proses tanpa retry, jadi proses harus dijalankan ulang oleh supervisor. Pastikan nilai database.host, database.port, dan kafka.bootstrap.servers benar sebelum start; nilai bawaannya menunjuk ke localhost config.json:12-26.