본문으로 건너뛰기

Symbol Type

앞 장에서 Semantic API를 통해 심볼에 접근하는 방법을 배웠습니다. GetDeclaredSymbol이나 ctx.TargetSymbol로 얻은 ISymbol을 실제로 활용하려면, 심볼 타입의 계층 구조를 이해하고 상황에 맞는 타입으로 캐스팅할 수 있어야 합니다.

우리 프로젝트의 ObservablePortGenerator는 INamedTypeSymbol에서 클래스와 인터페이스 정보를 추출하고, IMethodSymbol에서 메서드 시그니처를 분석하며, IParameterSymbol에서 파라미터 타입과 RefKind를 읽어옵니다. 이 장에서는 이러한 심볼 타입들의 속성과 실제 사용 패턴을 체계적으로 학습합니다.

  1. ISymbol 계층 구조 이해
    • 심볼 타입 간의 상속 관계와 용도별 선택 기준
  2. INamedTypeSymbol, IMethodSymbol 상세 학습
    • 클래스/인터페이스 분석과 메서드 시그니처 추출에 필요한 핵심 속성
  3. 소스 생성기에서 활용하는 심볼 API 습득
    • ObservablePortGenerator의 실제 코드를 통한 패턴 학습

모든 심볼은 ISymbol 인터페이스를 기반으로 합니다. 소스 생성기에서는 주로 INamedTypeSymbol(클래스/인터페이스 분석), IMethodSymbol(메서드 시그니처), IParameterSymbol(파라미터 정보)을 사용합니다:

ISymbol (기본 인터페이스)
├── INamespaceSymbol 네임스페이스
├── ITypeSymbol (추상) 타입
│ ├── INamedTypeSymbol 클래스, 인터페이스, 구조체, 열거형
│ ├── IArrayTypeSymbol 배열 타입
│ ├── IPointerTypeSymbol 포인터 타입
│ └── ITypeParameterSymbol 제네릭 타입 파라미터
├── IMethodSymbol 메서드, 생성자
├── IPropertySymbol 프로퍼티
├── IFieldSymbol 필드
├── IEventSymbol 이벤트
├── IParameterSymbol 파라미터
├── ILocalSymbol 지역 변수
└── IAliasSymbol using 별칭

ISymbol symbol = ...;
// 기본 정보
symbol.Name // 이름
symbol.Kind // 심볼 종류 (SymbolKind 열거형)
symbol.ContainingNamespace // 포함 네임스페이스
symbol.ContainingType // 포함 타입 (멤버인 경우)
symbol.ContainingSymbol // 포함 심볼 (부모)
// 접근성
symbol.DeclaredAccessibility // Public, Private, Internal 등
// 메타데이터
symbol.IsStatic // 정적 여부
symbol.IsAbstract // 추상 여부
symbol.IsVirtual // 가상 여부
symbol.IsOverride // 오버라이드 여부
symbol.IsSealed // sealed 여부
// 위치
symbol.Locations // 소스 코드 위치들
symbol.DeclaringSyntaxReferences // 선언 Syntax 참조

소스 생성기에서 가장 많이 사용하는 심볼 타입입니다. 클래스, 인터페이스, 구조체, 열거형을 나타내며, ObservablePortGenerator에서는 ctx.TargetSymbolINamedTypeSymbol로 캐스팅하여 클래스의 인터페이스 목록과 멤버를 분석합니다.

INamedTypeSymbol typeSymbol = ...;
// 타입 종류
typeSymbol.TypeKind // Class, Interface, Struct, Enum, Delegate
// 이름 관련
typeSymbol.Name // 짧은 이름
typeSymbol.MetadataName // 메타데이터 이름 (제네릭 포함)
typeSymbol.ToDisplayString() // 전체 이름
// 네임스페이스
typeSymbol.ContainingNamespace
typeSymbol.ContainingNamespace.IsGlobalNamespace // 글로벌 여부
// 기본 타입
typeSymbol.BaseType // 부모 클래스
typeSymbol.AllInterfaces // 모든 인터페이스 (직접 + 상속)
typeSymbol.Interfaces // 직접 구현한 인터페이스만
// 모든 멤버
var allMembers = typeSymbol.GetMembers();
// 특정 이름의 멤버
var namedMembers = typeSymbol.GetMembers("GetUser");
// 타입별 필터링
var methods = typeSymbol.GetMembers()
.OfType<IMethodSymbol>();
var properties = typeSymbol.GetMembers()
.OfType<IPropertySymbol>();
var constructors = typeSymbol.Constructors; // 생성자들
// 제네릭 여부
typeSymbol.IsGenericType // List<T>의 경우 true
typeSymbol.TypeArguments // 타입 인수 [int] for List<int>
typeSymbol.TypeParameters // 타입 파라미터 [T] for List<T>
// 원본 정의
typeSymbol.OriginalDefinition // List<> (unbounded)
// 예시: Dictionary<string, int>
// TypeArguments: [string, int]
// TypeParameters: [TKey, TValue] (OriginalDefinition에서)

