인터페이스 기반 로깅 시스템 설계

인터페이스 기반 로깅 시스템 설계

다양한 로깅 요구사항을 충족시키고, 시스템의 유연성과 유지보수성을 높이기 위해서는 인터페이스 기반의 로깅 시스템을 설계하는 것이 중요합니다. 이 글에서는 ILoggerILoggerFactory 인터페이스를 기반으로 한 로깅 시스템을 구현하고, 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)
        };
    }
}