Serilog 기록 방식별 성능 분석

Serilog 기록 방식별 성능 분석

애플리케이션에서 로깅은 필수적이지만, 적절한 설정이 없으면 성능 저하를 유발할 수 있습니다. Serilog는 파일 기록, 비동기 처리, 인터페이스를 활용한 다형성 지원 등 다양한 로깅 방식을 제공합니다. 이 글에서는 각 기록 방식의 성능 차이를 실험하고, 최적의 선택 기준을 제시합니다.

테스트 결과

로깅 방식평균 시간(ms)
직접 호출2216.6
비동기 직접 호출2342.2
인터페이스 기반 호출2605.8

테스트 방법

  • 동일한 조건 하에 500,000개의 로그 메시지를 기록
  • 5회 반복하여 평균 시간을 산출
  • 성능 측정을 위해 .NET의 Stopwatch 클래스 사용

테스트 코드

직접 호출

동기 호출 방식은 가장 기본적인 방식으로, 로그 기록이 완료될 때까지 애플리케이션이 대기합니다. 이 방식은 로그 양이 많지 않거나, 실시간 기록이 중요한 경우 적합합니다.

var path = $@"/logs/serilog.log";
var elapsedList = new List<long>();
var seriLog = new Serilog.LoggerConfiguration()
    .WriteTo.File(path)
    .CreateLogger();
    
for (int c = 0; c < 5; c++)
{
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < 500_000; i++)
        seriLog.Information($"SeriLog Performance Test. No : {c}.{i}. elapsed : {sw.ElapsedMilliseconds}");
    Serilog.Log.CloseAndFlush();
    elapsedList.Add(sw.ElapsedMilliseconds);
}
Console.WriteLine($"Elapsed Time : {elapsedList.Average()}");

비동기 직접 호출

비동기 호출 방식은 로그 기록이 애플리케이션의 주 흐름을 차단하지 않도록 별도의 스레드에서 처리됩니다. 성능이 약간 떨어지지만, 대규모 로그 기록 시 애플리케이션의 응답성을 유지하는 데 유리합니다.

var path = $@"/logs/serilog.log";
var elapsedList = new List<long>();
var seriLog = new Serilog.LoggerConfiguration()
    .WriteTo.Async(a => a.File(path), blockWhenFull: true, bufferSize: 10000)
    .CreateLogger();
for (int c = 0; c < 5; c++)
{
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < 500_000; i++)
        seriLog.Information($"SeriLog Performance Test. No : {c}.{i}. elapsed : {sw.ElapsedMilliseconds}");
    Serilog.Log.CloseAndFlush();
    elapsedList.Add(sw.ElapsedMilliseconds);
}
Console.WriteLine($"Elapsed Time : {elapsedList.Average()}");

인터페이스 기반 호출

인터페이스 호출 방식은 다형성을 활용하여 로깅 구현을 추상화합니다. 구조적으로 유연하지만, 호출 경로가 길어 성능이 다소 떨어질 수 있습니다.

var path = $@"/logs/serilog.log";
var elapsedList = new List<long>();
var seriLog = LoggingFactory.CreateLogger("File");
seriLog.Init(path);
for (int c = 0; c < 5; c++)
{
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < 500_000; i++)
        seriLog.Information($"SeriLog Performance Test. No : {c}.{i}. elapsed : {sw.ElapsedMilliseconds}");
    Serilog.Log.CloseAndFlush();
    elapsedList.Add(sw.ElapsedMilliseconds);
}
Console.WriteLine($"Elapsed Time : {elapsedList.Average()}");
public interface ILogging
{
    void Init(string path);
    void Information(string message);
}
public class LoggingFactory
{
    public static ILogging CreateLogger(string logType)
    {
        return logType switch
        {
            "File" => new FileLogger(),
            _ => throw new ArgumentException($"Invalid log type: {logType}")
        };
    }
}
public class FileLogger : ILogging
{
    private Logger _logger;
    public void Init(string path)
    {
        _logger = new Serilog.LoggerConfiguration()
            .WriteTo.Async(a => a.File(path), blockWhenFull: true, bufferSize: 10000)
            .CreateLogger();
    }
    public void Information(string message) => _logger?.Information(message);
}

맺음말

Serilog의 동기 호출 방식은 가장 높은 성능을 제공하지만, 대규모 로그 처리에서는 비동기 호출 방식이 더 안정적입니다. 비동기 방식은 성능을 약간 희생하는 대신, 애플리케이션의 주 흐름이 차단되지 않도록 도와줍니다. 인터페이스 호출 방식은 약간의 성능 저하가 있지만, 구조적 유연성과 유지보수성 면에서 매우 강력한 이점을 제공합니다. 결과적으로, 애플리케이션의 요구사항에 따라 로깅 방식을 선택하는 것이 중요합니다. 성능이 매우 중요한 실시간 시스템에서는 동기 또는 비동기 호출이 적합할 수 있지만, 대부분의 경우 성능 차이가 크지 않으므로 유지보수성과 확장성을 고려해 인터페이스 방식을 사용하는 것이 바람직합니다. 인터페이스 방식은 다양한 로깅 전략을 유연하게 적용할 수 있어 장기적인 관점에서 더 좋은 선택이 될 수 있습니다.