메서드, 생성자, 소멸자, 연산자를 나타냅니다. 우리 프로젝트에서는 인터페이스의 메서드 목록을 GetMembers().OfType<IMethodSymbol>()로 추출한 뒤, MethodKind.Ordinary로 필터링하여 프로퍼티 getter/setter와 생성자를 제외합니다.

IMethodSymbol method = ...;
// 이름
method.Name // 메서드 이름
// 메서드 종류
method.MethodKind // Ordinary, Constructor, PropertyGet, etc.
// 반환 타입
method.ReturnType // ITypeSymbol
method.ReturnsVoid // void 반환 여부
// 수정자
method.IsStatic // 정적 여부
method.IsAsync // async 여부
method.IsAbstract // 추상 여부
method.IsVirtual // 가상 여부
method.IsExtensionMethod // 확장 메서드 여부
public enum MethodKind
{
Ordinary, // 일반 메서드
Constructor, // 생성자
StaticConstructor, // 정적 생성자
Destructor, // 소멸자
PropertyGet, // 프로퍼티 getter
PropertySet, // 프로퍼티 setter
EventAdd, // 이벤트 add
EventRemove, // 이벤트 remove
ExplicitInterfaceImplementation, // 명시적 인터페이스 구현
Conversion, // 변환 연산자
UserDefinedOperator, // 사용자 정의 연산자
// ...
}
// 파라미터 목록
foreach (var param in method.Parameters)
{
Console.WriteLine($"이름: {param.Name}");
Console.WriteLine($"타입: {param.Type}");
Console.WriteLine($"RefKind: {param.RefKind}"); // None, Ref, Out, In
Console.WriteLine($"기본값 있음: {param.HasExplicitDefaultValue}");
if (param.HasExplicitDefaultValue)
{
Console.WriteLine($"기본값: {param.ExplicitDefaultValue}");
}
}
// 제네릭 여부
method.IsGenericMethod
method.TypeArguments // 타입 인수
method.TypeParameters // 타입 파라미터

ObservablePortGenerator.cs
var methods = classSymbol.AllInterfaces
.Where(ImplementsIObservablePort)
.SelectMany(i => i.GetMembers().OfType<IMethodSymbol>())
.Where(m => m.MethodKind == MethodKind.Ordinary) // ★ 일반 메서드만
.Select(m => new MethodInfo(
m.Name,
m.Parameters.Select(p => new ParameterInfo(
p.Name,
p.Type.ToDisplayString(SymbolDisplayFormats.GlobalQualifiedFormat),
p.RefKind)).ToList(),
m.ReturnType.ToDisplayString(SymbolDisplayFormats.GlobalQualifiedFormat)))
.ToList();
ConstructorParameterExtractor.cs
public static List<ParameterInfo> ExtractParameters(INamedTypeSymbol classSymbol)
{
// 1. 클래스 자체의 생성자에서 파라미터 찾기
var constructor = classSymbol.Constructors
.Where(c => c.DeclaredAccessibility == Accessibility.Public)
.OrderByDescending(c => c.Parameters.Length) // 파라미터 많은 것 우선
.FirstOrDefault();
if (constructor is not null && constructor.Parameters.Length > 0)
{
return constructor.Parameters
.Select(p => new ParameterInfo(
p.Name,
p.Type.ToDisplayString(SymbolDisplayFormats.GlobalQualifiedFormat),
p.RefKind))
.ToList();
}
// 2. 부모 클래스의 생성자에서 찾기
if (classSymbol.BaseType is not null)
{
return ExtractParameters(classSymbol.BaseType);
}
return [];
}
private static bool ImplementsIObservablePort(INamedTypeSymbol interfaceSymbol)
{
// IObservablePort 자체인지 확인
if (interfaceSymbol.Name == "IObservablePort")
{
return true;
}
// IObservablePort를 상속받은 인터페이스인지 확인
return interfaceSymbol.AllInterfaces.Any(i => i.Name == "IObservablePort");
}

IPropertySymbol property = ...;
// 기본 정보
property.Name
property.Type // 프로퍼티 타입
property.IsIndexer // 인덱서 여부
// Getter/Setter
property.GetMethod // getter (IMethodSymbol?)
property.SetMethod // setter (IMethodSymbol?)
property.IsReadOnly // 읽기 전용 (setter 없음)
property.IsWriteOnly // 쓰기 전용 (getter 없음)

