Configuration도 굉장히 중요한 영역 중에 하나죠
굉장히 다양한 방법과 의견이 나뉘는 분야입니다
JSON, YAML, 환경변수 등 종류도 다양하고
Cloud, Container, Command Line 등 환경도 다양합니다
그래서 golang에서 config를 어떻게 다루는게 좋을지 정리해보려 합니다
우선 12 factors에서 Configuration에 대해 언급하는 부분이 있습니다
2021.08.12 - [Cloud/Cloud Native] - The Twelve Factors
The Twelve Factors
Cloud-Native App은 Cloud 환경에 App을 배포하여 서비스하는 SaaS(Software As A Service) 방식입니다 그리고 Cloud-Native와 SaaS에서는 Agile Manifesto 만큼 유명한 문서가 있죠 The Twelve-Factor App The T..
syntaxsugar.tistory.com
간단히 살펴보면 아래와 같습니다
- 설정 정보는 코드로부터 분리
- 환경마다 다른 설정 파일을 사용하자 - 예) dev.conf, prod.conf 등
- 환경변수 활용
설정 정보를 모두 환경변수만으로 처리하기엔 사실상 어려운 점이 많습니다
대신 환경변수를 같이 활용하는 설정 방법들이 존재합니다
사내에서는 Scala & Akka framework를 활용해 개발을 해왔고
go에서의 Actor 사용을 위해 Proto Actor를 활용했기 때문에
HOCON(Human-Optimized Config Object Notation) config 방식을 go에서 사용했습니다
(go에서의 Actor도 나중에 포스팅 하겠습니다)
GitHub - AsynkronIT/protoactor-go: Proto Actor - Ultra fast distributed actors for Go, C# and Java/Kotlin
Proto Actor - Ultra fast distributed actors for Go, C# and Java/Kotlin - GitHub - AsynkronIT/protoactor-go: Proto Actor - Ultra fast distributed actors for Go, C# and Java/Kotlin
github.com
하지만 역시 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 라이브러리
GitHub - toml-lang/toml: Tom's Obvious, Minimal Language
Tom's Obvious, Minimal Language. Contribute to toml-lang/toml development by creating an account on GitHub.
github.com
3. Pflag
POSIX/GNU 스타일의 Pflag를 사용하는 방법
go의 기본 flag 라이브러리가 있지만 호환성 문제 때문에 Pflag를 사용하는 것이 더 좋습니다
간단한 프로그램이나 command 라인 방식의 App인 경우 적합해 보입니다
아래와 같이 사용할 수 있습니다
별도의 설명이 필요 없는 전통적인 argument 처리 방식
장점
- 전통적인 방식이므로 직관적이며 자연스러운 처리 가능
- 사용하기 쉬움
- 기본 값, description 등이 제공됨
- help, manual 같은 처리로 이해하기 쉬움
단점
- 설정 내용이 매우 많아질 경우 flag로 처리하기 어려움
- 복잡한 형식의 경우 처리하기 어려움
- 실행 명령이 번잡해지므로 설정 파일의 값을 같이 처리하도록 개발하는 것이 좋음
go의 Pflag 라이브러리
GitHub - spf13/pflag: Drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags.
Drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags. - GitHub - spf13/pflag: Drop-in replacement for Go's flag package, implementing POSIX/GNU-style --flags.
github.com
4. Viper
Viper 라이브러리를 사용하는 방법
Viper는 앞서 잠깐 설명한 12-Factor App을 명시적으로 포함하는 config solution
설정 파일 형식에 대한 고민을 대신 처리해주어 개발자가 소프트웨어 개발에만 집중하도록 하는 게 목표
내용이 많으므로 핵심적인 내용만 살펴보겠습니다
GitHub - spf13/viper: Go configuration with fangs
Go configuration with fangs. Contribute to spf13/viper development by creating an account on GitHub.
github.com
특징
- 기본값 설정 가능
- 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에 비해 간단한 기능들만 제공하지만
이들 모두 쉽게 사용할 수 있으며 괜찮은 라이브러리들입니다
GitHub - go-gcfg/gcfg: read INI-style configuration files into Go structs; supports user-defined types and subsections
read INI-style configuration files into Go structs; supports user-defined types and subsections - GitHub - go-gcfg/gcfg: read INI-style configuration files into Go structs; supports user-defined ty...
github.com
GitHub - creamdog/gonfig: a config parser for GO
a config parser for GO. Contribute to creamdog/gonfig development by creating an account on GitHub.
github.com
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
GitHub - go-akka/configuration: typesafe HOCON
typesafe HOCON. Contribute to go-akka/configuration development by creating an account on GitHub.
github.com
- 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 |