CodeGym /행동 /C# SELF /입문: Source Generators

입문: Source Generators

C# SELF
레벨 63 , 레슨 3
사용 가능

1. Введение

코드를 작성할 때 가끔 이렇게 생각한 적 없나요: "왜 매번 다른 클래스마다 같은 템플릿 코드를 복사하지?" 또는 "직렬화, 로깅, 데이터 매핑 같은 게 왜 이렇게 반복적인 코드가 많지?" 누군가(또는 무언가)가 귀찮은 템플릿을 대신 작성해줬으면 좋겠다는 생각이 들 때가 있죠.

바로 그때 등장하는 게 Source Generators예요 — .NET 5부터 도입되어 지금도 활발히 발전 중인 C#의 기능이에요. Source Generator는 컴파일 단계에서 실행되는 라이브러리로, 동적으로 C# 코드를 생성해서 최종 빌드 전에 프로젝트에 자동으로 포함시킬 수 있어요.

Зачем это нужно?

  • 루틴 자동화: 반복적인 클래스/메서드(boilerplate) 작성을 줄여줘요.
  • 컴파일러가 검증하는 안전성: 생성된 코드는 당신의 코드와 함께 컴파일돼요(예: T4나 리플렉션과 달라요).
  • 높은 성능: 런타임 리플렉션 비용 없이 직렬화, DI, 매핑 등을 처리할 수 있어요.
  • 현대적 패턴 지원: 코드 생성 없이는 구현하기 어렵거나 비용이 큰 접근법들을 가능하게 해줘요.

Как Source Generators работают «под капотом»?

Source Generator는 .NET 라이브러리(보통 Class Library 형식의 프로젝트)로, ISourceGenerator 인터페이스를 구현해요. 컴파일하는 동안 Roslyn이 연결된 모든 제너레이터를 실행하고, 코드의 구문 트리에 접근할 수 있게 해줍니다.

제너레이터는 코드를 분석해서 무엇을 어디에 생성할지 결정하고, 새로운 C# 파일을 만들어서 컴파일러가 즉시 컴파일하도록 추가합니다.

Автоматическая генерация ToString

간단한 예제부터 시작해봅시다. 멤버가 많은 클래스가 있고 ToString을 구현해야 한다고 가정해요. 손으로 쓰면 보통 이렇게 됩니다:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
        => $"Person(Name={Name}, Age={Age})";
}

하지만 속성이 많아지면 귀찮고, 업데이트를 잊기도 쉬워요. Source Generator가 이걸 대신 해줄 수 있어요!

2. Как создать свой Source Generator?

Создание проекта

JetBrains Rider나 Visual Studio를 열고, Class Library (.NET Standard) 타입의 새 프로젝트를 만드세요 — 이런 종류의 프로젝트가 제너레이터가 될 수 있어요. 그다음 NuGet 패키지를 추가합니다:

  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.Analyzers

Важные атрибуты

  • [Generator] — 이 클래스가 Source Generator임을 표시해요.

Простейший шаблон генератора

최소한의 동작하는 예제는 이렇습니다:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // 추가 동작을 등록할 수 있어요 (필수는 아님)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var code = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""안녕, 세상! 내가 생성되었어!"";
    }
}";
        context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
    }
}

이 간단한 제너레이터는 컴파일 시 항상 정적 클래스 HelloWorld와 메서드 SayHello를 추가합니다.

Как использовать Source Generators в основном приложении?

제너레이터 프로젝트를 NuGet 패키지로 추가하거나 Project Reference로 연결하고, Analyzer 섹션에 참조하세요(자세한 내용은 공식 문서 참조).

생성된 코드는 프로젝트에서 바로 사용 가능해요 — 별도로 뭘 더 임포트할 필요 없이:

// 이 코드는 자동으로 생성됩니다!
using Generated;

Console.WriteLine(HelloWorld.SayHello());

3. Реальный пример: автоматическая генерация ToString

예를 들어, [AutoToString] 속성이 붙은 모든 클래스에 자동으로 ToString 구현을 추가하고 싶다고 합시다. 필요한 작업은:

  • 커스텀 어트리뷰트를 만들기.
  • 그 어트리뷰트가 붙은 모든 클래스를 분석하기.
  • 각 클래스에 대해 ToString 메서드를 생성하기.

Атрибут

[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}

Использование в коде

[AutoToString]
public class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}

Простая логика генерации

제너레이터는 [AutoToString]가 붙은 클래스를 찾아서 대략 다음과 같은 코드를 생성할 거예요:

public override string ToString() 
    => $"Product(Name={Name}, Price={Price})";

Кусочек реального кода генератора

기본 아이디어는 Roslyn으로 구문 트리를 순회하는 거예요:

