[Go] environment variable
개요
환경 변수는 애플리케이션 설정을 외부에서 주입하는 표준 방법입니다.
주요 특징:
- OS 수준 설정: 프로세스별 독립적 환경
- 12-Factor App: 설정을 환경에서 분리
- 보안: 민감한 정보를 코드에서 분리
- 유연성: 환경별 다른 설정 (개발/스테이징/프로덕션)
- 컨테이너 친화적: Docker, Kubernetes 등에서 표준
- 클라우드 네이티브: AWS, GCP, Azure 등에서 활용
기본 사용법
1. Getenv - 환경 변수 읽기
package main
import (
"fmt"
"os"
)
func main() {
// 환경 변수 읽기
home := os.Getenv("HOME")
fmt.Println("Home directory:", home)
// 존재하지 않는 환경 변수
missing := os.Getenv("NONEXISTENT")
fmt.Printf("Missing: '%s' (빈 문자열)\n", missing)
// PATH 환경 변수
path := os.Getenv("PATH")
fmt.Println("PATH:", path)
}
특징:
- 존재하지 않으면 빈 문자열 반환
- 빈 값과 존재하지 않음을 구분 불가
2. LookupEnv - 존재 여부 확인
func main() {
// 환경 변수 존재 여부 확인
if value, exists := os.LookupEnv("HOME"); exists {
fmt.Println("HOME:", value)
} else {
fmt.Println("HOME not set")
}
// 빈 값과 존재하지 않음 구분
if value, exists := os.LookupEnv("EMPTY_VAR"); exists {
fmt.Printf("EMPTY_VAR exists with value: '%s'\n", value)
} else {
fmt.Println("EMPTY_VAR does not exist")
}
}
Getenv vs LookupEnv:
// 시나리오 1: 환경 변수가 설정되지 않음
os.Getenv("VAR") // "" 반환
os.LookupEnv("VAR") // "", false 반환
// 시나리오 2: 환경 변수가 빈 문자열
// export VAR=""
os.Getenv("VAR") // "" 반환
os.LookupEnv("VAR") // "", true 반환
// 시나리오 3: 환경 변수가 값을 가짐
// export VAR="value"
os.Getenv("VAR") // "value" 반환
os.LookupEnv("VAR") // "value", true 반환
3. Setenv - 환경 변수 설정
func main() {
// 환경 변수 설정
err := os.Setenv("MY_VAR", "my_value")
if err != nil {
log.Fatal(err)
}
fmt.Println(os.Getenv("MY_VAR")) // my_value
// 덮어쓰기
os.Setenv("MY_VAR", "new_value")
fmt.Println(os.Getenv("MY_VAR")) // new_value
}
주의:
- 현재 프로세스에만 영향
- 자식 프로세스에는 상속됨
- 부모 프로세스나 다른 프로세스에는 영향 없음
4. Unsetenv - 환경 변수 제거
func main() {
os.Setenv("TEMP_VAR", "temporary")
fmt.Println(os.Getenv("TEMP_VAR")) // temporary
// 환경 변수 제거
os.Unsetenv("TEMP_VAR")
fmt.Println(os.Getenv("TEMP_VAR")) // (빈 문자열)
_, exists := os.LookupEnv("TEMP_VAR")
fmt.Println("Exists:", exists) // false
}
5. Clearenv - 모든 환경 변수 제거
func main() {
fmt.Println("Before:", len(os.Environ()))
// 모든 환경 변수 제거
os.Clearenv()
fmt.Println("After:", len(os.Environ())) // 0
// ⚠️ 주의: PATH 등 시스템 변수도 모두 제거됨
}
사용 시나리오:
- 테스트 격리
- 보안이 중요한 환경
- 최소 권한 원칙
6. Environ - 모든 환경 변수 조회
func main() {
// 모든 환경 변수
envVars := os.Environ()
fmt.Printf("Total: %d environment variables\n", len(envVars))
// 형식: "KEY=VALUE"
for _, env := range envVars {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
key := parts[0]
value := parts[1]
fmt.Printf("%s = %s\n", key, value)
}
}
}
파싱 예제:
func parseEnviron() map[string]string {
envMap := make(map[string]string)
for _, env := range os.Environ() {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
return envMap
}
func main() {
envMap := parseEnviron()
fmt.Println("HOME:", envMap["HOME"])
fmt.Println("PATH:", envMap["PATH"])
}
7. ExpandEnv - 변수 확장
func main() {
os.Setenv("NAME", "World")
os.Setenv("GREETING", "Hello")
// $VAR 또는 ${VAR} 형식
expanded := os.ExpandEnv("$GREETING, $NAME!")
fmt.Println(expanded) // Hello, World!
// 중괄호 사용
expanded = os.ExpandEnv("${GREETING}, ${NAME}!")
fmt.Println(expanded) // Hello, World!
// 존재하지 않는 변수
expanded = os.ExpandEnv("Value: $NONEXISTENT")
fmt.Println(expanded) // Value: (빈 문자열)
}
고급 사용:
func main() {
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")
os.Setenv("DB_NAME", "mydb")
// 연결 문자열 생성
connStr := os.ExpandEnv("postgres://${DB_HOST}:${DB_PORT}/${DB_NAME}")
fmt.Println(connStr) // postgres://localhost:5432/mydb
}
타입 변환과 파싱
1. 문자열에서 다양한 타입으로
import "strconv"
func getEnvAsInt(key string, defaultVal int) int {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultVal
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return defaultVal
}
return value
}
func getEnvAsBool(key string, defaultVal bool) bool {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultVal
}
value, err := strconv.ParseBool(valueStr)
if err != nil {
return defaultVal
}
return value
}
func getEnvAsSlice(key string, sep string, defaultVal []string) []string {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultVal
}
return strings.Split(valueStr, sep)
}
func main() {
// PORT=8080
port := getEnvAsInt("PORT", 3000)
fmt.Println("Port:", port)
// DEBUG=true
debug := getEnvAsBool("DEBUG", false)
fmt.Println("Debug:", debug)
// HOSTS=host1,host2,host3
hosts := getEnvAsSlice("HOSTS", ",", []string{"localhost"})
fmt.Println("Hosts:", hosts)
}
2. 헬퍼 함수 라이브러리
package env
import (
"os"
"strconv"
"time"
)
// GetString returns string value or default
func GetString(key, defaultVal string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultVal
}
// MustGetString returns string or panics
func MustGetString(key string) string {
value := os.Getenv(key)
if value == "" {
panic(fmt.Sprintf("Environment variable %s is required", key))
}
return value
}
// GetInt returns int value or default
func GetInt(key string, defaultVal int) int {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultVal
}
value, err := strconv.Atoi(valueStr)
if err != nil {
return defaultVal
}
return value
}
// GetBool returns bool value or default
func GetBool(key string, defaultVal bool) bool {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultVal
}
value, err := strconv.ParseBool(valueStr)
if err != nil {
return defaultVal
}
return value
}
// GetDuration returns duration or default
func GetDuration(key string, defaultVal time.Duration) time.Duration {
valueStr := os.Getenv(key)
if valueStr == "" {
return defaultVal
}
value, err := time.ParseDuration(valueStr)
if err != nil {
return defaultVal
}
return value
}
// 사용 예제
func main() {
appName := env.MustGetString("APP_NAME")
port := env.GetInt("PORT", 8080)
debug := env.GetBool("DEBUG", false)
timeout := env.GetDuration("TIMEOUT", 30*time.Second)
fmt.Printf("App: %s, Port: %d, Debug: %v, Timeout: %v\n",
appName, port, debug, timeout)
}
설정 구조체
1. 기본 설정 구조체
type Config struct {
AppName string
Environment string
Port int
Debug bool
Database struct {
Host string
Port int
Name string
User string
Password string
}
Redis struct {
Host string
Port int
}
}
func LoadConfig() (*Config, error) {
cfg := &Config{
AppName: getEnv("APP_NAME", "myapp"),
Environment: getEnv("ENV", "development"),
Port: getEnvAsInt("PORT", 8080),
Debug: getEnvAsBool("DEBUG", false),
}
cfg.Database.Host = getEnv("DB_HOST", "localhost")
cfg.Database.Port = getEnvAsInt("DB_PORT", 5432)
cfg.Database.Name = mustGetEnv("DB_NAME")
cfg.Database.User = mustGetEnv("DB_USER")
cfg.Database.Password = mustGetEnv("DB_PASSWORD")
cfg.Redis.Host = getEnv("REDIS_HOST", "localhost")
cfg.Redis.Port = getEnvAsInt("REDIS_PORT", 6379)
return cfg, nil
}
func main() {
config, err := LoadConfig()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Starting %s on port %d\n", config.AppName, config.Port)
}
2. 태그 기반 설정
import "github.com/kelseyhightower/envconfig"
type Config struct {
AppName string `envconfig:"APP_NAME" default:"myapp"`
Port int `envconfig:"PORT" default:"8080"`
Debug bool `envconfig:"DEBUG" default:"false"`
Database DatabaseConfig
Redis RedisConfig
}
type DatabaseConfig struct {
Host string `envconfig:"DB_HOST" default:"localhost"`
Port int `envconfig:"DB_PORT" default:"5432"`
Name string `envconfig:"DB_NAME" required:"true"`
User string `envconfig:"DB_USER" required:"true"`
Password string `envconfig:"DB_PASSWORD" required:"true"`
}
type RedisConfig struct {
Host string `envconfig:"REDIS_HOST" default:"localhost"`
Port int `envconfig:"REDIS_PORT" default:"6379"`
}
func main() {
var cfg Config
err := envconfig.Process("", &cfg)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", cfg)
}
.env 파일 사용
1. godotenv 라이브러리
go get github.com/joho/godotenv
.env 파일:
# Application
APP_NAME=MyApp
ENV=development
PORT=8080
DEBUG=true
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mydb
DB_USER=user
DB_PASSWORD=secret
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# API Keys (민감 정보)
API_KEY=your-secret-key
JWT_SECRET=your-jwt-secret
코드:
import "github.com/joho/godotenv"
func main() {
// .env 파일 로드
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
// 환경 변수 사용
appName := os.Getenv("APP_NAME")
port := os.Getenv("PORT")
fmt.Printf("%s running on port %s\n", appName, port)
}
2. 환경별 .env 파일
func loadEnvFile() error {
env := os.Getenv("GO_ENV")
if env == "" {
env = "development"
}
// 환경별 파일 시도
envFile := fmt.Sprintf(".env.%s", env)
if err := godotenv.Load(envFile); err == nil {
return nil
}
// 기본 .env 파일
return godotenv.Load()
}
func main() {
if err := loadEnvFile(); err != nil {
log.Printf("Warning: %v", err)
}
config := LoadConfig()
// ...
}
.env.development:
DEBUG=true
DB_HOST=localhost
API_URL=http://localhost:3000
.env.production:
DEBUG=false
DB_HOST=prod-db.example.com
API_URL=https://api.example.com
3. 오버라이드 패턴
func main() {
// 1. 기본값 로드
godotenv.Load(".env.defaults")
// 2. 환경별 설정 로드 (오버라이드)
env := os.Getenv("GO_ENV")
if env != "" {
godotenv.Load(fmt.Sprintf(".env.%s", env))
}
// 3. 로컬 오버라이드 (git ignore)
godotenv.Load(".env.local")
// 4. 실제 환경 변수가 최우선
config := LoadConfig()
}
Viper 사용
go get github.com/spf13/viper
1. Viper 기본 사용
import "github.com/spf13/viper"
func initConfig() {
// 자동 환경 변수 바인딩
viper.AutomaticEnv()
// 환경 변수 접두사
viper.SetEnvPrefix("MYAPP")
// 기본값 설정
viper.SetDefault("port", 8080)
viper.SetDefault("debug", false)
// 설정 파일
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/myapp")
if err := viper.ReadInConfig(); err != nil {
log.Printf("Config file not found: %v", err)
}
}
func main() {
initConfig()
// 환경 변수 읽기
// MYAPP_PORT=9000
port := viper.GetInt("port")
debug := viper.GetBool("debug")
appName := viper.GetString("app.name")
fmt.Printf("Port: %d, Debug: %v, App: %s\n", port, debug, appName)
}
2. Viper 구조체 언마샬
type Config struct {
App struct {
Name string
Port int
}
Database struct {
Host string
Port int
Name string
User string
Password string
}
}
func main() {
viper.AutomaticEnv()
viper.SetEnvPrefix("MYAPP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
var config Config
if err := viper.Unmarshal(&config); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", config)
}
12-Factor App
1. 설정을 환경에 저장
// ❌ 나쁜 예: 하드코딩
const (
DatabaseURL = "postgres://localhost:5432/mydb"
APIKey = "hardcoded-key"
)
// ✅ 좋은 예: 환경 변수
func getDatabaseURL() string {
return os.Getenv("DATABASE_URL")
}
func getAPIKey() string {
return os.Getenv("API_KEY")
}
2. 환경별 분리
type Environment string
const (
Development Environment = "development"
Staging Environment = "staging"
Production Environment = "production"
)
func getEnvironment() Environment {
env := os.Getenv("GO_ENV")
switch env {
case "production":
return Production
case "staging":
return Staging
default:
return Development
}
}
func main() {
env := getEnvironment()
switch env {
case Production:
// 프로덕션 설정
case Staging:
// 스테이징 설정
default:
// 개발 설정
}
}
3. 민감한 정보 분리
type Secrets struct {
DatabasePassword string
APIKey string
JWTSecret string
EncryptionKey string
}
func LoadSecrets() (*Secrets, error) {
secrets := &Secrets{
DatabasePassword: mustGetEnv("DB_PASSWORD"),
APIKey: mustGetEnv("API_KEY"),
JWTSecret: mustGetEnv("JWT_SECRET"),
EncryptionKey: mustGetEnv("ENCRYPTION_KEY"),
}
return secrets, nil
}
보안 고려사항
1. 민감한 정보 로깅 방지
type Config struct {
AppName string
Port int
DatabaseURL string `json:"-"` // JSON 출력 제외
APIKey string `json:"-"`
}
func (c *Config) String() string {
// 민감한 정보 마스킹
maskedURL := maskCredentials(c.DatabaseURL)
maskedKey := maskAPIKey(c.APIKey)
return fmt.Sprintf("Config{AppName: %s, Port: %d, DatabaseURL: %s, APIKey: %s}",
c.AppName, c.Port, maskedURL, maskedKey)
}
func maskCredentials(url string) string {
// postgres://user:password@host:port/db
// -> postgres://user:***@host:port/db
re := regexp.MustCompile(`://([^:]+):([^@]+)@`)
return re.ReplaceAllString(url, "://$1:***@")
}
func maskAPIKey(key string) string {
if len(key) <= 8 {
return "***"
}
return key[:4] + "..." + key[len(key)-4:]
}
2. 환경 변수 검증
func ValidateConfig(cfg *Config) error {
var errors []string
if cfg.AppName == "" {
errors = append(errors, "APP_NAME is required")
}
if cfg.Port < 1 || cfg.Port > 65535 {
errors = append(errors, "PORT must be between 1 and 65535")
}
if cfg.Database.Password == "" {
errors = append(errors, "DB_PASSWORD is required")
}
if len(cfg.Database.Password) < 12 {
errors = append(errors, "DB_PASSWORD must be at least 12 characters")
}
if len(errors) > 0 {
return fmt.Errorf("configuration errors:\n- %s",
strings.Join(errors, "\n- "))
}
return nil
}
3. 기본값 보안
// ❌ 나쁜 예: 불안전한 기본값
func getEnv(key string) string {
if value := os.Getenv(key); value != "" {
return value
}
return "default_password" // 위험!
}
// ✅ 좋은 예: 민감한 정보는 기본값 없음
func mustGetEnv(key string) string {
value := os.Getenv(key)
if value == "" {
log.Fatalf("Environment variable %s is required", key)
}
return value
}
테스트에서 환경 변수
1. 테스트 격리
func TestLoadConfig(t *testing.T) {
// 테스트 전 상태 저장
oldPort := os.Getenv("PORT")
oldDebug := os.Getenv("DEBUG")
// 정리 함수
t.Cleanup(func() {
os.Setenv("PORT", oldPort)
os.Setenv("DEBUG", oldDebug)
})
// 테스트용 환경 변수 설정
os.Setenv("PORT", "9999")
os.Setenv("DEBUG", "true")
config := LoadConfig()
assert.Equal(t, 9999, config.Port)
assert.True(t, config.Debug)
}
2. 테스트 헬퍼
func setTestEnv(t *testing.T, envVars map[string]string) {
t.Helper()
// 기존 값 저장
oldVars := make(map[string]string)
for key := range envVars {
oldVars[key] = os.Getenv(key)
}
// 정리 함수 등록
t.Cleanup(func() {
for key, value := range oldVars {
if value == "" {
os.Unsetenv(key)
} else {
os.Setenv(key, value)
}
}
})
// 테스트 환경 변수 설정
for key, value := range envVars {
os.Setenv(key, value)
}
}
func TestWithHelper(t *testing.T) {
setTestEnv(t, map[string]string{
"APP_NAME": "test-app",
"PORT": "8888",
"DEBUG": "true",
})
config := LoadConfig()
assert.Equal(t, "test-app", config.AppName)
}
3. 테이블 기반 테스트
func TestGetEnvAsInt(t *testing.T) {
tests := []struct {
name string
key string
envValue string
defaultVal int
want int
}{
{
name: "valid int",
key: "TEST_PORT",
envValue: "8080",
defaultVal: 3000,
want: 8080,
},
{
name: "invalid int",
key: "TEST_PORT",
envValue: "invalid",
defaultVal: 3000,
want: 3000,
},
{
name: "empty value",
key: "TEST_PORT",
envValue: "",
defaultVal: 3000,
want: 3000,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envValue != "" {
os.Setenv(tt.key, tt.envValue)
defer os.Unsetenv(tt.key)
}
got := getEnvAsInt(tt.key, tt.defaultVal)
assert.Equal(t, tt.want, got)
})
}
}
4. 모킹
type EnvProvider interface {
Getenv(key string) string
Setenv(key, value string) error
}
type OSEnvProvider struct{}
func (p *OSEnvProvider) Getenv(key string) string {
return os.Getenv(key)
}
func (p *OSEnvProvider) Setenv(key, value string) error {
return os.Setenv(key, value)
}
type MockEnvProvider struct {
vars map[string]string
}
func (p *MockEnvProvider) Getenv(key string) string {
return p.vars[key]
}
func (p *MockEnvProvider) Setenv(key, value string) error {
p.vars[key] = value
return nil
}
// 테스트
func TestConfigWithMock(t *testing.T) {
mockEnv := &MockEnvProvider{
vars: map[string]string{
"APP_NAME": "test-app",
"PORT": "9999",
},
}
config := LoadConfigWithProvider(mockEnv)
assert.Equal(t, "test-app", config.AppName)
}
실전 예제
1. 웹 서버 설정
type ServerConfig struct {
Host string
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
ShutdownTimeout time.Duration
TLSEnabled bool
TLSCert string
TLSKey string
}
func LoadServerConfig() *ServerConfig {
return &ServerConfig{
Host: getEnv("SERVER_HOST", "0.0.0.0"),
Port: getEnvAsInt("SERVER_PORT", 8080),
ReadTimeout: getEnvAsDuration("SERVER_READ_TIMEOUT", 10*time.Second),
WriteTimeout: getEnvAsDuration("SERVER_WRITE_TIMEOUT", 10*time.Second),
ShutdownTimeout: getEnvAsDuration("SERVER_SHUTDOWN_TIMEOUT", 30*time.Second),
TLSEnabled: getEnvAsBool("SERVER_TLS_ENABLED", false),
TLSCert: getEnv("SERVER_TLS_CERT", ""),
TLSKey: getEnv("SERVER_TLS_KEY", ""),
}
}
func main() {
config := LoadServerConfig()
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", config.Host, config.Port),
ReadTimeout: config.ReadTimeout,
WriteTimeout: config.WriteTimeout,
}
log.Printf("Starting server on %s", server.Addr)
if config.TLSEnabled {
log.Fatal(server.ListenAndServeTLS(config.TLSCert, config.TLSKey))
} else {
log.Fatal(server.ListenAndServe())
}
}
2. 데이터베이스 설정
type DatabaseConfig struct {
Driver string
Host string
Port int
Name string
User string
Password string
SSLMode string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
}
func (c *DatabaseConfig) DSN() string {
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
c.Host, c.Port, c.User, c.Password, c.Name, c.SSLMode,
)
}
func LoadDatabaseConfig() *DatabaseConfig {
return &DatabaseConfig{
Driver: getEnv("DB_DRIVER", "postgres"),
Host: getEnv("DB_HOST", "localhost"),
Port: getEnvAsInt("DB_PORT", 5432),
Name: mustGetEnv("DB_NAME"),
User: mustGetEnv("DB_USER"),
Password: mustGetEnv("DB_PASSWORD"),
SSLMode: getEnv("DB_SSL_MODE", "disable"),
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 5),
ConnMaxLifetime: getEnvAsDuration("DB_CONN_MAX_LIFETIME", 5*time.Minute),
}
}
3. 로깅 설정
type LogConfig struct {
Level string
Format string // "json" or "text"
Output string // "stdout", "stderr", or file path
TimeFormat string
}
func LoadLogConfig() *LogConfig {
return &LogConfig{
Level: getEnv("LOG_LEVEL", "info"),
Format: getEnv("LOG_FORMAT", "json"),
Output: getEnv("LOG_OUTPUT", "stdout"),
TimeFormat: getEnv("LOG_TIME_FORMAT", time.RFC3339),
}
}
func SetupLogger(config *LogConfig) *logrus.Logger {
logger := logrus.New()
// 레벨 설정
level, err := logrus.ParseLevel(config.Level)
if err != nil {
level = logrus.InfoLevel
}
logger.SetLevel(level)
// 포맷 설정
if config.Format == "json" {
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: config.TimeFormat,
})
} else {
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: config.TimeFormat,
})
}
// 출력 설정
switch config.Output {
case "stdout":
logger.SetOutput(os.Stdout)
case "stderr":
logger.SetOutput(os.Stderr)
default:
file, err := os.OpenFile(config.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
logger.SetOutput(file)
}
}
return logger
}
일반적인 실수
1. 기본값 남용
// ❌ 나쁜 예
func getAPIKey() string {
return getEnv("API_KEY", "default-key") // 위험!
}
// ✅ 좋은 예
func getAPIKey() string {
key := os.Getenv("API_KEY")
if key == "" {
log.Fatal("API_KEY environment variable is required")
}
return key
}
2. 타입 변환 에러 무시
// ❌ 나쁜 예
func getPort() int {
port, _ := strconv.Atoi(os.Getenv("PORT"))
return port // 에러 시 0 반환
}
// ✅ 좋은 예
func getPort() int {
portStr := os.Getenv("PORT")
if portStr == "" {
return 8080 // 기본값
}
port, err := strconv.Atoi(portStr)
if err != nil {
log.Fatalf("Invalid PORT: %v", err)
}
return port
}
3. 환경 변수 하드코딩
// ❌ 나쁜 예
func getConfig() Config {
return Config{
Host: "localhost", // 하드코딩
Port: 8080, // 하드코딩
Debug: true, // 하드코딩
}
}
// ✅ 좋은 예
func getConfig() Config {
return Config{
Host: getEnv("HOST", "localhost"),
Port: getEnvAsInt("PORT", 8080),
Debug: getEnvAsBool("DEBUG", false),
}
}
4. 민감한 정보 로깅
// ❌ 나쁜 예
func main() {
config := LoadConfig()
log.Printf("Config: %+v", config) // 비밀번호 노출!
}
// ✅ 좋은 예
func main() {
config := LoadConfig()
log.Printf("Loaded configuration for %s", config.AppName)
// 민감한 정보는 로깅하지 않음
}
5. 환경 변수 검증 누락
// ❌ 나쁜 예
func main() {
port := getEnvAsInt("PORT", 8080)
// 범위 검증 없음
}
// ✅ 좋은 예
func main() {
port := getEnvAsInt("PORT", 8080)
if port < 1 || port > 65535 {
log.Fatalf("Invalid PORT: %d (must be 1-65535)", port)
}
}
6. .env 파일 커밋
# ❌ 나쁜 예: .env 파일을 Git에 커밋
git add .env
# ✅ 좋은 예: .gitignore에 추가
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.*.local" >> .gitignore
# .env.example은 커밋 (실제 값 없이)
.env.example:
# Application
APP_NAME=myapp
ENV=development
PORT=8080
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=
DB_USER=
DB_PASSWORD=
# API Keys (fill in your values)
API_KEY=
JWT_SECRET=
7. 프로덕션에서 .env 파일 사용
// ❌ 나쁜 예: 프로덕션에서도 .env 사용
func main() {
godotenv.Load() // 프로덕션에서는 실제 환경 변수 사용
// ...
}
// ✅ 좋은 예: 환경별 로딩
func main() {
if os.Getenv("GO_ENV") != "production" {
godotenv.Load()
}
// 프로덕션: 시스템 환경 변수 사용
// 개발: .env 파일 사용
}
베스트 프랙티스
1. 환경 변수 문서화
# Environment Variables
## Required
- `DB_NAME`: Database name
- `DB_USER`: Database user
- `DB_PASSWORD`: Database password (min 12 characters)
- `API_KEY`: External API key
## Optional
- `PORT`: Server port (default: 8080)
- `DEBUG`: Enable debug mode (default: false)
- `LOG_LEVEL`: Log level (default: info)
- `TIMEOUT`: Request timeout (default: 30s)
## Example
```bash
export DB_NAME=myapp
export DB_USER=admin
export DB_PASSWORD=super-secret-password
export API_KEY=abc123def456
export PORT=9000
export DEBUG=true
## 2. 네임스페이스 사용
```go
// ✅ 접두사로 그룹화
MYAPP_DB_HOST=localhost
MYAPP_DB_PORT=5432
MYAPP_REDIS_HOST=localhost
MYAPP_REDIS_PORT=6379
// 설정 읽기
func getEnvWithPrefix(key string) string {
prefix := os.Getenv("APP_PREFIX")
if prefix == "" {
prefix = "MYAPP"
}
return os.Getenv(prefix + "_" + key)
}
3. 설정 검증
func ValidateAndLoadConfig() (*Config, error) {
config, err := LoadConfig()
if err != nil {
return nil, err
}
if err := ValidateConfig(config); err != nil {
return nil, err
}
return config, nil
}
func main() {
config, err := ValidateAndLoadConfig()
if err != nil {
log.Fatalf("Configuration error: %v", err)
}
// 설정이 검증됨
}
4. 설정 불변성
// ✅ 설정을 읽기 전용으로
type Config struct {
appName string
port int
}
func (c *Config) AppName() string { return c.appName }
func (c *Config) Port() int { return c.port }
// 또는 sync.Once 사용
var (
config *Config
configOnce sync.Once
)
func GetConfig() *Config {
configOnce.Do(func() {
config = loadConfig()
})
return config
}
5. 단위 명시
// ✅ 단위를 환경 변수명에 포함
TIMEOUT_SECONDS=30
MAX_SIZE_MB=100
RETRY_INTERVAL_MS=500
// 또는 파싱 함수에서 처리
// TIMEOUT=30s
timeout := getEnvAsDuration("TIMEOUT", 30*time.Second)
정리
- 기본 함수: Getenv, LookupEnv, Setenv, Unsetenv, Clearenv, Environ, ExpandEnv
- 타입 변환: 문자열에서 int, bool, duration 등으로 변환
- 설정 구조체: 체계적인 설정 관리
- .env 파일: godotenv로 개발 환경 설정
- Viper: 고급 설정 관리 라이브러리
- 12-Factor: 설정을 환경에서 분리
- 보안: 민감한 정보 마스킹, 검증, 로깅 방지
- 테스트: 격리, 헬퍼, 모킹
- 실수: 기본값 남용, 타입 에러 무시, 로깅, 검증 누락
- 베스트: 문서화, 네임스페이스, 검증, 불변성
- 원칙: 설정과 코드 분리, 환경별 다른 설정