IParameterSymbol param = ...;
// 기본 정보
param.Name
param.Type
param.Ordinal // 파라미터 순서 (0부터)
// RefKind
param.RefKind // None, Ref, Out, In, RefReadOnlyParameter
// 기본값
param.HasExplicitDefaultValue
param.ExplicitDefaultValue
// 특수 파라미터
param.IsParams // params 배열 여부
param.IsOptional // 선택적 파라미터 여부
param.IsThis // 확장 메서드의 this 파라미터
public enum RefKind
{
None, // 일반 파라미터
Ref, // ref 파라미터
Out, // out 파라미터
In, // in 파라미터 (읽기 전용 ref)
RefReadOnlyParameter // ref readonly 파라미터
}

소스 생성기가 코드를 생성할 때, 타입 이름은 반드시 global:: 접두사를 포함하는 완전한 형태로 출력해야 합니다. 그래야 생성된 코드가 using 선언이나 네임스페이스 충돌에 영향받지 않습니다. 우리 프로젝트의 SymbolDisplayFormats.GlobalQualifiedFormat이 이를 위한 커스텀 포맷입니다.

심볼을 문자열로 변환할 때 포맷을 지정할 수 있습니다:

ITypeSymbol type = ...; // MyApp.Models.User
// 기본 포맷
type.ToDisplayString()
// → "User"
// 전체 이름 (네임스페이스 포함)
type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
// → "global::MyApp.Models.User"
// 커스텀 포맷 (소스 생성기에서 권장)
var format = new SymbolDisplayFormat(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
miscellaneousOptions:
SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
type.ToDisplayString(format)
// → "global::MyApp.Models.User"

심볼 타입의 계층 구조를 이해하면, Semantic API에서 얻은 ISymbol을 적절한 타입으로 캐스팅하여 필요한 정보를 추출할 수 있습니다. ObservablePortGenerator의 파이프라인에서 각 심볼 타입이 맡는 역할은 다음과 같습니다: INamedTypeSymbol로 클래스와 인터페이스 관계를 분석하고, IMethodSymbol로 메서드 시그니처를 추출하며, IParameterSymbol로 파라미터의 타입과 전달 방식을 확인합니다.

심볼 타입대표 멤버용도
INamedTypeSymbolName, AllInterfaces, GetMembers()클래스 분석
IMethodSymbolName, ReturnType, Parameters메서드 분석
IPropertySymbolType, GetMethod, SetMethod프로퍼티 분석
IParameterSymbolName, Type, RefKind파라미터 분석
주요 조회 패턴코드
모든 인터페이스typeSymbol.AllInterfaces
모든 메서드typeSymbol.GetMembers().OfType<IMethodSymbol>()
생성자typeSymbol.Constructors
일반 메서드만.Where(m => m.MethodKind == MethodKind.Ordinary)

Q1: INamedTypeSymbol.AllInterfacesINamedTypeSymbol.Interfaces의 차이는 무엇인가요?

섹션 제목: “Q1: INamedTypeSymbol.AllInterfaces와 INamedTypeSymbol.Interfaces의 차이는 무엇인가요?”

A: Interfaces는 해당 타입이 직접 선언한 인터페이스만 반환합니다. AllInterfaces는 상속 체인을 따라 올라가며 모든 인터페이스를 포함합니다. ObservablePortGenerator에서 IObservablePort 구현 여부를 확인할 때 AllInterfaces를 사용하는 이유는, 직접 구현하지 않고 상위 인터페이스를 통해 간접적으로 구현하는 경우도 포함해야 하기 때문입니다.

Q2: IMethodSymbol.MethodKindOrdinary만 필터링하는 이유는 무엇인가요?

섹션 제목: “Q2: IMethodSymbol.MethodKind로 Ordinary만 필터링하는 이유는 무엇인가요?”

A: GetMembers()는 생성자(Constructor), 프로퍼티 접근자(PropertyGet/PropertySet), 연산자(UserDefinedOperator) 등 모든 메서드류 멤버를 반환합니다. 소스 생성기가 래퍼 코드를 생성할 대상은 일반 메서드뿐이므로, MethodKind.Ordinary 필터로 불필요한 멤버를 제외합니다.

Q3: SymbolDisplayFormat을 커스텀으로 정의하는 이유는 무엇인가요?

섹션 제목: “Q3: SymbolDisplayFormat을 커스텀으로 정의하는 이유는 무엇인가요?”

A: 기본 제공 포맷인 FullyQualifiedFormatint 대신 System.Int32로 출력하는 등 C# 특수 타입 별칭을 사용하지 않습니다. Functorium의 GlobalQualifiedFormatUseSpecialTypesIncludeNullableReferenceTypeModifier 옵션을 추가하여, 생성된 코드가 자연스러운 C# 문법을 따르도록 합니다.


Roslyn의 세 가지 핵심 계층 - Syntax Tree, Semantic Model, Symbol - 을 모두 학습했습니다. 다음 장에서는 이 세 계층을 조합하여 실제 소스 생성기를 구현하는 IIncrementalGenerator 패턴을 학습합니다.

Part 2의 1장. IIncrementalGenerator 인터페이스