ComponentFactory API
ComponentFactory는 Loader 와 Parser 가 공유하는 컴포넌트를 생성하고 관리하며, 명확한 수명 주기 관리를 제공합니다.
유형 정의
type ComponentFactory struct {
// 개인 필드 포함
}핵심 책임:
- 공유 검증기, 감사기 및 변수 확장기 생성
- 컴포넌트 수명 주기 관리
- 사용자 정의 파서의 내부 컴포넌트 접근 지원
스레드 안전: ComponentFactory 의 모든 메서드는 스레드 안전합니다.
메서드
Validator
func (f *ComponentFactory) Validator() Validator검증기 컴포넌트를 반환하며, 키 이름과 값의 검증에 사용합니다.
// 사용자 정의 파서에서 사용
validator := factory.Validator()
if err := validator.ValidateKey("MY_KEY"); err != nil {
// 키 이름이 유효하지 않음
}
if err := validator.ValidateValue("some value"); err != nil {
// 값에 잘못된 내용이 포함됨 (예: 널 바이트, 제어 문자)
}Auditor
func (f *ComponentFactory) Auditor() FullAuditLogger감사 로그 컴포넌트를 반환하며, 전체 감사 로그 기능을 제공합니다.
auditor := factory.Auditor()
_ = auditor.Log(env.ActionSet, "KEY", "value set", true)
_ = auditor.LogError(env.ActionSet, "KEY", "validation failed")
_ = auditor.LogWithFile(env.ActionLoad, "KEY", ".env", "loaded", true)
_ = auditor.LogWithDuration(env.ActionParse, "", "parsed", true, time.Since(start))Expander
func (f *ComponentFactory) Expander() VariableExpander변수 확장기 컴포넌트를 반환하며, ${VAR} 구문의 변수 확장에 사용합니다.
expander := factory.Expander()
expanded, err := expander.Expand("${BASE_URL}/api")Close
func (f *ComponentFactory) Close() error팩토리가 보유한 리소스를 해제합니다. 닫은 후에는 팩토리 및 팩토리를 통해 생성된 컴포넌트를 더 이상 사용해서는 안 됩니다.
동작:
- 안전한 닫기, 여러 번 호출해도 nil 반환
- 감사기 리소스 해제
- 원자 연산을 사용하여 스레드 안전 보장
// 일반적으로 Loader 가 자동 관리
loader, _ := env.New(cfg)
defer loader.Close() // ComponentFactory 도 자동으로 닫힘IsClosed
func (f *ComponentFactory) IsClosed() bool팩토리가 닫혔는지 확인합니다.
if factory.IsClosed() {
// 팩토리가 닫혀 사용할 수 없음
}생성 방법
자동 생성 (권장)
Loader 생성 시 ComponentFactory 가 자동으로 생성되고 관리됩니다:
cfg := env.DefaultConfig()
loader, _ := env.New(cfg)
// Loader 내부에서 ComponentFactory 자동 생성
defer loader.Close() // 팩토리도 자동으로 닫힘사용자 정의 파서에서 사용
사용자 정의 파서를 등록할 때, ComponentFactory 를 통해 검증기와 감사기를 가져올 수 있습니다:
type CustomParser struct {
cfg env.Config
validator env.Validator
auditor env.FullAuditLogger
}
func newCustomParser(cfg env.Config, factory *env.ComponentFactory) *CustomParser {
return &CustomParser{
cfg: cfg,
validator: factory.Validator(),
auditor: factory.Auditor(),
}
}
// 사용자 정의 형식 상수 정의 (충돌 방지를 위해 100+ 사용 권장)
const FormatCustom env.FileFormat = 100
// 파서 등록
env.RegisterParser(FormatCustom, func(cfg env.Config, factory *env.ComponentFactory) (env.EnvParser, error) {
return newCustomParser(cfg, factory), nil
})수명 주기 관리
Config 생성
↓
env.New(cfg)
↓
ComponentFactory 자동 생성
↓
┌───────┼───────┐
↓ ↓ ↓
Validator Auditor Expander
↓ ↓ ↓
└───────┼───────┘
↓
Loader/Parser
↓
Close() 해제참고
- 각 Loader 는 일반적으로 자체 ComponentFactory 를 소유
- Close() 를 호출한 후 해당 팩토리를 통해 생성된 모든 컴포넌트를 더 이상 사용해서는 안 됨
- 팩토리는 스레드 안전하며, 동시 접근 가능
감사 핸들러 팩토리
NewJSONAuditHandler
func NewJSONAuditHandler(w io.Writer) *JSONAuditHandlerJSON 형식의 감사 핸들러를 생성하며, 구조화된 로그를 출력합니다.
매개변수:
w- 출력 대상 (예:os.Stdout, 파일)
cfg := env.ProductionConfig()
cfg.AuditEnabled = true
cfg.AuditHandler = env.NewJSONAuditHandler(os.Stdout)출력 예:
{"timestamp":"2024-01-15T10:30:00Z","action":"load","file":".env","success":true,"duration_ns":1234567}NewLogAuditHandler
func NewLogAuditHandler(logger *log.Logger) *LogAuditHandler표준 로그 형식의 감사 핸들러를 생성합니다.
매개변수:
logger- 표준 log.Logger 인스턴스
import "log"
logger := log.New(os.Stderr, "[AUDIT] ", log.LstdFlags)
cfg.AuditHandler = env.NewLogAuditHandler(logger)출력 예:
[AUDIT] 2024/01/15 10:30:00 load .env success (1.23ms)NewChannelAuditHandler
func NewChannelAuditHandler(ch chan<- AuditEvent) *ChannelAuditHandler채널 감사 핸들러를 생성하며, 감사 이벤트를 비동기적으로 처리합니다.
매개변수:
ch- 감사 이벤트 채널
ch := make(chan env.AuditEvent, 100)
cfg.AuditHandler = env.NewChannelAuditHandler(ch)
// 감사 이벤트 비동기 처리
go func() {
for event := range ch {
fmt.Printf("Audit: %+v\n", event)
}
}()NewNopAuditHandler
func NewNopAuditHandler() *NopAuditHandler아무 작업도 수행하지 않는 감사 핸들러를 생성하며, 감사 로그를 비활성화합니다.
cfg.AuditEnabled = true
cfg.AuditHandler = env.NewNopAuditHandler() // 어떤 로그도 기록하지 않음NewCloseableChannelHandler
func NewCloseableChannelHandler(bufferSize int) *CloseableChannelHandler자체 버퍼 채널을 가진 닫기 가능한 감사 핸들러를 생성합니다. ChannelAuditHandler가 외부 채널을 받는 것과 달리, CloseableChannelHandler는 자체 버퍼 채널을 생성하고 소유합니다. Close()를 호출하면 핸들러를 닫고 채널을 닫습니다. Channel()을 사용하여 이벤트를 수신합니다.
매개변수:
bufferSize- 버퍼 채널 크기 (음수는 0 으로 간주됨)
handler := env.NewCloseableChannelHandler(64)
defer handler.Close()
go func() {
for event := range handler.Channel() {
fmt.Printf("Audit: %+v\n", event)
}
}()CloseableChannelHandler 메서드
CloseableChannelHandler는 AuditHandler 인터페이스 (Log / Close) 를 구현하는 것 외에도 다음과 같은 특유의 메서드를 제공합니다:
func (h *CloseableChannelHandler) Channel() <-chan AuditEvent
func (h *CloseableChannelHandler) IsClosed() bool메서드 설명:
| 메서드 | 시그니처 | 용도 |
|---|---|---|
Channel | func (h *CloseableChannelHandler) Channel() <-chan AuditEvent | 감사 이벤트를 소비하기 위한 내부 읽기 전용 채널을 반환합니다. Close() 호출 후 이 채널은 닫히며, range 루프가 함께 종료됩니다 |
IsClosed | func (h *CloseableChannelHandler) IsClosed() bool | 핸들러가 닫혔는지 확인합니다 (스레드 안전, 동시 호출 가능) |
handler := env.NewCloseableChannelHandler(64)
defer handler.Close()
// 닫기 전에 상태를 확인할 수 있음
if !handler.IsClosed() {
// 핸들러는 여전히 사용 가능
}
// 채널이 닫힐 때까지 이벤트 소비
go func() {
for event := range handler.Channel() {
fmt.Printf("Audit: %+v\n", event)
}
// handler.Close() 후 채널이 닫히며 루프 종료
}()파일 시스템
OSFileSystem
기본 파일 시스템 구현으로, 운영 체제 파일 작업을 래핑합니다:
type OSFileSystem struct{}구현 인터페이스: FileSystem
// 메서드 목록
func (fs OSFileSystem) Open(name string) (File, error)
func (fs OSFileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, error)
func (fs OSFileSystem) Stat(name string) (os.FileInfo, error)
func (fs OSFileSystem) MkdirAll(path string, perm os.FileMode) error
func (fs OSFileSystem) Remove(name string) error
func (fs OSFileSystem) Rename(oldpath, newpath string) error
func (fs OSFileSystem) Getenv(key string) string
func (fs OSFileSystem) Setenv(key, value string) error
func (fs OSFileSystem) Unsetenv(key string) error
func (fs OSFileSystem) LookupEnv(key string) (string, bool)DefaultFileSystem
var DefaultFileSystem FileSystem = OSFileSystem{}전역 기본 파일 시스템 인스턴스입니다.
사용자 정의 파일 시스템 사용
테스트에서 파일 시스템 모킹:
type MockFileSystem struct {
files map[string]string
env map[string]string
}
func (m *MockFileSystem) Open(name string) (env.File, error) {
content, ok := m.files[name]
if !ok {
return nil, os.ErrNotExist
}
return &MockFile{content: content}, nil
}
func (m *MockFileSystem) Getenv(key string) string {
return m.env[key]
}
func (m *MockFileSystem) Setenv(key, value string) error {
m.env[key] = value
return nil
}
func (m *MockFileSystem) Unsetenv(key string) error {
delete(m.env, key)
return nil
}
func (m *MockFileSystem) LookupEnv(key string) (string, bool) {
val, ok := m.env[key]
return val, ok
}
func (m *MockFileSystem) OpenFile(name string, flag int, perm os.FileMode) (env.File, error) {
return m.Open(name)
}
func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) {
if _, ok := m.files[name]; !ok {
return nil, os.ErrNotExist
}
return nil, nil
}
func (m *MockFileSystem) MkdirAll(path string, perm os.FileMode) error {
return nil
}
func (m *MockFileSystem) Remove(name string) error {
delete(m.files, name)
return nil
}
func (m *MockFileSystem) Rename(oldpath, newpath string) error {
m.files[newpath] = m.files[oldpath]
delete(m.files, oldpath)
return nil
}
// 사용
cfg := env.TestingConfig()
cfg.FileSystem = &MockFileSystem{
files: map[string]string{".env": "KEY=value"},
env: make(map[string]string),
}형식 감지
DetectFormat
func DetectFormat(filename string) FileFormat파일 확장자로 형식을 감지합니다.
매개변수:
filename- 파일 이름 또는 경로
반환값:
FileFormat- 감지된 형식
감지 규칙:
| 확장자 | 반환 형식 |
|---|---|
.env | FormatEnv |
.json | FormatJSON |
.yaml, .yml | FormatYAML |
| 기타 | FormatAuto |
format := env.DetectFormat("config.json") // FormatJSON
format := env.DetectFormat("settings.yaml") // FormatYAML
format := env.DetectFormat("app.yml") // FormatYAML
format := env.DetectFormat(".env") // FormatEnv
format := env.DetectFormat(".env.local") // FormatAuto (실제로는 .env 로 처리)
format := env.DetectFormat("unknown.txt") // FormatAutoLoadFiles 에서의 적용:
loader.LoadFiles("config.env", "settings.json", "secrets.yaml")
// 각 파일의 형식을 자동으로 감지하고 해당 파서를 사용FileFormat 상수
const (
FormatAuto FileFormat = iota // 자동 감지
FormatEnv // .env 형식
FormatJSON // JSON 형식
FormatYAML // YAML 형식
)사용자 정의 형식:
// 사용자 정의 형식 상수 정의 (충돌 방지를 위해 100+ 값 사용 권장)
const (
FormatTOML env.FileFormat = 100
FormatINI env.FileFormat = 101
FormatXML env.FileFormat = 102
)FileFormat.String
func (f FileFormat) String() string형식의 문자열 표현을 반환합니다.
fmt.Println(env.FormatJSON.String()) // "json"
fmt.Println(env.FormatYAML.String()) // "yaml"
fmt.Println(env.FormatEnv.String()) // "dotenv"
fmt.Println(env.FormatAuto.String()) // "auto"
fmt.Println(env.FileFormat(999).String()) // "unknown"파서 등록
RegisterParser
func RegisterParser(format FileFormat, factory ParserFactory) error사용자 정의 형식 파서를 등록합니다.
매개변수:
format- 파일 형식 상수factory- 파서 팩토리 함수
반환값:
error- 등록 실패 시 오류 반환
오류 발생 경우:
- 내장 형식 (FormatEnv, FormatJSON, FormatYAML) 은 덮어쓸 수 없음
- 형식이 이미 등록된 경우
주의 사항:
env.New()를 호출하기 전에 등록해야 함- 내장 형식과의 충돌을 방지하기 위해 100+ 형식 값을 사용하는 것을 권장
- 팩토리 함수는 스레드 안전한 파서를 반환해야 함
package main
import (
"io"
"github.com/cybergodev/env"
)
// 1. 사용자 정의 형식 상수 정의
const FormatTOML env.FileFormat = 100
// 2. 파서 인터페이스 구현
type TOMLParser struct {
cfg env.Config
validator env.Validator
auditor env.FullAuditLogger
}
func (p *TOMLParser) Parse(r io.Reader, filename string) (map[string]string, error) {
// TOML 파싱 로직 구현
result := make(map[string]string)
// ... 파싱 코드
return result, nil
}
// 3. 사용 전 실행되도록 init() 에서 파서 등록
func init() {
err := env.RegisterParser(FormatTOML, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
return &TOMLParser{
cfg: cfg,
validator: f.Validator(),
auditor: f.Auditor(),
}, nil
})
if err != nil {
panic(err)
}
}
// 4. 사용자 정의 형식 사용
func main() {
// 등록은 init() 에서 완료됨 (main 보다 먼저 실행)
loader, _ := env.New(env.DefaultConfig())
defer loader.Close()
// 이제 .toml 파일을 로드할 수 있음
loader.LoadFiles("config.toml")
}ForceRegisterParser
func ForceRegisterParser(format FileFormat, factory ParserFactory) error파서를 강제로 등록하며, 내장 파서를 덮어쓸 수 있습니다.
매개변수:
format- 파일 형식 상수factory- 파서 팩토리 함수
반환값:
error- 등록 실패 시 오류 반환 (factory가 nil 인 경우)
경고
신중하게 사용하세요. 내장 파서를 덮어쓰면 대체 파서가 동일한 보안 검사 (키 검증, 값 검증, 크기 제한 등) 를 구현하지 않은 경우 보안 취약점이 발생할 수 있습니다.
다음과 같은 고급 시나리오에 적합합니다:
- 내장 파서에 사용자 정의 보안 검사 추가
- 형식 확장 구현 (예: HEREDOC, 여러 줄 값)
- 테스트용 모킹 파서 사용
// 기본 .env 파서 덮어쓰기 (고급 용도)
err := env.ForceRegisterParser(env.FormatEnv, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
return &MyCustomEnvParser{
validator: f.Validator(),
auditor: f.Auditor(),
}, nil
})ParserFactory 유형
type ParserFactory func(cfg Config, factory *ComponentFactory) (EnvParser, error)파서 팩토리 함수 서명입니다.
매개변수:
cfg- 설정 객체, 제한 및 보안 설정 포함factory- 컴포넌트 팩토리, 검증기 및 감사기에 접근 가능
반환값:
EnvParser- 파서 인스턴스error- 생성 오류
EnvParser 인터페이스
type EnvParser interface {
Parse(r io.Reader, filename string) (map[string]string, error)
}파서가 구현해야 하는 인터페이스입니다.
매개변수:
r- 파일 내용 리더filename- 파일 이름 (오류 메시지용)
반환값:
map[string]string- 파싱된 키 - 값 쌍error- 파싱 오류
내장 파서
라이브러리에 세 가지 형식의 파서가 내장되어 있습니다:
DotEnv 파서
.env 형식 파서, 지원 기능:
KEY=value구문export KEY=value구문- 작은따옴표
'value'및 큰따옴표"value" - 변수 확장
${VAR}및${VAR:-default} - 주석
#
JSON 파서
JSON 형식 파서, 지원 기능:
- 키 - 값 쌍 객체
- 중첩 구조 (평면화 처리)
- 숫자, 문자열, 부울값 변환
- 배열 (
KEY_0,KEY_1...으로 평면화)
YAML 파서
YAML 형식 파서, 지원 기능:
- 키 - 값 쌍
- 중첩 구조 (평면화 처리)
- 다양한 스칼라 유형
- 리스트 (인덱스 키로 평면화)
전체 예제
사용자 정의 파서 등록
package main
import (
"fmt"
"io"
"strings"
"github.com/cybergodev/env"
)
// 사용자 정의 INI 파서
type INIParser struct {
cfg env.Config
validator env.Validator
auditor env.FullAuditLogger
}
func (p *INIParser) Parse(r io.Reader, filename string) (map[string]string, error) {
content, err := io.ReadAll(r)
if err != nil {
return nil, err
}
result := make(map[string]string)
lines := strings.Split(string(content), "\n")
var section string
for lineNum, line := range lines {
line = strings.TrimSpace(line)
// 빈 행 및 주석 건너뛰기
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
// Section [section]
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
section = strings.Trim(line, "[]")
continue
}
// Key=Value
if idx := strings.Index(line, "="); idx > 0 {
key := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
// section 접두사 추가
if section != "" {
key = section + "_" + key
}
// 키 검증
if err := p.validator.ValidateKey(key); err != nil {
_ = p.auditor.LogError(env.ActionParse, key, err.Error())
return nil, fmt.Errorf("line %d: %w", lineNum+1, err)
}
result[strings.ToUpper(key)] = value
}
}
_ = p.auditor.Log(env.ActionParse, "", fmt.Sprintf("parsed %d variables from %s", len(result), filename), true)
return result, nil
}
func main() {
// 사용자 정의 형식 정의
const FormatINI env.FileFormat = 101
// 파서 등록
err := env.RegisterParser(FormatINI, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
return &INIParser{
cfg: cfg,
validator: f.Validator(),
auditor: f.Auditor(),
}, nil
})
if err != nil {
panic(err)
}
// 사용자 정의 형식 사용
cfg := env.DefaultConfig()
loader, _ := env.New(cfg)
defer loader.Close()
// 이제 .ini 파일을 로드할 수 있음
// loader.LoadFiles("config.ini")
fmt.Println("INI parser registered")
}사용자 정의 파일 시스템
package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/cybergodev/env"
)
// 메모리 파일 시스템 (테스트용)
type MemoryFileSystem struct {
files map[string]string
env map[string]string
}
func NewMemoryFileSystem() *MemoryFileSystem {
return &MemoryFileSystem{
files: make(map[string]string),
env: make(map[string]string),
}
}
func (m *MemoryFileSystem) Open(name string) (env.File, error) {
content, ok := m.files[name]
if !ok {
return nil, os.ErrNotExist
}
return &MemoryFile{reader: strings.NewReader(content)}, nil
}
func (m *MemoryFileSystem) OpenFile(name string, flag int, perm os.FileMode) (env.File, error) {
return m.Open(name)
}
func (m *MemoryFileSystem) Stat(name string) (os.FileInfo, error) {
content, ok := m.files[name]
if !ok {
return nil, os.ErrNotExist
}
return &MemoryFileInfo{name: name, size: int64(len(content))}, nil
}
func (m *MemoryFileSystem) MkdirAll(path string, perm os.FileMode) error {
return nil
}
func (m *MemoryFileSystem) Remove(name string) error {
delete(m.files, name)
return nil
}
func (m *MemoryFileSystem) Rename(oldpath, newpath string) error {
m.files[newpath] = m.files[oldpath]
delete(m.files, oldpath)
return nil
}
func (m *MemoryFileSystem) Getenv(key string) string {
return m.env[key]
}
func (m *MemoryFileSystem) Setenv(key, value string) error {
m.env[key] = value
return nil
}
func (m *MemoryFileSystem) Unsetenv(key string) error {
delete(m.env, key)
return nil
}
func (m *MemoryFileSystem) LookupEnv(key string) (string, bool) {
val, ok := m.env[key]
return val, ok
}
// MemoryFile 은 env.File 구현
type MemoryFile struct {
reader *strings.Reader
}
func (f *MemoryFile) Read(p []byte) (n int, err error) { return f.reader.Read(p) }
func (f *MemoryFile) Write(p []byte) (n int, err error) { return 0, errors.ErrUnsupported }
func (f *MemoryFile) Close() error { return nil }
func (f *MemoryFile) Stat() (os.FileInfo, error) { return nil, errors.ErrUnsupported }
func (f *MemoryFile) Sync() error { return nil }
// MemoryFileInfo 는 os.FileInfo 구현
type MemoryFileInfo struct {
name string
size int64
}
func (i *MemoryFileInfo) Name() string { return i.name }
func (i *MemoryFileInfo) Size() int64 { return i.size }
func (i *MemoryFileInfo) Mode() os.FileMode { return 0644 }
func (i *MemoryFileInfo) ModTime() time.Time { return time.Time{} }
func (i *MemoryFileInfo) IsDir() bool { return false }
func (i *MemoryFileInfo) Sys() interface{} { return nil }
// 사용 예제
func main() {
// 메모리 파일 시스템 생성
fs := NewMemoryFileSystem()
fs.files[".env"] = "APP_NAME=myapp\nPORT=8080\n"
// 사용자 정의 파일 시스템 사용 설정
cfg := env.TestingConfig()
cfg.FileSystem = fs
loader, _ := env.New(cfg)
defer loader.Close()
loader.LoadFiles(".env")
fmt.Println(loader.GetString("APP_NAME")) // myapp
fmt.Println(loader.GetInt("PORT")) // 8080
}