성능 최적화
env 라이브러리는 고성능 시나리오에 최적화되어 있습니다. 이 문서는 동시 안전, 객체 풀, 메모리 관리 등 성능 관련 기능을 소개합니다.
동시 안전
스레드 안전 보장
Loader의 모든 메서드는 스레드 안전합니다:
loader, _ := env.New(env.DefaultConfig())
defer loader.Close()
var wg sync.WaitGroup
// 동시 읽기
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
loader.GetString("KEY")
}()
}
// 동시 쓰기
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
loader.Set(fmt.Sprintf("KEY_%d", n), "value")
}(i)
}
wg.Wait()패키지 수준 함수 스레드 안전
패키지 수준 함수는 글로벌 로더를 사용하며, 마찬가지로 스레드 안전합니다:
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
env.GetString("KEY", "default")
}()
}
wg.Wait()내부 구현
라이브러리는 분할 저장(Sharded Storage)을 사용하여 잠금 경합을 줄입니다:
┌─────────────────────────────────────────┐
│ Loader(8개 분할) │
├─────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │ Shard 0 │ │ Shard 1 │... │ Shard 7│ │
│ │ Lock │ │ Lock │ │ Lock │ │
│ │ Data │ │ Data │ │ Data │ │
│ └─────────┘ └─────────┘ └────────┘ │
└─────────────────────────────────────────┘- 키는 해시 값에 따라 서로 다른 분할에 할당
- 각 분할은 독립적인 잠금 보유
- 잠금 경합 감소, 동시 성능 향상
객체 풀
객체 풀을 사용하는 이유
빈번한 객체 생성 및 파괴는 GC 압력을 증가시킵니다:
객체 풀 없음:
객체 생성 → 사용 → GC 회수 → 객체 생성 → 사용 → GC 회수 ...
객체 풀 있음:
객체 생성 → 사용 → 풀에 반환 → 가져오기 → 사용 → 풀에 반환 ...SecureValue 풀
SecureValue 객체는 풀로 관리됩니다:
// SecureValue 가져오기(풀에서 재사용 가능)
secret := env.GetSecure("API_KEY")
// 사용(Reveal은 평문 반환, String/Masked는 마스크 반환)
value := secret.Reveal()
// 풀에 반환
secret.Close() // 또는 secret.Release()객체 풀 올바른 사용
적시 해제:
func processData() {
secret := env.GetSecure("SECRET")
defer secret.Close() // 해제 보장
// secret 사용...
}참조를 보유하지 않기:
// 오류: 해제된 객체의 참조를 보유
var globalSecret *env.SecureValue
func init() {
globalSecret = env.GetSecure("KEY")
globalSecret.Close() // 해제 후 객체가 재사용됨
}
func later() {
// 위험: globalSecret은 다른 코드에서 이미 사용 중일 수 있음
globalSecret.String()
}
// 올바름: 필요할 때마다 가져오기
func getSecret() string {
secret := env.GetSecure("KEY")
defer secret.Close()
return secret.Reveal()
}닫힘 상태 확인:
secret := env.GetSecure("KEY")
// 사용 전 확인
if secret.IsClosed() {
// 객체가 닫혀 사용할 수 없음
}
// 사용 후 닫기
secret.Close()
// 닫은 후 확인
if secret.IsClosed() {
// 닫힘
}메모리 안전
메모리 잠금
메모리 잠금을 활성화하여 민감 데이터가 디스크로 스왑되는 것을 방지합니다:
// 플랫폼 지원 확인
if env.IsMemoryLockSupported() {
env.SetMemoryLockEnabled(true)
}플랫폼 지원:
| 플랫폼 | 지원 |
|---|---|
| Linux | ✅ |
| macOS | ✅ |
| Windows | ✅ |
| FreeBSD | ✅ |
| wasm | ❌ |
상세
SecureValue API - 메모리 잠금 구성에서 완전한 구성 설명을 확인하세요.
엄격 모드
엄격 모드에서는 메모리 잠금 실패가 오류를 발생시킵니다:
env.SetMemoryLockStrict(true)
secret, err := env.NewSecureValueStrict("sensitive_data")
if err != nil {
// 메모리 잠금 실패
}안전한 제로화
SecureValue는 닫을 때 자동으로 메모리를 제로화합니다:
secret := env.GetSecure("PASSWORD")
// 내부 저장: ['p', 'a', 's', 's', ...]
secret.Close()
// 내부 저장: [0, 0, 0, 0, ...]바이트 슬라이스 수동 제로화:
sensitiveBytes := []byte("secret")
env.ClearBytes(sensitiveBytes)
// sensitiveBytes는 이제 모두 0성능 패턴
초기화 후 읽기 전용
가장 효율적인 패턴: 시작 시 구성 로드, 런타임에는 읽기 전용:
var config *Config
func init() {
env.Load(".env")
config = &Config{}
env.ParseInto(config)
}
// 모든 goroutine에서 안전하게 읽기
func getValue() string {
return config.Key
}동적 구성 새로고침
구성을 동적으로 업데이트해야 할 때의 패턴:
type ConfigManager struct {
loader *env.Loader
mu sync.RWMutex
}
func (m *ConfigManager) Refresh() error {
m.mu.Lock()
defer m.mu.Unlock()
return m.loader.LoadFiles(".env")
}
func (m *ConfigManager) Get(key string) string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.loader.GetString(key)
}잠금 보유 시간 단축
// 비권장: 잠금 내에서 시간이 많이 걸리는 작업 수행
func (l *Loader) ProcessValue(key string) {
value := l.GetString(key)
// 시간이 많이 걸리는 작업...
processValue(value)
}
// 권장: 빠른 읽기, 잠금 외부에서 처리
func ProcessValue(key string) {
value := loader.GetString(key) // 빠르게 가져오기
go processValue(value) // 비동기 처리
}배치 작업
// 필요한 모든 값을 한 번에 가져오기
func LoadAllConfig(loader *env.Loader) *Config {
return &Config{
Host: loader.GetString("HOST"),
Port: loader.GetInt("PORT"),
Debug: loader.GetBool("DEBUG"),
Timeout: loader.GetDuration("TIMEOUT"),
}
}빈번한 호출 피하기
// 비권장: 매 요청마다 읽기
func Handler(w http.ResponseWriter, r *http.Request) {
apiKey := env.GetString("API_KEY") // 매 요청마다 잠금
// ...
}
// 권장: 시작 시 캐싱
var apiKey string
func init() {
env.Load(".env")
apiKey = env.GetString("API_KEY")
}
func Handler(w http.ResponseWriter, r *http.Request) {
// 캐시된 값 직접 사용
// ...
}성능 영향
객체 풀 이점
| 작업 | 풀 없음 | 풀 있음 |
|---|---|---|
| 할당 횟수 | N | ~상수 |
| GC 압력 | 높음 | 낮음 |
| 지연 | 불안정 | 안정 |
메모리 잠금 오버헤드
메모리 잠금(Linux의 mlock / Windows의 VirtualLock)은 SecureValue 생성 시 한 번만 추가 syscall 오버헤드를 발생시키며, 읽기 작업(Reveal / String / Masked)에는 차이가 없습니다. SecureValue를 작고 일시적으로 유지하는 것을 권장합니다 - 사용 후 즉시 Close() / Release()하여 객체 풀에 반환하고, 대규모 잠긴 메모리를 장기간 보유하지 마세요.
벤치마크
읽기 성능
func BenchmarkConcurrentRead(b *testing.B) {
loader, _ := env.New(env.DefaultConfig())
loader.Set("KEY", "value")
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
loader.GetString("KEY")
}
})
}쓰기 성능
func BenchmarkConcurrentWrite(b *testing.B) {
loader, _ := env.New(env.DefaultConfig())
var i int64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
n := atomic.AddInt64(&i, 1)
loader.Set(fmt.Sprintf("KEY_%d", n), "value")
}
})
}혼합 읽기/쓰기
func BenchmarkMixedReadWrite(b *testing.B) {
loader, _ := env.New(env.DefaultConfig())
loader.Set("KEY", "value")
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
if i%10 == 0 {
loader.Set("KEY", "new_value")
} else {
loader.GetString("KEY")
}
i++
}
})
}주의사항
잠금 내부에서 차단 피하기
// 위험: 교착 상태 가능성
func (l *Loader) BadMethod() {
// 잠금 내부에서 차단 가능한 작업 호출
l.Set("KEY", computeValue()) // computeValue가 느릴 수 있음
}
// 안전: 먼저 계산, 나중에 설정
func GoodMethod() {
value := computeValue() // 잠금 외부에서 계산
loader.Set("KEY", value) // 빠르게 설정
}Close 후 동시 접근
loader, _ := env.New(cfg)
// goroutine 시작
go func() {
time.Sleep(1 * time.Second)
loader.GetString("KEY") // 빈 문자열 반환(GetString은 error를 반환하지 않음)
}()
loader.Close() // 메인 goroutine에서 닫기글로벌 로더 재설정
// 동시성 안전하지 않음: 런타임에 호출하지 마세요
env.ResetDefaultLoader()
// 안전: 테스트 또는 시작 시에만 호출
func init() {
env.ResetDefaultLoader()
env.Load(".env")
}관련 문서
- SecureValue API - 보안 값 처리와 메모리 잠금
- Loader API - 로더 메서드
- 테스트 - 벤치마크 예제