자주 묻는 질문
기본 사용
Load()와 New() 중 어떤 것을 선택해야 하나요?
env.Load()(글로벌 모드)는 단순 애플리케이션에 적합합니다: 한 번 로드하고 패키지 수준 함수로 글로벌하게 사용합니다. 변수를 os.Environ에 자동으로 적용합니다.
env.New()(인스턴스 모드)는 테스트와 다중 구성 시나리오에 적합합니다: 격리된 인스턴스를 생성하며, 자동으로 적용하지 않고 명시적으로 Close()해야 합니다.
// 단순 애플리케이션 → 글로벌 모드
env.Load(".env")
port := env.GetInt("PORT", 8080)
// 테스트 / 다중 구성 → 인스턴스 모드
loader, _ := env.New(env.TestingConfig())
defer loader.Close()
port := loader.GetInt("PORT", 8080)선택 가이드
잘 모르겠으면 먼저 env.Load()를 사용하세요. 테스트 격리 또는 다중 구성 요구 사항이 발생하면 env.New()로 전환하세요.
Load()는 왜 한 번만 호출할 수 있나요?
Load()는 글로벌 기본 로더를 설정합니다(싱글톤 모드). 반복 호출 시 ErrAlreadyInitialized를 반환합니다. 이것은 설계 결정입니다: 런타임에 이미 로드된 구성을 실수로 덮어쓰는 것을 방지합니다.
// 첫 번째 호출 - 성공
env.Load(".env")
// 두 번째 호출 - 오류 반환
err := env.Load(".env.production")
// err == env.ErrAlreadyInitialized해결 방안:
// 방안 1: 한 번에 여러 파일 로드(권장)
env.Load(".env", ".env.production")
// 방안 2: 재초기화가 필요한 경우 먼저 재설정
env.ResetDefaultLoader() // 주로 테스트에 사용
env.Load(".env.production").env 파일이 없으면 어떻게 되나요?
기본 동작: 조용히 건너뜀, 오류 보고하지 않음. 이는 "있으면 로드, 없으면 무시"의 유연한 배포를 지원하기 위함입니다.
// DefaultConfig - 파일이 없으면 조용히 건너뜀
env.Load(".env", ".env.local")
// 두 파일 모두 존재하지 않아도 오류 없음파일이 없을 때 오류를 원하면(프로덕션 환경 권장):
cfg := env.ProductionConfig()
// FailOnMissingFile 기본값 true(ProductionConfig만)
loader, _ := env.New(cfg)JSON/YAML의 중첩 값에 어떻게 접근하나요?
JSON/YAML의 중첩 구조는 자동으로 평탄화되어 밑줄로 구분된 키 이름이 됩니다:
{
"database": {
"host": "localhost",
"port": 5432
}
}저장 형태: DATABASE_HOST=localhost, DATABASE_PORT=5432세 가지 접근 방식이 동등합니다:
host := env.GetString("DATABASE_HOST") // 플랫 키 이름(권장)
host := env.GetString("database.host") // 점 표기 경로
host := env.GetString("DATABASE.HOST") // 대문자 점 표기 경로타입과 제네릭
GetSlice는 왜 메서드가 아닌 제네릭 함수인가요?
Go는 메서드의 타입 매개변수를 지원하지 않습니다. GetSlice[T]는 메서드가 아닌 함수여야 합니다:
// ❌ 메서드 방식 - 컴파일 오류(Go는 지원하지 않음)
// loader.GetSlice[int]("PORTS")
// ✅ 함수 방식 - 가능
env.GetSliceFrom[int](loader, "PORTS")
// ✅ 패키지 수준 함수
env.GetSlice[int]("PORTS")GetSlice는 슬라이스 값을 어떻게 파싱하나요?
우선순위별로 검색합니다:
- 인덱스 키(권장):
KEY_0,KEY_1,KEY_2... - 쉼표 구분:
KEY=val1,val2,val3
# 방식 1: 인덱스 키
HOSTS_0=localhost
HOSTS_1=example.com
# 방식 2: 쉼표 구분
HOSTS=localhost,example.comhosts := env.GetSlice[string]("HOSTS") // ["localhost", "example.com"]어떤 불리언 값 형식을 지원하나요?
GetBool은 대소문자를 구분하지 않으며, 다음 값을 지원합니다:
| 참 값 | 거짓 값 |
|---|---|
true, 1, yes, on, enabled | false, 0, no, off, disabled |
동시성과 스레드 안전
여러 goroutine에서 동시에 Get을 호출할 수 있나요?
가능합니다. Loader의 모든 메서드는 스레드 안전합니다. 라이브러리는 분할 잠금(sharded locks)을 사용하여 고동시 시나리오에서의 읽기/쓰기 성능을 최적화합니다.
// 안전한 동시 접근
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = env.GetString("KEY") // 스레드 안전
}()
}
wg.Wait()Loader.Close() 후 Get을 호출하면 어떻게 되나요?
제로 값을 반환하며, panic하지 않습니다. Loader는 닫힌 후 읽기 전용 저하 모드로 진입합니다:
loader, _ := env.New()
defer loader.Close()
val := loader.GetString("KEY") // 정상 반환
// Close() 이후
val = loader.GetString("KEY") // "" 반환(제로 값)
err := loader.Set("KEY", "v") // ErrClosed 반환SecureValue
Release와 Close의 차이는 무엇인가요?
| 메서드 | 메모리 제로화 | 메모리 잠금 해제 | 객체 풀 반환 |
|---|---|---|---|
Release() | ✅ | ✅ | ✅ |
Close() | ✅ | ✅ | ❌ |
Release() 사용을 권장합니다. 객체를 풀에 반환하여 GC 압력을 줄입니다. Close()는 풀링이 필요 없는 시나리오에 적합합니다.
SecureValue는 GC 회수 시 자동으로 제로화되나요?
됩니다. SecureValue는 파이널라이저를 설정하여 가비지 컬렉션 시 자동으로 메모리를 제로화합니다. 하지만 적시 정리를 보장하기 위해 명시적으로 Release() 또는 Close()를 호출하는 것을 권장하며, GC의 불확실한 시점에 의존하지 마세요.
// ✅ 권장: 명시적 해제
sv := env.GetSecure("API_KEY")
defer sv.Release()
// ⚠️ GC에 의존 - 권장하지 않지만 안전함
sv := env.GetSecure("API_KEY")
// 결국 GC로 제로화되지만 시점은 불확실함안전하게 로그를 남기려면 어떻게 하나요?
Masked() 또는 마스킹 도구 함수를 사용하고, Reveal()의 값을 직접 출력하지 마세요:
sv := env.GetSecure("API_KEY")
defer sv.Release()
// ✅ 안전 - 마스크 출력
log.Printf("API Key: %s", sv.Masked()) // [SECURE:32 bytes locked]
log.Printf("API Key: %s", sv) // 동일(String()은 Masked() 반환)
// ✅ 안전 - 마스크 도구
masked := env.MaskValue("API_KEY", "sk-xxx") // sk-******************************
clean := env.SanitizeForLog(logMessage) // 자동 감지 및 마스킹
// ❌ 위험 - 평문 유출
plaintext := sv.Reveal()
log.Printf("API Key: %s", plaintext) // 이렇게 하지 마세요구성과 검증
특정 접두사의 변수만 로드하려면 어떻게 하나요?
Prefix 필드로 필터링합니다:
cfg := env.DefaultConfig()
cfg.Prefix = "MYAPP_" // MYAPP_로 시작하는 변수만 로드
loader, _ := env.New(cfg)
loader.LoadFiles(".env")# .env 파일 내용
MYAPP_HOST=localhost # ✅ 로드
MYAPP_PORT=8080 # ✅ 로드
OTHER_KEY=value # ❌ 무시(MYAPP_ 접두사 없음)구성 덮어쓰기를 방지하려면 어떻게 하나요?
OverwriteExisting이 이미 존재하는 변수를 덮어쓸지 여부를 제어합니다:
// 기본값: 덮어쓰지 않음(안전)
cfg := env.DefaultConfig()
cfg.OverwriteExisting = false
// 개발 환경: 덮어쓰기 허용
cfg := env.DevelopmentConfig()
// OverwriteExisting = trueRequiredKeys 검증은 언제 실행되나요?
명시적으로 Validate()를 호출할 때만 검사하며, 로드 시 자동으로 트리거되지 않습니다:
cfg := env.ProductionConfig()
cfg.RequiredKeys = []string{"DB_HOST", "API_KEY"}
loader, _ := env.New(cfg)
loader.LoadFiles(".env")
// 명시적 검증
if err := loader.Validate(); err != nil {
if errors.Is(err, env.ErrMissingRequired) {
log.Fatal("필수 환경 변수 누락")
}
}테스트
테스트에서 환경을 어떻게 격리하나요?
TestingConfig() + 독립 Loader 인스턴스를 사용합니다:
func TestConfig(t *testing.T) {
cfg := env.TestingConfig()
cfg.OverwriteExisting = true
loader, err := env.New(cfg)
if err != nil {
t.Fatal(err)
}
defer loader.Close()
// 각 테스트가 독립적이며 서로 영향을 주지 않음
loader.Set("KEY", "test-value")
val := loader.GetString("KEY")
// 테스트...
}글로벌 모드 테스트는 어떻게 재설정하나요?
ResetDefaultLoader()를 사용합니다:
func TestGlobalMode(t *testing.T) {
// 이전 테스트의 상태 정리
env.ResetDefaultLoader()
defer env.ResetDefaultLoader()
env.Load(".env.test")
// 테스트...
}완전한 테스트 가이드
자세한 내용은 테스트 가이드를 참조하세요.