์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿน Go์–ธ์–ด ์„ค์ • ๊ด€๋ฆฌ์˜ ๋ชจ๋“  ๊ฒƒ โ€” Viper๋กœ ๊ตฌ์ถ•ํ•˜๋Š” ํ™˜๊ฒฝ๋ณ„ ์™„๋ฒฝํ•œ ์„ค์ • ์‹œ์Šคํ…œ

๐Ÿน Go์–ธ์–ด ์„ค์ • ๊ด€๋ฆฌ์˜ ๋ชจ๋“  ๊ฒƒ โ€” Viper๋กœ ๊ตฌ์ถ•ํ•˜๋Š” ํ™˜๊ฒฝ๋ณ„ ์™„๋ฒฝํ•œ ์„ค์ • ์‹œ์Šคํ…œ

โš™๏ธ Config ยท Environment ยท Viper ยท Go ยท ์‹ค์ „ ์•„ํ‚คํ…์ฒ˜

์•ผ, ์†”์งํžˆ ๋งํ•ด๋ด. ๊ฐœ๋ฐœํ•˜๋‹ค๊ฐ€ ์ด๋Ÿฐ ์ƒํ™ฉ ํ•œ ๋ฒˆ์ฏค ๊ฒช์–ด๋ดค์ง€? ๐Ÿค”

๋กœ์ปฌ์—์„œ๋Š” ์ž˜ ๋Œ์•„๊ฐ€๋˜ ์ฝ”๋“œ๊ฐ€ ๋ฐฐํฌ ์„œ๋ฒ„์— ์˜ฌ๋ฆฌ๋Š” ์ˆœ๊ฐ„ "DB ์—ฐ๊ฒฐ ์‹คํŒจ" ๋œจ๋ฉด์„œ ๋ฉ˜ํƒˆ์ด ๋‚˜๊ฐ€๋Š” ๊ทธ ์ˆœ๊ฐ„... ๐Ÿ˜ญ
์•„๋‹ˆ๋ฉด ๊ฐœ๋ฐœํŒ€ ๋™๋ฃŒ๊ฐ€ "์•ผ, ๋„ค ์ฝ”๋“œ์— DB ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ•˜๋“œ์ฝ”๋”ฉ ๋˜์–ด์žˆ์–ด!" ๋ผ๊ณ  ์Šฌ๋ž™์œผ๋กœ DM ๋‚ ๋ฆฌ๋Š” ๊ทธ ์•„์ฐ”ํ•œ ๊ฒฝํ—˜...

์ด๊ฒŒ ๋‹ค ์„ค์ • ๊ด€๋ฆฌ(Configuration Management)๋ฅผ ์ œ๋Œ€๋กœ ์•ˆ ํ•ด์„œ ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ์•ผ. Go ์–ธ์–ด๋กœ ์‹ค๋ฌด ํ”„๋กœ์ ํŠธ๋ฅผ ๊ฐœ๋ฐœํ•  ๋•Œ ํ™˜๊ฒฝ๋ณ„ ์„ค์ •์„ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๋А๋ƒ๋Š” ์ง„์งœ ์ค‘์š”ํ•œ ๋ฌธ์ œ๊ฑฐ๋“ .

์˜ค๋Š˜์€ Go ์ƒํƒœ๊ณ„์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์“ฐ์ด๋Š” ์„ค์ • ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ Viper๋ฅผ ์ค‘์‹ฌ์œผ๋กœ, ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ์‹œ์Šคํ…œ์„ ์ œ๋Œ€๋กœ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์™„์ „ ์นœ์ ˆํ•˜๊ฒŒ ํŒŒํ—ค์ณ๋ณผ๊ฒŒ! ๐Ÿš€

Go๋ฅผ ์ฒ˜์Œ ์ ‘ํ•˜๋Š” ๋ถ„๋ถ€ํ„ฐ ์‹ค๋ฌด์—์„œ ์ด๋ฏธ ์“ฐ๊ณ  ์žˆ๋Š” ๋ถ„๊นŒ์ง€ ๋ชจ๋‘ ๋„์›€์ด ๋  ๋‚ด์šฉ์ด๋‹ˆ๊นŒ ๋๊นŒ์ง€ ๋”ฐ๋ผ์™€์ค˜! ๐Ÿ˜Ž


Go ์„ค์ • ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ๐ŸŒ ํ™˜๊ฒฝ๋ณ€์ˆ˜ OS Environment ๐Ÿ“„ YAML/JSON Config Files ๐Ÿšฉ CLI ํ”Œ๋ž˜๊ทธ Command Flags โ˜๏ธ ์›๊ฒฉ ์„ค์ • etcd / Consul ๐Ÿ VIPER ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ํ†ตํ•ฉ ์„ค์ • ๊ด€๋ฆฌ ์—”์ง„ ๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ Development ๐Ÿงช ์Šคํ…Œ์ด์ง• Staging ๐Ÿš€ ํ”„๋กœ๋•์…˜ Production โšก ์šฐ์„ ์ˆœ์œ„ (๋†’์Œ โ†’ ๋‚ฎ์Œ) ๋ช…์‹œ์  ์„ค์ • > ํ™˜๊ฒฝ๋ณ€์ˆ˜ > ์„ค์ •ํŒŒ์ผ > ๊ธฐ๋ณธ๊ฐ’ explicit > env > config > default
๐Ÿค” ์™œ ์„ค์ • ๊ด€๋ฆฌ๊ฐ€ ์ด๋ ‡๊ฒŒ ์ค‘์š”ํ•œ ๊ฑฐ์•ผ?

๋จผ์ € ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ์žก๊ณ  ๊ฐ€์ž. ์„ค์ • ๊ด€๋ฆฌ(Configuration Management)๋ž€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹คํ–‰๋˜๋Š” ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง€๋Š” ๊ฐ’๋“ค์„ ์ฝ”๋“œ์™€ ๋ถ„๋ฆฌํ•ด์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๋งํ•ด.

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๋„ˆ์˜ Go ์•ฑ์ด ์ด๋Ÿฐ ์ •๋ณด๋“ค์„ ํ•„์š”๋กœ ํ•œ๋‹ค๊ณ  ํ•ด๋ด:

๐Ÿ“‹ ์•ฑ์ด ํ•„์š”๋กœ ํ•˜๋Š” ์„ค์ •๊ฐ’๋“ค์˜ ์˜ˆ์‹œ ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: ํ˜ธ์ŠคํŠธ, ํฌํŠธ, ์‚ฌ์šฉ์ž๋ช…, ๋น„๋ฐ€๋ฒˆํ˜ธ, DB๋ช…
๐ŸŒ ์„œ๋ฒ„: ๋ฆฌ์Šค๋‹ ํฌํŠธ, ํƒ€์ž„์•„์›ƒ, ์ตœ๋Œ€ ์—ฐ๊ฒฐ ์ˆ˜
๐Ÿ”‘ ์ธ์ฆ: JWT ์‹œํฌ๋ฆฟ ํ‚ค, OAuth ํด๋ผ์ด์–ธํŠธ ID/Secret
๐Ÿ“ง ์™ธ๋ถ€ ์„œ๋น„์Šค: ์ด๋ฉ”์ผ ์„œ๋ฒ„, SMS API ํ‚ค, ๊ฒฐ์ œ ๊ฒŒ์ดํŠธ์›จ์ด
๐Ÿ“Š ๋กœ๊น…: ๋กœ๊ทธ ๋ ˆ๋ฒจ, ๋กœ๊ทธ ํŒŒ์ผ ๊ฒฝ๋กœ
๐Ÿšฆ ๊ธฐ๋Šฅ ํ”Œ๋ž˜๊ทธ: A/B ํ…Œ์ŠคํŠธ, ์ƒˆ ๊ธฐ๋Šฅ ํ™œ์„ฑํ™” ์—ฌ๋ถ€

์ด ๊ฐ’๋“ค์ด ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ, ํ…Œ์ŠคํŠธ ์„œ๋ฒ„, ํ”„๋กœ๋•์…˜ ์„œ๋ฒ„์—์„œ ๋ชจ๋‘ ๋‹ฌ๋ผ. ๋กœ์ปฌ์—์„œ๋Š” localhost:5432์— DB๊ฐ€ ์žˆ์ง€๋งŒ, ํ”„๋กœ๋•์…˜์—์„œ๋Š” AWS RDS ์—”๋“œํฌ์ธํŠธ๋ฅผ ์จ์•ผ ํ•˜์ž–์•„.

๋งŒ์•ฝ ์ด๊ฑธ ์ฝ”๋“œ์— ์ง์ ‘ ๋ฐ•์•„๋„ฃ์œผ๋ฉด (ํ•˜๋“œ์ฝ”๋”ฉ) ์–ด๋–ป๊ฒŒ ๋ ๊นŒ? ๐Ÿคฆ

โš ๏ธ ํ•˜๋“œ์ฝ”๋”ฉ์˜ ์žฌ์•™๋“ค 1. ํ™˜๊ฒฝ๋งˆ๋‹ค ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•จ โ†’ ์‹ค์ˆ˜ ์œ ๋ฐœ
2. ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ Git์— ์˜ฌ๋ผ๊ฐ โ†’ ๋ณด์•ˆ ์‚ฌ๊ณ 
3. ๋ฐฐํฌํ•  ๋•Œ๋งˆ๋‹ค ์ฝ”๋“œ ์žฌ๋นŒ๋“œ ํ•„์š” โ†’ ๋น„ํšจ์œจ
4. ํŒ€์›๋“ค์ด ๊ฐ์ž ๋‹ค๋ฅธ ์„ค์ •์œผ๋กœ ๊ฐœ๋ฐœ โ†’ ์ถฉ๋Œ ๋ฐœ์ƒ

๊ทธ๋ž˜์„œ ๋“ฑ์žฅํ•œ ๊ฒŒ ๋ฐ”๋กœ 12-Factor App ๋ฐฉ๋ฒ•๋ก ์ด์•ผ. ์ด ๋ฐฉ๋ฒ•๋ก ์˜ 3๋ฒˆ์งธ ์›์น™์ด ๋ฐ”๋กœ "์„ค์ •์„ ํ™˜๊ฒฝ์— ์ €์žฅํ•˜๋ผ(Store config in the environment)"์•ผ. ์ฝ”๋“œ์™€ ์„ค์ •์„ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•˜๋ผ๋Š” ๊ฑฐ์ง€.

Go ์ƒํƒœ๊ณ„์—์„œ ์ด ์›์น™์„ ๊ฐ€์žฅ ์šฐ์•„ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฒŒ ๋ฐ”๋กœ Viper์•ผ! ๐Ÿ


๐Ÿ Viper๊ฐ€ ๋ญ”๋ฐ? ์™œ ์ด๋ ‡๊ฒŒ ์œ ๋ช…ํ•ด?

Viper๋Š” Go ์–ธ์–ด๋ฅผ ์œ„ํ•œ ์™„์ „ํ•œ ์„ค์ • ์†”๋ฃจ์…˜์ด์•ผ. spf13(Steve Francia, ์ „ Docker/MongoDB ๊ฐœ๋ฐœ์ž)์ด ๋งŒ๋“ค์—ˆ๊ณ , GitHub์—์„œ ์ˆ˜๋งŒ ๊ฐœ์˜ ์Šคํƒ€๋ฅผ ๋ฐ›์€ Go ์ƒํƒœ๊ณ„์˜ ๋Œ€ํ‘œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ๐ŸŒŸ

