소스 생성기는 컴파일 타임에 코드를 자동으로 만들어주는 Roslyn 확장 기능입니다. 반복 코드를 줄이고, 리플렉션을 대체해 성능을 높이며, AOT 시나리오에도 유용합니다. 오늘은 Incremental Generator 기반 최소 예제부터 실제 적용 팁까지 빠르게 정리합니다.
1. 소스 생성기란?
Roslyn 컴파일러가 구문/의미 모델을 제공할 때, 소스 생성기는 이를 읽고 새로운 C# 코드를 추가합니다. 기존 사용자 코드를 수정하지 않고, 새로운 partial 타입/멤버를 "추가"만 할 수 있습니다. T4 템플릿이나 포스트컴파일러(IL 위빙)와 달리, 컴파일 파이프라인에 자연스럽게 통합됩니다.
2. 언제 쓰면 좋을까요?
- 애트리뷰트/메타데이터 기반 반복 코드 자동화가 필요할 때입니다.
- 런타임 리플렉션을 컴파일타임 생성 코드로 대체해 성능과 트리밍/AOT 호환성을 확보하고 싶을 때입니다.
- API 클라이언트, DTO 매핑, NotifyPropertyChanged, 강타입 리소스/설정 바인딩 등 패턴성 코드가 많을 때입니다.
3. 최소 예제로 시작하기 (Incremental Generator)
예제 목표: [AutoToString]가 붙은 클래스에 대해 속성들을 조합한 ToString()을 자동 생성합니다.
구성은 2개 프로젝트로 분리합니다.
1) 공유 라이브러리(특성 정의, 소비 프로젝트가 참조):
// Demo.Generator.Shared - netstandard2.0
namespace Demo;
[System.AttributeUsage(System.AttributeTargets.Class)]
public sealed class AutoToStringAttribute : System.Attribute { }
2) 소스 생성기(Incremental Generator 구현):
// Demo.Generator - netstandard2.0
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Demo.Generator;
[Generator]
public sealed class AutoToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax cls && cls.AttributeLists.Count > 0,
static (ctx, _) =>
{
var classDecl = (ClassDeclarationSyntax)ctx.Node;
var symbol = ctx.SemanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
if (symbol is null) return null;
foreach (var a in symbol.GetAttributes())
if (a.AttributeClass?.ToDisplayString() == "Demo.AutoToStringAttribute")
return symbol;
return null;
})
.Where(static s => s is not null);
context.RegisterSourceOutput(candidates, static (spc, s) =>
{
var type = (INamedTypeSymbol)s!;
var ns = type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToDisplayString();
var props = type.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.Select(p => $"{p.Name}={{this.{p.Name}}}");
var joined = string.Join(", ", props);
var code = $@"// <auto-generated/>
{(ns is null ? "" : $"namespace {ns};")}
partial class {type.Name}
{{
public override string ToString() => $"""{type.Name}({joined})""";
}}";
spc.AddSource($"{type.Name}.AutoToString.g.cs", code);
});
}
}
생성기 프로젝트(.csproj) 핵심 설정:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>Demo.Generator</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="$(TargetPath)" />
</ItemGroup>
</Project>
4. 사용 프로젝트에 연결
로컬 개발 단계에서는 프로젝트 참조로 연결합니다.
<!-- App.csproj -->
<ItemGroup>
<ProjectReference Include="..\Demo.Generator\Demo.Generator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Demo.Generator.Shared\Demo.Generator.Shared.csproj" />
</ItemGroup>
사용 예:
using Demo;
[AutoToString]
public partial class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
}
Console.WriteLine(new Person { Name = "이서준", Age = 29 });
// 출력: Person(Name=이서준, Age=29)
NuGet 배포 시에는 생성기 패키지를 소비 프로젝트에서 PackageReference로 추가하되 PrivateAssets=all을 권장합니다.
5. 생성 결과 확인/디버깅
생성된 코드를 빌드 중 파일로 떨어뜨리면 확인/디버깅이 편합니다.
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
빌드 후 obj/generated 아래에 *.g.cs가 생성됩니다. Visual Studio에서는 Generator 프로젝트에 디버거를 연결해 Initialize/Execute 흐름을 추적할 수 있습니다.
6. 성능 팁과 주의사항
- Incremental API를 사용해 불필요한 재계산을 줄입니다(context.SyntaxProvider + RegisterSourceOutput).
- 의미 모델 요청(semantic)과 심볼 걷기는 최소화합니다. 가능한 문법 필터로 먼저 거릅니다.
- 생성기는 사용자 코드를 수정/삭제할 수 없습니다. partial 확장 방식으로 설계합니다.
- 빌드 결정성에 영향을 주는 DateTime.Now, Random 등 비결정적 값 사용을 피합니다.
- AdditionalFiles, AnalyzerConfigOptions을 활용해 설정/스키마를 주입할 수 있습니다.
7. 실전 활용 사례 아이디어
- INotifyPropertyChanged: [Observable] 속성에서 RaisePropertyChanged 코드 자동 생성
- HTTP API 클라이언트: OpenAPI/JSON 스키마를 읽어 타입/엔드포인트 생성
- JSON 직렬화 힌트: 리플렉션 제거(System.Text.Json Source Gen 대체/보완)
- 매핑/Boilerplate: DTO↔Entity 매퍼, Equality/Deconstruct, 강타입 ID 등
8. 테스트 간단 팁
Generator를 직접 구동해 출력 소스를 스냅샷으로 검증합니다.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Demo.Generator;
var code = @"using Demo; [AutoToString] public partial class P { public int X {get;set;} }";
var compilation = CSharpCompilation.Create("Test",
new[] { CSharpSyntaxTree.ParseText(code) },
new[] {
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
// Demo.Generator.Shared 참조 추가 필요
},
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var driver = CSharpGeneratorDriver.Create(new AutoToStringGenerator());
driver.RunGeneratorsAndUpdateCompilation(compilation, out var updated, out var diagnostics);
// updated.SyntaxTrees 안에 생성된 *.g.cs 확인
실무에서는 Verify.SourceGenerators 같은 스냅샷 테스트 프레임워크로 변경 감지를 자동화하면 편리합니다.
정리: 소스 생성기는 반복 코드를 줄이고 빌드 타임 안전성과 성능을 동시에 잡을 수 있는 강력한 도구입니다. 작은 Incremental 예제로 시작해 팀 규약/패턴을 자동화해보세요.
'C#' 카테고리의 다른 글
| C# Parallel.For와 Parallel.ForEach로 데이터 병렬 처리 (0) | 2026.05.07 |
|---|---|
| C# Channel<T>를 이용한 고성능 생산자-소비자 패턴 구현 (1) | 2026.05.06 |
| C# 스레드 풀(ThreadPool)과 작업 큐(Task Queue) 이해하기 (0) | 2026.05.04 |
| C# 애셈블리 버전 관리와 strong naming (0) | 2026.04.30 |
| C# 메타데이터 읽기와 리플렉션(Reflection) (0) | 2026.04.30 |