홈으로 돌아가기

C#의 판별 합집합: 프로덕션용 분석 및 최적화

C# 판별 합집합 구현을 위한 기존 솔루션 분석. OneOf와 DuNet 성능 비교, 직렬화 및 Unity 지원 최적화 Source Generator 생성. 고부하 시스템 실전 추천.

C#의 효율적인 판별 합집합: 언어 제한 우회 방법
Advertisement 728x90

C#의 효율적인 판별 합집합: 솔루션 분석 및 사용자 정의 구현

C# 개발자들은 판별 합집합(DU)을 구현할 때 제한에 직면합니다. DU는 여러 데이터 변형을 타입 안전하게 처리하는 핵심 구성 요소입니다. F#이나 Rust처럼 언어에 내장된 DU와 달리, C#에서는 타협적인 솔루션을 사용해야 합니다. 이 글에서는 기존 라이브러리들을 검토하고, 성능 및 지원 부족점을 분석하며, Source Generators를 사용한 최적화된 DU 생성기를 만드는 방법을 보여줍니다.

DU 구현의 주요 접근 방식

C#에서 DU의 핵심 도전 과제는 메모리, 속도, 사용 편의성의 균형입니다. 네이티브 DU 지원 언어 분석을 통해 두 가지 기본 접근 방식을 확인할 수 있습니다:

C 스타일의 명시적 메모리 중첩

Google AdInline article slot

union과 태그 설명자를 사용하면 오버헤드가 최소화됩니다. C#에서 StructLayout(LayoutKind.Explicit)을 통해 이 패턴을 재현하려는 시도는 CLR 제한으로 인해 실패합니다: 참조 타입과 값 타입을 같은 메모리 영역에 섞어 사용할 수 없어 TypeLoadException 위험이 있습니다.

F# 스타일의 상속

여기서는 각 DU 변형이 별도의 파생 클래스입니다. 이를 통해 match로 모든 경우를 처리할 수 있지만, 힙 할당이 발생합니다. 고부하 시스템(예: Unity 게임이나 마이크로서비스)에서는 이 접근 방식이 받아들여지지 않습니다.

Google AdInline article slot

구성으로서의 타협

구조체에 모든 변형을 순차적으로 저장하면 타입 안전성은 확보되지만 메모리 사용량이 악화됩니다. 예를 들어 double, string, tuple 세 변형을 가진 구조체는 사용되는 하나만 있더라도 모든 필드 합계만큼 공간을 차지합니다. 이는 고부하 시나리오에서 캐시 효율성에 치명적입니다.

인기 라이브러리의 비판적 분석

OneOf (mcintyre321)

이 라이브러리는 고정 템플릿 세트(OneOf<T0...T7>)를 사용한 구성적 접근 방식을 채택합니다. 장점:

Google AdInline article slot
  • 모든 경우 처리 확인이 포함된 즉시 사용 가능한 Match/Switch 메서드
  • 암시적 변환 지원
  • 최소한의 보일러플레이트

그러나 실제 프로젝트에서 심각한 단점이 드러납니다:

  • 메모리 낭비: 구조체가 모든 필드를 동시에 저장
  • 명명된 액세스 불가: AsT0, AsT1 사용해야 함
  • 직렬화 지원 없음: API와 영속화에 치명적
  • 변형 수 고정 (확장 시 최대 32개)

DuNet (domn1995)

레코드 타입과 Source Generators 기반 솔루션입니다. [Union] 속성을 통해 코드를 생성하며 우아한 구문을 제공합니다:

[Union]
partial record Shape {
    partial record Circle(double Radius);
}

장점:

  • 명명된 변형 지원
  • 내장 JSON 직렬화
  • 부분 타입을 통한 유연한 구성

단점:

  • 필수 할당 (레코드 상속)
  • 타사 타입 직접 사용 불가
  • 구성적 접근 방식보다 성능 20-30% 저하

왜 표준 솔루션이 프로덕션에서 작동하지 않는가

DU 값 액세스 작업에 대한 벤치마크 분석 결과:

| 솔루션 | 시간 (ns) | 메모리 (바이트) |

|---------------|-----------|-----------------|

| OneOf | 8.2 | 48 |

| DuNet | 12.7 | 32 |

| C-style | 1.3 | 16 |

| Our generator | 1.9 | 18 |

주요 문제:

  • 일괄 필드 크기: OneOf의 구성 방식이 모든 변형에 메모리 할당
  • 숨겨진 할당: DuNet이 기본 타입에도 객체 생성
  • 레이아웃 제어 불가: 특정 시나리오 최적화 불가능

고성능 DU 생성기 만들기

아키텍처 결정

생성기는 OneOfLayoutKind 매개변수를 통해 저장 전략 선택을 제공합니다:

  • ExplicitUnion: 값 타입 중첩 (C 유사), 참조를 object로 캐스팅
  • Boxing: object로 범용 캐스팅 (코드 최소화, 할당 가능성 있음)
  • Composition: 고전적 구성 (최대 호환성)
  • Hybrid: 혼합 타입 최적화를 위한 조합

예제 선언:

[GenerateOneOf(["Number", "Text"], Layout = OneOfLayoutKind.Hybrid)]
public readonly partial struct Numeric : IOneOf<double, string>

핵심 최적화

Void 타입 지원

빈 구조체는 [VoidType] 속성으로 표시되어 메모리 할당을 제거합니다:

[VoidType]
public readonly struct Success {}

리플렉션 없는 메타데이터

생성기가 구조체 내부에 타입 안전한 메타데이터 액세스를 가진 정적 Info 클래스를 만듭니다:

Numeric.Info.VariantCount // 2
Numeric.Info.GetVariantName(0) // "Number"

직렬화기 통합

다음에 대한 변환기 구현:

  • System.Text.Json
  • MemoryPack
  • Newtonsoft.Json
  • MessagePack

주요 요점

  • 레이아웃 선택이 성능에 핵심: 하이브리드 모드가 OneOf 대비 메모리 사용량 40% 감소
  • Source Generators가 런타임 검사를 컴파일 타임 보증으로 대체: 빌드 시 모든 경우 처리 검사
  • Void 타입 지원으로 제로 할당 제거: 이벤트 기반 프로그래밍에 필수
  • 생성기 기반 직렬화로 리플렉션 회피: 역직렬화 속도가 수동 코드와 동일
  • Unity 호환성: 동적 메서드 없음, IL2CPP 작동

실제 적용

시나리오: API 응답 처리

[GenerateOneOf(["Success", "ValidationError", "NetworkError"], Layout = OneOfLayoutKind.Hybrid)]
public readonly partial struct ApiResponse : IOneOf<Success, ValidationError, NetworkError>

OneOf 대비 장점:

  • 구조체 크기 24바이트 (OneOf 56바이트 대비)
  • MemoryPack 역직렬화 35% 빠름
  • 핫 패스에서 할당 없음

미래 네이티브 구현과의 비교

Microsoft가 C# 14에서 DU를 발표하며 구문:

public union ApiResponse(Success, ValidationError, NetworkError);

우리 솔루션은 이미 제공:

  • .NET Standard 2.1 지원 (Unity, 레거시 프로젝트)
  • 레이아웃 선택 유연성
  • 기존 도구 통합

— Editorial Team

Advertisement 728x90

다음 읽기