Viper๊ฐ€ ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ๋“ค์„ ๋ณด๋ฉด ์™œ ์ด๋ ‡๊ฒŒ ์ธ๊ธฐ ์žˆ๋Š”์ง€ ๋ฐ”๋กœ ์ดํ•ด๊ฐ€ ๋ผ:

๊ธฐ๋Šฅ ์„ค๋ช… ์ง€์› ์—ฌ๋ถ€
๐Ÿ“„ ์„ค์ • ํŒŒ์ผ ์ฝ๊ธฐ JSON, TOML, YAML, HCL, envfile, Java properties โœ… ์ง€์›
๐ŸŒ ํ™˜๊ฒฝ๋ณ€์ˆ˜ OS ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ž๋™ ๋ฐ”์ธ๋”ฉ โœ… ์ง€์›
๐Ÿšฉ CLI ํ”Œ๋ž˜๊ทธ pflag ํŒจํ‚ค์ง€์™€ ํ†ตํ•ฉ โœ… ์ง€์›
โ˜๏ธ ์›๊ฒฉ ์„ค์ • etcd, Consul, Firestore โœ… ์ง€์›
๐Ÿ”„ ์‹ค์‹œ๊ฐ„ ๊ฐ์‹œ ์„ค์ • ํŒŒ์ผ ๋ณ€๊ฒฝ ๊ฐ์ง€ (fsnotify) โœ… ์ง€์›
๐ŸŽฏ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • SetDefault๋กœ ํด๋ฐฑ ๊ฐ’ ์ง€์ • โœ… ์ง€์›
๐Ÿ”’ ์•”ํ˜ธํ™” ์›๊ฒฉ ์„ค์ • ์•”ํ˜ธํ™” ์ง€์› โœ… ์ง€์›

ํŠนํžˆ Viper์˜ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ํŠน์ง•์€ ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ์„ค์ • ๋ณ‘ํ•ฉ์ด์•ผ. ์—ฌ๋Ÿฌ ์†Œ์Šค์—์„œ ์„ค์ •์„ ์ฝ์–ด์˜ฌ ๋•Œ ์ž๋™์œผ๋กœ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ ์šฉํ•ด์ค˜:

๐Ÿ† Viper ์„ค์ • ์šฐ์„ ์ˆœ์œ„ (๋†’์Œ โ†’ ๋‚ฎ์Œ) 1๏ธโƒฃ explicit call to Set โ€” viper.Set()์œผ๋กœ ์ง์ ‘ ์„ค์ •ํ•œ ๊ฐ’
2๏ธโƒฃ flag โ€” CLI ํ”Œ๋ž˜๊ทธ๋กœ ์ „๋‹ฌ๋œ ๊ฐ’
3๏ธโƒฃ env โ€” ํ™˜๊ฒฝ๋ณ€์ˆ˜
4๏ธโƒฃ config โ€” ์„ค์ • ํŒŒ์ผ
5๏ธโƒฃ key/value store โ€” etcd, Consul ๋“ฑ ์›๊ฒฉ ์ €์žฅ์†Œ
6๏ธโƒฃ default โ€” SetDefault()๋กœ ์ง€์ •ํ•œ ๊ธฐ๋ณธ๊ฐ’

์ด๊ฒŒ ์™œ ์ข‹๋ƒ๋ฉด, ์˜ˆ๋ฅผ ๋“ค์–ด ์„ค์ • ํŒŒ์ผ์— DB ํฌํŠธ๋ฅผ 5432๋กœ ์„ค์ •ํ•ด๋’€์–ด๋„, ๋ฐฐํฌํ•  ๋•Œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ DB_PORT=5433์„ ์ฃผ๋ฉด ์ž๋™์œผ๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์ด ์šฐ์„  ์ ์šฉ๋ผ. ์ฝ”๋“œ ์ˆ˜์ • ์—†์ด! ๐ŸŽ‰


๐Ÿš€ Viper ์„ค์น˜ํ•˜๊ณ  ์ฒซ ๋ฐœ ๋‚ด๋”›๊ธฐ

์ž, ์ด์ œ ์‹ค์ œ๋กœ ์จ๋ณด์ž! ๋จผ์ € Go ๋ชจ๋“ˆ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  Viper๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•ด.

1
Go ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
mkdir my-go-app
cd my-go-app
go mod init github.com/yourname/my-go-app
2
Viper ์„ค์น˜
go get github.com/spf13/viper
3
๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ• ํ™•์ธ
package main

import (
    "fmt"
    "github.com/spf13/viper"
)

func main() {
    // ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
    viper.SetDefault("server.port", 8080)
    viper.SetDefault("server.host", "localhost")
    viper.SetDefault("database.name", "mydb")

    // ๊ฐ’ ์ฝ๊ธฐ
    port := viper.GetInt("server.port")
    host := viper.GetString("server.host")
    dbName := viper.GetString("database.name")

    fmt.Printf("์„œ๋ฒ„: %s:%d\n", host, port)
    fmt.Printf("DB: %s\n", dbName)
}
๐Ÿ’ก Tip! Viper๋Š” ์ (.)์œผ๋กœ ๊ตฌ๋ถ„๋œ ๊ณ„์ธต์  ํ‚ค๋ฅผ ์ง€์›ํ•ด. "server.port"์ฒ˜๋Ÿผ ์“ฐ๋ฉด YAML์˜ ์ค‘์ฒฉ ๊ตฌ์กฐ์™€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋งคํ•‘๋ผ!

์ด์ œ Viper๊ฐ€ ์ง€์›ํ•˜๋Š” Get ํ•จ์ˆ˜๋“ค์„ ์•Œ์•„๋ณด์ž. ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•ด ๋‹ค์–‘ํ•œ ํƒ€์ž…๋ณ„ ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•ด:

// ๋‹ค์–‘ํ•œ ํƒ€์ž…์œผ๋กœ ๊ฐ’ ์ฝ๊ธฐ
viper.GetString("key")      // string
viper.GetInt("key")         // int
viper.GetInt64("key")       // int64
viper.GetFloat64("key")     // float64
viper.GetBool("key")        // bool
viper.GetDuration("key")    // time.Duration
viper.GetTime("key")        // time.Time
viper.GetStringSlice("key") // []string
viper.GetStringMap("key")   // map[string]interface{}
viper.GetStringMapString("key") // map[string]string

// ํ‚ค ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
viper.IsSet("key")          // bool

// ๋ชจ๋“  ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ
viper.AllSettings()         // map[string]interface{}

๐Ÿ“„ ์„ค์ • ํŒŒ์ผ ์ฝ๊ธฐ โ€” YAML์ด ์ œ์ผ ์ธ๊ธฐ ์žˆ์–ด!

์‹ค๋ฌด์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ๋ฐฉ์‹์€ YAML ํŒŒ์ผ๋กœ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฑฐ์•ผ. YAML์€ ๊ฐ€๋…์„ฑ์ด ์ข‹๊ณ  ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ํ‘œํ˜„ํ•˜๊ธฐ ์‰ฌ์›Œ์„œ ์„ค์ • ํŒŒ์ผ๋กœ ๋”ฑ์ด๊ฑฐ๋“ . ๐Ÿ˜Š

๋จผ์ € ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ์ด๋ ‡๊ฒŒ ์žก์•„๋ณด์ž:

my-go-app/
โ”œโ”€โ”€ config/
โ”‚   โ”œโ”€โ”€ config.go          # ์„ค์ • ๋กœ๋”ฉ ๋กœ์ง
โ”‚   โ”œโ”€โ”€ config.yaml        # ๊ธฐ๋ณธ ์„ค์ •
โ”‚   โ”œโ”€โ”€ config.dev.yaml    # ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •
โ”‚   โ”œโ”€โ”€ config.staging.yaml # ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ ์„ค์ •
โ”‚   โ””โ”€โ”€ config.prod.yaml   # ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ์„ค์ •
โ”œโ”€โ”€ main.go
โ””โ”€โ”€ go.mod

๊ธฐ๋ณธ ์„ค์ • ํŒŒ์ผ config.yaml์„ ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•ด:

# config/config.yaml
app:
  name: "My Awesome App"
  version: "1.0.0"
  env: "development"

server:
  host: "0.0.0.0"
  port: 8080
  read_timeout: 30s
  write_timeout: 30s
  idle_timeout: 120s

database:
  host: "localhost"
  port: 5432
  name: "myapp_dev"
  user: "postgres"
  password: ""
  ssl_mode: "disable"
  max_open_conns: 25
  max_idle_conns: 5
  conn_max_lifetime: 5m

redis:
  host: "localhost"
  port: 6379
  password: ""
  db: 0

jwt:
  secret: "your-super-secret-key-change-in-production"
  expiry: 24h

log:
  level: "debug"
  format: "text"  # text ๋˜๋Š” json

์ด์ œ Go ์ฝ”๋“œ์—์„œ ์ด ํŒŒ์ผ์„ ์ฝ์–ด๋ณด์ž:

// config/config.go
package config

import (
    "fmt"
    "strings"
    "github.com/spf13/viper"
)

// Config ๊ตฌ์กฐ์ฒด - ์„ค์ •๊ฐ’์„ ๋‹ด๋Š” ํƒ€์ž… ์•ˆ์ „ํ•œ ๊ตฌ์กฐ์ฒด
type Config struct {
    App      AppConfig      `mapstructure:"app"`
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    Redis    RedisConfig    `mapstructure:"redis"`
    JWT      JWTConfig      `mapstructure:"jwt"`
    Log      LogConfig      `mapstructure:"log"`
}

type AppConfig struct {
    Name    string `mapstructure:"name"`
    Version string `mapstructure:"version"`
    Env     string `mapstructure:"env"`
}

type ServerConfig struct {
    Host         string        `mapstructure:"host"`
    Port         int           `mapstructure:"port"`
    ReadTimeout  time.Duration `mapstructure:"read_timeout"`
    WriteTimeout time.Duration `mapstructure:"write_timeout"`
    IdleTimeout  time.Duration `mapstructure:"idle_timeout"`
}

type DatabaseConfig struct {
    Host            string        `mapstructure:"host"`
    Port            int           `mapstructure:"port"`
    Name            string        `mapstructure:"name"`
    User            string        `mapstructure:"user"`
    Password        string        `mapstructure:"password"`
    SSLMode         string        `mapstructure:"ssl_mode"`
    MaxOpenConns    int           `mapstructure:"max_open_conns"`
    MaxIdleConns    int           `mapstructure:"max_idle_conns"`
    ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}

type RedisConfig struct {
    Host     string `mapstructure:"host"`
    Port     int    `mapstructure:"port"`
    Password string `mapstructure:"password"`
    DB       int    `mapstructure:"db"`
}

type JWTConfig struct {
    Secret string        `mapstructure:"secret"`
    Expiry time.Duration `mapstructure:"expiry"`
}

type LogConfig struct {
    Level  string `mapstructure:"level"`
    Format string `mapstructure:"format"`
}

