Configuration도 굉장히 중요한 영역 중에 하나죠
굉장히 다양한 방법과 의견이 나뉘는 분야입니다
JSON, YAML, 환경변수 등 종류도 다양하고
Cloud, Container, Command Line 등 환경도 다양합니다
그래서 golang에서 config를 어떻게 다루는게 좋을지 정리해보려 합니다
우선 12 factors에서 Configuration에 대해 언급하는 부분이 있습니다
2021.08.12 - [Cloud/Cloud Native] - The Twelve Factors
간단히 살펴보면 아래와 같습니다
- 설정 정보는 코드로부터 분리
- 환경마다 다른 설정 파일을 사용하자 - 예) dev.conf, prod.conf 등
- 환경변수 활용
설정 정보를 모두 환경변수만으로 처리하기엔 사실상 어려운 점이 많습니다
대신 환경변수를 같이 활용하는 설정 방법들이 존재합니다
사내에서는 Scala & Akka framework를 활용해 개발을 해왔고
go에서의 Actor 사용을 위해 Proto Actor를 활용했기 때문에
HOCON(Human-Optimized Config Object Notation) config 방식을 go에서 사용했습니다
(go에서의 Actor도 나중에 포스팅 하겠습니다)
하지만 역시 go에 왔으면 go의 문화를 따라야 한다고 생각합니다
go에서는 어떤 방법이 있고, 프로젝트에 어떤 방식의 configuration을 채택하는 게 좋을지 알아보겠습니다
1. 단순 JSON을 사용하는 방법
가장 기본적인 방법입니다
conf.json
{
"Users": ["UserA","UserB"],
"Groups": ["GroupA"]
}
import (
"encoding/json"
"os"
"fmt"
)
type Configuration struct {
Users []string
Groups []string
}
file, _ := os.Open("conf.json")
defer file.Close()
decoder := json.NewDecoder(file)
configuration := Configuration{}
err := decoder.Decode(&configuration)
if err != nil {
fmt.Println("error:", err)
}
fmt.Println(configuration.Users) // output: [UserA, UserB]
장점
- Standard library 만으로 사용 가능
- JSON 형식으로 익숙함
단점
- JSON은 주석을 달 수 없음
(UI가 없는 경우, 운용자가 직접 제어할 수도 있기 때문에 설명이 들어가야 합니다. 이 부분은 꽤나 중요합니다)
2. 단순 TOML을 사용하는 방법
Tom Preston-Werner가 만든 INI-like 형식
Tom's Obvious, Minimal Language라고 하네요
최근에는 YAML보다도 잘 채용되는 추세인 듯합니다
conf.toml
Age = 198
Cats = [ "Cauchy", "Plato" ]
Pi = 3.14
Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z
type Config struct {
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time
}
var conf Config
if _, err := toml.DecodeFile("something.toml", &conf); err != nil {
// handle error
}
장점
- JSON 처럼 쉽고 파싱하기 쉬우며, JSON과 달리 주석 사용 가능
- YAML 처럼 사람이 읽기 쉽고 주석 사용 가능하며, YAML 보다 더 단순함
- 데이터 직렬화 목적이 아닌 오직 설정 파일 목적의 형식
- Top-level 해쉬 테이블 사용하여 nested key 방식 사용 가능
단점
- Top-level array, float은 불가능
- 파일의 시작과 끝은 정해지지 않음(App에서 처리해야 함)
- 대중적이지 않음
go toml 라이브러리
3. Pflag
POSIX/GNU 스타일의 Pflag를 사용하는 방법
go의 기본 flag 라이브러리가 있지만 호환성 문제 때문에 Pflag를 사용하는 것이 더 좋습니다
간단한 프로그램이나 command 라인 방식의 App인 경우 적합해 보입니다
아래와 같이 사용할 수 있습니다
별도의 설명이 필요 없는 전통적인 argument 처리 방식
장점
- 전통적인 방식이므로 직관적이며 자연스러운 처리 가능
- 사용하기 쉬움
- 기본 값, description 등이 제공됨
- help, manual 같은 처리로 이해하기 쉬움
단점
- 설정 내용이 매우 많아질 경우 flag로 처리하기 어려움
- 복잡한 형식의 경우 처리하기 어려움
- 실행 명령이 번잡해지므로 설정 파일의 값을 같이 처리하도록 개발하는 것이 좋음
go의 Pflag 라이브러리
4. Viper
Viper 라이브러리를 사용하는 방법
Viper는 앞서 잠깐 설명한 12-Factor App을 명시적으로 포함하는 config solution
설정 파일 형식에 대한 고민을 대신 처리해주어 개발자가 소프트웨어 개발에만 집중하도록 하는 게 목표
내용이 많으므로 핵심적인 내용만 살펴보겠습니다
특징
- 기본값 설정 가능
- JSON, TOML, YAML, HCL, INI, envfile, Java properites config 파일들 읽기 가능
- 다양한 source로부터 설정을 읽을 수 있으며 설정 우선순위 존재(높은 순위가 값 override)
- explicit call to Set
- flag
- env
- config
- key/value store
- default
- 대소문자 구분 X : env 변수 등의 특징 때문
- 실시간 파일 리로드 가능(optional)
- 환경변수 읽기 가능
- etcd, Consul 같은 remote config system 읽기, 변화 감시 가능
- command line flag 읽기 가능
- buffer 읽기 가능
- 명시적 값 설정 가능
기본 값 설정
viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
설정 파일 읽기
- JSON, TOML, YAML, HCL, INI, envfile, Java Properties files
- 적어도 하나의 config path는 제공되어야 함
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
viper.AddConfigPath("/etc/appname/") // path to look for the config file in
viper.AddConfigPath("$HOME/.appname") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("Fatal error config file: %w \n", err))
}
설정 파일 쓰기
- 런타임에서 수정된 설정을 저장 가능
- WriteConfig : predefined path에 현재 설정값 쓰기 - predefined path 없으면 error, 이미 있으면 overwrite O
- SafeWriteConfig : predefined path에 현재 설정값 쓰기 - predefined path 없으면 error, 이미 있으면 overwrite X
- WriteConfigAs : 주어진 path에 현재 설정값 쓰기 - 이미 있으면 overwrite O
- SafeWriteConfigAs : 주어진 path에 현재 설정값 쓰기 - 이미 있으면 overwrite X
viper.WriteConfig() // writes current config to predefined path set by 'viper.AddConfigPath()' and 'viper.SetConfigName'
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // will error since it has already been written
viper.SafeWriteConfigAs("/path/to/my/.other_config")
config 감시, reload
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
Buffer를 이용한 설정 가능
viper.SetConfigType("yaml") // or viper.SetConfigType("YAML")
// any approach to require this configuration into your program.
var yamlExample = []byte(`
Hacker: true
name: steve
hobbies:
- skateboarding
- snowboarding
- go
clothing:
jacket: leather
trousers: denim
age: 35
eyes : brown
beard: true
`)
viper.ReadConfig(bytes.NewBuffer(yamlExample))
viper.Get("name") // this would be "steve"
12 Factors를 고려한 5가지 환경 변수 처리 방법 제공
- 자세한 내용은 문서를 직접 참고해주세요
- 간단히 요약
- AutomaticEnv()
- SetEnvPrefix와 혼합하여 Get이 호출될 때 Prefix를 적용
- BindEnv(string...) : error
- 파라미터를 받아 매칭 되는 변수를 적용
- 여러 파라미터를 받아 우선순위 설정 가능
- SetEnvPrefix와 혼합하여 Prefix가 적용
- SetEnvPrefix(string)
- AutomaticEnv, BindEnv, Get 등의 메소드에 Prefix를 적용
- SetEnvKeyReplacer(string...) *strings.Replacer
- env key를 변경하기 위해 사용
- AllowEmptyEnv(bool)
- 보통 비어있는 env는 unset으로 고려되지만 비어있는 env 값을 허용할지 여부
SetEnvPrefix("spf") // will be uppercased automatically
BindEnv("id")
os.Setenv("SPF_ID", "13") // typically done outside of the app
id := Get("id") // 13
Pflag 처리 가능
- Viper는 내부적으로 Cobra 라이브러리를 사용하여 Pflag 지원
- Pflag 말고 그냥 go flag 처리도 가능
package main
import (
"flag"
"github.com/spf13/pflag"
)
func main() {
// using standard library "flag" package
flag.Int("flagname", 1234, "help message for flagname")
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
viper.BindPFlags(pflag.CommandLine)
i := viper.GetInt("flagname") // retrieve value from viper
...
}
장점
- 실시간 config 감시를 따로 구현할 필요 없음
- 다양한 format 지원
- write 지원
- buffer를 이용한 설정으로 테스트 구현 용이
- 12 Factors를 고려한 환경 변수 처리
단점
- 대소문자 구분 X(모두 소문자로 사용됨. 설정 값을 동일한 대소문자로 나누는 것은 비상식적이므로 사실 별문제 아님)
- 일반적으로 설정 기능은, get을 하는 순간에 실패하면 default 값을 취하는 방식을 많이 사용(직관적이기 때문)
허나 Viper에서는 기본값 설정을 위해 viper.SetDefault를 미리 사용해야 하여 DI 방식으로 사용하여 interface만 노출하려 할 때, 문제가 될 수 있음(wrapping하여 사용하면 해결) - viper.SetDefault를 호출하지 않으면 해당 타입의 기본값으로 자동설정되어져 오해가 발생할 수 있음(wrapping하여 사용하면 해결)
wrapping 예시
func (vc *ViperConfig) GetString(key string, defaultVal string) string {
if vc.viper.IsSet(key) {
return vc.viper.GetString(key)
}
return defaultVal
}
- viper.GetString을 defaultVal로 처리할 수 있음
5. gcfg, gonfig
다른 라이브러리보다 적게 사용되지만 여전히 채택되는 라이브러리들
Viper에 비해 간단한 기능들만 제공하지만
이들 모두 쉽게 사용할 수 있으며 괜찮은 라이브러리들입니다
gcfg example
cfgStr := `; Comment line
[section]
name=value # comment`
cfg := struct {
Section struct {
Name string
}
}{}
err := gcfg.ReadStringInto(&cfg, cfgStr)
if err != nil {
log.Fatalf("Failed to parse gcfg data: %s", err)
}
fmt.Println(cfg.Section.Name)
gonfig example
import(
"github.com/creamdog/gonfig"
"os"
)
type ExampleMessageStruct struct {
Message string
Subject string
}
func main() {
f, err := os.Open("myconfig.json")
if err != nil {
// TODO: error handling
}
defer f.Close();
config, err := gonfig.FromJson(f)
if err != nil {
// TODO: error handling
}
username, err := config.GetString("credentials/facebook/username", "scooby")
if err != nil {
// TODO: error handling
}
password, err := config.GetString("credentials/facebook/password", "123456")
if err != nil {
// TODO: error handling
}
host, err := config.GetString("services/facebook/host", "localhost")
if err != nil {
// TODO: error handling
}
port, err := config.GetInt("services/facebook/port", 80)
if err != nil {
// TODO: error handling
}
// TODO: example something
// login(host, port, username, password)
var template ExampleMessageStruct
if err := config.GetAs("services/facebook/message/template", &template); err != nil {
// TODO: error handling
}
/// TODO: example something
// template.Message = "I just want to say, oh my!"
// sendMessage(host, port, template)
}
장점
- 작고 사용하기 단순함
단점
- 기능이 적음
- 지원하는 형식이 많지 않음
6. HOCON
- Typesafe사를 좋아하는 분들이 있습니다(현재는 lightbend)
- Scala, Martin Odersky, Akka 등등의 개념을 좋아하시는 분들이 굉장히 많죠
- Akka에서는 HOCON(Human-Optimized Config Object Notation) 이라고 하는 config 방식을 사용
- 역시나 go에서도 해당 스타일로 개발할 수 있도록 config 라이브러리 또한 존재하고 있습니다
- 물론 Typesafe, Akka는 정말 멋지고 저도 좋아합니다만..
저희처럼 Akka 개념을 그대로 사용하려는 것이 아니라면 굳이 go에서 써야 할필요는 없습니다ㅎㅎ
(go는 go스럽게) - 일반적인 선택은 아니지만 Akka Actor 개념을 위해 쓰고 있고 꽤나 사용하는 사람들이 있으니
관심 있으신 분들은 따로 살펴보시면 좋을 것 같습니다
살펴본 결과
- command line에서 주로 처리되는 App의 경우 : Pflag
- Cloud, Container 위주의 경우 : 환경변수
- key만으로 용도가 충분히 설명되는 간단한 경우 : 익숙하게 사용되는 JSON
- 그 외의 경우 : TOML 등
고려할 수 있을 걸로 보이나
이 모두 Viper가 완벽하게 지원하고 있습니다
실시간 감시, 리로드, ETCD 연동 등 모두 쉽게 처리할 수 있는 강력한 라이브러리들이 존재합니다
또한, CNCF에 포함된 많은 프로젝트들이 Viper를 포함하고 있으며
사실상 Container 환경에서의 config 스탠다드로 자리잡고 있습니다
물론 개발자들은 모두 자신만의 개성이 있으며 그저 마음에 드는 방식의 config를 사용할 수 있습니다
하지만 저는 무언가 사용할 때 나름의 이유가 있는 걸 좋아합니다
Viper말고 다른 걸 써야 하는 이유를 찾을 수가 없네요
프로젝트에는 Viper를 적용해보고 괜찮으면 정착하게 될 것 같습니다
'Programming > Go' 카테고리의 다른 글
Go/Golang Test (2) | 2021.08.26 |
---|---|
Go/Golang Memory Management (16) | 2021.08.22 |
Go/Golang Protobuf Compile Guide (0) | 2021.08.16 |
Go/Golang awesome-go (0) | 2021.08.11 |
Go/Golang Long Time Performance Test - Memory Leak 해결 과정 (0) | 2021.07.11 |