Symbol Type
앞 장에서 Semantic API를 통해 심볼에 접근하는 방법을 배웠습니다. GetDeclaredSymbol이나 ctx.TargetSymbol로 얻은 ISymbol을 실제로 활용하려면, 심볼 타입의 계층 구조를 이해하고 상황에 맞는 타입으로 캐스팅할 수 있어야 합니다.
우리 프로젝트의 ObservablePortGenerator는 INamedTypeSymbol에서 클래스와 인터페이스 정보를 추출하고, IMethodSymbol에서 메서드 시그니처를 분석하며, IParameterSymbol에서 파라미터 타입과 RefKind를 읽어옵니다. 이 장에서는 이러한 심볼 타입들의 속성과 실제 사용 패턴을 체계적으로 학습합니다.
학습 목표
섹션 제목: “학습 목표”핵심 학습 목표
섹션 제목: “핵심 학습 목표”- ISymbol 계층 구조 이해
- 심볼 타입 간의 상속 관계와 용도별 선택 기준
- INamedTypeSymbol, IMethodSymbol 상세 학습
- 클래스/인터페이스 분석과 메서드 시그니처 추출에 필요한 핵심 속성
- 소스 생성기에서 활용하는 심볼 API 습득
- ObservablePortGenerator의 실제 코드를 통한 패턴 학습
ISymbol 계층 구조
섹션 제목: “ISymbol 계층 구조”모든 심볼은 ISymbol 인터페이스를 기반으로 합니다. 소스 생성기에서는 주로 INamedTypeSymbol(클래스/인터페이스 분석), IMethodSymbol(메서드 시그니처), IParameterSymbol(파라미터 정보)을 사용합니다:
ISymbol (기본 인터페이스)│├── INamespaceSymbol 네임스페이스│├── ITypeSymbol (추상) 타입│ ├── INamedTypeSymbol 클래스, 인터페이스, 구조체, 열거형│ ├── IArrayTypeSymbol 배열 타입│ ├── IPointerTypeSymbol 포인터 타입│ └── ITypeParameterSymbol 제네릭 타입 파라미터│├── IMethodSymbol 메서드, 생성자├── IPropertySymbol 프로퍼티├── IFieldSymbol 필드├── IEventSymbol 이벤트├── IParameterSymbol 파라미터├── ILocalSymbol 지역 변수└── IAliasSymbol using 별칭ISymbol 공통 속성
섹션 제목: “ISymbol 공통 속성”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 참조INamedTypeSymbol
섹션 제목: “INamedTypeSymbol”소스 생성기에서 가장 많이 사용하는 심볼 타입입니다. 클래스, 인터페이스, 구조체, 열거형을 나타내며, ObservablePortGenerator에서는 ctx.TargetSymbol을 INamedTypeSymbol로 캐스팅하여 클래스의 인터페이스 목록과 멤버를 분석합니다.
기본 속성
섹션 제목: “기본 속성”INamedTypeSymbol typeSymbol = ...;
// 타입 종류typeSymbol.TypeKind // Class, Interface, Struct, Enum, Delegate
// 이름 관련typeSymbol.Name // 짧은 이름typeSymbol.MetadataName // 메타데이터 이름 (제네릭 포함)typeSymbol.ToDisplayString() // 전체 이름
// 네임스페이스typeSymbol.ContainingNamespacetypeSymbol.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>의 경우 truetypeSymbol.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에서)IMethodSymbol
섹션 제목: “IMethodSymbol”메서드, 생성자, 소멸자, 연산자를 나타냅니다. 우리 프로젝트에서는 인터페이스의 메서드 목록을 GetMembers().OfType<IMethodSymbol>()로 추출한 뒤, MethodKind.Ordinary로 필터링하여 프로퍼티 getter/setter와 생성자를 제외합니다.
기본 속성
섹션 제목: “기본 속성”IMethodSymbol method = ...;
// 이름method.Name // 메서드 이름
// 메서드 종류method.MethodKind // Ordinary, Constructor, PropertyGet, etc.
// 반환 타입method.ReturnType // ITypeSymbolmethod.ReturnsVoid // void 반환 여부
// 수정자method.IsStatic // 정적 여부method.IsAsync // async 여부method.IsAbstract // 추상 여부method.IsVirtual // 가상 여부method.IsExtensionMethod // 확장 메서드 여부MethodKind 열거형
섹션 제목: “MethodKind 열거형”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.IsGenericMethodmethod.TypeArguments // 타입 인수method.TypeParameters // 타입 파라미터실제 활용: ObservablePortGenerator
섹션 제목: “실제 활용: ObservablePortGenerator”메서드 정보 추출
섹션 제목: “메서드 정보 추출”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();생성자 파라미터 추출
섹션 제목: “생성자 파라미터 추출”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 [];}IObservablePort 구현 확인
섹션 제목: “IObservablePort 구현 확인”private static bool ImplementsIObservablePort(INamedTypeSymbol interfaceSymbol){ // IObservablePort 자체인지 확인 if (interfaceSymbol.Name == "IObservablePort") { return true; }
// IObservablePort를 상속받은 인터페이스인지 확인 return interfaceSymbol.AllInterfaces.Any(i => i.Name == "IObservablePort");}IPropertySymbol
섹션 제목: “IPropertySymbol”IPropertySymbol property = ...;
// 기본 정보property.Nameproperty.Type // 프로퍼티 타입property.IsIndexer // 인덱서 여부
// Getter/Setterproperty.GetMethod // getter (IMethodSymbol?)property.SetMethod // setter (IMethodSymbol?)property.IsReadOnly // 읽기 전용 (setter 없음)property.IsWriteOnly // 쓰기 전용 (getter 없음)IParameterSymbol
섹션 제목: “IParameterSymbol”IParameterSymbol param = ...;
// 기본 정보param.Nameparam.Typeparam.Ordinal // 파라미터 순서 (0부터)
// RefKindparam.RefKind // None, Ref, Out, In, RefReadOnlyParameter
// 기본값param.HasExplicitDefaultValueparam.ExplicitDefaultValue
// 특수 파라미터param.IsParams // params 배열 여부param.IsOptional // 선택적 파라미터 여부param.IsThis // 확장 메서드의 this 파라미터RefKind 열거형
섹션 제목: “RefKind 열거형”public enum RefKind{ None, // 일반 파라미터 Ref, // ref 파라미터 Out, // out 파라미터 In, // in 파라미터 (읽기 전용 ref) RefReadOnlyParameter // ref readonly 파라미터}SymbolDisplayFormat 활용
섹션 제목: “SymbolDisplayFormat 활용”소스 생성기가 코드를 생성할 때, 타입 이름은 반드시 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로 파라미터의 타입과 전달 방식을 확인합니다.
| 심볼 타입 | 대표 멤버 | 용도 |
|---|---|---|
INamedTypeSymbol | Name, AllInterfaces, GetMembers() | 클래스 분석 |
IMethodSymbol | Name, ReturnType, Parameters | 메서드 분석 |
IPropertySymbol | Type, GetMethod, SetMethod | 프로퍼티 분석 |
IParameterSymbol | Name, Type, RefKind | 파라미터 분석 |
| 주요 조회 패턴 | 코드 |
|---|---|
| 모든 인터페이스 | typeSymbol.AllInterfaces |
| 모든 메서드 | typeSymbol.GetMembers().OfType<IMethodSymbol>() |
| 생성자 | typeSymbol.Constructors |
| 일반 메서드만 | .Where(m => m.MethodKind == MethodKind.Ordinary) |
FAQ
섹션 제목: “FAQ”Q1: INamedTypeSymbol.AllInterfaces와 INamedTypeSymbol.Interfaces의 차이는 무엇인가요?
섹션 제목: “Q1: INamedTypeSymbol.AllInterfaces와 INamedTypeSymbol.Interfaces의 차이는 무엇인가요?”A: Interfaces는 해당 타입이 직접 선언한 인터페이스만 반환합니다. AllInterfaces는 상속 체인을 따라 올라가며 모든 인터페이스를 포함합니다. ObservablePortGenerator에서 IObservablePort 구현 여부를 확인할 때 AllInterfaces를 사용하는 이유는, 직접 구현하지 않고 상위 인터페이스를 통해 간접적으로 구현하는 경우도 포함해야 하기 때문입니다.
Q2: IMethodSymbol.MethodKind로 Ordinary만 필터링하는 이유는 무엇인가요?
섹션 제목: “Q2: IMethodSymbol.MethodKind로 Ordinary만 필터링하는 이유는 무엇인가요?”A: GetMembers()는 생성자(Constructor), 프로퍼티 접근자(PropertyGet/PropertySet), 연산자(UserDefinedOperator) 등 모든 메서드류 멤버를 반환합니다. 소스 생성기가 래퍼 코드를 생성할 대상은 일반 메서드뿐이므로, MethodKind.Ordinary 필터로 불필요한 멤버를 제외합니다.
Q3: SymbolDisplayFormat을 커스텀으로 정의하는 이유는 무엇인가요?
섹션 제목: “Q3: SymbolDisplayFormat을 커스텀으로 정의하는 이유는 무엇인가요?”A: 기본 제공 포맷인 FullyQualifiedFormat은 int 대신 System.Int32로 출력하는 등 C# 특수 타입 별칭을 사용하지 않습니다. Functorium의 GlobalQualifiedFormat은 UseSpecialTypes와 IncludeNullableReferenceTypeModifier 옵션을 추가하여, 생성된 코드가 자연스러운 C# 문법을 따르도록 합니다.
Roslyn의 세 가지 핵심 계층 - Syntax Tree, Semantic Model, Symbol - 을 모두 학습했습니다. 다음 장에서는 이 세 계층을 조합하여 실제 소스 생성기를 구현하는 IIncrementalGenerator 패턴을 학습합니다.