// Load ํ•จ์ˆ˜ - ์„ค์ •์„ ๋กœ๋“œํ•˜๊ณ  Config ๊ตฌ์กฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜
func Load(configPath string) (*Config, error) {
    v := viper.New()

    // ์„ค์ • ํŒŒ์ผ ๊ฒฝ๋กœ ์ง€์ •
    v.SetConfigFile(configPath)
    v.SetConfigType("yaml")

    // ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ž๋™ ๋ฐ”์ธ๋”ฉ (APP_SERVER_PORT โ†’ server.port)
    v.AutomaticEnv()
    v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    v.SetEnvPrefix("APP")

    // ์„ค์ • ํŒŒ์ผ ์ฝ๊ธฐ
    if err := v.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("์„ค์ • ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ: %w", err)
    }

    // ๊ตฌ์กฐ์ฒด๋กœ ์–ธ๋งˆ์ƒฌ๋ง
    var cfg Config
    if err := v.Unmarshal(&cfg); err != nil {
        return nil, fmt.Errorf("์„ค์ • ํŒŒ์‹ฑ ์‹คํŒจ: %w", err)
    }

    return &cfg, nil
}
๐Ÿ’ก mapstructure ํƒœ๊ทธ๊ฐ€ ํ•ต์‹ฌ! Viper๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ mapstructure ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•ด์„œ map์„ ๊ตฌ์กฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด. ๊ทธ๋ž˜์„œ ๊ตฌ์กฐ์ฒด ํ•„๋“œ์— mapstructure:"yaml_key" ํƒœ๊ทธ๋ฅผ ๋ถ™์—ฌ์ค˜์•ผ ์ •ํ™•ํ•˜๊ฒŒ ๋งคํ•‘๋ผ. YAML ํ‚ค์™€ Go ํ•„๋“œ๋ช…์ด ๋‹ค๋ฅผ ๋•Œ ํŠนํžˆ ์ค‘์š”ํ•ด!

ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ณ‘ํ•ฉ ์ „๋žต ๐Ÿ“„ config.yaml ๊ธฐ๋ณธ ์„ค์ • ๋ชจ๋“  ํ™˜๊ฒฝ ๊ณตํ†ต๊ฐ’ ๊ธฐ๋ณธ๊ฐ’ ์ •์˜ ๐Ÿ› ๏ธ config.dev.yaml ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์˜ค๋ฒ„๋ผ์ด๋“œ localhost DB, debug ๋กœ๊ทธ ๋А์Šจํ•œ ๋ณด์•ˆ ์„ค์ • ๐ŸŒ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฏผ๊ฐํ•œ ์ •๋ณด ์ฃผ์ž… DB_PASSWORD, JWT_SECRET API ํ‚ค ๋“ฑ ๐Ÿ”€ Viper ๋ณ‘ํ•ฉ MergeInConfig() AutomaticEnv() ์šฐ์„ ์ˆœ์œ„ ์ž๋™ ์ ์šฉ โœ… ์ตœ์ข… ์„ค์ • Config server.port: 8080 database.host: localhost database.password: **** log.level: debug jwt.secret: **** ๐Ÿ’ก ํ•ต์‹ฌ ์›์น™: ๋ฏผ๊ฐํ•œ ์ •๋ณด(๋น„๋ฐ€๋ฒˆํ˜ธ, APIํ‚ค)๋Š” ์ ˆ๋Œ€ ํŒŒ์ผ์— ์ €์žฅํ•˜์ง€ ๋ง๊ณ  ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ๋งŒ ์ฃผ์ž…! config.yaml์€ Git์— ์ปค๋ฐ‹ ๊ฐ€๋Šฅ | .env ํŒŒ์ผ์€ .gitignore์— ์ถ”๊ฐ€ | ํ”„๋กœ๋•์…˜์€ ์‹œํฌ๋ฆฟ ๋งค๋‹ˆ์ € ์‚ฌ์šฉ ๊ถŒ์žฅ
๐ŸŒ ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ์‹œ์Šคํ…œ ๊ตฌ์ถ•ํ•˜๊ธฐ โ€” ์‹ค์ „ ํŒจํ„ด!

์ž, ์ด์ œ ์ง„์งœ ์‹ค๋ฌด์—์„œ ์“ฐ๋Š” ํŒจํ„ด์„ ์•Œ์•„๋ณด์ž. ํ™˜๊ฒฝ๋ณ„๋กœ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€์•ผ:

๐ŸŽฏ ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๊ด€๋ฆฌ ์ „๋žต ๋‘ ๊ฐ€์ง€ ์ „๋žต 1: ํ™˜๊ฒฝ๋ณ„ ํŒŒ์ผ ๋ถ„๋ฆฌ โ€” config.yaml, config.dev.yaml, config.prod.yaml
์ „๋žต 2: ๋‹จ์ผ ํŒŒ์ผ + ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์˜ค๋ฒ„๋ผ์ด๋“œ โ€” config.yaml + ํ™˜๊ฒฝ๋ณ€์ˆ˜

์‹ค๋ฌด์—์„œ๋Š” ๋‘ ์ „๋žต์„ ์กฐํ•ฉํ•ด์„œ ์“ฐ๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ข‹์•„. ๊ณตํ†ต ์„ค์ •์€ ํŒŒ์ผ๋กœ, ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ! ์ด๋ ‡๊ฒŒ ๊ตฌํ˜„ํ•ด๋ณด์ž:

// config/config.go (์™„์ „ํ•œ ๋ฒ„์ „)
package config

import (
    "fmt"
    "os"
    "strings"
    "time"

    "github.com/spf13/viper"
)

type Config struct {
    App      AppConfig      `mapstructure:"app"`
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    Redis    RedisConfig    `mapstructure:"redis"`
    JWT      JWTConfig      `mapstructure:"jwt"`
    Log      LogConfig      `mapstructure:"log"`
}

// ... (๊ตฌ์กฐ์ฒด ์ •์˜๋Š” ์•ž์—์„œ ๋ณธ ๊ฒƒ๊ณผ ๋™์ผ)

// LoadConfig - ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์„ค์ • ํŒŒ์ผ์„ ๋กœ๋“œ
func LoadConfig(configDir string) (*Config, error) {
    v := viper.New()

    // 1. ๊ธฐ๋ณธ ์„ค์ • ํŒŒ์ผ ๋กœ๋“œ
    v.SetConfigName("config")
    v.SetConfigType("yaml")
    v.AddConfigPath(configDir)
    v.AddConfigPath(".")

    if err := v.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("๊ธฐ๋ณธ ์„ค์ • ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ: %w", err)
    }

    // 2. ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ณ‘ํ•ฉ (์žˆ์œผ๋ฉด)
    env := os.Getenv("APP_ENV")
    if env == "" {
        env = "development" // ๊ธฐ๋ณธ๊ฐ’
    }

    v.SetConfigName(fmt.Sprintf("config.%s", env))
    if err := v.MergeInConfig(); err != nil {
        // ํ™˜๊ฒฝ๋ณ„ ํŒŒ์ผ์ด ์—†์–ด๋„ ์—๋Ÿฌ ์•„๋‹˜ (์„ ํƒ์ )
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return nil, fmt.Errorf("ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ณ‘ํ•ฉ ์‹คํŒจ: %w", err)
        }
    }

    // 3. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ
    v.SetEnvPrefix("APP")
    v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    v.AutomaticEnv()

    // 4. ํŠน์ • ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ช…์‹œ์  ๋ฐ”์ธ๋”ฉ
    // ํ™˜๊ฒฝ๋ณ€์ˆ˜ APP_DATABASE_PASSWORD โ†’ database.password
    bindEnvs := []struct {
        key string
        env string
    }{
        {"database.password", "APP_DATABASE_PASSWORD"},
        {"database.user", "APP_DATABASE_USER"},
        {"jwt.secret", "APP_JWT_SECRET"},
        {"redis.password", "APP_REDIS_PASSWORD"},
    }

    for _, b := range bindEnvs {
        if err := v.BindEnv(b.key, b.env); err != nil {
            return nil, fmt.Errorf("ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ์‹คํŒจ (%s): %w", b.key, err)
        }
    }

    // 5. ๊ตฌ์กฐ์ฒด๋กœ ์–ธ๋งˆ์ƒฌ๋ง
    var cfg Config
    if err := v.Unmarshal(&cfg); err != nil {
        return nil, fmt.Errorf("์„ค์ • ํŒŒ์‹ฑ ์‹คํŒจ: %w", err)
    }

    // 6. ์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
    if err := cfg.Validate(); err != nil {
        return nil, fmt.Errorf("์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ: %w", err)
    }

    return &cfg, nil
}

// Validate - ํ•„์ˆ˜ ์„ค์ •๊ฐ’ ๊ฒ€์ฆ
func (c *Config) Validate() error {
    if c.Database.Password == "" && c.App.Env == "production" {
        return fmt.Errorf("ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ DB ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค")
    }
    if c.JWT.Secret == "" {
        return fmt.Errorf("JWT ์‹œํฌ๋ฆฟ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค")
    }
    if len(c.JWT.Secret) < 32 && c.App.Env == "production" {
        return fmt.Errorf("ํ”„๋กœ๋•์…˜ JWT ์‹œํฌ๋ฆฟ์€ ์ตœ์†Œ 32์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค")
    }
    return nil
}
โœ… ์ด ํŒจํ„ด์˜ ์žฅ์  โ€ข ๊ธฐ๋ณธ ์„ค์ •์€ config.yaml์— (Git ์ปค๋ฐ‹ ๊ฐ€๋Šฅ)
โ€ข ํ™˜๊ฒฝ๋ณ„ ์˜ค๋ฒ„๋ผ์ด๋“œ๋Š” config.dev.yaml ๋“ฑ์—
โ€ข ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ๋งŒ ์ฃผ์ž…
โ€ข ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋กœ ์ž˜๋ชป๋œ ์„ค์ • ์กฐ๊ธฐ ๋ฐœ๊ฒฌ

์ด์ œ main.go์—์„œ ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•ด:

// main.go
package main

import (
    "log"
    "github.com/yourname/my-go-app/config"
)

func main() {
    cfg, err := config.LoadConfig("./config")
    if err != nil {
        log.Fatalf("์„ค์ • ๋กœ๋“œ ์‹คํŒจ: %v", err)
    }

    log.Printf("์•ฑ ์‹œ์ž‘: %s v%s (ํ™˜๊ฒฝ: %s)",
        cfg.App.Name,
        cfg.App.Version,
        cfg.App.Env,
    )
    log.Printf("์„œ๋ฒ„ ์ฃผ์†Œ: %s:%d", cfg.Server.Host, cfg.Server.Port)

    // ์ด์ œ cfg๋ฅผ ์˜์กด์„ฑ ์ฃผ์ž…์œผ๋กœ ์ „๋‹ฌ
    // server := NewServer(cfg)
    // server.Start()
}

๐Ÿ”ง ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ์™„์ „ ์ •๋ณต

Viper์˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์€ ์ •๋ง ๊ฐ•๋ ฅํ•œ๋ฐ, ์ฒ˜์Œ์—” ์ข€ ํ—ท๊ฐˆ๋ฆด ์ˆ˜ ์žˆ์–ด. ์ฐจ๊ทผ์ฐจ๊ทผ ์„ค๋ช…ํ•ด์ค„๊ฒŒ! ๐Ÿ˜Š

AutomaticEnv()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Viper๊ฐ€ ์ž๋™์œผ๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ฝ์–ด์˜ค๋Š”๋ฐ, ์ด๋•Œ ํ‚ค ๋ณ€ํ™˜ ๊ทœ์น™์ด ์žˆ์–ด:

