커스텀 파서
이 가이드는 커스텀 파일 형식 파서를 생성하고 등록하여 env 라이브러리가 지원하는 구성 형식을 확장하는 방법을 소개합니다.
파서 인터페이스
EnvParser
모든 파서는 이 인터페이스를 구현해야 합니다:
go
type EnvParser interface {
Parse(r io.Reader, filename string) (map[string]string, error)
}매개변수:
r- 파일 콘텐츠 리더filename- 파일 이름(오류 정보에 사용)
반환값:
map[string]string- 파싱된 키-값 쌍error- 파싱 오류
커스텀 파서 생성
기본 구조
go
package myparser
import (
"io"
"strings"
"github.com/cybergodev/env"
)
// 커스텀 파서
type CustomParser struct {
cfg env.Config
validator env.Validator
auditor env.FullAuditLogger
}
// EnvParser 인터페이스 구현
func (p *CustomParser) Parse(r io.Reader, filename string) (map[string]string, error) {
result := make(map[string]string)
// 1. 콘텐츠 읽기(크기 제한 주의)
content, err := io.ReadAll(io.LimitReader(r, p.cfg.MaxFileSize))
if err != nil {
return nil, err
}
// 2. 콘텐츠를 키-값 쌍으로 파싱
for _, line := range strings.Split(string(content), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
idx := strings.Index(line, "=")
if idx <= 0 {
continue
}
result[strings.TrimSpace(line[:idx])] = strings.TrimSpace(line[idx+1:])
}
// 3. 결과 검증
for key := range result {
if err := p.validator.ValidateKey(key); err != nil {
return nil, err
}
}
// 4. 결과 반환
return result, nil
}TOML 파서 예제
go
package tomlparser
import (
"fmt"
"io"
"strings"
"time"
"github.com/cybergodev/env"
)
// TOMLParser는 TOML 형식 파싱
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) {
start := time.Now()
// 읽기 크기 제한
content, err := io.ReadAll(io.LimitReader(r, p.cfg.MaxFileSize+1))
if err != nil {
return nil, err
}
if int64(len(content)) > p.cfg.MaxFileSize {
return nil, fmt.Errorf("file exceeds size limit")
}
result := make(map[string]string)
lines := strings.Split(string(content), "\n")
var currentSection string
for lineNum, line := range lines {
line = strings.TrimSpace(line)
// 빈 줄과 주석 건너뛰기
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// section [section] 파싱
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
currentSection = strings.Trim(line, "[]")
continue
}
// key = value 파싱
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue // 또는 오류 반환
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// section 접두사 추가
if currentSection != "" {
key = currentSection + "_" + key
}
// 인용부호 제거
value = strings.Trim(value, "\"'")
// 대문자로 변환
key = strings.ToUpper(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[key] = value
}
// 변수 수량 확인
if len(result) > p.cfg.MaxVariables {
return nil, fmt.Errorf("exceeds max variables: %d > %d", len(result), p.cfg.MaxVariables)
}
_ = p.auditor.LogWithDuration(env.ActionParse, "", "parsed TOML: "+filename, true, time.Since(start))
return result, nil
}INI 파서 예제
go
package iniparser
import (
"fmt"
"io"
"strings"
"github.com/cybergodev/env"
)
// INIParser는 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(io.LimitReader(r, p.cfg.MaxFileSize+1))
if err != nil {
return nil, err
}
result := make(map[string]string)
lines := strings.Split(string(content), "\n")
var currentSection string
for lineNum, line := range lines {
line = strings.TrimSpace(line)
// 빈 줄과 주석 건너뛰기
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
// Section
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
currentSection = strings.Trim(line, "[]")
continue
}
// Key=Value
if idx := strings.Index(line, "="); idx > 0 {
key := strings.TrimSpace(line[:idx])
value := strings.TrimSpace(line[idx+1:])
if currentSection != "" {
key = currentSection + "_" + key
}
// 검증
if err := p.validator.ValidateKey(strings.ToUpper(key)); err != nil {
return nil, fmt.Errorf("line %d: %w", lineNum+1, err)
}
result[strings.ToUpper(key)] = value
}
}
return result, nil
}파서 등록
ParserFactory 타입
go
type ParserFactory func(cfg Config, factory *ComponentFactory) (EnvParser, error)팩토리 함수는 Config와 ComponentFactory를 받아 파서 인스턴스를 반환합니다.
매개변수 설명:
cfg- 구성 객체, 모든 제한과 보안 설정 포함factory- 컴포넌트 팩토리, Validator, Auditor 등의 컴포넌트 획득 가능
RegisterParser 함수
go
func RegisterParser(format FileFormat, factory ParserFactory) error커스텀 형식 파서를 등록합니다.
매개변수:
format- 파일 형식 상수(충돌 방지를 위해 100+ 값 사용 권장)factory- 파서 팩토리 함수
반환값:
error- 등록 실패 시 오류 반환
오류 상황:
- 내장 형식(FormatEnv, FormatJSON, FormatYAML)은 덮어쓸 수 없음
- 형식이 이미 등록됨
주의사항:
env.New()호출 전에 등록해야 함init()함수에서 등록 권장
ComponentFactory 사용
ComponentFactory를 통해 검증기와 감사기를 획득합니다:
go
type SecureParser struct {
cfg env.Config
validator env.Validator
auditor env.FullAuditLogger
}
func NewSecureParser(cfg env.Config, factory *env.ComponentFactory) (env.EnvParser, error) {
return &SecureParser{
cfg: cfg,
validator: factory.Validator(),
auditor: factory.Auditor(),
}, nil
}
func (p *SecureParser) Parse(r io.Reader, filename string) (map[string]string, error) {
result := make(map[string]string)
// ... 파싱 로직
// 검증기로 키 이름 검증
for key := range result {
if err := p.validator.ValidateKey(key); err != nil {
_ = p.auditor.Log(env.ActionParse, key, "invalid key", false)
return nil, err
}
}
_ = p.auditor.Log(env.ActionParse, "", "parse completed", true)
return result, nil
}완전한 등록 예제
go
package main
import (
"github.com/cybergodev/env"
)
// 1. 형식 상수 정의(100+ 값 사용 권장)
const (
FormatTOML env.FileFormat = 100
FormatINI env.FileFormat = 101
FormatXML env.FileFormat = 102
)
// 2. init에서 등록
func init() {
// TOML 파서 등록
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) // 형식이 이미 등록되었거나 기타 오류
}
// INI 파서 등록
env.RegisterParser(FormatINI, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
return &INIParser{
cfg: cfg,
validator: f.Validator(),
auditor: f.Auditor(),
}, nil
})
}
func main() {
// 등록은 New 전에 완료되어야 함(이미 init에서 완료됨).
//
// 중요한 제한: LoadFiles는 .toml 확장자를 기준으로 위의
// TOMLParser로 자동 라우팅하지 않습니다 - DetectFormat은
// .env/.json/.yaml/.yml만 인식하며, 다른 확장자는
// 내장 dotenv 파서로 폴백합니다(format.go의 DetectFormat 참조).
// LoadFiles가 실제로 TOMLParser를 호출하게 하려면
// ForceRegisterParser로 FormatEnv를 덮어쓰고 파일 이름을 *.env로 지정해야 합니다:
err := env.ForceRegisterParser(env.FormatEnv, 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)
}
cfg := env.DefaultConfig()
loader, _ := env.New(cfg)
defer loader.Close()
// 파일 확장자가 .env여야 함(콘텐츠는 TOML 형식) - 덮어쓴 파서로 라우팅
if err := loader.LoadFiles("config.env"); err != nil {
panic(err)
}
}LoadFiles 라우팅 제한
RegisterParser로 등록된 커스텀 형식 번호(예: FormatTOML = 100)는 LoadFiles에서 파일 확장자로 자동 인식되지 않습니다. LoadFiles 내부적으로 DetectFormat(filename)을 호출하여 파서를 선택하지만, DetectFormat은 .env / .json / .yaml / .yml 네 가지 확장자만 인식하며, 다른 확장자는 FormatAuto를 반환하고 최종적으로 내장 dotenv 파서로 폴백합니다 - 커스텀 파서는 결코 호출되지 않습니다.
커스텀 형식 파일을 로드하는 두 가지 경로:
.env확장자 +ForceRegisterParser(권장): 커스텀 형식 파일의 이름을*.env로 지정하고env.ForceRegisterParser(env.FormatEnv, ...)로 내장 dotenv 파서를 덮어씁니다. 키 이름/값/크기 등 보안 검사를 유지해야 하며, 그렇지 않으면 보안 취약점이 도입됩니다.- 파서 수동 호출: 파일을 읽어
io.Reader를 얻고, 파서 인스턴스를 직접 구성하여parser.Parse(reader, filename)을 호출해map[string]string을 얻은 다음loader.Set으로 하나씩 기록합니다. 파서 내부의validator/auditor는 일반적으로*ComponentFactory에 의존하므로 등록 팩토리에서 함께 획득하여 전달해야 합니다.
모범 사례
1. 구성 제한 준수
go
func (p *CustomParser) checkLimits(result map[string]string) error {
// 변수 수량 확인
if len(result) > p.cfg.MaxVariables {
return fmt.Errorf("exceeds max variables: %d > %d", len(result), p.cfg.MaxVariables)
}
// 키와 값 길이 확인
for key, value := range result {
if len(key) > p.cfg.MaxKeyLength {
return fmt.Errorf("key too long: %s", key)
}
if len(value) > p.cfg.MaxValueLength {
return fmt.Errorf("value too long for: %s", key)
}
}
return nil
}2. 검증기 사용
go
func (p *CustomParser) Parse(r io.Reader, filename string) (map[string]string, error) {
result := make(map[string]string)
// ... 파싱 로직
// 모든 키 검증
for key := range result {
if err := p.validator.ValidateKey(key); err != nil {
return nil, fmt.Errorf("invalid key %q: %w", key, err)
}
}
// 모든 값 검증(활성화된 경우)
if p.cfg.ValidateValues {
for key, value := range result {
if err := p.validator.ValidateValue(value); err != nil {
return nil, fmt.Errorf("invalid value for %q: %w", key, err)
}
}
}
return result, nil
}3. 의미 있는 오류 제공
go
type CustomParseError struct {
File string
Line int
Content string
Err error
}
func (e *CustomParseError) Error() string {
if e.Line > 0 {
return fmt.Sprintf("%s:%d: %s: %v", e.File, e.Line, e.Content, e.Err)
}
return fmt.Sprintf("%s: %s: %v", e.File, e.Content, e.Err)
}
func (e *CustomParseError) Unwrap() error {
return e.Err
}4. 감사 로그 기록
go
func (p *CustomParser) Parse(r io.Reader, filename string) (map[string]string, error) {
start := time.Now()
result := make(map[string]string)
// ... 파싱 로직
// 성공 기록
_ = p.auditor.LogWithDuration(
env.ActionParse,
"",
fmt.Sprintf("parsed %d variables", len(result)),
true,
time.Since(start),
)
return result, nil
}완전한 예제
XML 파서 구현
go
package main
import (
"encoding/xml"
"fmt"
"io"
"strings"
"time"
"github.com/cybergodev/env"
)
// XML 구성 구조
type XMLConfig struct {
XMLName xml.Name `xml:"config"`
Entries []XMLEntry `xml:"entry"`
}
type XMLEntry struct {
Key string `xml:"key,attr"`
Value string `xml:",chardata"`
}
// XMLParser는 XML 형식 파싱
type XMLParser struct {
cfg env.Config
validator env.Validator
auditor env.FullAuditLogger
}
func (p *XMLParser) Parse(r io.Reader, filename string) (map[string]string, error) {
start := time.Now()
// 읽기 크기 제한
content, err := io.ReadAll(io.LimitReader(r, p.cfg.MaxFileSize+1))
if err != nil {
return nil, err
}
if int64(len(content)) > p.cfg.MaxFileSize {
_ = p.auditor.LogError(env.ActionParse, "", "file exceeds size limit")
return nil, fmt.Errorf("file exceeds size limit: %d > %d", len(content), p.cfg.MaxFileSize)
}
var xmlConfig XMLConfig
if err := xml.Unmarshal(content, &xmlConfig); err != nil {
_ = p.auditor.LogError(env.ActionParse, "", "xml parse error: "+err.Error())
return nil, fmt.Errorf("xml parse error: %w", err)
}
result := make(map[string]string)
for _, entry := range xmlConfig.Entries {
key := strings.ToUpper(entry.Key)
// 키 길이 검증
if len(key) > p.cfg.MaxKeyLength {
return nil, fmt.Errorf("key too long: %s", key)
}
// 키 형식 검증
if err := p.validator.ValidateKey(key); err != nil {
return nil, fmt.Errorf("invalid key %q: %w", key, err)
}
// 값 길이 검증
if len(entry.Value) > p.cfg.MaxValueLength {
return nil, fmt.Errorf("value too long for key: %s", key)
}
result[key] = entry.Value
}
// 변수 수량 확인
if len(result) > p.cfg.MaxVariables {
return nil, fmt.Errorf("too many variables: %d > %d", len(result), p.cfg.MaxVariables)
}
_ = p.auditor.LogWithDuration(env.ActionParse, "", "parsed XML: "+filename, true, time.Since(start))
return result, nil
}
// XML 형식 상수 정의
const FormatXML env.FileFormat = 102
func init() {
// XML 파서 등록
env.RegisterParser(FormatXML, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
return &XMLParser{
cfg: cfg,
validator: f.Validator(),
auditor: f.Auditor(),
}, nil
})
}
func main() {
// LoadFiles는 .xml 확장자를 기준으로 XML 파서로 자동 라우팅하지 않음 - DetectFormat은
// .env/.json/.yaml/.yml만 인식. 여기서는 ForceRegisterParser로
// FormatEnv를 덮어쓰고 파일을 .env 확장자로 로드(콘텐츠는 XML 형식):
err := env.ForceRegisterParser(env.FormatEnv, func(cfg env.Config, f *env.ComponentFactory) (env.EnvParser, error) {
return &XMLParser{
cfg: cfg,
validator: f.Validator(),
auditor: f.Auditor(),
}, nil
})
if err != nil {
panic(err)
}
cfg := env.DefaultConfig()
loader, _ := env.New(cfg)
defer loader.Close()
/*
config.env 파일 내용(XML 형식):
<?xml version="1.0"?>
<config>
<entry key="DATABASE_HOST">localhost</entry>
<entry key="DATABASE_PORT">5432</entry>
</config>
*/
if err := loader.LoadFiles("config.env"); err != nil {
panic(err)
}
fmt.Println(loader.GetString("DATABASE_HOST")) // localhost
fmt.Println(loader.GetInt("DATABASE_PORT")) // 5432
}관련 문서
- ComponentFactory API - ComponentFactory와 RegisterParser
- 인터페이스 - EnvParser 인터페이스 정의
- Config API - 구성 옵션 상세
- 다중 포맷 설정 - JSON/YAML 형식 상세