C# Incremental Source Generator 완전 가이드
Incremental Source Generator(ISG)는 C# 컴파일러가 소스를 분석하는 동안 추가 코드를 생성하는 Roslyn 기반 기능입니다. 기존 Source Generator보다 증분 캐싱이 뛰어나 빌드 성능이 훨씬 좋으며, .NET 6+ 프로젝트에서 DI 등록, 직렬화, 로깅, Mapper 자동화에 사용됩니다.
1. 프로젝트 설정
섹션 제목: “1. 프로젝트 설정”<!-- Generator 프로젝트 .csproj --><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" /> </ItemGroup></Project>
<!-- 소비 프로젝트 .csproj --><ItemGroup> <ProjectReference Include="..\MyGenerator\MyGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /></ItemGroup>2. IIncrementalGenerator 기본 구조
섹션 제목: “2. IIncrementalGenerator 기본 구조”using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp.Syntax;
[Generator]public sealed class HelloWorldGenerator : IIncrementalGenerator{ public void Initialize(IncrementalGeneratorInitializationContext context) { // 1단계: 관심 노드 필터링 (SyntaxProvider) var classDeclarations = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (node, _) => node is ClassDeclarationSyntax cls && cls.AttributeLists.Count > 0, transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node) .Where(static c => c is not null);
// 2단계: Compilation과 결합 var combined = context.CompilationProvider .Combine(classDeclarations.Collect());
// 3단계: 소스 출력 context.RegisterSourceOutput(combined, static (spc, source) => Execute(spc, source.Left, source.Right)); }
static void Execute( SourceProductionContext ctx, Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes) { foreach (var cls in classes) { var model = compilation.GetSemanticModel(cls.SyntaxTree); var symbol = model.GetDeclaredSymbol(cls); if (symbol is null) continue;
string source = GenerateClass(symbol); ctx.AddSource($"{symbol.Name}.g.cs", source); } }
static string GenerateClass(INamedTypeSymbol symbol) => $$""" namespace {{symbol.ContainingNamespace}};
partial class {{symbol.Name}} { public static string GeneratedName => "{{symbol.Name}}"; } """;}3. 어트리뷰트 기반 코드 생성
섹션 제목: “3. 어트리뷰트 기반 코드 생성”[Generator]public sealed class AutoNotifyGenerator : IIncrementalGenerator{ private const string AttributeSource = """ [System.AttributeUsage(System.AttributeTargets.Field)] internal sealed class AutoNotifyAttribute : System.Attribute { } """;
public void Initialize(IncrementalGeneratorInitializationContext context) { // 마커 어트리뷰트 주입 context.RegisterPostInitializationOutput(ctx => ctx.AddSource("AutoNotifyAttribute.g.cs", AttributeSource));
// [AutoNotify] 필드를 가진 클래스 찾기 var fields = context.SyntaxProvider .ForAttributeWithMetadataName( "AutoNotifyAttribute", predicate: static (node, _) => node is FieldDeclarationSyntax, transform: static (ctx, _) => GetFieldInfo(ctx)) .Where(static f => f is not null) .Select(static (f, _) => f!);
context.RegisterSourceOutput(fields, static (spc, field) => spc.AddSource( $"{field.ClassName}.notify.g.cs", GenerateNotify(field))); }
// ... GetFieldInfo, GenerateNotify 구현}4. 증분 캐싱 — EquatableArray
섹션 제목: “4. 증분 캐싱 — EquatableArray”// ISG의 핵심: 동일한 입력이면 재실행하지 않음// ImmutableArray는 기본 동등성 비교가 없으므로 래핑 필요
readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>> where T : IEquatable<T>{ private readonly ImmutableArray<T> _array; public EquatableArray(ImmutableArray<T> array) => _array = array;
public bool Equals(EquatableArray<T> other) => _array.SequenceEqual(other._array);
public override bool Equals(object? obj) => obj is EquatableArray<T> other && Equals(other);
public override int GetHashCode() => _array.Aggregate(0, (h, v) => HashCode.Combine(h, v));}
// 파이프라인에서 사용var data = context.SyntaxProvider .CreateSyntaxProvider(...) .Collect() .Select(static (arr, _) => new EquatableArray<MyData>(arr));// 동일한 입력 → 다운스트림 재실행 없음5. 진단 리포트
섹션 제목: “5. 진단 리포트”static readonly DiagnosticDescriptor MustBePartial = new( id: "GEN001", title: "클래스가 partial이어야 합니다", messageFormat: "'{0}'에 partial 키워드를 추가하세요", category: "Generator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true);
static void Execute(SourceProductionContext ctx, INamedTypeSymbol symbol){ bool isPartial = symbol.DeclaringSyntaxReferences .Any(r => r.GetSyntax() is ClassDeclarationSyntax cls && cls.Modifiers.Any(SyntaxKind.PartialKeyword));
if (!isPartial) { ctx.ReportDiagnostic(Diagnostic.Create( MustBePartial, symbol.Locations[0], symbol.Name)); return; } // 생성 계속...}6. 실전 패턴 — Mapper 자동 생성
섹션 제목: “6. 실전 패턴 — Mapper 자동 생성”// 대상 코드[AutoMapper(typeof(UserEntity))]public partial class UserDto{ public int Id { get; set; } public string Name { get; set; } = "";}
// 생성된 코드 (컴파일 타임)public partial class UserDto{ public static UserDto From(UserEntity entity) => new() { Id = entity.Id, Name = entity.Name, };
public UserEntity ToEntity() => new() { Id = Id, Name = Name, };}Incremental Source Generator는 SyntaxProvider.ForAttributeWithMetadataName으로 어트리뷰트 대상을 정확히 찾고, 증분 캐싱을 위해 IEquatable을 구현한 데이터 모델을 사용하세요. 진단 API로 사용자에게 컴파일 오류를 명확히 전달하고, RegisterPostInitializationOutput으로 마커 어트리뷰트를 함께 배포하면 외부 NuGet 의존 없이 완결된 생성기를 만들 수 있습니다.