v := viper.New()
v.SetEnvPrefix("APP")           // ์ ‘๋‘์‚ฌ ์„ค์ •
v.SetEnvKeyReplacer(
    strings.NewReplacer(".", "_") // ์ ์„ ์–ธ๋”์Šค์ฝ”์–ด๋กœ ๋ณ€ํ™˜
)
v.AutomaticEnv()

// ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด:
// viper ํ‚ค "server.port" โ†’ ํ™˜๊ฒฝ๋ณ€์ˆ˜ "APP_SERVER_PORT"
// viper ํ‚ค "database.host" โ†’ ํ™˜๊ฒฝ๋ณ€์ˆ˜ "APP_DATABASE_HOST"
// viper ํ‚ค "jwt.secret" โ†’ ํ™˜๊ฒฝ๋ณ€์ˆ˜ "APP_JWT_SECRET"

์‹ค์ œ๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•๋“ค:

# ๋ฐฉ๋ฒ• 1: ์‰˜์—์„œ ์ง์ ‘ ์„ค์ •
export APP_ENV=production
export APP_DATABASE_PASSWORD=super_secret_password
export APP_JWT_SECRET=my-very-long-jwt-secret-key-32chars

# ๋ฐฉ๋ฒ• 2: .env ํŒŒ์ผ (๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ!)
# .env ํŒŒ์ผ ๋‚ด์šฉ:
APP_ENV=development
APP_DATABASE_PASSWORD=devpassword
APP_JWT_SECRET=dev-jwt-secret-key

# ๋ฐฉ๋ฒ• 3: Docker ์‹คํ–‰ ์‹œ
docker run -e APP_ENV=production \
           -e APP_DATABASE_PASSWORD=secret \
           my-app

# ๋ฐฉ๋ฒ• 4: Kubernetes Secret
kubectl create secret generic app-secrets \
  --from-literal=APP_DATABASE_PASSWORD=secret \
  --from-literal=APP_JWT_SECRET=my-jwt-secret
๐Ÿ’ก .env ํŒŒ์ผ์„ Go์—์„œ ์ž๋™์œผ๋กœ ์ฝ์œผ๋ ค๋ฉด? godotenv ํŒจํ‚ค์ง€๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋ผ:
go get github.com/joho/godotenv

๊ทธ๋ฆฌ๊ณ  main.go ๋งจ ์œ„์—:
godotenv.Load() // .env ํŒŒ์ผ ์ž๋™ ๋กœ๋“œ

๋‹จ, ์ด๊ฑด ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ! ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์‹ค์ œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์จ์•ผ ํ•ด.

ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์—์„œ ์ž์ฃผ ์‹ค์ˆ˜ํ•˜๋Š” ๋ถ€๋ถ„์ด ์žˆ์–ด. ๋Œ€์†Œ๋ฌธ์ž ์ฒ˜๋ฆฌ์•ผ:

// โš ๏ธ ์ฃผ์˜! Viper๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ํ‚ค๋ฅผ ์†Œ๋ฌธ์ž๋กœ ์ฒ˜๋ฆฌํ•ด
// ํ™˜๊ฒฝ๋ณ€์ˆ˜๋Š” ๋Œ€๋ฌธ์ž๊ฐ€ ๊ด€๋ก€์ด๋ฏ€๋กœ SetEnvKeyReplacer๊ฐ€ ํ•„์š”ํ•ด

v.SetEnvKeyReplacer(strings.NewReplacer(
    ".", "_",   // ์  โ†’ ์–ธ๋”์Šค์ฝ”์–ด
    "-", "_",   // ํ•˜์ดํ”ˆ โ†’ ์–ธ๋”์Šค์ฝ”์–ด (ํ•„์š”์‹œ)
))

// ๋ช…์‹œ์  ๋ฐ”์ธ๋”ฉ์ด ๊ฐ€์žฅ ํ™•์‹คํ•ด
v.BindEnv("database.password", "DB_PASSWORD")
// ์ด์ œ DB_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ database.password๋กœ ๋งคํ•‘๋จ

๐Ÿ—๏ธ ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ โ€” ์˜์กด์„ฑ ์ฃผ์ž…๊ณผ ํ•จ๊ป˜

์„ค์ •์„ ์ž˜ ๋กœ๋“œํ–ˆ์œผ๋ฉด, ์ด๊ฑธ ์•ฑ ์ „์ฒด์— ์–ด๋–ป๊ฒŒ ์ „๋‹ฌํ•˜๋А๋ƒ๊ฐ€ ์ค‘์š”ํ•ด. Go์—์„œ๋Š” ์˜์กด์„ฑ ์ฃผ์ž…(Dependency Injection) ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค์•ผ.

์ „์—ญ ๋ณ€์ˆ˜๋กœ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฑด ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ต๊ณ  ์ฝ”๋“œ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋†’์•„์ ธ์„œ ์ข‹์ง€ ์•Š์•„. ์ด๋ ‡๊ฒŒ ํ•ด๋ณด์ž:

// ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
my-go-app/
โ”œโ”€โ”€ cmd/
โ”‚   โ””โ”€โ”€ server/
โ”‚       โ””โ”€โ”€ main.go          # ์ง„์ž…์ 
โ”œโ”€โ”€ config/
โ”‚   โ”œโ”€โ”€ config.go            # ์„ค์ • ๊ตฌ์กฐ์ฒด ๋ฐ ๋กœ๋”ฉ
โ”‚   โ”œโ”€โ”€ config.yaml          # ๊ธฐ๋ณธ ์„ค์ •
โ”‚   โ””โ”€โ”€ config.dev.yaml      # ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •
โ”œโ”€โ”€ internal/
โ”‚   โ”œโ”€โ”€ server/
โ”‚   โ”‚   โ””โ”€โ”€ server.go        # HTTP ์„œ๋ฒ„
โ”‚   โ”œโ”€โ”€ database/
โ”‚   โ”‚   โ””โ”€โ”€ database.go      # DB ์—ฐ๊ฒฐ
โ”‚   โ”œโ”€โ”€ handler/
โ”‚   โ”‚   โ””โ”€โ”€ user.go          # HTTP ํ•ธ๋“ค๋Ÿฌ
โ”‚   โ””โ”€โ”€ service/
โ”‚       โ””โ”€โ”€ user.go          # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
โ””โ”€โ”€ go.mod
// internal/database/database.go
package database

import (
    "database/sql"
    "fmt"
    "time"

    "github.com/yourname/my-go-app/config"
    _ "github.com/lib/pq" // PostgreSQL ๋“œ๋ผ์ด๋ฒ„
)

type DB struct {
    *sql.DB
}

// New - ์„ค์ •์„ ๋ฐ›์•„์„œ DB ์—ฐ๊ฒฐ ์ƒ์„ฑ
func New(cfg config.DatabaseConfig) (*DB, error) {
    dsn := fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
        cfg.Host,
        cfg.Port,
        cfg.User,
        cfg.Password,
        cfg.Name,
        cfg.SSLMode,
    )

    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("DB ์—ฐ๊ฒฐ ์‹คํŒจ: %w", err)
    }

    // ์—ฐ๊ฒฐ ํ’€ ์„ค์ •
    db.SetMaxOpenConns(cfg.MaxOpenConns)
    db.SetMaxIdleConns(cfg.MaxIdleConns)
    db.SetConnMaxLifetime(cfg.ConnMaxLifetime)

    // ์—ฐ๊ฒฐ ํ™•์ธ
    if err := db.Ping(); err != nil {
        return nil, fmt.Errorf("DB Ping ์‹คํŒจ: %w", err)
    }

    return &DB{db}, nil
}
// cmd/server/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/yourname/my-go-app/config"
    "github.com/yourname/my-go-app/internal/database"
    "github.com/yourname/my-go-app/internal/server"
)

func main() {
    // 1. ์„ค์ • ๋กœ๋“œ
    cfg, err := config.LoadConfig("./config")
    if err != nil {
        log.Fatalf("โŒ ์„ค์ • ๋กœ๋“œ ์‹คํŒจ: %v", err)
    }
    log.Printf("โœ… ์„ค์ • ๋กœ๋“œ ์™„๋ฃŒ (ํ™˜๊ฒฝ: %s)", cfg.App.Env)

    // 2. DB ์—ฐ๊ฒฐ (์„ค์ • ์ฃผ์ž…)
    db, err := database.New(cfg.Database)
    if err != nil {
        log.Fatalf("โŒ DB ์—ฐ๊ฒฐ ์‹คํŒจ: %v", err)
    }
    defer db.Close()
    log.Printf("โœ… DB ์—ฐ๊ฒฐ ์™„๋ฃŒ")

    // 3. ์„œ๋ฒ„ ์ƒ์„ฑ (์„ค์ • ์ฃผ์ž…)
    srv := server.New(cfg, db)

    // 4. Graceful Shutdown ์„ค์ •
    go func() {
        addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
        log.Printf("๐Ÿš€ ์„œ๋ฒ„ ์‹œ์ž‘: http://%s", addr)
        if err := srv.Start(addr); err != nil && err != http.ErrServerClosed {
            log.Fatalf("์„œ๋ฒ„ ์—๋Ÿฌ: %v", err)
        }
    }()

    // 5. ์ข…๋ฃŒ ์‹œ๊ทธ๋„ ๋Œ€๊ธฐ
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("๐Ÿ›‘ ์„œ๋ฒ„ ์ข…๋ฃŒ ์ค‘...")
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("์„œ๋ฒ„ ๊ฐ•์ œ ์ข…๋ฃŒ: %v", err)
    }
    log.Println("โœ… ์„œ๋ฒ„ ์ •์ƒ ์ข…๋ฃŒ")
}
โœ… ์ด ํŒจํ„ด์˜ ํ•ต์‹ฌ ์žฅ์  โ€ข config.Config๋ฅผ ๊ฐ ์ปดํฌ๋„ŒํŠธ์— ๋ช…์‹œ์ ์œผ๋กœ ์ „๋‹ฌ
โ€ข ์ „์—ญ ๋ณ€์ˆ˜ ์—†์Œ โ†’ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์›€
โ€ข ๊ฐ ์ปดํฌ๋„ŒํŠธ๋Š” ์ž์‹ ์—๊ฒŒ ํ•„์š”ํ•œ ์„ค์ •๋งŒ ๋ฐ›์Œ
โ€ข ์„ค์ • ๋ณ€๊ฒฝ ์‹œ ์˜ํ–ฅ ๋ฒ”์œ„๊ฐ€ ๋ช…ํ™•ํ•จ

๐Ÿ”„ ์„ค์ • ํŒŒ์ผ ์‹ค์‹œ๊ฐ„ ๊ฐ์‹œ โ€” Hot Reload!

Viper์˜ ์ˆจ๊ฒจ์ง„ ํ‚ฌ๋Ÿฌ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ์„ค์ • ํŒŒ์ผ ์‹ค์‹œ๊ฐ„ ๊ฐ์‹œ์•ผ! ์„œ๋ฒ„๋ฅผ ์žฌ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ณ ๋„ ์„ค์ •์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์–ด. ๋กœ๊ทธ ๋ ˆ๋ฒจ์„ ํ”„๋กœ๋•์…˜์—์„œ ๊ฐ‘์ž๊ธฐ debug๋กœ ๋ฐ”๊ฟ”์•ผ ํ•  ๋•Œ ์ง„์งœ ์œ ์šฉํ•˜๊ฑฐ๋“ . ๐Ÿ˜Ž

