인터페이스 기반 로깅 시스템 설계
다양한 로깅 요구사항을 충족시키고, 시스템의 유연성과 유지보수성을 높이기 위해서는 인터페이스 기반의 로깅 시스템을 설계하는 것이 중요합니다. 이 글에서는 ILogger
와 ILoggerFactory
인터페이스를 기반으로 한 로깅 시스템을 구현하고, Zerolog과 Serilog를 활용한 로깅 전략을 소개합니다.
핵심 인터페이스 정의
먼저, 로깅을 위한 핵심 인터페이스인 ILogger
와 로거를 생성하는 ILoggerFactory
인터페이스를 정의합니다.
public interface ILogger
{
void Trace(string message);
void Debug(string message);
void Info(string message);
void Error(string message);
}
public interface ILoggerFactory
{
ILogger CreateLogger(string path, LogLevel level);
}
이 인터페이스들은 다양한 로깅 프레임워크에서 공통적으로 제공해야 하는 기능을 정의하며, 각 프레임워크에 대한 구체적인 구현은 별도의 클래스에서 처리합니다.
로깅 유형과 레벨 설정
로깅 시스템에서 사용될 로깅 유형과 로깅 레벨을 열거형으로 정의하여, 시스템 전반에서 일관된 방식으로 로깅을 설정할 수 있도록 합니다.
public enum LogType
{
Zero,
File,
Sql,
}
public enum LogLevel
{
Trace,
Debug,
Info,
Warn,
Error,
}
로깅 시스템 초기화
다음은 Log
클래스에 싱글톤 패턴과 빌더 패턴을 결합하여 구현한 내용입니다. 이 클래스는 로깅 시스템을 초기화하고, 다양한 로깅 프레임워크와 설정을 유연하게 관리할 수 있도록 설계되었습니다.
public class Log
{
private static readonly Lazy<Log> instance = new Lazy<Log>(() => new Log());
public static Log Instance => instance.Value;
private ILogger? _logger;
private ILoggerFactory? _loggerFactory;
LogType _type = LogType.File;
LogLevel _level = LogLevel.Debug;
string _path = $@"C:\Logs\cLib.log";
public LogType Type
{
get => _type;
set
{
if (_type != value)
{
_type = value;
Build().Initialize();
}
}
}
public LogLevel Level
{
get => _level;
set
{
if (_level != value)
{
_level = value;
Initialize();
}
}
}
public string Path
{
get => _path;
set
{
if (_path != value)
{
_path = value;
Initialize();
}
}
}
public Log SetType(LogType type)
{
Type = type;
return this;
}
public Log SetLevel(LogLevel level)
{
Level = level;
return this;
}
public Log SetPath(string path)
{
Path = path;
return this;
}
public Log Build()
{
_loggerFactory = _type switch
{
LogType.Zero => new ZeroLoggerFactory(),
LogType.File => new SerilogLoggerFactory(),
_ => throw new ArgumentException("Invalid LogType")
};
return this;
}
public void Initialize()
{
if (_loggerFactory == null)
throw new ArgumentException("LoggerFactory is null");
else
_logger = _loggerFactory?.CreateLogger(_path, _level);
}
public static void Trace(string msg) => Instance._logger?.Trace(msg);
public static void Debug(string msg) => Instance._logger?.Debug(msg);
public static void Info(string msg) => Instance._logger?.Info(msg);
public static void Error(string msg) => Instance._logger?.Error(msg);
}
싱글톤 패턴
Log
클래스는 애플리케이션 전역에 걸쳐 하나의 인스턴스만을 제공합니다. 이는 Lazy<Log>
를 통해 구현되며, Instance
프로퍼티를 통해 접근할 수 있습니다. 싱글톤 패턴을 사용함으로써 모든 로그 호출이 동일한 로깅 설정을 공유하게 됩니다. 이는 여러 클래스에서 로깅을 사용할 때 서로 다른 설정으로 인해 발생할 수 있는 혼란을 방지해줍니다.
빌더 패턴
SetType
, SetLevel
, SetPath
메서드들은 빌더 패턴을 통해 메서드 체이닝을 지원합니다. 이를 통해 로깅 시스템을 설정할 때 코드가 더 간결해지고 가독성이 향상됩니다. Build
메서드는 설정된 값을 기반으로 로깅 시스템을 구성하는 역할을 하며, Initialize
메서드는 이 설정을 기반으로 실제 로거를 초기화합니다.
다양한 로거 구현
각 로거 구현체를 별도의 클래스로 정의합니다. ILoggerFactory
인터페이스를 통해 로거를 생성하며, 이를 Log
클래스에서 사용할 수 있도록 설정합니다.
Zerolog Logger 구현
public class ZeroLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger(string path, LogLevel level) => new ZeroFileLogger(path, level);
}
public class ZeroFileLogger : ILogger
{
private ZeroLog.Log? _logger;
public ZeroFileLogger(string path, LogLevel level)
{
Initialize(path, level);
}
public void Initialize(string path, LogLevel level)
{
LogManager.Shutdown();
LogManager.Initialize(new ZeroLogConfiguration
{
LogMessagePoolSize = 10000000,
RootLogger =
{
Level = (ZeroLog.LogLevel)level,
LogMessagePoolExhaustionStrategy = LogMessagePoolExhaustionStrategy.WaitUntilAvailable,
Appenders =
{
new DateAndSizeRollingFileAppender(path)
}
}
});
_logger = LogManager.GetLogger("ZeroLogger");
}
public void Trace(string message) => _logger?.Trace(message);
public void Debug(string message) => _logger?.Debug(message);
public void Info(string message) => _logger?.Info(message);
public void Error(string message) => _logger?.Error(message);
}
Serilog Logger 구현
public class SerilogLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger(string path, LogLevel level) => new SeriFileLogger(path, level);
}
public class SeriFileLogger : ILogger
{
private Serilog.ILogger? _logger;
LoggingLevelSwitch levelSwitch;
public SeriFileLogger(string path, LogLevel level)
{
levelSwitch = new LoggingLevelSwitch(ParseLogLevel(level));
Initialize(path, level);
}
public void Initialize(string path, LogLevel level)
{
_logger = new Serilog.LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitch)
.WriteTo.Async(a => a.File(path), blockWhenFull: true, bufferSize: 10000)
.CreateLogger();
levelSwitch.MinimumLevel = ParseLogLevel(level);
}
public void Trace(string message) => _logger?.Verbose(message);
public void Debug(string message) => _logger?.Debug(message);
public void Info(string message) => _logger?.Information(message);
public void Error(string message) => _logger?.Error(message);
LogEventLevel ParseLogLevel(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => LogEventLevel.Verbose,
LogLevel.Debug => LogEventLevel.Debug,
LogLevel.Info => LogEventLevel.Information,
LogLevel.Warn => LogEventLevel.Warning,
LogLevel.Error => LogEventLevel.Error,
_ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null)
};
}
}
사용 예시
Log.Instance
.SetLevel(LogLevel.Info)
.SetType(LogType.Zero)
.SetPath(path1)
.Build()
.Initialize();
Log.Info("Test log count");
결론
이 글에서는 인터페이스 기반의 로깅 시스템을 설계하고, Zerolog과 Serilog를 통합하여 다양한 로깅 요구사항을 충족할 수 있는 유연한 구조를 구현했습니다. 이러한 설계는 성능 최적화와 다형성 유지라는 두 가지 목표를 동시에 달성할 수 있는 강력한 로깅 전략을 제공합니다.
전체 코드
public enum LogType
{
Zero,
File,
Sql,
}
public enum LogLevel
{
Trace,
Debug,
Info,
Warn,
Error,
}
public interface ILogger
{
void Trace(string message);
void Debug(string message);
void Info(string message);
void Error(string message);
}
public interface ILoggerFactory
{
ILogger CreateLogger(string path, LogLevel level);
}
public class Log
{
private static readonly Lazy<Log> instance = new Lazy<Log>(() => new Log());
public static Log Instance => instance.Value;
private ILogger? _logger;
private ILoggerFactory? _loggerFactory;
LogType _type = LogType.File;
LogLevel _level = LogLevel.Debug;
string _path = $@"C:\Logs\cLib.log";
public LogType Type
{
get => _type;
set
{
if (_type != value)
{
_type = value;
Build().Initialize();
}
}
}
public LogLevel Level
{
get => _level;
set
{
if (_level != value)
{
_level = value;
Initialize();
}
}
}
public string Path
{
get => _path;
set
{
if (_path != value)
{
_path = value;
Initialize();
}
}
}
public Log SetType(LogType type)
{
Type = type;
return this;
}
public Log SetLevel(LogLevel level)
{
Level = level;
return this;
}
public Log SetPath(string path)
{
Path = path;
return this;
}
public Log Build()
{
_loggerFactory = _type switch
{
LogType.Zero => new ZeroLoggerFactory(),
LogType.File => new SerilogLoggerFactory(),
_ => throw new ArgumentException("Invalid LogType")
};
return this;
}
public void Initialize()
{
if (_loggerFactory == null)
throw new ArgumentException("LoggerFactory is null");
else
_logger = _loggerFactory?.CreateLogger(_path, _level);
}
public static void Trace(string msg) => Instance._logger?.Trace(msg);
public static void Debug(string msg) => Instance._logger?.Debug(msg);
public static void Info(string msg) => Instance._logger?.Info(msg);
public static void Error(string msg) => Instance._logger?.Error(msg);
}
public class ZeroLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger(string path, LogLevel level) => new ZeroFileLogger(path, level);
}
public class SerilogLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger(string path, LogLevel level) => new SeriFileLogger(path, level);
}
public class ZeroFileLogger : ILogger
{
private ZeroLog.Log? _logger;
public ZeroFileLogger(string path, LogLevel level)
{
Initialize(path, level);
}
public void Initialize(string path, LogLevel level)
{
LogManager.Shutdown();
LogManager.Initialize(new ZeroLogConfiguration
{
LogMessagePoolSize = 10000000,
RootLogger =
{
Level = (ZeroLog.LogLevel)level,
LogMessagePoolExhaustionStrategy = LogMessagePoolExhaustionStrategy.WaitUntilAvailable,
Appenders =
{
new DateAndSizeRollingFileAppender(path)
}
}
});
_logger = LogManager.GetLogger("ZeroLogger");
}
public void Trace(string message) => _logger?.Trace(message);
public void Debug(string message) => _logger?.Debug(message);
public void Info(string message) => _logger?.Info(message);
public void Error(string message) => _logger?.Error(message);
}
public class SeriFileLogger : ILogger
{
private Serilog.ILogger? _logger;
LoggingLevelSwitch levelSwitch;
public SeriFileLogger(string path, LogLevel level)
{
levelSwitch = new LoggingLevelSwitch(ParseLogLevel(level));
Initialize(path, level);
}
public void Initialize(string path, LogLevel level)
{
_logger = new Serilog.LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitch)
.WriteTo.Async(a => a.File(path), blockWhenFull: true, bufferSize: 10000)
.CreateLogger();
levelSwitch.MinimumLevel = ParseLogLevel(level);
}
public void Trace(string message) => _logger?.Verbose(message);
public void Debug(string message) => _logger?.Debug(message);
public void Info(string message) => _logger?.Information(message);
public void Error(string message) => _logger?.Error(message);
LogEventLevel ParseLogLevel(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => LogEventLevel.Verbose,
LogLevel.Debug => LogEventLevel.Debug,
LogLevel.Info => LogEventLevel.Information,
LogLevel.Warn => LogEventLevel.Warning,
LogLevel.Error => LogEventLevel.Error,
_ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null)
};
}
}