Go Gin 애플리케이션에서 Viper를 사용한 설정 간소화
Wenhao Wang
Dev Intern · Leapcell

소개
백엔드 개발의 세계에서 강력하고 유지보수하기 쉬운 애플리케이션을 구축하는 것은 잘 설계된 구성 전략에 달려있는 경우가 많습니다. 코드를 직접 하드코딩하면 다양한 환경(개발, 테스트 또는 실제 환경)에 걸쳐 배포하기 어려운 유연하지 않은 시스템으로 빠르게 이어질 수 있습니다. 데이터베이스 연결 문자열, API 키 또는 서버 포트 번호를 애플리케이션을 이동할 때마다 변경해야 한다고 상상해 보세요. 이는 상당한 유지보수 부담을 초래할 뿐만 아니라 인적 오류의 위험도 증가시킵니다.
이것이 바로 구조화된 구성 관리가 필수적인 이유입니다. 이러한 변경 가능한 매개변수를 외부화함으로써 코드 수정 및 재컴필 없이 다양한 운영 컨텍스트에 원활하게 적응할 수 있도록 애플리케이션을 강화합니다. 인기 있는 Gin 프레임워크로 구축된 Go 애플리케이션의 경우 강력한 구성 라이브러리를 통합하는 것은 매우 중요합니다. 이 기사에서는 포괄적인 구성 솔루션인 Viper를 활용하여 Go Gin 프로젝트 내에서 유연하고 계층적인 구성 관리를 달성하는 방법을 안내하며, 적응성과 유지보수성을 크게 향상시킵니다.
핵심 개념 설명
구현 세부 사항을 자세히 알아보기 전에 논의할 핵심 용어와 개념에 대한 공통된 이해를 확립해 보겠습니다.
- 구성 관리: 이는 외부 매개변수에 따라 애플리케이션이 설정되고 작동하는 방식을 관리하는 관행입니다. 코드를 직접 코딩하는 대신 별도로 저장(예: 파일, 환경 변수)하고 런타임에 로드합니다.
- Viper: Go 애플리케이션을 위한 완벽한 구성 솔루션입니다. JSON, TOML, YAML, HCL, INI, 환경 변수, 명령줄 플래그 및 원격 구성 시스템과 같은 다양한 소스에서 구성을 읽을 수 있습니다. 또한 기본값 설정, 구성 변경 사항 감시 및 Go 구조체로 구성 매핑과 같은 기능도 제공합니다.
- Gin 프레임워크: Go용 고성능 경량 웹 프레임워크입니다. 속도와 단순성으로 인해 RESTful API 및 웹 서비스를 구축하는 데 널리 사용됩니다. 우리의 목표는 Gin 애플리케이션 내에서 Viper의 기능을 원활하게 통합하는 것입니다.
- 환경 변수: 컴퓨터에서 실행 중인 프로세스의 동작 방식에 영향을 줄 수 있는 동적 명명 값입니다. 특히 컨테이너화된 환경이나 클라우드 환경에서 애플리케이션에 구성을 전달하는 일반적이고 효과적인 방법입니다.
- 구성 파일: 애플리케이션 설정을 저장하는 데 사용되는 구조화된 파일(예:
config.yaml
,config.json
)입니다. 구성 정의를 위한 사람이 읽을 수 있고 버전 관리 가능한 방법을 제공합니다. - 기본값: 명시적인 외부 구성 없이도 애플리케이션이 실행될 수 있도록 보장하는 애플리케이션이 사용할 사전 정의된 설정입니다.
Go Gin에서 Viper를 사용한 구조화된 구성 구현
Viper를 사용하는 핵심 원칙은 구성 로딩을 위한 계층 구조를 정의하는 것입니다. Viper는 일반적으로 특정 순서로 설정을 검색합니다. 기본값, 구성 파일, 환경 변수, 그리고 명령줄 플래그(여기서는 주로 파일과 환경 변수에 중점을 둘 것입니다). 이 계층적 접근 방식은 보다 구체적인 설정이 일반적인 설정을 재정의하도록 보장합니다.
1단계: Go Gin 프로젝트 초기화
먼저 기본 Go Gin 프로젝트를 설정해 보겠습니다.
mkdir gin-viper-config cd gin-viper-config go mod init gin-viper-config go get github.com/gin-gonic/gin go get github.com/spf13/viper
main.go
파일을 생성합니다.
package main import ( "fmt" "log" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) // 서버 실행 전 구성을 통합합니다. err := r.Run(":8080") // 나중에 구성 가능해질 기본 포트 if err != nil { log.Fatalf("Server failed to start: %v", err) } }
2단계: 구성 구조 정의
예상되는 구성과 일치하는 Go 구조체를 정의하는 것이 좋습니다. 이를 통해 Viper는 구성 값을 직접 구조체로 역직렬화하여 유형 안전성과 쉬운 액세스를 제공할 수 있습니다.
우리 애플리케이션에 서버 포트, 데이터베이스 연결 문자열, 그리고 일부 사용자 정의 애플리케이션 설정이 필요하다고 가정해 보겠습니다.
새 파일 config/config.go
를 생성합니다.
package config import ( "log" "strings" "time" "github.com/spf13/viper" ) // AppConfig는 모든 애플리케이션 전체 구성을 보유합니다. type AppConfig struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` App AppCustomConfig `mapstructure:"app"` } // ServerConfig는 서버 관련 구성을 보유합니다. type ServerConfig struct { Port int `mapstructure:"port"` ReadTimeout time.Duration `mapstructure:"readTimeout"` WriteTimeout time.Duration `mapstructure:"writeTimeout"` } // DatabaseConfig는 데이터베이스 관련 구성을 보유합니다. type DatabaseConfig struct { Driver string `mapstructure:"driver"` Host string `mapstructure:"host"` Port int `mapstructure:"port"` User string `mapstructure:"user"` Password string `mapstructure:"password"` DBName string `mapstructure:"dbName"` SSLMode string `mapstructure:"sslMode"` } // AppCustomConfig는 사용자 정의 애플리케이션 설정을 보유합니다. type AppCustomConfig struct { APIVersion string `mapstructure:"apiVersion"` DebugMode bool `mapstructure:"debugMode"` } var Cfg *AppConfig // 파싱된 구성 정보를 보유할 전역 변수 // LoadConfig는 Viper를 초기화하고 다양한 소스에서 구성을 로드합니다. func LoadConfig() { v.SetConfigFile(".env") // 먼저 .env를 찾습니다. v.ReadInConfig() // .env가 존재하면 읽습니다. // 기본값 설정 v.SetDefault("server.port", 8080) v.SetDefault("server.readTimeout", 5*time.Second) v.SetDefault("server.writeTimeout", 10*time.Second) v.SetDefault("database.driver", "postgres") v.SetDefault("database.host", "localhost") v.SetDefault("database.port", 5432) v.SetDefault("database.user", "user") v.SetDefault("database.password", "password") v.SetDefault("database.dbName", "app_db") v.SetDefault("database.sslMode", "disable") v.SetDefault("app.apiVersion", "v1.0") v.SetDefault("app.debugMode", true) // 구성 파일 이름 및 경로 설정 v.SetConfigName("config") // 구성 파일 이름(예: config.yaml) v.AddConfigPath(".") // 현재 디렉토리에서 구성 검색 v.AddConfigPath("./config") // 'config' 하위 디렉토리에서 구성 검색 // 구성 파일 유형 설정 v.SetConfigType("yaml") // "json", "toml" 등 가능 // 구성 파일 읽기 if err := v.ReadInConfig(); err != nil { if _, ok := err.(v.ConfigFileNotFoundError); ok { log.Println("Config file not found, using defaults and environment variables.") } else { log.Fatalf("Fatal error reading config file: %s \n", err) } } // ENV 변수 오버라이드 활성화 v.AutomaticEnv() // 환경 변수를 구성 필드에 매핑합니다. // 예: `APP_SERVER_PORT`는 `server.port`에 매핑됩니다. v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) v.AllowEmptyEnv(true) // 비어 있는 환경 변수를 존재하지 않는 것으로 간주(선택적 값에 유용) // 구성을 구조체로 역직렬화합니다. if err := v.Unmarshal(&Cfg); err != nil { log.Fatalf("Unable to decode into struct, %v", err) } log.Println("Configuration loaded successfully!") // 시연을 위해 일부 값 출력 log.Printf("Server Port: %d", Cfg.Server.Port) log.Printf("DB Host: %s", Cfg.Database.Host) log.Printf("API Version: %s", Cfg.App.APIVersion) }
3단계: 구성 파일 생성
프로젝트 루트에 config.yaml
파일을 만들어 설정을 정의해 보겠습니다.
# config.yaml server: port: 8081 readTimeout: 10s writeTimeout: 15s database: driver: "mysql" host: "db.example.com" port: 3306 user: "root" password: "secure_password" dbName: "my_app_prod" sslMode: "require" app: apiVersion: "v2.0" debugMode: false
4단계: main.go
에 통합
이제 main.go
를 수정하여 구성을 로드하고 값을 사용해 보겠습니다.
package main import ( "fmt" "log" "net/http" "time" "gin-viper-config/config" // 구성 패키지 가져오기 "github.com/gin-gonic/gin" ) func main() { // 가장 먼저 구성을 로드합니다. config.LoadConfig() r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", "db_host": config.Cfg.Database.Host, "api_version": config.Cfg.App.APIVersion, "debug_mode": config.Cfg.App.DebugMode, }) }) r.GET("/env-test", func(c *gin.Context) { // 구성 및 환경 변수 액세스 시연 c.JSON(http.StatusOK, gin.H{ "server_port": config.Cfg.Server.Port, "db_user": config.Cfg.Database.User, }) }) // 구성된 서버 설정 사용 srv := &http.Server{ Addr: fmt.Sprintf(":%d", config.Cfg.Server.Port), Handler: r, ReadTimeout: config.Cfg.Server.ReadTimeout, WriteTimeout: config.Cfg.Server.WriteTimeout, } log.Printf("Starting Gin server on port %d...", config.Cfg.Server.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed to start: %v", err) } }
5단계: 구성 테스트
애플리케이션을 실행합니다.
go run main.go
p config.yaml
값이 사용되고 있음을 나타내는 다음과 유사한 출력이 표시될 것입니다.
2023/10/27 10:30:00 Configuration loaded successfully!
2023/10/27 10:30:00 Server Port: 8081
2023/10/27 10:30:00 DB Host: db.example.com
2023/10/27 10:30:00 API Version: v2.0
2023/10/27 10:30:00 Starting Gin server on port 8081...
이제 환경 변수를 사용하여 값을 재정의해 보겠습니다. viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
를 설정했으므로 server.port
는 SERVER_PORT
가 되고 database.user
는 DATABASE_USER
가 됩니다.
DATABASE_USER=admin SERVER_PORT=9000 go run main.go
출력은 이제 환경 변수 재정의를 반영해야 합니다.
2023/10/27 10:30:30 Configuration loaded successfully!
2023/10/27 10:30:30 Server Port: 9000
2023/10/27 10:30:30 DB Host: db.example.com
2023/10/27 10:30:30 API Version: v2.0
2023/10/27 10:30:30 Starting Gin server on port 9000...
브라우저나 curl
을 사용하여 http://localhost:9000/env-test
에 액세스해 보겠습니다.
curl http://localhost:9000/env-test
다음에 대한 응답을 받아야 합니다.
{"db_user":"admin","server_port":9000}
이는 Viper의 구성 계층 구조의 힘을 보여줍니다. 환경 변수는 파일 설정을 재정의하고, 파일 설정은 기본값을 재정의합니다.
애플리케이션 시나리오
- 다중 환경 배포: 다른
config.yaml
파일을 갖거나 배포 파이프라인에서 환경 변수를 설정하여 개발, 스테이징 및 실제 구성 간에 쉽게 전환할 수 있습니다. - 중앙 집중식 구성: 이 기본 예에서는 다루지 않았지만 Viper는 etcd 또는 Consul과 같은 원격 구성 시스템을 지원하여 동적이고 중앙 집중식 구성 업데이트를 허용합니다.
- 비밀 관리: Viper를 환경 변수와 결합하여 데이터베이스 암호 또는 API 키와 같은 민감한 데이터를 버전 제어된 구성 파일에 직접 저장하는 대신 환경 변수로 저장합니다(예: Kubernetes 비밀 또는 AWS Parameter Store에서).
- 명령줄 재정의: CLI 도구 또는 특정 임시 실행의 경우 Viper는 명령줄 플래그를 처리하여 런타임 사용자 정의 계층을 제공할 수 있습니다.
결론
Viper 라이브러리를 Go Gin 애플리케이션에 통합함으로써 구성을 관리하기 위한 강력하고 유연하며 견고한 솔루션을 얻을 수 있습니다. 이 접근 방식은 깔끔한 코드 작성을 촉진하고 다양한 환경에 걸친 배포를 단순화하며 애플리케이션 설정 변경과 관련된 위험을 최소화합니다. 구조화된 구성 관리를 채택하는 것은 복원력 있고 확장 가능한 백엔드 서비스를 구축하기 위한 기본 단계입니다. 잘 구성된 애플리케이션은 진정으로 적응력 있는 애플리케이션입니다.