Go 앱 설정을 Viper와 Struct Tag로 간소화하기
Grace Collins
Solutions Engineer · Leapcell

소개
웹 개발의 역동적인 세계에서 애플리케이션은 거의 독립적으로 존재하지 않습니다. 로컬 개발부터 스테이징, 그리고 최종적으로 프로덕션에 이르기까지 각 환경은 데이터베이스 연결 문자열, API 키, 포트 번호 등 고유한 설정 집합을 요구합니다. 이러한 설정을 수동으로 관리하는 것은 지루할 뿐만 아니라, 특히 프로젝트 규모가 커지고 복잡성이 증가함에 따라 오류가 발생하기 쉽습니다. 강력하고 유연하며 유지보수하기 쉬운 구성 관리 전략의 필요성이 무엇보다 중요해집니다. 이 글은 Go 생태계에서 강력한 조합을 탐구합니다: Viper
라이브러리와 Go의 네이티브 struct tag를 함께 사용하여 웹 애플리케이션의 다중 환경 설정을 우아하게 처리하고, 일관성을 보장하며, 상용구 코드를 줄이고, 개발자 경험을 개선합니다.
핵심 개념 설명
구현 세부 사항에 들어가기 전에, 활용할 핵심 도구와 개념에 대한 명확한 이해를 확립해 보겠습니다.
- Viper: Go 애플리케이션을 위한 완벽한 구성 솔루션입니다. 다양한 소스(파일, 환경 변수, 명령줄 플래그, 원격 KV)에서 구성을 읽고, 기본값을 처리하고, 변경 사항을 감시하는 강력한 기능을 제공합니다. 주요 강점은 구성 소스를 추상화할 수 있어 애플리케이션이 설정의 출처에 상관없이 무관심하게 유지할 수 있다는 것입니다.
- Struct Tags: Go 언어 기능으로, struct 필드에 메타데이터를 연결할 수 있습니다. 이 태그는 필드 선언과 관련된 문자열 리터럴이며 런타임에 리플렉션을 통해 접근할 수 있습니다. Go에서 마샬링, 언마샬링, 유효성 검사, 그리고 여기 우리 경우에는 구성 키를 struct 필드에 매핑하는 데 널리 사용됩니다. 예를 들어,
json:"name"
은encoding/json
패키지에서 널리 사용되는 struct tag입니다. - 다중 환경 구성: 여러 배포 환경(예:
development
,staging
,production
)에 대해 별도의 구성 설정을 유지하는 관행입니다. 이를 통해 애플리케이션은 어디에서 실행되든 올바르고 안전하게 작동합니다.
구성 관리 원칙
우리의 접근 방식은 몇 가지 핵심 원칙을 따를 것입니다.
- 중앙 집중식이지만 유연함: 구성은 쉽게 접근할 수 있어야 하지만 환경별 재정의를 허용해야 합니다.
- 타입 안전성: 구성 값은 Go 타입으로 언마샬링되어 컴파일 시간 확인을 제공하고 런타임 오류를 줄여야 합니다.
- 가독성 및 유지보수성: 구성 파일과 이를 처리하는 코드는 이해하고 수정하기 쉬워야 합니다.
- 환경 변경에 대한 코드 변경 없음: 이상적으로 환경 간 전환은 애플리케이션 코드가 아닌 구성 변경만 필요해야 합니다.
다중 환경 구성 구현
Viper와 struct tag를 사용하여 다중 환경 구성을 설정하는 실제 예제를 살펴보겠습니다.
프로젝트 설정
먼저 새 Go 모듈을 초기화합니다:
mkdir go-config-app cd go-config-app go mod init go-config-app go get github.com/spf13/viper
구성 구조 정의
애플리케이션 구성 설정을 나타내는 Go struct를 정의하는 것으로 시작하겠습니다. 이 struct는 구성 키를 필드에 매핑하기 위한 viper
struct tag를 포함할 것입니다.
package config import ( "log" time "time" "github.com/spf13/viper" ) // AppConfig는 애플리케이션의 구성 설정을 보유합니다. type AppConfig struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` Logger LoggerConfig `mapstructure:"logger"` } // ServerConfig는 서버 관련 설정을 정의합니다. type ServerConfig struct { Port int `mapstructure:"port"` ReadTimeout time.Duration `mapstructure:"read_timeout"` WriteTimeout time.Duration `mapstructure:"write_timeout"` IdleTimeout time.Duration `mapstructure:"idle_timeout"` } // DatabaseConfig는 데이터베이스 연결 설정을 정의합니다. type DatabaseConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` User string `mapstructure:"user"` Password string `mapstructure:"password"` DBName string `mapstructure:"dbname"` SSLMode string `mapstructure:"sslmode"` } // LoggerConfig는 로깅 설정을 정의합니다. type LoggerConfig struct { Level string `mapstructure:"level"` Path string `mapstructure:"path"` } var cfg AppConfig // LoadConfig는 애플리케이션 구성을 초기화하고 로드합니다. func LoadConfig() *AppConfig { vm.SetConfigFile(".env") // 환경별 재정의를 위해 먼저 .env를 찾습니다. vm.SetConfigName("config") // 구성 파일 이름(확장자 없이) vm.SetConfigType("yaml") // 또는 yaml, json 등 vm.AddConfigPath("./config") // 구성 파일을 찾을 경로 // 기본값 설정 vm.SetDefault("server.port", 8080) vm.SetDefault("server.read_timeout", "5s") vm.SetDefault("server.write_timeout", "10s") vm.SetDefault("server.idle_timeout", "60s") vm.SetDefault("database.host", "localhost") vm.SetDefault("database.port", 5432) vm.SetDefault("database.user", "default_user") vm.SetDefault("database.password", "default_password") vm.SetDefault("database.dbname", "app_database") vm.SetDefault("database.sslmode", "disable") vm.SetDefault("logger.level", "info") vm.SetDefault("logger.path", "/var/log/app.log") // 환경 변수 읽기 vm.AutomaticEnv() // 일치하는 환경 변수 읽기 vm.SetEnvPrefix("APP") // APP_SERVER_PORT와 같은 env 변수를 찾게 됩니다. // 파일에서 구성 읽기 시도 if err := vm.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { log.Println("Config file not found, using defaults and environment variables.") } else { log.Fatalf("Fatal error reading config file: %s \n", err) } } // 최종 구성 구성을 AppConfig struct에 언마샬링합니다. if err := vm.Unmarshal(&cfg); err != nil { log.Fatalf("Unable to unmarshal config into struct: %s \n", err) } return &cfg } // GetConfig는 로드된 애플리케이션 구성을 반환합니다. func GetConfig() *AppConfig { return &cfg }
이 config
패키지에서:
AppConfig
,ServerConfig
,DatabaseConfig
,LoggerConfig
는 구성 스키마를 정의하는 Go struct입니다.mapstructure
태그는 중요합니다. 이는 Viper에 구성 소스(예: YAML 파일의server.port
)의 키를 해당 struct 필드(ServerConfig
의Port
)에 매핑하는 방법을 알려줍니다.LoadConfig
함수는 다음을 담당합니다:- Viper의 구성 파일 이름과 유형 설정.
- Viper가 구성 파일을 찾을 경로 추가.
viper.SetDefault
를 사용하여 적절한 기본값 정의.viper.AutomaticEnv()
및viper.SetEnvPrefix("APP")
활성화하여 환경 변수(예:APP_SERVER_PORT
,APP_DATABASE_HOST
)가 파일 기반 설정이나 기본값을 재정의하도록 허용합니다. 이는 다중 환경 배포에 중요합니다.- 구성 파일 읽기.
- 최종적으로 병합된 구성을
AppConfig
struct에 언마샬링합니다.
구성 파일
go-config-app
의 루트에 config
디렉토리를 만들고 구성 파일을 추가해 보겠습니다.
go-config-app/config/config.yaml
:
server: port: 8080 read_timeout: 10s write_timeout: 15s idle_timeout: 90s database: host: localhost port: 5432 user: app_user_dev password: dev_password dbname: app_dev_db sslmode: disable logger: level: debug path: /tmp/app_dev.log
환경별 재정의의 경우, Viper
는 현재 작업 디렉토리에도 .env
라는 파일을 찾게 되며, 이는 로컬 개발 트윅이나 민감한 데이터에 매우 유용합니다.
go-config-app/.env
(로컬 개발 또는 특정 재정의용):
APP_DATABASE_PASSWORD=local_dev_password_override
APP_LOGGER_LEVEL=trace
애플리케이션에서 구성 사용
이제 메인 애플리케이션 파일에서 이 구성을 사용하는 방법을 살펴보겠습니다.
go-config-app/main.go
:
package main import ( "fmt" "log" "net/http" time "time" "go-config-app/config" // config 패키지를 가져옵니다. ) func main() { cfg := config.LoadConfig() // 로드된 구성의 예시 사용 fmt.Printf("Server Port: %d\n", cfg.Server.Port) fmt.Printf("Database Host: %s\n", cfg.Database.Host) fmt.Printf("Database User: %s\n", cfg.Database.User) fmt.Printf("Logger Level: %s\n", cfg.Logger.Level) fmt.Printf("Read Timeout: %s\n", cfg.Server.ReadTimeout) // 서버 시작 시뮬레이션 mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from the Go app on port %d, DB: %s, Logger: %s", c.Server.Port, c.Database.DBName, c.Logger.Level) }) server := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Server.Port), Handler: mux, ReadTimeout: cfg.Server.ReadTimeout, WriteTimeout: cfg.Server.WriteTimeout, IdleTimeout: cfg.Server.IdleTimeout, } log.Printf("Starting server on :%d", cfg.Server.Port) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed to start: %v", err) } }
재정의 시연
애플리케이션을 실행하고 구성을 관찰해 봅시다:
-
기본/파일 기반 구성:
go run main.go
출력:
Server Port: 8080 Database Host: localhost Database User: app_user_dev Logger Level: trace # .env에 의해 재정의됨 Read Timeout: 10s Starting server on :8080
Logger Level
이.env
파일이config.yaml
설정을 재정의했기 때문에trace
인 것을 확인하세요. 이는 우선순위 순서를 보여줍니다: 명시적인.env
파일 > 일반 구성 파일 > 기본값. -
환경 변수 재정의:
APP_SERVER_PORT=9000 APP_DATABASE_HOST=production_db.com go run main.go
출력:
Server Port: 9000 Database Host: production_db.com Database User: app_user_dev Logger Level: trace Read Timeout: 10s Starting server on :9000
여기서
APP_SERVER_PORT
및APP_DATABASE_HOST
환경 변수는.env
와config.yaml
모두보다 우선 순위를 가졌으며, 최고 수준의 재정의를 보여줍니다. 이는 CI/CD 파이프라인에서 파일을 수정하지 않고 환경별 값을 주입하는 데 매우 유용합니다.
이 접근 방식의 장점
- 명확성 및 구성: 구성은 Go struct로 깔끔하게 구조화되어 애플리케이션의 구성 스키마를 이해하기 쉽게 만듭니다.
- 타입 안전성: struct로의 언마샬링은 구성 값이 올바른 타입인지 확인하여 오류를 조기에 잡아냅니다.
time.Duration
파싱은mapstructure
에 의해 자동으로 처리됩니다. - 유연성: 명확한 우선순위 순서로 여러 구성 소스(파일, 환경 변수, 기본값)를 지원합니다.
- 유지보수성:
config
패키지의 중앙 집중식 구성 로드는 업데이트하거나 확장하기 쉽게 만듭니다. - 테스트 용이성: 구성을 쉽게 모킹하거나 단위 테스트용으로 주입할 수 있습니다.
- 개발자 경험: 개발자는 Go struct에서 바로 사용 가능한 구성 옵션과 해당 유형을 빠르게 확인할 수 있습니다.
결론
효과적인 다중 환경 구성 관리는 강력하고 확장 가능한 Go 웹 애플리케이션을 구축하는 기본적입니다. 다양한 구성 로드를 위한 Viper
의 강력함과 구조화되고 타입이 안전한 언마샬링을 위한 Go의 네이티브 struct tag를 결합함으로써 개발자는 매우 유연하고 유지보수 가능한 구성 시스템을 달성할 수 있습니다. 이 접근 방식은 구성 로직을 중앙 집중화하고, 명확한 재정의 메커니즘을 제공하며, 다양한 배포 환경에 걸쳐 애플리케이션의 안정성과 이식성을 크게 향상시킵니다. 궁극적으로 이 방법은 개발자가 적응성이 뛰어나고 오류에 강하며 유지보수하기 즐거운 애플리케이션을 구축할 수 있도록 하여, 구성을 끊임없는 장애물이 아닌 해결된 문제로 만듭니다.