콘텐츠로 이동

C# Regex Source Generator — 컴파일 타임 정규식

.NET 7에서 도입된 [GeneratedRegex] 어트리뷰트는 정규식을 런타임이 아닌 컴파일 타임에 C# 코드로 변환합니다. 런타임 Regex.Compile()보다 JIT 없이 최적화된 코드를 생성해 첫 실행 속도와 AOT 호환성을 모두 개선합니다.


using System.Text.RegularExpressions;
public partial class EmailValidator
{
// 컴파일 타임에 Regex 구현 코드 생성
[GeneratedRegex(
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
RegexOptions.IgnoreCase)]
private static partial Regex EmailRegex();
public static bool IsValid(string email) =>
EmailRegex().IsMatch(email);
}
// 사용
bool ok = EmailValidator.IsValid("[email protected]"); // true

public partial class Patterns
{
// ❌ 매 호출마다 파싱 (가장 느림)
static bool SlowCheck(string s) =>
Regex.IsMatch(s, @"\d{3}-\d{4}");
// ✅ 한 번 컴파일, 캐시됨 (중간)
static readonly Regex _compiled =
new Regex(@"\d{3}-\d{4}", RegexOptions.Compiled);
// ✅✅ 컴파일 타임 생성 (가장 빠름, AOT 안전)
[GeneratedRegex(@"\d{3}-\d{4}")]
private static partial Regex PhoneRegex();
}

public partial class Parsers
{
// 멀티라인 + 대소문자 무시
[GeneratedRegex(@"^hello$",
RegexOptions.Multiline | RegexOptions.IgnoreCase)]
public static partial Regex HelloLine();
// 타임아웃 설정 (무한 루프 방지)
[GeneratedRegex(@"(a+)+b",
RegexOptions.None,
matchTimeoutMilliseconds: 1000)]
public static partial Regex SafePattern();
// 명명된 그룹 캡처
[GeneratedRegex(
@"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})")]
public static partial Regex DatePattern();
}
// 명명된 그룹 사용
var match = Parsers.DatePattern().Match("2024-03-15");
if (match.Success)
{
string year = match.Groups["year"].Value; // "2024"
string month = match.Groups["month"].Value; // "03"
string day = match.Groups["day"].Value; // "15"
}

public partial class SpanParsers
{
[GeneratedRegex(@"\b\w+\b")]
private static partial Regex WordPattern();
public static int CountWords(ReadOnlySpan<char> text)
{
// .NET 7+: Span<char> 직접 매칭 (할당 없음)
int count = 0;
foreach (var _ in WordPattern().EnumerateMatches(text))
count++;
return count;
}
// 모든 매치 열거 (할당 최소화)
public static IEnumerable<string> ExtractWords(string text)
{
foreach (Match m in WordPattern().Matches(text))
yield return m.Value;
}
}

| Method | Mean | Allocated |
|----------------- |----------:|----------:|
| RuntimeNew | 2,145 ns | 1,456 B |
| RuntimeCompiled | 312 ns | 0 B |
| GeneratedRegex | 198 ns | 0 B |
- GeneratedRegex는 RuntimeCompiled 대비 ~36% 빠름
- 첫 호출에서 JIT 비용 없음
- NativeAOT 환경에서 유일하게 지원됨

public partial class LogParser
{
// 로그 라인 파싱: [2024-03-15 10:30:00] ERROR Message
[GeneratedRegex(
@"^\[(?<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] " +
@"(?<level>DEBUG|INFO|WARN|ERROR) (?<msg>.+)$",
RegexOptions.Compiled)]
private static partial Regex LogLine();
public record LogEntry(DateTime Timestamp, string Level, string Message);
public static LogEntry? Parse(string line)
{
var m = LogLine().Match(line);
if (!m.Success) return null;
return new LogEntry(
DateTime.Parse(m.Groups["ts"].Value),
m.Groups["level"].Value,
m.Groups["msg"].Value);
}
}

// NativeAOT 빌드에서 Regex.Compile()은 지원 안 됨
// [GeneratedRegex]는 완전 AOT 호환
// .pubxml 또는 csproj
// <PublishAot>true</PublishAot>
// AOT 환경에서 동적 Regex가 필요하면:
// RegexOptions.None 사용 (인터프리터 방식, 느리지만 동작함)
var regex = new Regex(pattern, RegexOptions.None);

[GeneratedRegex]는 정적으로 알 수 있는 모든 정규식 패턴에 사용하세요. 컴파일 타임 생성으로 첫 실행 지연이 없고 NativeAOT와 호환됩니다. 클래스에 partial 키워드를 붙이고 메서드를 private static partial Regex로 선언하면 Source Generator가 구현을 자동 생성합니다. EnumerateMatches(ReadOnlySpan<char>)로 힙 할당 없는 고성능 텍스트 스캔이 가능합니다.