출력 대상
DD 는 3 종의 출력 writer 를 제공하여 파일 로테이션, 버퍼 쓰기, 다중 대상 출력을 지원합니다.
FileWriter
자동 로테이션이 있는 파일 writer.
생성
func NewFileWriter(path string, cfg FileWriterConfig) (*FileWriter, error)경로는 내부 경로 보안 검사 (경로 순회, null 바이트, overlong UTF-8) 를 거친 후 cfg.Validate()로 수량 상한을 검사하며, 제로/음수값에는 기본 구성으로 폴백합니다. 오류를 반환하는 경우:
- 경로류:
ErrEmptyFilePath/ErrNullByte/ErrPathTooLong(>4096 바이트) /ErrPathTraversal/ErrInvalidPath/ErrOverlongEncoding - 구성류:
ErrMaxSizeExceeded(MaxSizeMB > 10240) /ErrMaxBackupsExceeded(MaxBackups > 1000) - I/O 류: 디렉터리 생성 실패 (
failed to create directory: …로 래핑) 또는 파일 열기 실패 (failed to open file …: %w로 래핑,ErrSymlinkNotAllowed/ErrHardlinkNotAllowed포함)
// 기본 구성 사용
fw, _ := dd.NewFileWriter("logs/app.log", dd.DefaultFileWriterConfig())
// 커스텀 구성
cfg := dd.DefaultFileWriterConfig()
cfg.MaxSizeMB = 50
fw, _ = dd.NewFileWriter("logs/app.log", cfg)FileWriterConfig
파일 writer 구성.
type FileWriterConfig struct {
MaxSizeMB int // 파일 크기 상한 MB(기본 100)
MaxAge time.Duration // 이전 파일 보존 기간 (기본 30 일)
MaxBackups int // 보존 백업 수 (기본 10)
Compress bool // gzip 압축 여부 (기본 false)
}기본 구성
func DefaultFileWriterConfig() FileWriterConfig기본값: 100MB 크기 제한, 30 일 보존, 10 개 백업 파일.
Validate
func (c FileWriterConfig) Validate() error파일 writer 구성의 유효성을 검증합니다. 오류를 반환하는 경우:
MaxSizeMB가 10240 초과 (ErrMaxSizeExceeded반환)MaxBackups가 1000 초과 (ErrMaxBackupsExceeded반환)
음수값은 Validate 오류를 트리거하지 않습니다. 그중 제로 또는 음수 MaxSizeMB는 기본값 적용 시 100MB 로 폴백하며, MaxBackups / MaxAge의 음수값은 원래대로 유지됩니다 (아래 '기본값 적용 규칙' 참조).
메서드
| 메서드 | 시그니처 | 설명 |
|---|---|---|
Write | (p []byte) (int, error) | 데이터 쓰기 (io.Writer 구현); 쓰기 전 크기를 검사해 로테이션 트리거, 이미 종료된 경우 os.ErrClosed 반환 |
SetOnRotateCallback | (fn func(path string)) | 파일 로테이션 성공 후 콜백 설정 |
Close | () error | 정리 goroutine 중지 및 기본 파일 닫기; 여러 번 호출해도 안전 (CAS 가드) |
로테이션 콜백
func (fw *FileWriter) SetOnRotateCallback(fn func(path string))파일 로테이션 성공 후 호출되는 콜백 함수를 설정합니다. 콜백 매개변수 path는 현재 로그 파일의 기준 경로입니다 (NewFileWriter 생성 시 경로 정규화 후 저장된 경로 - 절대 경로 입력의 경우 대개 입력과 동일하며, 상대 경로는 절대 경로로 해석됨). 이때 이전 로그는 이미 백업 파일로 보관 처리되었으며, 새 파일이 해당 경로에서 다시 열립니다. 설정 시 내부 뮤텍스 락을 획득하여 진행 중인 로테이션과 경쟁하지 않습니다.
정보 내부 용도
이 메서드는 주로 Logger 내부에서 사용됩니다 - FileWriter가 Logger 의 출력 대상일 때 Logger 는 이를 통해 HookOnRotate 훅 이벤트를 트리거합니다 (훅 시스템 참조). 일반 사용자는 직접 호출할 필요가 없으며, 로테이션 후 동작을 커스터마이징하려면 직접 설정해도 됩니다.
fw, _ := dd.NewFileWriter("logs/app.log", dd.DefaultFileWriterConfig())
// 로테이션 콜백 설정: 매 로테이션 후 현재 파일 경로 출력
fw.SetOnRotateCallback(func(path string) {
fmt.Println("로그가 로테이션됨, 현재 파일:", path)
})
// 파일이 MaxSizeMB 초과로 로테이션 트리거 후 콜백 호출
fw.Write([]byte("로그 내용\n"))파일 로테이션과 정리
FileWriter 의 로테이션과 정리는 두 개의 독립된 경로로 실행됩니다.
- 로테이션 (rotation):
Write호출 시 현재 파일 크기를 검사하여MaxSizeMB(기본 100MB) 초과 시 트리거 - 이전 파일은 백업으로 이름 변경, 새 파일은O_EXCL로 다시 열림 (심볼릭 링크 TOCTOU 방지), 이어서internal.RotateBackups가MaxBackups에 따라 백업 체인을 잘라내고,Compress=true일 때 독립 goroutine 이 백업을 gzip 압축. - 정리 (cleanup):
MaxAge > 0이고MaxBackups > 0일 때만 백그라운드 goroutine 시작, 매시간 스캔,internal.CleanupOldFiles호출로MaxAge(기본 30 일) 초과한 이전 백업 삭제.
팁 기본값 적용 규칙
제로값 또는 음수 MaxSizeMB는 모두 100MB 로 폴백합니다. MaxAge/MaxBackups의 조합 규칙: ① 둘 다 0 → 완전 기본값 활성화 (30 일 + 10 개, 정리 goroutine 시작); ② MaxBackups만 설정 → 수량으로만 백업 체인 잘라냄, MaxAge=0은 정리 goroutine 시작 안 함; ③ MaxAge만 설정 → MaxBackups는 기본값 10 으로 폴백.
fw, _ := dd.NewFileWriter("logs/app.log", dd.DefaultFileWriterConfig())
// 쓰기 시 MaxSizeMB 초과하면 자동으로 로테이션 트리거
fw.Write([]byte("로그 내용\n"))
// 로테이션 후 생성되는 파일:
// logs/app.log (현재 파일)
// logs/app_log_1.log (가장 최근 백업)
// logs/app_log_2.log (더 이전 백업)
// Compress 활성화 시 이전 백업은 logs/app_log_1.log.gz로 비동기 압축팁 보안 기능
FileWriter 는 경로 순회, null 바이트, 심볼릭 링크, 하드링크, overlong UTF-8 보호를 내장합니다. 새 파일은 TOCTOU 공격 방지를 위해 O_EXCL로 열립니다.
BufferedWriter
버퍼가 있는 writer 로 시스템 콜 횟수를 줄입니다.
생성
func NewBufferedWriter(w io.Writer, cfg BufferedWriterConfig) (*BufferedWriter, error)생성 시 먼저 nil 기반 writer 거부 (ErrNilWriter), 다음으로 cfg.Validate() 호출, 마지막으로 BufferSize가 기본값 (1KB) 미만인 값을 기본값으로 클램프하고 FlushTime <= 0을 100ms 로 클램프합니다. 오류를 반환하는 경우:
ErrNilWriter:w == nilErrBufferSizeTooLarge:BufferSize > 10MB(Validate가 반환)- 구성 무효:
BufferSize < 0또는FlushTime < 0(Validate가 반환)
// 기본 구성 사용
bw, _ := dd.NewBufferedWriter(os.Stdout, dd.DefaultBufferedWriterConfig())
// 커스텀 구성
cfg := dd.DefaultBufferedWriterConfig()
cfg.BufferSize = 4096
bw, _ = dd.NewBufferedWriter(os.Stdout, cfg)BufferedWriterConfig
버퍼 writer 구성.
type BufferedWriterConfig struct {
BufferSize int // 버퍼 크기 (바이트, 기본 1024 = 1KB, 상한 10MB)
FlushTime time.Duration // 정기 flush 간격 (기본 100ms)
}기본 구성
func DefaultBufferedWriterConfig() BufferedWriterConfig기본값: 1KB 버퍼, 100ms flush 간격.
Validate
func (c BufferedWriterConfig) Validate() error버퍼 writer 구성의 유효성을 검증합니다. 오류를 반환하는 경우:
BufferSize가 음수BufferSize가 10MB 초과 (ErrBufferSizeTooLarge반환)FlushTime이 음수
메서드
| 메서드 | 시그니처 | 설명 |
|---|---|---|
Write | (p []byte) (int, error) | 버퍼에 쓰기; 이미 버퍼링된 바이트 ≥ BufferSize/2일 때 자동 flush |
Flush | () error | 버퍼를 기반 Writer 에 명시적으로 flush |
Close | () error | 버퍼를 먼저 flush 하고 백그라운드 goroutine 을 중지한 후 기반 Writer 닫기 (io.Closer 구현 시) |
백그라운드 goroutine 은 FlushTime 주기로 검사: 버퍼가 비어 있지 않고 마지막 flush 로부터 FlushTime 이상 경과했을 때만 자동 flush 를 트리거합니다. Close는 여러 번 호출해도 안전 (CAS 가드); 이미 종료된 경우 Write는 os.ErrClosed를 반환합니다.
cfg := dd.DefaultBufferedWriterConfig()
cfg.BufferSize = 8192
bw, _ := dd.NewBufferedWriter(file, cfg)
bw.Write([]byte("로그 라인\n"))
_ = bw.Flush() // 기반 Writer 에 명시적 flush
defer bw.Close() // Close 가 먼저 Flush 한 후 기반 Writer 닫기MultiWriter
다중 writer 관리, 여러 대상에 동시에 쓰기.
생성
func NewMultiWriter(writers ...io.Writer) *MultiWriternil writer 는 생성 시 조용히 무시됩니다. 반환값은 절대 nil이 아닙니다 (생성 시 오류가 발생하지 않음).
mw := dd.NewMultiWriter(os.Stdout, fileWriter)메서드
| 메서드 | 시그니처 | 설명 |
|---|---|---|
Write | (p []byte) (int, error) | 모든 대상에 쓰기 (아래 오류 정책 참조) |
AddWriter | (w io.Writer) error | 동적으로 대상 추가 (중복 writer 는 조용히 수락) |
RemoveWriter | (w io.Writer) error | 동적으로 대상 제거 |
Close | () error | 모든 기반 io.Closer 닫기 (표준 스트림 제외) |
AddWriter가 오류를 반환하는 경우: ErrNilMultiWriter(리시버가 nil) / ErrNilWriter(매개변수가 nil) / ErrMaxWritersExceeded(이미 100 개 이상 등록). RemoveWriter가 오류를 반환하는 경우: ErrNilMultiWriter / ErrWriterNotFound.
mw := dd.NewMultiWriter(console, file)
// 동적 관리
_ = mw.AddWriter(anotherFile)
_ = mw.RemoveWriter(console)
// 모든 기반 writer 닫기 (os.Stdout 등 표준 스트림은 닫히지 않음)
_ = mw.Close()오류 타입
Write 실패 시 반환되는 오류는 두 개의 공개 타입으로 표현됩니다 (errors.go에 정의).
// 단일 writer 의 오류
type WriterError struct {
Index int // MultiWriter 에서 해당 writer 의 인덱스
Writer io.Writer // 오류가 발생한 writer
Err error // 기반 오류
}
// 다중 writer 오류 집계 (Write 는 *MultiWriterError 반환)
type MultiWriterError struct {
Errors []WriterError
}두 타입의 메서드:
| 타입 | 메서드 | 설명 |
|---|---|---|
*WriterError | Error() string | writer[i]: <err> 형식; Err이 nil 이면 unknown error 표시 |
*WriterError | Unwrap() error | Err 반환, errors.Is 체인 매칭에 사용 |
*MultiWriterError | Error() string | 단일 오류는 직접 반환; 여러 오류는 multiple writer errors: [...]로 결합 |
*MultiWriterError | Unwrap() []error | 모든 WriterError.Err 반환, errors.As / errors.Is에 사용 |
*MultiWriterError | HasErrors() bool | 오류 수집 여부 |
*MultiWriterError | ErrorCount() int | 오류 수 |
*MultiWriterError | FirstError() error | 첫 번째 오류 (*WriterError), 없으면 nil |
오류 정책
Write는 '최선 쓰기'를 채택: 단일 writer 실패가 다른 writer 에 영향을 주지 않습니다. 기반 writer 의 오류는 MultiWriterError로 수집되며, Unwrap() []error를 구현해 errors.As/errors.Is로 사용할 수 있습니다. 전체 실패 시 (0, *MultiWriterError) 반환; 부분 실패 시 (pLen, *MultiWriterError) 반환; 쓰기 바이트 수 부족은 short write 오류로 기록됩니다.
경고 단일 writer 최적화
기반 writer 가 하나뿐일 때 Write는 빠른 경로로 직접 전달하며, 오류는 원형 그대로 반환되고 MultiWriterError로 래핑되지 않습니다.
조합 사용
// 파일 + 버퍼 + 다중 대상
fw, _ := dd.NewFileWriter("logs/app.log", dd.DefaultFileWriterConfig())
bw, _ := dd.NewBufferedWriter(fw, dd.DefaultBufferedWriterConfig())
mw := dd.NewMultiWriter(os.Stdout, bw)
logger, _ := dd.New(dd.Config{
Level: dd.LevelInfo,
Targets: []dd.OutputTarget{dd.CustomOutput(mw)},
})
defer logger.Close() // 버퍼를 자동으로 flush 하고 파일 닫기