콘텐츠로 이동

C# Source Generators 실전 가이드

Source Generator는 C# 컴파일러가 소스 코드를 분석하는 동안 추가 C# 소스를 생성하는 컴포넌트입니다. Reflection이나 IL weaving 없이 컴파일 타임에 코드를 생성하므로 런타임 오버헤드가 없고 AOT(Ahead-of-Time) 컴파일과도 호환됩니다.


1. Source Generator가 해결하는 문제

섹션 제목: “1. Source Generator가 해결하는 문제”
// 기존 방식: Reflection 기반 직렬화 (런타임 비용)
var json = JsonSerializer.Serialize(myObject); // 런타임에 타입 분석
// Source Generator 방식: 컴파일 타임에 직렬화 코드 생성
[JsonSerializable(typeof(MyObject))]
public partial class MyContext : JsonSerializerContext { }
var json = JsonSerializer.Serialize(myObject, MyContext.Default.MyObject);
// 생성된 코드는 Reflection 없이 직접 프로퍼티 접근

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

.NET 6+에서는 증분 생성기가 성능 면에서 권장됩니다.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Text;
[Generator]
public class AutoToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// [AutoToString] 어트리뷰트가 붙은 클래스만 수집
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => IsClassWithAttribute(s),
transform: static (ctx, _) => GetTargetClass(ctx)
)
.Where(static m => m is not null);
// 컴파일과 결합
var compilationAndClasses = context.CompilationProvider
.Combine(classDeclarations.Collect());
context.RegisterSourceOutput(compilationAndClasses,
static (spc, source) => Execute(source.Left, source.Right, spc));
}
static bool IsClassWithAttribute(SyntaxNode node)
=> node is ClassDeclarationSyntax c && c.AttributeLists.Count > 0;
static ClassDeclarationSyntax? GetTargetClass(GeneratorSyntaxContext ctx)
{
var classDeclaration = (ClassDeclarationSyntax)ctx.Node;
foreach (var attributeList in classDeclaration.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
var symbol = ctx.SemanticModel.GetSymbolInfo(attribute).Symbol;
if (symbol?.ContainingType?.ToDisplayString() == "AutoToStringAttribute")
return classDeclaration;
}
}
return null;
}
static void Execute(
Compilation compilation,
ImmutableArray<ClassDeclarationSyntax?> classes,
SourceProductionContext context)
{
foreach (var classDecl in classes)
{
if (classDecl is null) continue;
var model = compilation.GetSemanticModel(classDecl.SyntaxTree);
var symbol = model.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
if (symbol is null) continue;
var source = GenerateToString(symbol);
context.AddSource($"{symbol.Name}_AutoToString.g.cs", source);
}
}
static string GenerateToString(INamedTypeSymbol symbol)
{
var sb = new StringBuilder();
var ns = symbol.ContainingNamespace.ToDisplayString();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine($"namespace {ns};");
sb.AppendLine();
sb.AppendLine($"partial class {symbol.Name}");
sb.AppendLine("{");
sb.Append(" public override string ToString() => ");
sb.Append($"\"{symbol.Name} {{ ");
var props = symbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public);
bool first = true;
foreach (var prop in props)
{
if (!first) sb.Append(", ");
sb.Append($"{prop.Name} = {{{prop.Name}}}");
first = false;
}
sb.AppendLine(" }}\";");
sb.AppendLine("}");
return sb.ToString();
}
}

4. 어트리뷰트 생성 (마커 어트리뷰트)

섹션 제목: “4. 어트리뷰트 생성 (마커 어트리뷰트)”
[Generator]
public class AutoToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 어트리뷰트 소스를 먼저 등록
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("AutoToStringAttribute.g.cs", @"
// <auto-generated/>
[System.AttributeUsage(System.AttributeTargets.Class)]
public sealed class AutoToStringAttribute : System.Attribute { }
");
});
// 나머지 파이프라인...
}
}

// 사용자 코드
[AutoToString]
public partial class Player
{
public string Name { get; set; } = "";
public int Level { get; set; }
public float Health { get; set; }
}
// 생성된 코드 (빌드 출력에서 확인 가능)
// Player_AutoToString.g.cs
partial class Player
{
public override string ToString() =>
$"Player {{ Name = {Name}, Level = {Level}, Health = {Health} }}";
}
// 사용
var player = new Player { Name = "Alice", Level = 10, Health = 100f };
Console.WriteLine(player); // Player { Name = Alice, Level = 10, Health = 100 }

static void Execute(/* ... */, SourceProductionContext context)
{
var descriptor = new DiagnosticDescriptor(
id: "ATG001",
title: "AutoToString requires partial class",
messageFormat: "Class '{0}' must be declared as partial",
category: "AutoToStringGenerator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);
if (!classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
context.ReportDiagnostic(
Diagnostic.Create(descriptor, classDecl.GetLocation(), symbol.Name)
);
return;
}
}

<!-- .csproj에 추가하면 obj 폴더에 생성된 파일 출력 -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

  • System.Text.Json 소스 생성 직렬화 (JsonSerializerContext)
  • Microsoft.Extensions.Logging 로거 생성 (LoggerMessage.Define 자동화)
  • AutoMapper 대체 (컴파일 타임 매핑 코드 생성)
  • DI 컨테이너 등록 코드 자동 생성
  • Enum ToString 성능 최적화 (switch 표현식으로 변환)
  • INotifyPropertyChanged 보일러플레이트 자동 생성

Source Generator는 Reflection의 성능 비용과 IL weaving의 복잡성을 피하면서 코드 중복을 제거하는 강력한 도구입니다. IIncrementalGenerator를 사용하여 증분 빌드 성능을 최적화하고, 진단 API를 통해 사용자에게 명확한 오류 메시지를 제공하는 것이 좋은 Generator의 핵심입니다.