package config

import (
    "log"
    "sync"

    "github.com/fsnotify/fsnotify"
    "github.com/spf13/viper"
)

type DynamicConfig struct {
    mu  sync.RWMutex
    cfg *Config
    v   *viper.Viper
}

func NewDynamicConfig(configDir string) (*DynamicConfig, error) {
    v := viper.New()
    v.SetConfigName("config")
    v.SetConfigType("yaml")
    v.AddConfigPath(configDir)

    if err := v.ReadInConfig(); err != nil {
        return nil, err
    }

    var cfg Config
    if err := v.Unmarshal(&cfg); err != nil {
        return nil, err
    }

    dc := &DynamicConfig{cfg: &cfg, v: v}

    // ํŒŒ์ผ ๋ณ€๊ฒฝ ๊ฐ์‹œ ์‹œ์ž‘
    v.WatchConfig()
    v.OnConfigChange(func(e fsnotify.Event) {
        log.Printf("โšก ์„ค์ • ํŒŒ์ผ ๋ณ€๊ฒฝ ๊ฐ์ง€: %s", e.Name)

        dc.mu.Lock()
        defer dc.mu.Unlock()

        var newCfg Config
        if err := v.Unmarshal(&newCfg); err != nil {
            log.Printf("โŒ ์„ค์ • ์žฌ๋กœ๋“œ ์‹คํŒจ: %v", err)
            return
        }

        dc.cfg = &newCfg
        log.Printf("โœ… ์„ค์ • ์žฌ๋กœ๋“œ ์™„๋ฃŒ (๋กœ๊ทธ ๋ ˆ๋ฒจ: %s)", newCfg.Log.Level)
    })

    return dc, nil
}

// Get - ํ˜„์žฌ ์„ค์ • ๋ฐ˜ํ™˜ (์Šค๋ ˆ๋“œ ์•ˆ์ „)
func (dc *DynamicConfig) Get() *Config {
    dc.mu.RLock()
    defer dc.mu.RUnlock()
    return dc.cfg
}
โš ๏ธ Hot Reload ์ฃผ์˜์‚ฌํ•ญ โ€ข DB ์—ฐ๊ฒฐ ์ •๋ณด๋‚˜ ์„œ๋ฒ„ ํฌํŠธ ๊ฐ™์€ ๊ฑด ์žฌ์‹œ์ž‘ ์—†์ด ๋ฐ”๊พธ๊ธฐ ์–ด๋ ค์›Œ
โ€ข ๋กœ๊ทธ ๋ ˆ๋ฒจ, ๊ธฐ๋Šฅ ํ”Œ๋ž˜๊ทธ, ํƒ€์ž„์•„์›ƒ ๊ฐ’ ๊ฐ™์€ ๊ฒƒ๋“ค์ด Hot Reload์— ์ ํ•ฉํ•ด
โ€ข ๋ฉ€ํ‹ฐ ๊ณ ๋ฃจํ‹ด ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ sync.RWMutex๋กœ ๋™์‹œ์„ฑ ๋ณดํ˜ธ ํ•„์š”!
โ€ข ์„ค์ • ๋ณ€๊ฒฝ ํ›„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ๊ผญ ํ•ด์•ผ ํ•ด

์ „์—ญ Viper vs ์ธ์Šคํ„ด์Šค Viper ๋น„๊ต โŒ ์ „์—ญ Viper ์‚ฌ์šฉ (๋น„์ถ”์ฒœ) // ์ „์—ญ viper ํŒจํ‚ค์ง€ ์ง์ ‘ ์‚ฌ์šฉ import "github.com/spf13/viper" func GetDBHost() string {"{"} return viper.GetString("db.host") {"}"} // ํ…Œ์ŠคํŠธ์—์„œ ์ „์—ญ ์ƒํƒœ ์˜ค์—ผ ์œ„ํ—˜! ๋ฌธ์ œ์ : โ€ข ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ ๋ถˆ๊ฐ€๋Šฅ โ€ข ์ „์—ญ ์ƒํƒœ โ†’ ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ๋™์ž‘ โ€ข ๋ฉ€ํ‹ฐ ์„ค์ • ํŒŒ์ผ ๊ด€๋ฆฌ ์–ด๋ ค์›€ โ€ข ์˜์กด์„ฑ์ด ์ˆจ๊ฒจ์ ธ ์ฝ”๋“œ ์ดํ•ด ์–ด๋ ค์›€ ๐Ÿ˜ฑ โœ… Viper ์ธ์Šคํ„ด์Šค ์‚ฌ์šฉ (์ถ”์ฒœ) // Viper ์ธ์Šคํ„ด์Šค๋ฅผ ๊ตฌ์กฐ์ฒด์— ์ฃผ์ž… v := viper.New() // ์ƒˆ ์ธ์Šคํ„ด์Šค! type Config struct {"{"} v *viper.Viper {"}"} // ํ…Œ์ŠคํŠธ์—์„œ ๋…๋ฆฝ์  ์ธ์Šคํ„ด์Šค ์‚ฌ์šฉ! ์žฅ์ : โ€ข ํ…Œ์ŠคํŠธ ์™„์ „ ๊ฒฉ๋ฆฌ ๊ฐ€๋Šฅ โ€ข ์—ฌ๋Ÿฌ ์„ค์ • ํŒŒ์ผ ๋™์‹œ ๊ด€๋ฆฌ โ€ข ์˜์กด์„ฑ์ด ๋ช…์‹œ์ ์œผ๋กœ ๋“œ๋Ÿฌ๋‚จ โ€ข ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ์„ฑ ๋ฐ ์œ ์ง€๋ณด์ˆ˜ ํ–ฅ์ƒ ๐ŸŽ‰
๐Ÿงช ์„ค์ • ํ…Œ์ŠคํŠธ โ€” ์ด๊ฒŒ ์ง„์งœ ์ค‘์š”ํ•ด!

์„ค์ • ์ฝ”๋“œ๋„ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ด! ํŠนํžˆ ํ™˜๊ฒฝ๋ณ„๋กœ ๋‹ค๋ฅธ ์„ค์ •์ด ์ œ๋Œ€๋กœ ๋กœ๋“œ๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. Viper ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ:

// config/config_test.go
package config_test

import (
    "os"
    "testing"

    "github.com/spf13/viper"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestLoadConfig_DefaultValues(t *testing.T) {
    // ํ…Œ์ŠคํŠธ์šฉ ์ž„์‹œ ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ
    configContent := `
app:
  name: "Test App"
  env: "test"
server:
  port: 9090
database:
  host: "testhost"
  port: 5432
jwt:
  secret: "test-secret-key-that-is-long-enough"
`
    // ์ž„์‹œ ํŒŒ์ผ ์ƒ์„ฑ
    tmpFile, err := os.CreateTemp("", "config-*.yaml")
    require.NoError(t, err)
    defer os.Remove(tmpFile.Name())

    _, err = tmpFile.WriteString(configContent)
    require.NoError(t, err)
    tmpFile.Close()

    // ์„ค์ • ๋กœ๋“œ
    cfg, err := LoadConfigFromFile(tmpFile.Name())
    require.NoError(t, err)

    // ๊ฒ€์ฆ
    assert.Equal(t, "Test App", cfg.App.Name)
    assert.Equal(t, "test", cfg.App.Env)
    assert.Equal(t, 9090, cfg.Server.Port)
    assert.Equal(t, "testhost", cfg.Database.Host)
}

func TestLoadConfig_EnvOverride(t *testing.T) {
    // ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์˜ค๋ฒ„๋ผ์ด๋“œ ํ…Œ์ŠคํŠธ
    os.Setenv("APP_SERVER_PORT", "7777")
    os.Setenv("APP_DATABASE_PASSWORD", "test_password")
    defer func() {
        os.Unsetenv("APP_SERVER_PORT")
        os.Unsetenv("APP_DATABASE_PASSWORD")
    }()

    v := viper.New()
    v.SetDefault("server.port", 8080)
    v.SetDefault("database.password", "")
    v.SetEnvPrefix("APP")
    v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    v.AutomaticEnv()

    // ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์„ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜๋Š”์ง€ ํ™•์ธ
    assert.Equal(t, 7777, v.GetInt("server.port"))
    assert.Equal(t, "test_password", v.GetString("database.password"))
}

func TestConfig_Validate_Production(t *testing.T) {
    // ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ํ•„์ˆ˜๊ฐ’ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ
    cfg := &Config{
        App: AppConfig{Env: "production"},
        Database: DatabaseConfig{Password: ""}, // ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์Œ!
        JWT: JWTConfig{Secret: "short"}, // ๋„ˆ๋ฌด ์งง์€ ์‹œํฌ๋ฆฟ!
    }

    err := cfg.Validate()
    assert.Error(t, err) // ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•จ
}
๐Ÿ’ก ํ…Œ์ŠคํŠธ ํŒ! โ€ข ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ธ Viper ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ด
โ€ข ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ–ˆ์œผ๋ฉด ํ…Œ์ŠคํŠธ ํ›„ ๋ฐ˜๋“œ์‹œ os.Unsetenv()๋กœ ์ •๋ฆฌ
โ€ข testify ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด assertion์ด ํ›จ์”ฌ ํŽธํ•ด
โ€ข ์ž„์‹œ ์„ค์ • ํŒŒ์ผ์€ os.CreateTemp()๋กœ ๋งŒ๋“ค๊ณ  defer os.Remove()๋กœ ์ •๋ฆฌ

โ˜๏ธ ์›๊ฒฉ ์„ค์ • โ€” etcd์™€ Consul ์—ฐ๋™

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ๋Š” ์„ค์ •์„ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. Viper๋Š” etcd์™€ Consul ๊ฐ™์€ ๋ถ„์‚ฐ ํ‚ค-๊ฐ’ ์ €์žฅ์†Œ์™€ ์—ฐ๋™์„ ์ง€์›ํ•ด. ์ด๊ฑธ ์“ฐ๋ฉด ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์˜ ์„ค์ •์„ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๊ณ , ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿš€

// ์›๊ฒฉ ์„ค์ • ์‚ฌ์šฉ์„ ์œ„ํ•œ ์ถ”๊ฐ€ ํŒจํ‚ค์ง€
import (
    _ "github.com/spf13/viper/remote"
)

func LoadRemoteConfig() (*Config, error) {
    v := viper.New()

    // etcd ์—ฐ๊ฒฐ
    err := v.AddRemoteProvider(
        "etcd3",
        "http://etcd-server:2379",
        "/config/myapp",
    )
    if err != nil {
        return nil, fmt.Errorf("etcd ์—ฐ๊ฒฐ ์‹คํŒจ: %w", err)
    }

    v.SetConfigType("yaml")

    // ์›๊ฒฉ ์„ค์ • ์ฝ๊ธฐ
    if err := v.ReadRemoteConfig(); err != nil {
        return nil, fmt.Errorf("์›๊ฒฉ ์„ค์ • ์ฝ๊ธฐ ์‹คํŒจ: %w", err)
    }

    var cfg Config
    if err := v.Unmarshal(&cfg); err != nil {
        return nil, err
    }

    // ์›๊ฒฉ ์„ค์ • ์‹ค์‹œ๊ฐ„ ๊ฐ์‹œ
    go func() {
        for {
            time.Sleep(5 * time.Second)
            if err := v.WatchRemoteConfig(); err != nil {
                log.Printf("์›๊ฒฉ ์„ค์ • ๊ฐ์‹œ ์—๋Ÿฌ: %v", err)
                continue
            }
            v.Unmarshal(&cfg)
            log.Println("์›๊ฒฉ ์„ค์ • ์—…๋ฐ์ดํŠธ๋จ!")
        }
    }()

    return &cfg, nil
}
๐Ÿข ์‹ค๋ฌด์—์„œ ์›๊ฒฉ ์„ค์ •์„ ์“ฐ๋Š” ๊ฒฝ์šฐ ๐Ÿ”น Kubernetes + ConfigMap: K8s ConfigMap์„ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋‚˜ ๋ณผ๋ฅจ์œผ๋กœ ๋งˆ์šดํŠธ
๐Ÿ”น AWS Parameter Store: AWS ํ™˜๊ฒฝ์—์„œ ์„ค์ • ์ค‘์•™ ๊ด€๋ฆฌ
๐Ÿ”น HashiCorp Vault: ์‹œํฌ๋ฆฟ ๊ด€๋ฆฌ ์ „์šฉ (DB ๋น„๋ฐ€๋ฒˆํ˜ธ, API ํ‚ค ๋“ฑ)
๐Ÿ”น Consul: ์„œ๋น„์Šค ๋””์Šค์ปค๋ฒ„๋ฆฌ์™€ ํ•จ๊ป˜ ์„ค์ • ๊ด€๋ฆฌ
๐Ÿ”น etcd: Kubernetes ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ถ„์‚ฐ ์ €์žฅ์†Œ

๐ŸŽฏ ์‹ค์ „ ํŒ ๋ชจ์Œ โ€” ์ด๊ฒƒ๋งŒ ์•Œ๋ฉด ํ”„๋กœ!

์ž, ์ด์ œ ์‹ค๋ฌด์—์„œ ๋ฐ”๋กœ ์จ๋จน์„ ์ˆ˜ ์žˆ๋Š” ํŒ๋“ค์„ ๋ชจ์•„๋ดค์–ด. ์žฌ๋Šฅ๋„ท์—์„œ Go ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๋Š” ๋ถ„๋“ค์ด๋ผ๋ฉด ํŠนํžˆ ์œ ์šฉํ•  ๋‚ด์šฉ๋“ค์ด์•ผ! ๐Ÿ˜Š

ํŒ 1: ์„ค์ • ๊ตฌ์กฐ์ฒด์— DSN ์ƒ์„ฑ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ํ•˜๊ธฐ

// DatabaseConfig์— ํŽธ์˜ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
func (d DatabaseConfig) DSN() string {
    return fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
        d.Host, d.Port, d.User, d.Password, d.Name, d.SSLMode,
    )
}