public void Execute(GeneratorExecutionContext context)
{
    // 모든 구문 트리를 분석합니다
    foreach (var tree in context.Compilation.SyntaxTrees)
    {
        var root = tree.GetRoot();
        // 필요한 어트리뷰트가 붙은 모든 클래스를 찾습니다 (예시!)
        var classes = root.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .Where(c => c.AttributeLists
                         .SelectMany(al => al.Attributes)
                         .Any(a => a.Name.ToString().Contains("AutoToString")));

        foreach (var @class in classes)
        {
            var className = @class.Identifier.Text;
            // 클래스의 모든 속성(property)을 가져옵니다
            var props = @class.Members
                .OfType<PropertyDeclarationSyntax>()
                .Select(p => p.Identifier.Text)
                .ToArray();

            var toStringCode = string.Join(", ", props.Select(p => $"{p}={{this.{p}}}"));
            var generated = $@"
partial class {className}
{{
    public override string ToString() => $""{className}({toStringCode})"";
}}";

            context.AddSource($"{className}_ToString", SourceText.From(generated, Encoding.UTF8));
        }
    }
}

주의: production 코드에서는 Roslyn의 SemanticModel을 통해 더 정확한 분석을 합니다.

4. Полезные нюансы

На что стоит обращать внимание

Source Generators는 기존 소스 코드를 수정할 수 없고 — 새로운 파일만 생성할 수 있어요(예: 추가적인 partial 클래스나 메서드 등). 따라서 클래스가 partial로 선언되어 있으면 여기에 메서드나 속성을 생성해서 추가할 수 있어요.

구문을 올바르게 파싱하고 언어의 모든 뉘앙스(중첩 클래스, 제네릭, 접근자 등)를 고려하는 건 때때로 까다로워요. 제너레이터 작성자는 생성된 코드가 컴파일 가능하고 프로젝트를 망가뜨리지 않도록 신경 써야 합니다.

또 다른 함정은 인터페이스를 구현하는 메서드를 생성할 때 파일이 매 빌드마다 생성되도록 하는 거예요. 그렇지 않으면 이상한 컴파일 오류가 날 수 있어요. 최근 도구들은 이런 문제를 잘 처리하지만, 유념하세요.

Source Generators vs. рефлексия

리플렉션: 런타임에 호출되고 비용이 많이 들며, 컴파일러가 검증하지 않아서 큰 데이터량에선 느릴 수 있어요.

Source Generator: 컴파일 단계에서 코드를 생성해요. 모든 것이 정적으로 검증되고, IDE에서 메서드를 인식하고 자동완성도 동작하며, 성능은 일반 C# 코드와 동일해요.

Практическая польза

  • System.Text.Json: 리플렉션 없이 직렬화/역직렬화 코드를 생성.
  • DI 컨테이너 설계: 예를 들어 Microsoft.Extensions.DependencyInjection에서 의존성 그래프를 생성.
  • Mapster 같은 매퍼: 리플렉션에서 컴파일 타임 매핑 코드로 전환.
  • 테스트 프레임워크: 어트리뷰트를 기반으로 테스트 메서드 자동 생성.
  • ASP.NET Minimal APIs(.NET 7부터): endpoint 핸들러 자동 생성.

Конфигурация, параметры и опции

제너레이터는 MSBuild 파라미터, 추가 파일, 컨벤션을 통해 설정할 수 있어요. 예를 들어 환경(Debug/Release)이나 앱 설정에 따라 다른 형태의 ToString을 생성하도록 할 수 있어요.

Как Source Generators связаны с реальными задачами

깔끔하고 빠른 코드를 지향하는 개발자에게 매우 유용한 도구예요: 반복 작업이 줄고, 컴파일 타임 검사와 IDE 힌트, 명확한 리팩터링이 가능해지죠. 제너레이터에 대한 지식은 직무 면접에서도 점점 더 자주 묻습니다 — 직렬화, DI, 매핑 같은 분야에서 특히요.

Жизненный цикл Source Generator

단계 무슨 일이 일어나는지
1. Проект подключён 제너레이터가 analyzer/reference로 추가됩니다
2. Roslyn компилирует исходник 제너레이터가 AST(추상 구문 트리)를 받습니다
3. Генератор исполняется 컴파일용으로 새로운 .cs 파일을 추가합니다
4. Всё компилируется 생성된 파일들이 빌드의 일부가 됩니다
5. Код готов! 생성된 메서드/클래스를 호출할 수 있습니다

5. Отладка и типичные ошибки

제너레이터를 처음 작성할 때 흔한 실수 중 하나는 코드를 추가하려는 클래스에 partial 키워드를 빼먹는 거예요. partial을 지정하지 않으면 컴파일러가 당신이 추가한 내용을 못 봐요. 또한 IDE가 생성된 파일을 첫 리빌드 전까지 인식하지 못할 수 있으니 너무 놀라지 마세요.

생성된 파일 이름을 잘못 관리하면 서로 덮어쓸 수 있으니 조심하세요. 꿀팁: 파일 이름에 해당 클래스 이름을 포함시키면 좋아요 — 예: context.AddSource($"{className}_ToString", ...).

어트리뷰트가 중복으로 임포트되는 오류도 있어요 — 이미 메인 프로젝트에 같은 어트리뷰트가 있는데 제너레이터가 또 생성하면 충돌이 납니다. 필요한 어트리뷰트는 공통 프로젝트로 빼거나, 조건에 따라 생성하도록 하는 게 낫습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION