팀의 코드 품질과 일관성을 IDE와 CI에서 자동으로 보장하려면 Roslyn Analyzer가 효과적입니다. 이 글은 간단한 규칙부터 배포까지, 최소한의 단계로 Analyzer와 Code Fix를 만드는 실전 가이드를 제공합니다.
1. Roslyn Analyzer란?
Roslyn Analyzer는 C# 컴파일러 파이프라인에 연결되어 소스 코드를 검사하고 경고/오류를 보고합니다. 개발자는 Visual Studio/VS Code에서 실시간 진단과 자동 수정(Code Fix)을 받을 수 있고, CI에서는 빌드 실패로 정책을 강제할 수 있습니다.
2. 프로젝트 만들기
CLI: dotnet new install Microsoft.CodeAnalysis.ProjectTemplates 설치 후, dotnet new analyzer -n Demo.Analyzers로 생성합니다. Visual Studio: "Analyzer with code fix (.NET Standard)" 템플릿을 선택합니다. 템플릿은 Analyzer, CodeFix, 유닛 테스트 프로젝트를 제공합니다.
3. 규칙 설계 예시
예제로 DateTime.Now 사용을 경고하고 DateTime.UtcNow로 자동 치환하는 규칙을 만듭니다. 로그/캐싱/비교에서 로컬 시간은 버그를 유발할 수 있어 UTC 사용을 권장합니다.
4. Analyzer 구현
SimpleMemberAccessExpression(DateTime.Now)만을 정확히 검사하고, 생성 코드 제외 및 동시 실행을 활성화합니다.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Demo.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AvoidDateTimeNowAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "DT0001";
private static readonly DiagnosticDescriptor Rule = new(
id: DiagnosticId,
title: "Use DateTime.UtcNow instead of DateTime.Now",
messageFormat: "Use DateTime.UtcNow instead of DateTime.Now for timezone-agnostic time",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "DateTime.Now는 로컬 시간으로 동작합니다. 로깅/캐싱/비교에는 UtcNow를 권장합니다.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
}
private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
{
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
if (memberAccess.Expression is not IdentifierNameSyntax left || memberAccess.Name is not IdentifierNameSyntax right)
return;
if (left.Identifier.Text == "DateTime" && right.Identifier.Text == "Now")
{
var diagnostic = Diagnostic.Create(Rule, memberAccess.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
5. Code Fix 구현
진단 지점(DateTime.Now)을 DateTime.UtcNow로 치환하는 자동 수정입니다. Trivia(공백/주석)를 보존합니다.
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
namespace Demo.Analyzers;
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AvoidDateTimeNowCodeFix)), Shared]
public sealed class AvoidDateTimeNowCodeFix : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AvoidDateTimeNowAnalyzer.DiagnosticId);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics[0];
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null) return;
if (root.FindNode(diagnostic.Location.SourceSpan) is not MemberAccessExpressionSyntax memberAccess)
return;
context.RegisterCodeFix(
CodeAction.Create(
title: "Use DateTime.UtcNow",
createChangedDocument: c => ReplaceNowWithUtcNow(context.Document, memberAccess, c),
equivalenceKey: "UseUtcNow"),
diagnostic);
}
private static async Task<Document> ReplaceNowWithUtcNow(Document document, MemberAccessExpressionSyntax memberAccess, CancellationToken ct)
{
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
var replacement = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("DateTime"),
SyntaxFactory.IdentifierName("UtcNow"))
.WithTriviaFrom(memberAccess);
editor.ReplaceNode(memberAccess, replacement);
return editor.GetChangedDocument();
}
}
6. 단위 테스트
Microsoft.CodeAnalysis.Testing 기반 테스트로 진단과 수정이 기대대로 동작하는지 검증합니다.
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Testing.XUnit;
using Xunit;
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.CodeFixVerifier<
Demo.Analyzers.AvoidDateTimeNowAnalyzer,
Demo.Analyzers.AvoidDateTimeNowCodeFix>;
public class AvoidDateTimeNowTests
{
[Fact]
public async Task Reports_and_fixes_DateTimeNow()
{
var test = @"\nusing System;\n\nclass C\n{\n void M()\n {\n var t = [|DateTime.Now|];\n }\n}\n";
var fixedCode = @"\nusing System;\n\nclass C\n{\n void M()\n {\n var t = DateTime.UtcNow;\n }\n}\n";
await VerifyCS.VerifyCodeFixAsync(test, fixedCode);
}
}
7. 빌드, 패키징, 배포
템플릿 기본 설정으로 빌드는 dotnet build, 패키징은 dotnet pack으로 가능합니다. 사설/공용 NuGet 피드에 dotnet nuget push로 게시합니다. 개발 중에는 솔루션의 소비 프로젝트에서 Analyzer 프로젝트를 직접 참조해 빠르게 피드백을 받을 수 있습니다.
8. 팀 적용과 규칙 강도 설정
.editorconfig에서 심각도를 관리합니다. 예) dotnet_diagnostic.DT0001.severity = warning 또는 error로 설정합니다. 폴더/파일 스코프별로 다르게 설정할 수 있으며, 생성 파일이나 테스트 폴더는 none으로 비활성화 가능합니다. CI에서 경고를 오류로 취급하려면 빌드 파이프라인에서 경고 수준을 조정하거나 위 규칙을 error로 올리면 됩니다.
9. 성능과 품질 팁
불필요한 구문 종류를 등록하지 말고 정확한 SyntaxKind만 등록합니다. context.EnableConcurrentExecution과 ConfigureGeneratedCodeAnalysis를 적절히 설정합니다. 문자열 비교/할당을 최소화하고 DiagnosticDescriptor를 static으로 유지합니다. 구문 기반 분석이 모호할 때는 심볼/의미 분석을 사용해 오탐을 줄입니다.
10. 마무리
위 예시는 하나의 규칙과 자동 수정만 다뤘지만, 같은 패턴으로 팀 규칙을 체계화할 수 있습니다. 작은 규칙부터 시작해 테스트를 곁들여 배포하면, IDE와 CI에서 즉시 일관성과 안정성을 얻을 수 있습니다.
'C#' 카테고리의 다른 글
| C# ViewModel과 모델 변환 로직 설계 (0) | 2026.06.23 |
|---|---|
| C# 동적 프로퍼티 생성 및 바인딩 (0) | 2026.06.23 |
| C# 이벤트 기반 비동기 패턴(EAP) 사용법 (0) | 2026.06.22 |
| C# Thread.Join과 Thread.Sleep 차이와 활용 (0) | 2026.06.19 |
| C# 대리자 체인 관리와 예외 처리 (0) | 2026.06.19 |