func (d DatabaseConfig) RedactedDSN() string {
    // ๋กœ๊ทธ์šฉ - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋งˆ์Šคํ‚น
    return fmt.Sprintf(
        "host=%s port=%d user=%s password=*** dbname=%s sslmode=%s",
        d.Host, d.Port, d.User, d.Name, d.SSLMode,
    )
}

// ์‚ฌ์šฉ
log.Printf("DB ์—ฐ๊ฒฐ: %s", cfg.Database.RedactedDSN())
db, err := sql.Open("postgres", cfg.Database.DSN())

ํŒ 2: ์„ค์ • ๋กœ๋“œ ์‹œ ํ™˜๊ฒฝ ์ž๋™ ๊ฐ์ง€

func detectEnvironment() string {
    // ์šฐ์„ ์ˆœ์œ„: ํ™˜๊ฒฝ๋ณ€์ˆ˜ > ๊ธฐ๋ณธ๊ฐ’
    if env := os.Getenv("APP_ENV"); env != "" {
        return env
    }
    if env := os.Getenv("GO_ENV"); env != "" {
        return env
    }
    if env := os.Getenv("ENVIRONMENT"); env != "" {
        return env
    }
    // ๊ธฐ๋ณธ๊ฐ’: development
    return "development"
}

// ์‚ฌ์šฉ
env := detectEnvironment()
log.Printf("๊ฐ์ง€๋œ ํ™˜๊ฒฝ: %s", env)

ํŒ 3: ์„ค์ • ๋คํ”„ (๋””๋ฒ„๊น…์šฉ)

func (c *Config) Debug() {
    // ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ๋งˆ์Šคํ‚นํ•˜๊ณ  ์„ค์ • ์ถœ๋ ฅ
    log.Printf("=== ํ˜„์žฌ ์„ค์ • ===")
    log.Printf("์•ฑ: %s v%s (%s)", c.App.Name, c.App.Version, c.App.Env)
    log.Printf("์„œ๋ฒ„: %s:%d", c.Server.Host, c.Server.Port)
    log.Printf("DB: %s@%s:%d/%s",
        c.Database.User,
        c.Database.Host,
        c.Database.Port,
        c.Database.Name,
    )
    log.Printf("๋กœ๊ทธ ๋ ˆ๋ฒจ: %s", c.Log.Level)
    log.Printf("JWT ์‹œํฌ๋ฆฟ: %s***", c.JWT.Secret[:4]) // ์•ž 4์ž๋งŒ ํ‘œ์‹œ
}

ํŒ 4: ์„ค์ • ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ฑ„๋„๋กœ ์ „๋‹ฌ

type ConfigManager struct {
    v       *viper.Viper
    current *Config
    mu      sync.RWMutex
    changes chan *Config // ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ฑ„๋„
}

func NewConfigManager(path string) (*ConfigManager, error) {
    cm := &ConfigManager{
        v:       viper.New(),
        changes: make(chan *Config, 10),
    }

    // ... ์„ค์ • ๋กœ๋“œ ...

    cm.v.WatchConfig()
    cm.v.OnConfigChange(func(e fsnotify.Event) {
        var newCfg Config
        if err := cm.v.Unmarshal(&newCfg); err != nil {
            return
        }
        cm.mu.Lock()
        cm.current = &newCfg
        cm.mu.Unlock()

        // ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋…ผ๋ธ”๋กœํ‚น)
        select {
        case cm.changes <- &newCfg:
        default: // ์ฑ„๋„์ด ๊ฝ‰ ์ฐผ์œผ๋ฉด ์Šคํ‚ต
        }
    })

    return cm, nil
}

// ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๊ตฌ๋…
func (cm *ConfigManager) Subscribe() <-chan *Config {
    return cm.changes
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
go func() {
    for newCfg := range configManager.Subscribe() {
        log.Printf("์„ค์ • ๋ณ€๊ฒฝ๋จ! ์ƒˆ ๋กœ๊ทธ ๋ ˆ๋ฒจ: %s", newCfg.Log.Level)
        // ๋กœ๊ฑฐ ์žฌ์„ค์ • ๋“ฑ ์ฒ˜๋ฆฌ
        updateLogger(newCfg.Log.Level)
    }
}()
๐Ÿ’ก ๋ณด์•ˆ ํŒ! โ€ข config.yaml์—๋Š” ์ ˆ๋Œ€ ์‹ค์ œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋‚˜ API ํ‚ค๋ฅผ ๋„ฃ์ง€ ๋งˆ
โ€ข .env ํŒŒ์ผ์€ ๋ฐ˜๋“œ์‹œ .gitignore์— ์ถ”๊ฐ€
โ€ข ํ”„๋กœ๋•์…˜์—์„œ๋Š” AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager ๊ฐ™์€ ์ „์šฉ ์‹œํฌ๋ฆฟ ๊ด€๋ฆฌ ์„œ๋น„์Šค ์‚ฌ์šฉ
โ€ข ์„ค์ •์„ ๋กœ๊ทธ์— ์ถœ๋ ฅํ•  ๋•Œ๋Š” ๋ฐ˜๋“œ์‹œ ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋งˆ์Šคํ‚น
โ€ข ์„ค์ • ํŒŒ์ผ ๊ถŒํ•œ์„ chmod 600์œผ๋กœ ์ œํ•œ (์†Œ์œ ์ž๋งŒ ์ฝ๊ธฐ/์“ฐ๊ธฐ)

๐Ÿš€ ํ™˜๊ฒฝ๋ณ„ ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ๊ณผ ์„ค์ • ๊ด€๋ฆฌ ๐Ÿ“ ์†Œ์Šค์ฝ”๋“œ config.yaml (Git ์ปค๋ฐ‹) โš™๏ธ CI/CD ๋นŒ๋“œ & ํ…Œ์ŠคํŠธ Docker ์ด๋ฏธ์ง€ ๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ์„œ๋ฒ„ APP_ENV=development config.dev.yaml ๋กœ๋“œ ๐Ÿงช ์Šคํ…Œ์ด์ง• ์„œ๋ฒ„ APP_ENV=staging config.staging.yaml ๋กœ๋“œ ๐Ÿš€ ํ”„๋กœ๋•์…˜ ์„œ๋ฒ„ APP_ENV=production Secrets Manager ์—ฐ๋™ ๐Ÿ” Secrets Manager DB_PASSWORD JWT_SECRET API_KEYS ๋™์ผํ•œ ๋ฐ”์ด๋„ˆ๋ฆฌ โ†’ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ํ™˜๊ฒฝ ๊ตฌ๋ถ„ โ†’ ๊ฐ ํ™˜๊ฒฝ์— ๋งž๋Š” ์„ค์ • ์ž๋™ ๋กœ๋“œ
๐Ÿ” ๋ณด์•ˆ ๊ฐ•ํ™” โ€” ๋ฏผ๊ฐํ•œ ์„ค์ • ์•ˆ์ „ํ•˜๊ฒŒ ๋‹ค๋ฃจ๊ธฐ

์„ค์ • ๊ด€๋ฆฌ์—์„œ ๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•ด. ํŠนํžˆ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ DB ๋น„๋ฐ€๋ฒˆํ˜ธ, API ํ‚ค, JWT ์‹œํฌ๋ฆฟ ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋ฅผ ์–ด๋–ป๊ฒŒ ๋‹ค๋ฃจ๋А๋ƒ๊ฐ€ ํ•ต์‹ฌ์ด์•ผ.

์ ˆ๋Œ€ ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ๋“ค:

๐Ÿšซ ์ด๊ฒƒ๋“ค์€ ์ ˆ๋Œ€ ํ•˜์ง€ ๋งˆ! โ€ข config.yaml์— ์‹ค์ œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ ํ›„ Git ์ปค๋ฐ‹
โ€ข ์ฝ”๋“œ์— API ํ‚ค ํ•˜๋“œ์ฝ”๋”ฉ
โ€ข ๋กœ๊ทธ์— ๋น„๋ฐ€๋ฒˆํ˜ธ ์ถœ๋ ฅ
โ€ข ํ”„๋กœ๋•์…˜ ์„ค์ • ํŒŒ์ผ์„ ๊ฐœ๋ฐœ์ž ๋กœ์ปฌ์— ๋ณต์‚ฌ
โ€ข ๋™์ผํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ฐœ๋ฐœ/์Šคํ…Œ์ด์ง•/ํ”„๋กœ๋•์…˜์—์„œ ์‚ฌ์šฉ

AWS Secrets Manager์™€ Viper ์—ฐ๋™ ์˜ˆ์‹œ:

// AWS Secrets Manager์—์„œ ์‹œํฌ๋ฆฟ ๋กœ๋“œ
package config

import (
    "encoding/json"
    "fmt"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
    "github.com/spf13/viper"
)

type AWSSecrets struct {
    DBPassword string `json:"db_password"`
    JWTSecret  string `json:"jwt_secret"`
    RedisPass  string `json:"redis_password"`
}

func loadAWSSecrets(secretName string) (*AWSSecrets, error) {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return nil, fmt.Errorf("AWS ์„ค์ • ๋กœ๋“œ ์‹คํŒจ: %w", err)
    }

    client := secretsmanager.NewFromConfig(cfg)
    result, err := client.GetSecretValue(context.TODO(),
        &secretsmanager.GetSecretValueInput{
            SecretId: aws.String(secretName),
        },
    )
    if err != nil {
        return nil, fmt.Errorf("์‹œํฌ๋ฆฟ ์กฐํšŒ ์‹คํŒจ: %w", err)
    }

    var secrets AWSSecrets
    if err := json.Unmarshal([]byte(*result.SecretString), &secrets); err != nil {
        return nil, fmt.Errorf("์‹œํฌ๋ฆฟ ํŒŒ์‹ฑ ์‹คํŒจ: %w", err)
    }

    return &secrets, nil
}

// Viper์— ์‹œํฌ๋ฆฟ ์ฃผ์ž…
func injectSecrets(v *viper.Viper, secrets *AWSSecrets) {
    v.Set("database.password", secrets.DBPassword)
    v.Set("jwt.secret", secrets.JWTSecret)
    v.Set("redis.password", secrets.RedisPass)
}

Kubernetes ํ™˜๊ฒฝ์—์„œ๋Š” Secret ๋ฆฌ์†Œ์Šค๋ฅผ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…ํ•˜๋Š” ๋ฐฉ์‹์ด ์ผ๋ฐ˜์ ์ด์•ผ:

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-go-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-go-app:latest
        env:
        - name: APP_ENV
          value: "production"
        - name: APP_DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: db-password
        - name: APP_JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: jwt-secret

๐Ÿ“Š Viper vs ๋‹ค๋ฅธ ์„ค์ • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋น„๊ต

Go ์ƒํƒœ๊ณ„์—๋Š” Viper ๋ง๊ณ ๋„ ๋‹ค๋ฅธ ์„ค์ • ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์žˆ์–ด. ์–ด๋–ค ๊ฒŒ ๋” ๋‚˜์€์ง€ ๋น„๊ตํ•ด๋ณผ๊ฒŒ:

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŠน์ง• ์žฅ์  ๋‹จ์ 
Viper ์ข…ํ•ฉ ์„ค์ • ์†”๋ฃจ์…˜ ๋‹ค์–‘ํ•œ ์†Œ์Šค ์ง€์›, ํ’๋ถ€ํ•œ ๊ธฐ๋Šฅ ๋ฌด๊ฑฐ์›€, ์˜์กด์„ฑ ๋งŽ์Œ
envconfig ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ „์šฉ ๊ฐ€๋ณ๊ณ  ์‹ฌํ”Œ, ํƒ€์ž… ์•ˆ์ „ ํŒŒ์ผ ์ง€์› ์—†์Œ
godotenv .env ํŒŒ์ผ ๋กœ๋” 12-Factor ์•ฑ ์นœํ™”์  ๊ธฐ๋Šฅ ์ œํ•œ์ 
cleanenv ํ™˜๊ฒฝ๋ณ€์ˆ˜ + ํŒŒ์ผ ๊ฐ€๋ณ๊ณ  ํƒ€์ž… ์•ˆ์ „ ์›๊ฒฉ ์„ค์ • ๋ฏธ์ง€์›
koanf Viper ๋Œ€์•ˆ ๋” ๊ฐ€๋ณ๊ณ  ๋ชจ๋“ˆํ™” ์ƒํƒœ๊ณ„ ์ž‘์Œ

์–ด๋–ค ๊ฑธ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ? ๐Ÿค”

๐ŸŽฏ ์„ ํƒ ๊ฐ€์ด๋“œ ๐Ÿ”น ์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ / ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค: envconfig + godotenv ์กฐํ•ฉ์ด ๊ฐ€๋ณ๊ณ  ์ข‹์•„
๐Ÿ”น ์ค‘๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ / ๋ชจ๋…ธ๋ฆฌ์Šค: Viper์˜ ํ’๋ถ€ํ•œ ๊ธฐ๋Šฅ์ด ๋น›์„ ๋ฐœํ•ด
๐Ÿ”น Kubernetes ๋„ค์ดํ‹ฐ๋ธŒ: ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜ envconfig๋‚˜ cleanenv๊ฐ€ ์ ํ•ฉ
๐Ÿ”น ์›๊ฒฉ ์„ค์ • ํ•„์š”: Viper๊ฐ€ ์œ ์ผํ•œ ์„ ํƒ์ง€์— ๊ฐ€๊นŒ์›Œ
๐Ÿ”น ์„ฑ๋Šฅ ๋ฏผ๊ฐ: koanf๊ฐ€ Viper๋ณด๋‹ค ๊ฐ€๋ณ๊ณ  ๋น ๋ฆ„

๐ŸŽ ์™„์„ฑ๋œ ์„ค์ • ์‹œ์Šคํ…œ โ€” ๋ชจ๋“  ๊ฒƒ์„ ํ•ฉ์ณ๋ณด์ž!

์ž, ์ด์ œ ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋ชจ๋“  ๊ฒƒ์„ ํ•ฉ์ณ์„œ ์™„์„ฑ๋œ ์„ค์ • ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž. ์ด๊ฒŒ ์‹ค๋ฌด์—์„œ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” ์™„์ „ํ•œ ๋ฒ„์ „์ด์•ผ! ๐Ÿ’ช

// config/config.go - ์™„์ „ํ•œ ํ”„๋กœ๋•์…˜ ๋ ˆ๋”” ๋ฒ„์ „
package config

import (
    "fmt"
    "os"
    "strings"
    "time"

    "github.com/spf13/viper"
)

// ===== ์„ค์ • ๊ตฌ์กฐ์ฒด =====

type Config struct {
    App      AppConfig      `mapstructure:"app"`
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    Redis    RedisConfig    `mapstructure:"redis"`
    JWT      JWTConfig      `mapstructure:"jwt"`
    Log      LogConfig      `mapstructure:"log"`
}

type AppConfig struct {
    Name    string `mapstructure:"name"`
    Version string `mapstructure:"version"`
    Env     string `mapstructure:"env"`
}

type ServerConfig struct {
    Host         string        `mapstructure:"host"`
    Port         int           `mapstructure:"port"`
    ReadTimeout  time.Duration `mapstructure:"read_timeout"`
    WriteTimeout time.Duration `mapstructure:"write_timeout"`
}

type DatabaseConfig struct {
    Host            string        `mapstructure:"host"`
    Port            int           `mapstructure:"port"`
    Name            string        `mapstructure:"name"`
    User            string        `mapstructure:"user"`
    Password        string        `mapstructure:"password"`
    SSLMode         string        `mapstructure:"ssl_mode"`
    MaxOpenConns    int           `mapstructure:"max_open_conns"`
    MaxIdleConns    int           `mapstructure:"max_idle_conns"`
    ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}

func (d DatabaseConfig) DSN() string {
    return fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
        d.Host, d.Port, d.User, d.Password, d.Name, d.SSLMode,
    )
}

type RedisConfig struct {
    Host     string `mapstructure:"host"`
    Port     int    `mapstructure:"port"`
    Password string `mapstructure:"password"`
    DB       int    `mapstructure:"db"`
}

func (r RedisConfig) Addr() string {
    return fmt.Sprintf("%s:%d", r.Host, r.Port)
}

type JWTConfig struct {
    Secret string        `mapstructure:"secret"`
    Expiry time.Duration `mapstructure:"expiry"`
}

type LogConfig struct {
    Level  string `mapstructure:"level"`
    Format string `mapstructure:"format"`
}

// ===== ์„ค์ • ๋กœ๋” =====

type Loader struct {
    v          *viper.Viper
    configDir  string
    configName string
}

func NewLoader(configDir string) *Loader {
    return &Loader{
        v:          viper.New(),
        configDir:  configDir,
        configName: "config",
    }
}

func (l *Loader) Load() (*Config, error) {
    // 1. ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
    l.setDefaults()

    // 2. ๊ธฐ๋ณธ ์„ค์ • ํŒŒ์ผ ๋กœ๋“œ
    l.v.SetConfigName(l.configName)
    l.v.SetConfigType("yaml")
    l.v.AddConfigPath(l.configDir)
    l.v.AddConfigPath(".")

    if err := l.v.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return nil, fmt.Errorf("์„ค์ • ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ: %w", err)
        }
        // ์„ค์ • ํŒŒ์ผ ์—†์–ด๋„ ๊ณ„์† ์ง„ํ–‰ (๊ธฐ๋ณธ๊ฐ’ + ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ๋งŒ ๋™์ž‘)
    }

    // 3. ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ณ‘ํ•ฉ
    env := l.detectEnv()
    if env != "" && env != "development" {
        l.v.SetConfigName(fmt.Sprintf("%s.%s", l.configName, env))
        if err := l.v.MergeInConfig(); err != nil {
            if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
                return nil, fmt.Errorf("ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ๋ณ‘ํ•ฉ ์‹คํŒจ: %w", err)
            }
        }
    }

    // 4. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ
    l.v.SetEnvPrefix("APP")
    l.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    l.v.AutomaticEnv()

    // 5. ๊ตฌ์กฐ์ฒด ์–ธ๋งˆ์ƒฌ๋ง
    var cfg Config
    if err := l.v.Unmarshal(&cfg); err != nil {
        return nil, fmt.Errorf("์„ค์ • ํŒŒ์‹ฑ ์‹คํŒจ: %w", err)
    }

    // 6. ํ™˜๊ฒฝ ์ •๋ณด ์„ค์ •
    cfg.App.Env = env

    // 7. ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
    if err := cfg.validate(); err != nil {
        return nil, fmt.Errorf("์„ค์ • ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ: %w", err)
    }

    return &cfg, nil
}

func (l *Loader) setDefaults() {
    l.v.SetDefault("app.name", "Go App")
    l.v.SetDefault("app.version", "0.0.1")
    l.v.SetDefault("server.host", "0.0.0.0")
    l.v.SetDefault("server.port", 8080)
    l.v.SetDefault("server.read_timeout", "30s")
    l.v.SetDefault("server.write_timeout", "30s")
    l.v.SetDefault("database.port", 5432)
    l.v.SetDefault("database.ssl_mode", "disable")
    l.v.SetDefault("database.max_open_conns", 25)
    l.v.SetDefault("database.max_idle_conns", 5)
    l.v.SetDefault("database.conn_max_lifetime", "5m")
    l.v.SetDefault("redis.port", 6379)
    l.v.SetDefault("redis.db", 0)
    l.v.SetDefault("jwt.expiry", "24h")
    l.v.SetDefault("log.level", "info")
    l.v.SetDefault("log.format", "json")
}

