Updated:

15 minute read

개요

환경 변수는 애플리케이션 설정을 외부에서 주입하는 표준 방법입니다.

주요 특징:

  • 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: 설정을 환경에서 분리
  • 보안: 민감한 정보 마스킹, 검증, 로깅 방지
  • 테스트: 격리, 헬퍼, 모킹
  • 실수: 기본값 남용, 타입 에러 무시, 로깅, 검증 누락
  • 베스트: 문서화, 네임스페이스, 검증, 불변성
  • 원칙: 설정과 코드 분리, 환경별 다른 설정