[.NET] 기본로거를 위한 유틸리티 만들기
로거의 특성상 자신에게 편한 설정이 끝나면 어떤 프로젝트든 거의 손대지 않고 그대로 갔다 쓰기 마련입니다.
매번 이런 코드를 복사 & 붙여 넣기 한다는 건 너무 번거로운 일이므로 유틸리티 클래스를 만들어서 관리하면 편합니다.
이 포스팅에서는 'NReco.Logging.File'를 이용하여 파일 출력을 하고 있습니다.
1. 개요
이 유틸은 범용이라기보다는 프로젝트 전용 유틸입니다.
단지 특수한 경우가 아니라면 설정의 거의 동일할 것이라는 가정이라 이 클래스만 복사하여 프로젝트에 세팅하면 됩니다.
(복사용 원본 : github - dang-gun/DotNetSamples/LoggingNReco_DotNetLogging/DotNetLogging_Copy.cs )
기본 로거를 위한 유틸을 만들려면 로거팩토리 개체를 관리해야 합니다.
이 로거팩토리를 기반으로 설정이나 자주 쓰는 기능들을 만들어 두어 재사용성을 높여줍니다.
클래스 라이브러리(DLL)로 만들긴 했지만
프로젝트에 맞게 직접 코드를 수정하는 편이 로그로 인한 부하를 줄일 수 있다고 판단되어 복사 붙여넣기로 사용하는 것을 권장합니다.
클래스 라이브러리도 프로젝트를 복사하여 자신의 솔루션이 맞게끔 수정하여 쓰도록 하는 것이 목적이라
범용성을 포기한 구성입니다.
2. 유틸리티 구현
클래스 이름은 'DotNetLogging'를 정했지만 각자 원하는 대로 만드시면 됩니다.
(참고 : github - dang-gun/DotNetSamples/LoggingNReco_DotNetLogging/DotNetLogging.cs)
2-1. 설정 함수
설정함수는 정적(static)변수로 선언하여 필요한 시점에 아무 코드에서나 사용할 수 있도록 합니다.
이렇게 되면 프로젝트 내에서 항상 같은 설정의 로거를 사용할 수 있습니다.
/// <summary>
/// 미리 세팅된 기본 옵션으로 로거를 생성한다.
/// </summary>
/// <param name="loggingBuilder"></param>
/// <param name="sPathFormat">로그 파일을 생성할 경로 포맷</param>
/// <param name="bConsole">콘솔 표시 여부</param>
/// <returns></returns>
public static ILoggingBuilder configure(
ILoggingBuilder loggingBuilder
, string sPathFormat
, bool bConsole)
{
if (true == bConsole)
{//콘솔 사용
loggingBuilder.AddSimpleConsole(x => x.TimestampFormat = "[yyyy-MM-dd HH:mm:ss] ");
//loggingBuilder.AddSimpleConsole(x=>x.TimestampFormat)
}
//로거 표시 설정(디버거 등의 메시지 출력 설정)
loggingBuilder.AddFilter(
(provider, category, logLevel) =>
{
return true;
});
loggingBuilder.AddFile(sPathFormat
, fileLoggerOpts =>
{
fileLoggerOpts.FormatLogFileName = sNameFormat =>
{
return String.Format(sNameFormat, DateTime.Now);
};
fileLoggerOpts.FormatLogEntry = (lmMsg) =>
{
//string sLevel = string.Empty;
//switch (lmMsg.LogLevel)
//{
// case LogLevel.Information:
// sLevel = "Info";
// break;
// case LogLevel.Warning:
// sLevel = "Warn";
// break;
// default:
// sLevel = lmMsg.LogLevel.ToString();
// break;
//}
return $"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}] {lmMsg.LogLevel} [{lmMsg.LogName}] {lmMsg.Message}";
};
});
return loggingBuilder;
}
13번 줄 : 콘솔(Console)에 출력할지 여부 결정
'AddSimpleConsole'를 사용하여 콘솔 출력을 설정합니다.
이 예제에서는 "[yyyy-MM-dd HH:mm:ss]" 포맷을 설정하여 날짜와 시간을 표시합니다.
20번 줄 : 로그파일 출력을 위한 'NReco.Logging.File' 설정입니다.
파일 경로 및 출력 파일 이름 포멧을 지정합니다.
이 예제에서는 'Logs'폴더에 'Log_{0:yyyy}-{0:MM}-{0:dd}.log'형식으로 출력됩니다.
26번 줄 : 파일 출력 경로 및 이름 지정
'sPathFormat'로 전달받은 파일 경로 + 포맷을 설정합니다.
이 포맷은 29번 줄에 전달되어 30번 줄에서 사용됩니다.
30번 줄 : 출력할 파일 경로 및 이름 확정
26번 줄에서 전달받은 경로 및 포맷을 여기서 확정 지어 파일로 내보냅니다.
'NReco.Logging.File'는 이름 기준으로 파일이 생성됩니다.
날짜가 바뀌면 자동으로 파일명이 바뀌면서 새 파일이 생성되게 됩니다.
34번 줄 : 로그 내용 지정
자기 마음대로 로그에 출력될 내용을 구성할 수 있습니다.
이 예제에서는 '[날짜와 시간] 로그 레벨 [카테고리 이름] 로그 내용'으로 출력됩니다.
36번 줄 : 'NReco.Logging.File'기능 샘플
'NReco.Logging.File'기능에 대한 샘플입니다.
지워도 됩니다.
52번 줄 : 파일에 출력되는 로그 메시지 포맷
'NReco.Logging.File'에서 파일로 출력할 때 사용되는 로그 메시지 포맷입니다.
최종 확정된 출력되는 내용입니다.
2-2. 생성자
종속성이 있는 프로젝트든 없는 프로젝트든 같이 사용해야 하므로 초기화 시점이 일정하지 않습니다.
그래서 'CS8618 null을 허용하지 않는 변수는 생성자를 종료할 때 null이 아닌 값을 포함해야 합니다. nullable로 선언하는 것이 좋습니다.' 경고를 피하기 위한 생성자가 따로 필요합니다.
(참고 : MS Learn - Nullable 경고 해결 - 초기화되지 않은 nullable 참조 )
2-2-1. 기본 생성자
로거가 생성되기 전에 미리 생성이 필요할 때 사용하는 기본 생성자입니다.
임시 사용을 목적으로 하므로 프로젝트가 세팅되고 나면 다시 생성해서 사용해야 합니다.
/// <summary>
/// 기본 생성
/// <para>로거를 초기화를 하지 않으므로 임시 생성일때만 사용한다.</para>
/// </summary>
internal DotNetLogging()
{
this.LoggerFactory_My = new LoggerFactory();
}
2-2-2. 로거 생성 생성자
'configure'내용으로 로거를 새로 생성합니다.
로거가 생성되어야 로깅 기능을 사용할 수 있습니다.
/// <summary>
/// 로거는 자동 생성하고 로거를 기본 옵션으로 초기화 한다.
/// </summary>
/// <param name="sPathFormat">로그 파일을 생성할 경로 포맷</param>
/// <param name="bConsole">콘솔 표시 여부</param>
public DotNetLogging(
string? sPathFormat
, bool bConsole)
{
if(null == sPathFormat)
{
//https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0#non-host-console-app
this.LoggerFactory_My
= LoggerFactory.Create(
loggingBuilder => DotNetLogging.configure(loggingBuilder, bConsole));
}
else
{
this.LoggerFactory_My
= LoggerFactory.Create(
loggingBuilder => DotNetLogging.configure(loggingBuilder, sPathFormat, bConsole));
}
}
7번 줄 : 'sPathFormat'은 파일이 생성될 경로와 파일명 포맷입니다.
'null'을 전달하면 클래스가 가지고 있는 기본값으로 지정됩니다.
8번 줄 : 'bConsole'은 콘솔에 로그를 표시할지 여부입니다.
콘솔에 출력될 내용도 'configure'에서 설정할 수 있습니다.
2-2-3. 로거팩토리(LoggerFactory)를 전달받는 생성자
외부에서 이미 로거팩토리(LoggerFactory)가 생성됐다면 해당 개체를 전달받아 클래스를 초기화해 주는 생성자입니다.
/// <summary>
/// 전달받은 ILoggerFactory개체를 이용하여 로거를 초기화한다.
/// <para>null이면 기본 옵션으로 새로 생성한다.</para>
/// </summary>
/// <remarks>
/// 자동 생성시 NReco.Logging.File를 기준으로 작성된다.
/// </remarks>
/// <param name="loggerFactory">생성한 ILoggerFactory 개채. null이면 자동생성</param>
/// <param name="sPathFormat">로그 파일을 생성할 경로 포맷</param>
/// <param name="bConsole">콘솔 표시 여부</param>
public DotNetLogging(
ILoggerFactory? loggerFactory
, string? sPathFormat
, bool bConsole)
{
if (null != loggerFactory)
{
this.LoggerFactory_My = loggerFactory;
}
else
{
if (null == sPathFormat)
{
//https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0#non-host-console-app
this.LoggerFactory_My
= LoggerFactory.Create(
loggingBuilder => DotNetLogging.configure(loggingBuilder, bConsole));
}
else
{
this.LoggerFactory_My
= LoggerFactory.Create(
loggingBuilder => DotNetLogging.configure(loggingBuilder, sPathFormat, bConsole));
}
}
}
21번 줄 : 로그팩토리가 생성되지 않았다면 ' 2-2-2. 로거 생성 생성자'와 똑같이 동작합니다.
2-3. 로거 공통 기능 구현
이 클래스에 있는 로거의 기능을 쓰기 위한 함수들입니다.
2-3-1. 형식으로 로거 생성
지정된 형식으로 카테고리로 로거를 생성하는 함수입니다.
/// <summary>
/// 개체를 전달하여 로거 생성
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
internal ILogger CreateLogger<T>() => LoggerFactory_My.CreateLogger<T>();
6번 줄 : 제네릭(T)으로 전달받은 형식으로 로거를 생성합니다.
2-3-2. 문자열로 로거 생성
카테고리로 사용할 문자열을 전달하여 로거를 생성합니다.
/// <summary>
/// 카테고리이름을 직접 입력하여 로거 생성
/// </summary>
/// <param name="sCategoryName"></param>
/// <returns></returns>
internal ILogger CreateLogger(string sCategoryName)
=> LoggerFactory_My.CreateLogger(sCategoryName);
6번 줄 : 문자열을 전달하고 이 문자열을 카테고리 이름으로 사용하는 로거를 생성합니다.
2-3-3. 시스템에서 이름 받기
시스템에서 추출한 이름을 리턴 해주는 함수입니다.
로그의 카테고리를 자동으로 출력하기 위해 사용합니다.
로그에 표시될 카테고리를 어떻게 표시하고 싶은지에 따라 이 코드를 수정해야 합니다.
(아래 코드는 '메소드 이름'을 출력합니다.)
/// <summary>
/// 호출한 클래스를 찾아 카테고리 이름으로 출력한다.
/// </summary>
/// <returns>없으면 빈값</returns>
private string CategoryName()
{
string sReturn = string.Empty;
//= new StackTrace().GetFrame(2).GetMethod().ReflectedType.Name;
StackTrace stTemp = new StackTrace();
StackFrame? sfTemp = stTemp.GetFrame(2);
if (null != sfTemp)
{
MethodBase? mbTemp = sfTemp.GetMethod();
if (null != mbTemp)
{
Type? typeTemp = mbTemp.ReflectedType;
if (null != typeTemp)
{
//sReturn = typeTemp.Name;
sReturn = mbTemp.Name;
}
}
}
return sReturn;
}
10번 줄 : 시스템 스택을 추적하기 위한 개체를 생성합니다.
11번 줄 : 클래스 네임이 있는 프레임을 받아옵니다.(2 = 클래스)
15번 줄 : MethodBase는 메서드 이름을 받을 수 있습니다.
mbTemp.Name : 메서드 이름(예> btnDotNetLogging_Form_Info_Click )
19번 줄 : typeTemp는 클래스 이름을 받을 수 있습니다.
mbTemp.Name : 클래스 이름(예> Form1)
3. 사용
종속성 주입이 있고 없고에 따라서 사용 방법이 약간 다릅니다.
3-1. 종속성 주입이 없는 경우
종속성 주입이 없다면 개체를 지역/전역/정적 으로 선언하여 개체를 생성하고 사용하면 됩니다.
아래 예제는 정적 변수로 선언한 예입니다.
(참고 : github - dang-gun/DotNetSamples/LoggingNReco_WinForm/Global/GlobalStatic.cs)
/// <summary>
/// DotNetLogging를 이용한 로거
/// </summary>
internal static DotNetLogging Log = new DotNetLogging(true);
사용할 때는 생성한 개체를 이용하면 됩니다.
GlobalStatic.Log.LogInfo("btnDotNetLogging_Form_Info 클릭!");
3-2. 종속성 주입이 있는 경우
종속성 주입이 있다면 'DotNetLogging.configure'에 직접 접근하여 로거 생성을 컨트롤할 수 있습니다.
(참고 : dang-gun/DotNetSamples/LoggingNReco_Aspnet/Program.cs )
//로깅 주입 및 로그 파일 설정
builder.Services.AddLogging(loggingBuilder
=> DotNetLogging.configure(loggingBuilder, true));
사용할 때는 종속성을 전달받아서 사용하면 됩니다.
(참고 : dang-gun/DotNetSamples/LoggingNReco_Aspnet/Controllers/TestController.cs)
/// <summary>
/// 사용할 로거
/// </summary>
private ILogger _logger;
public TestController(ILogger<TestController> logger)
{
this._logger = logger;
}
//로그 표시 예제
this._logger.LogInformation("SuccessCall에서 호출~");
전역 로거를 사용하고 싶다면
로거를 주입한 이후
'Services.GetRequiredService<ILoggerFactory>()'를 통해 로거팩토리를 전달받을 수 있습니다.
이것을 이용하여 'DotNetLogging'을 전역(static)변수에 다시 생성합니다.
if (false == bLogger)
{//DotNetLogging를 이용하는 경우
//생성한 로거팩토리를 전달하여 DotNetLogging를 다시 생성한다.
GlobalStatic.Log
= new DotNetLogging(
app.Services.GetRequiredService<ILoggerFactory>()
, true);
}
//호출 예제
GlobalStatic.Log.LogInfo($"LogGlobalStatic_Info에서 호출!!");
이렇게 하면 주입된 종속성을 전달받을 필요 없이 프로젝트의 어디에서든 로그를 출력할 수 있습니다.
4. 여러 파일 생성
로그 발생 위치에 따라 여러 파일이나 여러 경로에 파일을 저장하고 싶다면
1) DotNetLogging를 여러 개 생성한 다음
2) 'configure'를 호출하거나 생성할 때
3) 'sPathFormat'을 다르게 전달하면 됩니다.
/// <summary>
/// 다른 파일 로거
/// </summary>
internal static DotNetLogging Log_AnotherFile
= new DotNetLogging(
Path.Combine("Log", "LogAnotherFile_{0:yyyy}-{0:MM}-{0:dd}.log")
, true);
마무리
유틸리티 : github - dang-gun/DotNetSamples/LoggingNReco_DotNetLogging
ASP.NET Core 예제 : github - dang-gun/DotNetSamples/LoggingNReco_Aspnet
WinForm 예제 : github - dang-gun/DotNetSamples/LoggingNReco_WinForm
WPF 예제 : github - dang-gun/DotNetSamples/LoggingNReco_WPF
쉽다면 쉽고 어렵다면 어려운 로그 출력 공통화를 해보았습니다.
사실 종속성 주입(Inversion of Control (IoC)) 개념 때문에 복잡해지는 거지 전역변수 혹은 싱글톤 개념으로 보면 별것 아닙니다.
원래 프레임워크가 고도화될수록 특정 상황에서는 좋은데 범용적으로 좋은가? 싶은 게 많아지기 마련입니다.
프로그래머는 이런 기술들을 잘 파악하고 있다가 적재적소에 써야 하는 것이죠.