func (l *Loader) detectEnv() string {
    for _, key := range []string{"APP_ENV", "GO_ENV", "ENVIRONMENT"} {
        if env := os.Getenv(key); env != "" {
            return env
        }
    }
    return "development"
}

func (cfg *Config) validate() error {
    if cfg.App.Env == "production" {
        if cfg.Database.Password == "" {
            return fmt.Errorf("ํ”„๋กœ๋•์…˜: DB ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„์ˆ˜")
        }
        if len(cfg.JWT.Secret) < 32 {
            return fmt.Errorf("ํ”„๋กœ๋•์…˜: JWT ์‹œํฌ๋ฆฟ ์ตœ์†Œ 32์ž")
        }
    }
    if cfg.Server.Port < 1 || cfg.Server.Port > 65535 {
        return fmt.Errorf("์œ ํšจํ•˜์ง€ ์•Š์€ ์„œ๋ฒ„ ํฌํŠธ: %d", cfg.Server.Port)
    }
    return nil
}

// ===== ํŽธ์˜ ํ•จ์ˆ˜ =====

// MustLoad - ์‹คํŒจ ์‹œ ํŒจ๋‹‰ (main์—์„œ ์‚ฌ์šฉ)
func MustLoad(configDir string) *Config {
    cfg, err := NewLoader(configDir).Load()
    if err != nil {
        panic(fmt.Sprintf("์„ค์ • ๋กœ๋“œ ์‹คํŒจ: %v", err))
    }
    return cfg
}
โœ… ์ด ์™„์„ฑ ๋ฒ„์ „์˜ ํŠน์ง• โ€ข Loader ํŒจํ„ด์œผ๋กœ ์„ค์ • ๋กœ๋”ฉ ๋กœ์ง ์บก์Аํ™”
โ€ข ๊ธฐ๋ณธ๊ฐ’ โ†’ ํŒŒ์ผ โ†’ ํ™˜๊ฒฝ๋ณ„ ํŒŒ์ผ โ†’ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ˆœ์„œ๋กœ ๋ณ‘ํ•ฉ
โ€ข ํ™˜๊ฒฝ ์ž๋™ ๊ฐ์ง€ (APP_ENV, GO_ENV, ENVIRONMENT ์ˆœ์„œ)
โ€ข ํŽธ์˜ ๋ฉ”์„œ๋“œ (DSN(), Addr()) ์ œ๊ณต
โ€ข ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
โ€ข MustLoad()๋กœ main์—์„œ ๊ฐ„ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉ

๐ŸŒŸ ๋งˆ๋ฌด๋ฆฌ โ€” ์„ค์ • ๊ด€๋ฆฌ์˜ ํ™ฉ๊ธˆ ๋ฒ•์น™

์™€, ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! ๐ŸŽ‰ Go ์–ธ์–ด์—์„œ Viper๋ฅผ ํ™œ์šฉํ•œ ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ์‹œ์Šคํ…œ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋‹ค ์‚ดํŽด๋ดค์–ด. ๋งˆ์ง€๋ง‰์œผ๋กœ ํ•ต์‹ฌ ์›์น™๋“ค์„ ์ •๋ฆฌํ•ด์ค„๊ฒŒ:

๐Ÿ† Go ์„ค์ • ๊ด€๋ฆฌ ํ™ฉ๊ธˆ ๋ฒ•์น™ 10๊ฐ€์ง€ 1๏ธโƒฃ ์ฝ”๋“œ์™€ ์„ค์ •์„ ๋ถ„๋ฆฌํ•˜๋ผ โ€” ํ•˜๋“œ์ฝ”๋”ฉ์€ ์ ˆ๋Œ€ ๊ธˆ์ง€
2๏ธโƒฃ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ โ€” ํŒŒ์ผ์— ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ ๊ธˆ์ง€
3๏ธโƒฃ ๊ธฐ๋ณธ๊ฐ’์„ ํ•ญ์ƒ ์„ค์ •ํ•˜๋ผ โ€” SetDefault()๋กœ ํด๋ฐฑ ๋ณด์žฅ
4๏ธโƒฃ ํƒ€์ž… ์•ˆ์ „ํ•œ ๊ตฌ์กฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋ผ โ€” Unmarshal๋กœ ํƒ€์ž… ๋ณด์žฅ
5๏ธโƒฃ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ๋ฐ˜๋“œ์‹œ ํ•ด๋ผ โ€” ์ž˜๋ชป๋œ ์„ค์ •์€ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌ
6๏ธโƒฃ ์ „์—ญ Viper ๋Œ€์‹  ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ผ โ€” ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ ํ™•๋ณด
7๏ธโƒฃ ์˜์กด์„ฑ ์ฃผ์ž…์œผ๋กœ ์„ค์ •์„ ์ „๋‹ฌํ•˜๋ผ โ€” ๋ช…์‹œ์  ์˜์กด์„ฑ
8๏ธโƒฃ ์„ค์ • ํŒŒ์ผ์€ Git์— ์ปค๋ฐ‹ ๊ฐ€๋Šฅํ•˜๊ฒŒ โ€” ๋ฏผ๊ฐ ์ •๋ณด ์ œ์™ธ
9๏ธโƒฃ ํ™˜๊ฒฝ๋ณ„ ํŒŒ์ผ๋กœ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜๋ผ โ€” config.dev.yaml ๋“ฑ
๐Ÿ”Ÿ ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์‹œํฌ๋ฆฟ ๋งค๋‹ˆ์ €๋ฅผ ์จ๋ผ โ€” AWS SM, Vault ๋“ฑ

Go ์–ธ์–ด๋กœ ๊ฐœ๋ฐœํ•  ๋•Œ ์„ค์ • ๊ด€๋ฆฌ๋ฅผ ์ œ๋Œ€๋กœ ํ•˜๋ฉด ํŒ€ ํ˜‘์—…๋„ ์‰ฌ์›Œ์ง€๊ณ , ๋ฐฐํฌ๋„ ์•ˆ์ „ํ•ด์ง€๊ณ , ๋ณด์•ˆ ์‚ฌ๊ณ ๋„ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์žˆ์–ด. Viper๋Š” ์ด ๋ชจ๋“  ๊ฑธ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์•ผ.

ํ˜น์‹œ Go ๊ฐœ๋ฐœ ๊ด€๋ จํ•ด์„œ ๋” ๋ฐฐ์šฐ๊ณ  ์‹ถ๊ฑฐ๋‚˜, ์‹ค๋ฌด ํ”„๋กœ์ ํŠธ์—์„œ ์„ค์ • ์‹œ์Šคํ…œ ๊ตฌ์ถ•์„ ๋„์›€๋ฐ›๊ณ  ์‹ถ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ Go ์ „๋ฌธ ๊ฐœ๋ฐœ์ž๋“ค์˜ ์žฌ๋Šฅ์„ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์–ด. ๋‹ค์–‘ํ•œ Go ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋“ค์ด ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ถ€ํ„ฐ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„๊นŒ์ง€ ๋„์›€์„ ์ค„ ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ˜Š

์ž, ์ด์ œ ๋„ˆ์˜ Go ํ”„๋กœ์ ํŠธ์— Viper๋ฅผ ๋„์ž…ํ•ด๋ด! ์ฒ˜์Œ์—” ์ข€ ๋ณต์žกํ•ด ๋ณด์—ฌ๋„, ํ•œ ๋ฒˆ ์ œ๋Œ€๋กœ ์„ธํŒ…ํ•ด๋‘๋ฉด ๋‚˜์ค‘์— ์ •๋ง ํŽธํ•ด. ํŠนํžˆ ํŒ€ ํ”„๋กœ์ ํŠธ๋‚˜ ๋ฐฐํฌ ์ž๋™ํ™”๋ฅผ ํ•  ๋•Œ ๊ทธ ์ง„๊ฐ€๋ฅผ ๋ฐœํœ˜ํ•˜๊ฑฐ๋“ . ํ™”์ดํŒ…! ๐Ÿš€

๐Ÿ’ก ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ถ”์ฒœํ•˜๋Š” ํ•™์Šต ์ฃผ์ œ๋“ค ๐Ÿ”น cobra + viper: CLI ์•ฑ์—์„œ ํ”Œ๋ž˜๊ทธ์™€ ์„ค์ • ํ†ตํ•ฉ
๐Ÿ”น Wire / fx: Go ์˜์กด์„ฑ ์ฃผ์ž… ํ”„๋ ˆ์ž„์›Œํฌ
๐Ÿ”น HashiCorp Vault: ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ์‹œํฌ๋ฆฟ ๊ด€๋ฆฌ
๐Ÿ”น Kubernetes ConfigMap/Secret: K8s ๋„ค์ดํ‹ฐ๋ธŒ ์„ค์ • ๊ด€๋ฆฌ
๐Ÿ”น OpenTelemetry: ์„ค์ • ๊ธฐ๋ฐ˜ ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ ๊ตฌํ˜„
๐ŸŽฏ Viper ์„ค์ • ์‹œ์Šคํ…œ ํ•ต์‹ฌ ์š”์•ฝ ๐Ÿ“ฆ ์„ค์น˜ go get github.com/spf13/viper v1.x ์ตœ์‹  ๋ฒ„์ „ ์‚ฌ์šฉ โš™๏ธ ํ•ต์‹ฌ API SetDefault / ReadInConfig MergeInConfig / AutomaticEnv Unmarshal / WatchConfig ๐Ÿ”’ ๋ณด์•ˆ ๋ฏผ๊ฐ์ •๋ณด โ†’ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋งŒ ํ”„๋กœ๋•์…˜ โ†’ Secrets Manager .env โ†’ .gitignore ํ•„์ˆ˜ ๐ŸŒ ํ™˜๊ฒฝ ๋ถ„๋ฆฌ config.yaml (๊ณตํ†ต) config.dev/staging/prod.yaml APP_ENV ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์„ ํƒ ๐Ÿงช ํ…Œ์ŠคํŠธ viper.New() ์ธ์Šคํ„ด์Šค ์‚ฌ์šฉ ์ž„์‹œ ํŒŒ์ผ๋กœ ๊ฒฉ๋ฆฌ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •/ํ•ด์ œ ์Œ์œผ๋กœ ๐Ÿ”„ Hot Reload WatchConfig() ํ™œ์„ฑํ™” OnConfigChange ์ฝœ๋ฐฑ RWMutex๋กœ ๋™์‹œ์„ฑ ๋ณดํ˜ธ ๐Ÿน Go + ๐Ÿ Viper = ์™„๋ฒฝํ•œ ์„ค์ • ๊ด€๋ฆฌ ์‹œ์Šคํ…œ

Go ์–ธ์–ด์˜ ์„ค์ • ๊ด€๋ฆฌ๋Š” ์ฒ˜์Œ์—” ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, Viper๋ฅผ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋ผ. ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋„ˆ์˜ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด๊ณ , ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“ ์ง€ Go ์ปค๋ฎค๋‹ˆํ‹ฐ๋‚˜ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์•„๋ด! ๐ŸŽ‰

์ฝ”๋”ฉ ์ฆ๊ฒ๊ฒŒ ํ•˜๊ณ , ์„ค์ • ๋•Œ๋ฌธ์— ๋ฐค์ƒˆ๋Š” ์ผ ์—†๊ธฐ๋ฅผ ๋ฐ”๋ž„๊ฒŒ! ๐ŸŒ™โœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.