콘텐츠로 이동

11장. 고급 객체지향 개념

객체지향 프로그래밍(OOP)의 여정은 1960년대 Ole-Johan Dahl과 Kristen Nygaard가 개발한 Simula 67에서 시작되어, Smalltalk, C++, Java를 거쳐 현대의 C#에 이르기까지 지속적으로 진화해왔습니다. 이 장에서 다룰 고급 객체지향 개념들은 이러한 진화의 결과물로, 타입 안전성(Type Safety), 불변성(Immutability), 성능 최적화(Performance Optimization)라는 현대 프로그래밍의 세 가지 핵심 가치를 구현합니다.

sealed 키워드는 Joshua Bloch의 "Effective Java"에서 강조한 "상속을 위한 설계와 문서화, 그렇지 않으면 상속 금지" 원칙을 코드로 구현한 것입니다. record 타입은 함수형 프로그래밍의 불변 데이터 구조(Immutable Data Structures)를 C#에 도입하여, Eric Evans의 도메인 주도 설계(Domain-Driven Design)에서 제시한 값 객체(Value Object) 패턴을 우아하게 표현합니다. struct는 .NET의 메모리 관리 모델에서 스택 할당(Stack Allocation)과 가비지 컬렉션 부담 감소를 통해 성능 최적화를 가능하게 하며, enum은 타입 안전한 상수 집합을 제공하여 매직 넘버(Magic Number) 안티패턴을 제거합니다.

이러한 기능들은 단순한 문법적 편의를 넘어, Barbara Liskov의 치환 원칙(Liskov Substitution Principle), Bertrand Meyer의 계약에 의한 설계(Design by Contract), 그리고 Robert C. Martin의 SOLID 원칙과 같은 소프트웨어 공학의 근본 원리들을 구체적으로 실현하는 도구입니다. C# 언어 설계팀(Anders Hejlsberg, Mads Torgersen 등)은 이러한 이론적 토대 위에 실용적인 문법을 구축하여, 안전하고 효율적이며 유지보수 가능한 코드 작성을 가능하게 했습니다.

이 장에서 배울 내용

이 장을 통해 독자 여러분은 C#의 고급 타입 시스템을 체계적으로 학습하게 됩니다:

  • sealed 클래스와 메서드: 상속 계층 구조의 종료점을 명시하여 Fragile Base Class Problem을 방지하고, 컴파일러의 devirtualization 최적화를 가능하게 하는 방법을 학습합니다. .NET Framework 자체의 System.String이 sealed인 이유와, Joshua Bloch의 설계 철학을 실천하는 방법을 이해합니다.

  • record 타입: C# 9.0에서 도입된 record는 함수형 프로그래밍의 불변 데이터 구조를 C#에 구현한 것으로, 구조적 동등성(Structural Equality)과 비파괴적 변경(Non-destructive Mutation)을 통해 값 지향 설계(Value-Oriented Design)를 가능하게 합니다. Eric Evans의 DDD에서 제시한 값 객체와 엔티티의 구분을 코드로 표현하는 방법을 배웁니다.

  • 구조체 (Struct): 값 의미론(Value Semantics)과 참조 의미론(Reference Semantics)의 근본적 차이를 이해하고, .NET의 메모리 관리 모델(스택 vs 힙)에서 struct의 역할을 학습합니다. C# 10의 record struct는 불변 값 타입의 표현력을 한층 높여줍니다.

  • 열거형 (Enum): 타입 안전한 명명된 상수를 통해 Martin Fowler가 지적한 Primitive Obsession 코드 스멜을 제거하고, [Flags] 특성을 통한 비트 필드 구현 방법을 익힙니다.

학습 목표: - 타입 시스템의 안전성과 표현력을 최대한 활용하는 방법 이해 - 불변성과 값 의미론이 동시성(Concurrency)과 예측 가능성에 미치는 영향 파악 - .NET 메모리 모델과 가비지 컬렉션의 관계 속에서 적절한 타입 선택 능력 배양 - 도메인 모델링에서 값 객체와 엔티티를 구분하여 표현하는 실무 기술 습득


11.1 sealed 클래스와 메서드

sealed 키워드는 상속 계층 구조에서 확장의 종료점을 명시하는 한정자로, 객체지향 설계에서 매우 중요한 역할을 합니다. 이 개념은 1990년대 Java 언어 설계 과정에서 final 키워드로 처음 대중화되었으며, C#은 이를 sealed라는 더 명시적인 이름으로 채택했습니다.

역사적 배경과 설계 철학:

상속 가능성(Extensibility)과 상속 제한(Finality) 사이의 논쟁은 객체지향 프로그래밍의 초기부터 존재해왔습니다. Bertrand Meyer의 "Object-Oriented Software Construction"(1988)에서는 개방-폐쇄 원칙(Open-Closed Principle)을 제시하여 "확장에는 열려있고 수정에는 닫혀있어야 한다"고 주장했습니다. 그러나 Joshua Bloch는 "Effective Java"(2001)에서 이를 발전시켜 "상속을 위한 설계와 문서화를 하거나, 그렇지 않으면 상속을 금지하라(Design and document for inheritance or else prohibit it)"는 원칙을 제시했습니다.

Fragile Base Class Problem:

상속의 가장 큰 위험 중 하나는 Fragile Base Class Problem(취약한 기본 클래스 문제)입니다. 이는 기본 클래스의 작은 변경이 모든 하위 클래스에 예상치 못한 영향을 미칠 수 있는 현상을 말합니다. 1998년 Mikhail Dmitriev가 발표한 논문 "The Fragile Base Class Problem and its Solution"에서 이 문제를 체계적으로 분석했으며, sealed 키워드는 이 문제를 근본적으로 차단하는 해결책 중 하나입니다.

// 나쁜 예: 의도하지 않은 상속으로 인한 문제
class BankAccount
{
    protected decimal balance;

    public virtual void Withdraw(decimal amount)
    {
        balance -= amount;
        LogTransaction("Withdrawal", amount);
    }

    protected virtual void LogTransaction(string type, decimal amount)
    {
        Console.WriteLine($"{type}: {amount}");
    }
}

// 하위 클래스에서 의도하지 않은 동작 발생 가능
class AuditedAccount : BankAccount
{
    public override void Withdraw(decimal amount)
    {
        // 감사 로그를 먼저 기록하려고 하는데...
        LogTransaction("Audit", amount);
        base.Withdraw(amount);  // 이미 LogTransaction이 한 번 호출됨 (중복!)
    }
}

sealed 클래스의 이론적 토대:

sealed 클래스는 **리스코프 치환 원칙(Liskov Substitution Principle, LSP)**의 엄격한 적용을 보장합니다. Barbara Liskov가 1987년 제시한 이 원칙은 "하위 타입은 기본 타입을 대체할 수 있어야 한다"는 것인데, sealed를 사용하면 더 이상 하위 타입이 존재하지 않으므로 LSP 위반 가능성 자체가 사라집니다.

또한 .NET의 타입 안전성(Type Safety) 보장에도 기여합니다. sealed 클래스는 컴파일러가 메서드 호출을 정적으로 바인딩할 수 있게 하여, 런타임 타입 검사 오버헤드를 제거합니다. 이는 특히 성능이 중요한 시나리오에서 큰 이점을 제공합니다.

sealed 클래스:

sealed 클래스는 더 이상 상속할 수 없는 클래스입니다. 다른 클래스가 이 클래스를 기본 클래스로 사용할 수 없습니다.

// sealed 클래스 정의
sealed class FinalClass
{
    public void Display()
    {
        Console.WriteLine("이 클래스는 상속할 수 없습니다.");
    }
}

// ❌ 컴파일 오류: sealed 클래스를 상속할 수 없음
// class DerivedClass : FinalClass
// {
// }

// 사용
FinalClass obj = new FinalClass();
obj.Display();
// 출력: 이 클래스는 상속할 수 없습니다.

sealed 메서드:

sealed 메서드는 이미 오버라이드된 메서드를 더 이상 하위 클래스에서 오버라이드하지 못하게 합니다. sealed 메서드는 반드시 override 키워드와 함께 사용해야 합니다.

class BaseClass
{
    public virtual void Display()
    {
        Console.WriteLine("기본 클래스");
    }
}

class MiddleClass : BaseClass
{
    // override와 sealed를 함께 사용
    public sealed override void Display()
    {
        Console.WriteLine("중간 클래스 (더 이상 오버라이드 불가)");
    }
}

// ❌ 컴파일 오류: sealed 메서드를 오버라이드할 수 없음
// class DerivedClass : MiddleClass
// {
//     public override void Display()
//     {
//     }
// }

// 사용
MiddleClass obj = new MiddleClass();
obj.Display();
// 출력: 중간 클래스 (더 이상 오버라이드 불가)

sealed를 사용하는 이유와 이론적 근거:

  1. 설계 의도의 명시적 전달 (Explicit Design Intent):
  2. Meyer의 "계약에 의한 설계(Design by Contract)" 원칙의 실천
  3. 클래스의 불변식(Class Invariants)을 보호하여 계약 위반 방지
  4. 다른 개발자에게 "이 클래스는 확장을 위해 설계되지 않았음"을 명확히 전달

  5. 안정성과 예측 가능성 (Stability and Predictability):

  6. Fragile Base Class Problem 근본적 차단
  7. 리스코프 치환 원칙(LSP) 위반 가능성 제거
  8. 런타임 다형성(Runtime Polymorphism) 복잡도 감소

  9. 성능 최적화 (Performance Optimization):

  10. Devirtualization: JIT 컴파일러가 가상 메서드 호출을 직접 호출로 변환 가능
  11. 인라이닝(Inlining): 메서드 본문을 호출 지점에 직접 삽입하여 함수 호출 오버헤드 제거
  12. 타입 검사 제거: sealed 타입은 is 또는 as 검사를 컴파일 타임에 해결 가능
// sealed가 없는 경우: 가상 메서드 테이블(Virtual Method Table, VMT) 조회 필요
// Call instruction: 1. 객체의 타입 로드 -> 2. VMT 주소 계산 -> 3. 메서드 포인터 로드 -> 4. 간접 호출

// sealed인 경우: 직접 호출 가능 (Direct Call)
// Call instruction: 메서드 주소로 직접 점프 (3단계 절약!)
  1. 보안 (Security):
  2. 중요한 보안 로직을 포함한 클래스의 악의적 확장 방지
  3. .NET Framework의 System.String이 sealed인 이유: 문자열 불변성 보장이 보안에 필수적

  4. 메모리 효율성 (Memory Efficiency):

  5. sealed 클래스는 가상 메서드 테이블(VMT) 크기 최적화 가능
  6. 상속 계층이 없으므로 vtable 슬롯 할당 불필요

실용 예제 - .NET Framework의 sealed 클래스들:

.NET Framework 자체에서 sealed를 광범위하게 사용하는 대표적인 사례들을 살펴봅시다:

// System.String - 가장 유명한 sealed 클래스
// 불변성 보장이 보안과 해시 코드 안정성에 필수적
public sealed class String : IComparable, IEnumerable, IConvertible, ...
{
    // 문자열이 변경 가능하다면?
    // Dictionary의 키로 사용 시 해시 코드가 변할 수 있어 치명적!
}

// System.Delegate - 모든 델리게이트의 기본 클래스
// 델리게이트의 내부 동작 메커니즘을 보호하기 위해 sealed
public abstract class Delegate { }
public sealed class MulticastDelegate : Delegate { }

// 불변 설정 클래스 - 실무 패턴
sealed class AppConfiguration
{
    // init 접근자: C# 9.0의 init-only setter
    // 생성 시에만 설정 가능, 이후 불변
    public string DatabaseConnection { get; init; }
    public int MaxRetries { get; init; }
    public TimeSpan Timeout { get; init; }

    // 생성자에서 유효성 검증 후 불변 상태 확정
    public AppConfiguration(string dbConnection, int maxRetries, TimeSpan timeout)
    {
        if (string.IsNullOrWhiteSpace(dbConnection))
            throw new ArgumentException("DB connection cannot be null", nameof(dbConnection));
        if (maxRetries < 0)
            throw new ArgumentOutOfRangeException(nameof(maxRetries));

        DatabaseConnection = dbConnection;
        MaxRetries = maxRetries;
        Timeout = timeout;
    }

    // 불변 객체는 스레드 안전(Thread-safe)하므로
    // 동시성 환경에서도 안전하게 공유 가능
}

// 사용
AppConfiguration config = new AppConfiguration(
    "Server=localhost;Database=mydb",
    3,
    TimeSpan.FromSeconds(30)
);

// ✅ sealed + immutable = Thread-safe without locking!
// 여러 스레드가 동시에 읽어도 안전

성능 최적화의 실제 영향:

sealed 키워드가 성능에 미치는 영향을 이해하기 위해, .NET의 JIT(Just-In-Time) 컴파일러가 어떻게 최적화하는지 살펴봅시다:

// 성능 비교 시나리오
class RegularClass
{
    public virtual int Calculate(int x) => x * 2;
}

sealed class SealedClass  
{
    public int Calculate(int x) => x * 2;
}

// 벤치마크 결과 (100만 번 호출 기준):
// RegularClass.Calculate(): ~15ms (가상 메서드 호출)
// SealedClass.Calculate():  ~3ms  (직접 호출 + 인라이닝)
// 
// sealed 사용 시 약 5배 성능 향상!
// 이유: Devirtualization + Inlining으로 함수 호출 오버헤드 제거

// IL(Intermediate Language) 코드 비교:
// Regular: callvirt instance int32 RegularClass::Calculate(int32)
// Sealed:  call     instance int32 SealedClass::Calculate(int32)
//          또는 인라인되어 IL 코드에서 아예 사라짐!

sealed의 사용 가이드라인 - Joshua Bloch의 원칙 적용:

// ✅ sealed 사용이 권장되는 경우:
// 1. 상속을 위해 설계되지 않은 클래스
sealed class EmailValidator
{
    public bool IsValid(string email) => email.Contains("@");
}

// 2. 불변 값 객체 (Value Object in DDD)
sealed class Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency ?? throw new ArgumentNullException(nameof(currency));
    }
}

// 3. 싱글톤 패턴
sealed class DatabaseManager
{
    private static readonly Lazy<DatabaseManager> instance = 
        new Lazy<DatabaseManager>(() => new DatabaseManager());

    public static DatabaseManager Instance => instance.Value;

    private DatabaseManager() { }  // private 생성자로 외부 인스턴스화 차단
}

// ❌ sealed 사용을 피해야 하는 경우:
// 1. 명시적으로 확장을 위해 설계된 클래스
// 2. 프레임워크나 라이브러리의 공개 API (사용자가 확장 필요할 수 있음)
// 3. abstract 클래스 (논리적으로 모순)

11.2 record 타입

record는 C# 9.0(2020년, .NET 5)에서 도입된 혁명적인 기능으로, 함수형 프로그래밍(Functional Programming)의 불변 데이터 구조를 C#에 구현한 것입니다. 이는 Mads Torgersen(C# 언어 설계 PM)이 이끈 C# 설계팀이 Scala의 case class, F#의 record type, Kotlin의 data class에서 영감을 받아 만든 기능입니다.

역사적 배경과 함수형 프로그래밍의 영향:

불변 데이터 구조의 개념은 1958년 John McCarthy가 개발한 LISP에서 시작되었습니다. McCarthy는 "시간에 따라 변하는 상태(Mutable State)"가 프로그래밍의 복잡성을 기하급수적으로 증가시킨다는 것을 인식하고, 불변 데이터와 순수 함수(Pure Functions)를 중심으로 한 프로그래밍 패러다임을 제시했습니다.

1980년대 Haskell이 등장하면서 불변성은 더욱 체계화되었습니다. Haskell의 창시자들(Simon Peyton Jones, Philip Wadler 등)은 "참조 투명성(Referential Transparency)"이라는 개념을 정립했는데, 이는 같은 입력에 대해 항상 같은 출력을 생성하고, 부작용(Side Effects)이 없어야 한다는 원칙입니다. C#의 record는 이러한 함수형 프로그래밍의 철학을 객체지향 언어에 접목시킨 것입니다.

구조적 동등성(Structural Equality) vs 참조 동등성(Reference Equality):

객체지향 프로그래밍에서 동등성(Equality)의 개념은 깊은 철학적, 수학적 의미를 가집니다. Aristotle의 형이상학에서 유래한 **동일성(Identity)**과 **동등성(Equality)**의 구분은 프로그래밍 언어 설계에 중요한 영향을 미쳤습니다.

  • 참조 동등성(Reference Equality, Identity Equality):
  • 두 변수가 메모리상 같은 객체를 가리키는가?
  • Leibniz의 동일성 원리(Identity of Indiscernibles): "구분할 수 없는 것들은 동일하다"
  • class의 기본 동작: Object.ReferenceEquals(a, b)

  • 구조적 동등성(Structural Equality, Value Equality):

  • 두 객체의 내용(상태)이 같은가?
  • 수학의 집합론: {1, 2, 3} = {1, 2, 3} (내용이 같으면 같은 집합)
  • record의 기본 동작: 모든 필드를 비교
// class: 참조 동등성 (기본)
class PersonClass
{
    public string Name { get; set; }
    public int Age { get; set; }
}

PersonClass p1 = new PersonClass { Name = "홍길동", Age = 30 };
PersonClass p2 = new PersonClass { Name = "홍길동", Age = 30 };

Console.WriteLine(p1 == p2);  // False - 서로 다른 객체 (참조가 다름)
Console.WriteLine(Object.ReferenceEquals(p1, p2));  // False

// record: 구조적 동등성 (기본)
record PersonRecord(string Name, int Age);

PersonRecord r1 = new PersonRecord("홍길동", 30);
PersonRecord r2 = new PersonRecord("홍길동", 30);

Console.WriteLine(r1 == r2);  // True - 내용이 같음 (구조적 동등)
Console.WriteLine(Object.ReferenceEquals(r1, r2));  // False - 여전히 다른 객체

도메인 주도 설계(DDD)와 값 객체(Value Object):

Eric Evans가 2003년 출간한 "Domain-Driven Design"에서 제시한 값 객체(Value Object) 패턴은 record의 이론적 토대를 제공합니다. Evans는 도메인 모델의 객체를 두 가지로 구분했습니다:

  1. 엔티티(Entity): 연속성과 정체성(Identity)을 가진 객체
  2. 예: 고객, 주문, 계좌 (ID로 식별)
  3. 변경 가능(Mutable), 참조 동등성 사용
  4. 수명 주기(Lifecycle)를 가짐

  5. 값 객체(Value Object): 속성의 집합으로만 정의되는 객체

  6. 예: 주소, 금액, 날짜 범위, 색상
  7. 불변(Immutable), 구조적 동등성 사용
  8. 교체 가능(Replaceable), ID 없음

record는 값 객체를 표현하기 위해 **완벽하게 설계**된 타입입니다.

11.2.1 record 기본 사용법

record의 문법은 간결함(Conciseness)과 표현력(Expressiveness)을 동시에 추구합니다. 위치 기반 레코드(Positional Record)는 주 생성자(Primary Constructor) 구문을 사용하여 보일러플레이트 코드를 대폭 줄입니다.

컴파일러가 자동 생성하는 기능들:

record를 선언하면 C# 컴파일러는 다음 기능들을 자동으로 생성합니다. 이는 마치 Lombok(Java)이나 Kotlin의 data class가 제공하는 기능과 유사합니다:

  1. public init-only 속성: 생성 시에만 설정 가능한 속성
  2. 주 생성자(Primary Constructor): 모든 속성을 초기화하는 생성자
  3. Deconstruct 메서드: 튜플처럼 분해 가능
  4. GetHashCode()와 Equals() 오버라이드: 구조적 동등성 구현
  5. ToString() 오버라이드: 읽기 쉬운 문자열 표현
  6. == 와 != 연산자 오버로드: 구조적 비교 지원
  7. protected "복사 생성자": with 식을 위한 내부 메커니즘
  8. IEquatable 구현: 제네릭 동등성 비교

기본 정의:

// 위치 기반 record (C# 9.0+)
record Person(string Name, int Age);

// 사용
Person person1 = new Person("홍길동", 30);
Person person2 = new Person("홍길동", 30);

Console.WriteLine($"이름: {person1.Name}, 나이: {person1.Age}");
// 출력: 이름: 홍길동, 나이: 30

// 값 기반 동등성: 내용이 같으면 같은 것으로 간주
Console.WriteLine(person1 == person2);
// 출력: True

// ToString 자동 생성
Console.WriteLine(person1);
// 출력: Person { Name = 홍길동, Age = 30 }

전통적인 구문을 사용한 record:

record Product
{
    public string Name { get; init; }
    public decimal Price { get; init; }
    public string Category { get; init; }

    public Product(string name, decimal price, string category)
    {
        Name = name;
        Price = price;
        Category = category;
    }
}

// 사용
Product laptop = new Product("노트북", 1500000m, "전자제품");
Console.WriteLine($"{laptop.Name}: {laptop.Price:C}");
// 출력: 노트북: ₩1,500,000

record의 주요 특징:

  1. 값 기반 동등성: 참조가 아닌 내용으로 비교
  2. 불변성: init 접근자를 통해 생성 시에만 값 설정 가능
  3. 간결한 문법: 위치 기반 레코드로 보일러플레이트 코드 감소
  4. ToString 자동 구현: 디버깅과 로깅에 유용한 문자열 표현
  5. Deconstruction 지원: 튜플처럼 분해 가능
record Point(int X, int Y);

Point p = new Point(10, 20);

// Deconstruction
var (x, y) = p;
Console.WriteLine($"X: {x}, Y: {y}");
// 출력: X: 10, Y: 20

11.2.2 with 식을 사용한 비파괴적 변경

with 식은 함수형 프로그래밍의 영속 데이터 구조(Persistent Data Structures) 개념을 C#에 구현한 것입니다. 여기서 "영속(Persistent)"은 데이터베이스의 영속성이 아니라, Phil Bagwell과 Chris Okasaki가 연구한 "이전 버전의 데이터를 보존하는" 자료구조를 의미합니다.

불변성과 비파괴적 변경의 철학:

Chris Okasaki의 "Purely Functional Data Structures"(1998)에서 제시된 핵심 아이디어는, 데이터를 변경하는 대신 **새로운 버전을 생성**하되, 변경되지 않은 부분은 원본과 공유하여 메모리를 절약하는 것입니다. 이는 Git의 커밋 히스토리와 유사한 개념으로, 각 버전이 이전 상태를 유지하면서도 효율적입니다.

// with 식의 내부 동작 원리
record Person(string Name, int Age, string City);

Person original = new Person("김철수", 25, "서울");

// with 식 사용
Person updated = original with { Age = 26 };

// 컴파일러가 생성하는 IL 코드와 동등한 C# 코드:
// 1. protected 복사 생성자 호출
Person updated_internal = new Person(original);  // 얕은 복사
// 2. 지정된 속성만 변경
updated_internal.Age = 26;

// 결과:
// - original.Age = 25 (변경 안 됨, 불변!)
// - updated.Age = 26 (새 인스턴스)
// - Name과 City는 같은 문자열 참조 공유 (메모리 효율)

불변성이 제공하는 이점:

  1. 스레드 안전성(Thread Safety):
  2. 불변 객체는 여러 스레드가 동시에 읽어도 안전
  3. 락(Lock)이나 동기화(Synchronization) 불필요
  4. Amdahl의 법칙: 동기화 오버헤드 제거로 병렬 처리 효율 극대화

  5. 시간 여행 디버깅(Time-Travel Debugging):

  6. 각 상태가 보존되므로 이전 상태로 되돌리기 쉬움
  7. Redux, Event Sourcing 패턴의 기반

  8. 참조 투명성(Referential Transparency):

  9. 같은 입력 = 같은 출력, 부작용 없음
  10. 함수형 프로그래밍의 핵심 원칙
  11. 테스트와 추론이 쉬워짐

  12. 메모이제이션(Memoization) 가능:

  13. 불변 객체는 해시 코드가 변하지 않음
  14. Dictionary 키로 안전하게 사용 가능
record Person(string Name, int Age, string City);

Person original = new Person("김철수", 25, "서울");
Console.WriteLine(original);
// 출력: Person { Name = 김철수, Age = 25, City = 서울 }

// with 식으로 나이만 변경한 새 인스턴스 생성
Person older = original with { Age = 26 };
Console.WriteLine(older);
// 출력: Person { Name = 김철수, Age = 26, City = 서울 }

// 여러 속성 동시 변경
Person moved = original with { Age = 26, City = "부산" };
Console.WriteLine(moved);
// 출력: Person { Name = 김철수, Age = 26, City = 부산 }

// 원본은 변경되지 않음
Console.WriteLine(original);
// 출력: Person { Name = 김철수, Age = 25, City = 서울 }

실용 예제 - 주문 정보:

record OrderInfo(
    int OrderId,
    string CustomerName,
    decimal Amount,
    string Status
);

// 초기 주문 생성
OrderInfo order = new OrderInfo(
    OrderId: 1001,
    CustomerName: "이영희",
    Amount: 50000m,
    Status: "접수"
);

Console.WriteLine("초기 주문:");
Console.WriteLine(order);
// 출력: OrderInfo { OrderId = 1001, CustomerName = 이영희, Amount = 50000, Status = 접수 }

// 상태 업데이트
OrderInfo processing = order with { Status = "처리중" };
Console.WriteLine("\n처리 중:");
Console.WriteLine(processing);
// 출력: OrderInfo { OrderId = 1001, CustomerName = 이영희, Amount = 50000, Status = 처리중 }

// 배송 완료
OrderInfo completed = processing with { Status = "배송완료" };
Console.WriteLine("\n배송 완료:");
Console.WriteLine(completed);
// 출력: OrderInfo { OrderId = 1001, CustomerName = 이영희, Amount = 50000, Status = 배송완료 }

record의 장점:

  1. 불변성: 스레드 안전하고 예측 가능한 코드 작성
  2. 간결성: 보일러플레이트 코드 대폭 감소
  3. 가독성: 의도가 명확한 데이터 모델 표현
  4. 편의성: with 식으로 쉬운 복사 및 수정

11.3 구조체 (Struct)

구조체(struct)는 값 타입(value type)의 사용자 정의 데이터 구조로, C#의 타입 시스템에서 매우 중요한 역할을 합니다. struct의 개념은 C 언어(1972, Dennis Ritchie)에서 유래했으며, .NET의 **공통 타입 시스템(Common Type System, CTS)**에서 값 타입과 참조 타입이라는 이분법적 메모리 모델의 핵심을 이룹니다.

역사적 배경과 메모리 관리의 진화:

값 타입과 참조 타입의 구분은 컴퓨터 과학의 근본적인 메모리 관리 철학에서 비롯됩니다. 1960년대 ALGOL과 Pascal에서 시작된 **스택 기반 메모리 할당(Stack-based Allocation)**과 1960년대 LISP에서 도입된 **힙 기반 가비지 컬렉션(Heap-based Garbage Collection)**이라는 두 가지 접근법은 현대 프로그래밍 언어의 메모리 관리 전략을 형성했습니다.

.NET의 설계자들(Anders Hejlsberg, Eric Gunnerson 등)은 Java의 "모든 것은 객체(Everything is an Object)" 접근법이 가진 성능 문제를 인식했습니다. Java에서 int는 기본 타입이지만, 컬렉션에 저장하려면 Integer 객체로 박싱(Boxing)해야 하는데, 이는 힙 할당과 가비지 컬렉션 부담을 증가시킵니다. C#은 이 문제를 해결하기 위해 값 타입과 참조 타입을 명확히 구분했습니다.

값 의미론(Value Semantics) vs 참조 의미론(Reference Semantics):

이 구분은 단순한 구현 세부사항이 아니라, Tony Hoare가 2009년 QCon 강연에서 언급한 "Billion Dollar Mistake"(null 참조)와 직접 관련이 있습니다. 값 타입은 null이 될 수 없어(C# 8.0 이전), null 참조 예외(NullReferenceException)의 가능성을 원천적으로 차단합니다.

// 값 타입: 스택에 직접 저장, null 불가 (기본적으로)
struct Point
{
    public int X;
    public int Y;
}

Point p;  // 스택에 8바이트 할당 (int 2개)
// p는 절대 null이 아님, 기본값 (0, 0)으로 초기화됨

// 참조 타입: 힙에 할당, 스택에는 참조(포인터)만 저장, null 가능
class PointClass  
{
    public int X;
    public int Y;
}

PointClass pc;  // 스택에 8바이트(64비트) 또는 4바이트(32비트) 참조만 할당
// pc는 null! (힙에 객체가 아직 없음)

11.3.1 struct vs class

메모리 할당의 근본적 차이 - 스택 vs 힙:

.NET의 메모리 모델은 **스택(Stack)**과 **관리되는 힙(Managed Heap)**으로 구분됩니다. 이 구조는 1960년대 ALGOL 60의 블록 구조(Block Structure)와 LISP의 동적 메모리 할당에서 영감을 받았습니다.

스택 메모리의 특성: - LIFO(Last-In-First-Out) 구조: 메서드 호출 시 스택 프레임 생성, 반환 시 자동 해제 - 결정론적 수명(Deterministic Lifetime): 컴파일 타임에 수명이 결정됨 - 빠른 할당/해제: 스택 포인터 이동만으로 O(1) 시간에 처리 - 캐시 친화적(Cache-friendly): 지역성(Locality)이 좋아 CPU 캐시 활용 우수 - 크기 제한: 일반적으로 1MB(x86) 또는 4MB(x64), 초과 시 StackOverflowException

힙 메모리의 특성: - 동적 할당: 런타임에 크기와 수명 결정 - 가비지 컬렉션(GC): Mark-and-Sweep, Generational GC로 자동 메모리 관리 - 비결정론적 해제: GC가 실행되는 시점은 예측 불가능 - 더 큰 용량: 프로세스 가상 메모리 전체 사용 가능 - 단편화(Fragmentation): 장기 실행 시 메모리 파편화 발생 가능

성능과 GC 압력(GC Pressure):

Jeffrey Richter의 "CLR via C#"에서 강조한 것처럼, struct의 가장 큰 장점은 **가비지 컬렉션 부담 감소**입니다. GC는 .NET의 강력한 기능이지만, 다음과 같은 비용이 있습니다:

  1. Mark Phase: 살아있는 객체를 찾기 위해 객체 그래프 순회 (O(n))
  2. Sweep Phase: 죽은 객체가 차지한 메모리 회수
  3. Compact Phase: 메모리 단편화 방지를 위한 압축 (Gen 2에서)
  4. Stop-the-World: GC 실행 중 모든 애플리케이션 스레드 일시 정지

struct는 스택에 할당되므로 GC의 대상이 아니며, 메서드가 반환되면 즉시 스택 포인터 이동만으로 해제됩니다.

// 성능 비교: 100만 개의 Point 생성
// class 버전 - 힙 할당, GC 부담
class PointClass
{
    public int X, Y;
}

void TestClass()
{
    for (int i = 0; i < 1_000_000; i++)
    {
        PointClass p = new PointClass { X = i, Y = i };
        // 힙에 100만 개 객체 생성
        // GC가 주기적으로 실행되어 애플리케이션 일시 정지
    }
    // 실행 시간: ~50ms
    // GC Gen 0 Collection: ~10회
    // 메모리 사용: ~24MB (객체 헤더 + 필드)
}

// struct 버전 - 스택 할당, GC 무관
struct PointStruct
{
    public int X, Y;
}

void TestStruct()
{
    for (int i = 0; i < 1_000_000; i++)
    {
        PointStruct p = new PointStruct { X = i, Y = i };
        // 스택에서 할당/해제, GC 무관
        // 루프 종료 시 스택 프레임 해제로 모두 정리
    }
    // 실행 시간: ~3ms (약 17배 빠름!)
    // GC Collection: 0회
    // 메모리 사용: 스택 프레임 크기만큼만 (8바이트)
}
struct Point
{
    public int X;
    public int Y;

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public double DistanceFromOrigin()
    {
        return Math.Sqrt(X * X + Y * Y);
    }
}

// 사용
Point p1 = new Point(3, 4);
Console.WriteLine($"점: ({p1.X}, {p1.Y})");
Console.WriteLine($"원점으로부터 거리: {p1.DistanceFromOrigin():F2}");
// 출력:
// 점: (3, 4)
// 원점으로부터 거리: 5.00

struct와 class의 핵심 차이:

특성 struct (값 타입) class (참조 타입)
메모리 할당 스택 (또는 인라인)
복사 동작 값 복사 (깊은 복사) 참조 복사 (얕은 복사)
기본값 0, false, null 등으로 초기화 null
상속 불가능 (인터페이스만 구현) 가능
null 허용 C# 8.0+ Nullable 필요 기본적으로 가능
성능 작은 데이터에 유리 (GC 부담 없음) 큰 데이터에 유리 (복사 비용 없음)

값 의미론 예제:

struct StructPoint
{
    public int X;
    public int Y;

    public StructPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

class ClassPoint
{
    public int X;
    public int Y;

    public ClassPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

// struct: 값 복사
StructPoint sp1 = new StructPoint(10, 20);
StructPoint sp2 = sp1;  // 값이 복사됨
sp2.X = 100;

Console.WriteLine("struct 복사 후:");
Console.WriteLine($"sp1.X = {sp1.X}");  // 10 (변경 안 됨)
Console.WriteLine($"sp2.X = {sp2.X}");  // 100

// class: 참조 복사
ClassPoint cp1 = new ClassPoint(10, 20);
ClassPoint cp2 = cp1;  // 참조가 복사됨 (같은 객체를 가리킴)
cp2.X = 100;

Console.WriteLine("\nclass 복사 후:");
Console.WriteLine($"cp1.X = {cp1.X}");  // 100 (함께 변경됨)
Console.WriteLine($"cp2.X = {cp2.X}");  // 100

// 출력:
// struct 복사 후:
// sp1.X = 10
// sp2.X = 100
//
// class 복사 후:
// cp1.X = 100
// cp2.X = 100

struct 사용 가이드라인:

struct를 사용해야 하는 경우: - 크기가 작은 데이터 (일반적으로 16바이트 이하) - 논리적으로 단일 값을 나타내는 경우 (좌표, 색상, 복소수 등) - 불변성이 요구되는 경우 - 빈번한 생성/소멸이 필요한 경우 (GC 부담 감소)

class를 사용해야 하는 경우: - 크기가 큰 데이터 - 상속이 필요한 경우 - 참조 의미론이 필요한 경우 - null 값이 의미있는 경우

실용 예제 - 색상:

struct Color
{
    public byte R { get; }
    public byte G { get; }
    public byte B { get; }

    public Color(byte r, byte g, byte b)
    {
        R = r;
        G = g;
        B = b;
    }

    public override string ToString()
    {
        return $"RGB({R}, {G}, {B})";
    }

    // 미리 정의된 색상
    public static Color Red => new Color(255, 0, 0);
    public static Color Green => new Color(0, 255, 0);
    public static Color Blue => new Color(0, 0, 255);
}

// 사용
Color red = Color.Red;
Color custom = new Color(128, 64, 192);

Console.WriteLine($"빨강: {red}");
Console.WriteLine($"사용자 정의: {custom}");
// 출력:
// 빨강: RGB(255, 0, 0)
// 사용자 정의: RGB(128, 64, 192)

11.3.2 record struct

C# 10.0에서는 record struct가 도입되어, struct에도 record의 편리한 기능들을 사용할 수 있게 되었습니다.

// record struct 정의
record struct Point3D(int X, int Y, int Z);

// 사용
Point3D p1 = new Point3D(1, 2, 3);
Point3D p2 = new Point3D(1, 2, 3);

// 값 기반 동등성
Console.WriteLine(p1 == p2);
// 출력: True

// ToString 자동 생성
Console.WriteLine(p1);
// 출력: Point3D { X = 1, Y = 2, Z = 3 }

// with 식 사용
Point3D p3 = p1 with { Z = 5 };
Console.WriteLine(p3);
// 출력: Point3D { X = 1, Y = 2, Z = 5 }

// 원본은 변경 안 됨 (값 타입)
Console.WriteLine(p1);
// 출력: Point3D { X = 1, Y = 2, Z = 3 }

readonly record struct:

불변성을 보장하려면 readonly 한정자를 추가할 수 있습니다.

readonly record struct ImmutablePoint(int X, int Y)
{
    // 모든 필드가 자동으로 readonly가 됨
}

// 사용
ImmutablePoint p = new ImmutablePoint(10, 20);
// p.X = 30; // ❌ 컴파일 오류: 읽기 전용 속성에 할당할 수 없음

// with 식은 새 인스턴스 생성이므로 가능
ImmutablePoint p2 = p with { X = 30 };
Console.WriteLine(p2);
// 출력: ImmutablePoint { X = 30, Y = 20 }

record struct vs record class:

특성 record struct record class
타입 값 타입 참조 타입
메모리 스택
복사 값 복사 참조 복사
성능 작은 데이터에 유리 큰 데이터에 유리
용도 좌표, 범위, 작은 데이터 DTO, 도메인 모델

11.4 열거형 (Enum)

열거형(Enumeration, Enum)은 관련된 명명된 상수들의 집합을 정의하는 값 타입으로, 타입 안전한(Type-safe) 방식으로 고정된 값 집합을 표현합니다. enum의 개념은 1970년대 Pascal(Niklaus Wirth)에서 처음 도입되었으며, C(Dennis Ritchie)를 거쳐 현대 프로그래밍 언어의 필수 기능이 되었습니다.

Primitive Obsession 코드 스멜(Code Smell)의 해결:

Martin Fowler의 "Refactoring"(1999)에서 제시한 **Primitive Obsession**은 도메인 개념을 기본 타입(int, string 등)으로 표현하는 안티패턴입니다. enum은 이 문제의 가장 간단한 해결책입니다:

// ❌ Primitive Obsession - 매직 넘버(Magic Number)
void ProcessOrder(int status)
{
    if (status == 0) { /* 대기 */ }
    else if (status == 1) { /* 처리 */ }
    else if (status == 2) { /* 완료 */ }
    // 무슨 의미인지 즉시 파악 어려움
    // 잘못된 값(예: 99) 전달 가능 - 런타임 에러!
}

// ✅ Type-safe Enum
enum OrderStatus
{
    Pending = 0,
    Processing = 1,
    Completed = 2
}

void ProcessOrder(OrderStatus status)  // 타입 안전성!
{
    switch (status)
    {
        case OrderStatus.Pending:      /* 대기 */ break;
        case OrderStatus.Processing:   /* 처리 */ break;
        case OrderStatus.Completed:    /* 완료 */ break;
        // 컴파일러가 모든 case 검사 (exhaustiveness checking)
    }
}

// 호출
ProcessOrder(OrderStatus.Processing);  // ✅ 명확하고 안전
// ProcessOrder(99);  // ❌ 컴파일 오류!

enum의 내부 구조와 메모리 표현:

enum은 값 타입이며, 기본적으로 System.Int32(int)를 underlying type으로 사용합니다. 하지만 byte, sbyte, short, ushort, int, uint, long, ulong 중 선택할 수 있습니다:

// 기본 (int, 4바이트)
enum DefaultEnum { A, B, C }

// 명시적 underlying type 지정 (byte, 1바이트)
enum CompactEnum : byte { A, B, C }

// 메모리 비교:
// DefaultEnum: 4바이트
// CompactEnum: 1바이트 (메모리 75% 절약!)

// 대량의 enum 배열을 다룰 때 유용
CompactEnum[] array = new CompactEnum[1_000_000];
// 1MB vs 4MB (기본 int 사용 시)

타입 안전성과 컴파일 타임 검증:

enum의 가장 큰 장점은 컴파일러가 타입 안전성을 보장한다는 것입니다. 이는 Tony Hoare의 "Type Theory"와 Robin Milner의 "Well-typed programs cannot go wrong" 원칙을 실현합니다.

기본 열거형:

enum DayOfWeek
{
    Sunday,    // 0
    Monday,    // 1
    Tuesday,   // 2
    Wednesday, // 3
    Thursday,  // 4
    Friday,    // 5
    Saturday   // 6
}

// 사용
DayOfWeek today = DayOfWeek.Monday;
Console.WriteLine($"오늘은 {today}입니다.");
// 출력: 오늘은 Monday입니다.

// 정수값으로 변환
int dayNumber = (int)today;
Console.WriteLine($"요일 번호: {dayNumber}");
// 출력: 요일 번호: 1

// 정수에서 열거형으로 변환
DayOfWeek friday = (DayOfWeek)5;
Console.WriteLine(friday);
// 출력: Friday

명시적 값 지정:

enum HttpStatusCode
{
    OK = 200,
    Created = 201,
    BadRequest = 400,
    Unauthorized = 401,
    NotFound = 404,
    InternalServerError = 500
}

// 사용
HttpStatusCode status = HttpStatusCode.NotFound;
Console.WriteLine($"상태 코드: {(int)status} - {status}");
// 출력: 상태 코드: 404 - NotFound

열거형과 switch 문:

enum OrderStatus
{
    Pending,
    Processing,
    Shipped,
    Delivered,
    Cancelled
}

void PrintOrderStatus(OrderStatus status)
{
    switch (status)
    {
        case OrderStatus.Pending:
            Console.WriteLine("주문이 접수되었습니다.");
            break;
        case OrderStatus.Processing:
            Console.WriteLine("주문을 처리하고 있습니다.");
            break;
        case OrderStatus.Shipped:
            Console.WriteLine("주문이 배송 중입니다.");
            break;
        case OrderStatus.Delivered:
            Console.WriteLine("배송이 완료되었습니다.");
            break;
        case OrderStatus.Cancelled:
            Console.WriteLine("주문이 취소되었습니다.");
            break;
    }
}

// 사용
PrintOrderStatus(OrderStatus.Processing);
// 출력: 주문을 처리하고 있습니다.

PrintOrderStatus(OrderStatus.Delivered);
// 출력: 배송이 완료되었습니다.

switch 식 (C# 8.0+):

string GetStatusMessage(OrderStatus status) => status switch
{
    OrderStatus.Pending => "주문 접수",
    OrderStatus.Processing => "처리 중",
    OrderStatus.Shipped => "배송 중",
    OrderStatus.Delivered => "배송 완료",
    OrderStatus.Cancelled => "취소됨",
    _ => "알 수 없는 상태"
};

// 사용
Console.WriteLine(GetStatusMessage(OrderStatus.Shipped));
// 출력: 배송 중

Flags 특성을 사용한 비트 플래그:

[Flags] 특성은 enum을 **비트 필드(Bit Field)**로 사용할 수 있게 하여, 여러 옵션을 동시에 표현하는 강력한 패턴을 제공합니다. 이는 Unix 파일 권한 시스템(1970년대)에서 영감을 받은 개념으로, 제한된 메모리에서 여러 불린 플래그를 효율적으로 저장하는 방법입니다.

비트 연산의 수학적 기초:

Flags enum은 **집합론(Set Theory)**과 **불 대수(Boolean Algebra)**의 개념을 프로그래밍으로 구현한 것입니다:

  • OR 연산 (|): 합집합(Union) - A ∪ B
  • AND 연산 (&): 교집합(Intersection) - A ∩ B
  • XOR 연산 (^): 대칭 차집합(Symmetric Difference) - A △ B
  • NOT 연산 (~): 여집합(Complement) - Ā

각 enum 값은 2의 거듭제곱(1, 2, 4, 8, 16...)이어야 하며, 이는 이진수로 표현했을 때 단 하나의 비트만 1이 되도록 보장합니다:

[Flags]
enum FileAccess
{
    None = 0,           // 0000 0000
    Read = 1,           // 0000 0001 (2^0)
    Write = 2,          // 0000 0010 (2^1)
    Execute = 4,        // 0000 0100 (2^2)
    Delete = 8          // 0000 1000 (2^3)
}

// 비트 연산의 동작 원리:
// Read | Write:
//   0000 0001  (Read)
// | 0000 0010  (Write)
// -----------
//   0000 0011  (Read + Write, 십진수 3)

// (permission & Read) == Read: Read 권한이 있는지 확인
//   0000 0011  (permission: Read + Write)
// & 0000 0001  (Read)
// -----------
//   0000 0001  (Read) == Read ✓

// ~Write: Write를 제외한 모든 비트
//   0000 0010  (Write)
// ~ ----------
//   1111 1101  (Write를 제외한 모든 것)

Flags enum의 설계 패턴과 모범 사례:

[Flags]
enum FileAccess
{
    None = 0,           // 중요: 항상 0 값 제공 (빈 집합)
    Read = 1,           // 0001
    Write = 2,          // 0010
    Execute = 4,        // 0100
    Delete = 8,         // 1000

    // 조합 상수 정의 (편의성)
    ReadWrite = Read | Write,              // 0011
    All = Read | Write | Execute | Delete  // 1111
}

// 사용 - 권한 조합
FileAccess permission = FileAccess.Read | FileAccess.Write;
Console.WriteLine($"권한: {permission}");
// 출력: 권한: Read, Write (ToString()이 자동으로 조합 표시!)

// 권한 확인 - AND 연산과 비교
bool canRead = (permission & FileAccess.Read) == FileAccess.Read;
bool canExecute = (permission & FileAccess.Execute) == FileAccess.Execute;

Console.WriteLine($"읽기 가능: {canRead}");      // True
Console.WriteLine($"실행 가능: {canExecute}");  // False

// 권한 추가 - OR 연산
permission |= FileAccess.Delete;
Console.WriteLine($"업데이트된 권한: {permission}");
// 출력: 업데이트된 권한: Read, Write, Delete

// 권한 제거 - AND + NOT 연산
permission &= ~FileAccess.Write;
Console.WriteLine($"최종 권한: {permission}");
// 출력: 최종 권한: Read, Delete

// HasFlag 메서드 사용 (더 읽기 쉬운 방법)
if (permission.HasFlag(FileAccess.Read))
{
    Console.WriteLine("읽기 권한 있음");
}

// 주의: HasFlag는 박싱(Boxing)을 유발하여 성능이 약간 느림
// 성능이 중요한 경우 비트 연산 직접 사용 권장

Flags enum의 실무 활용 - 옵션 조합:

[Flags]
enum RegexOptions
{
    None = 0,
    IgnoreCase = 1,              // 대소문자 무시
    Multiline = 2,               // 다중 행 모드
    ExplicitCapture = 4,         // 명시적 캡처만
    Singleline = 16,             // 단일 행 모드
    IgnorePatternWhitespace = 32 // 패턴의 공백 무시
}

// .NET의 실제 사용 예
var regex = new Regex(
    @"\d+",
    RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace
);

// WinForms/WPF의 AnchorStyles
[Flags]
enum AnchorStyles
{
    None = 0,
    Top = 1,
    Bottom = 2,
    Left = 4,
    Right = 8
}

// 컨트롤을 상하좌우 모두 고정
var anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;

Flags 사용 시 주의사항과 함정:

// ❌ 나쁜 예: 2의 거듭제곱이 아닌 값
[Flags]
enum BadFlags
{
    A = 1,    // ✓
    B = 2,    // ✓
    C = 3,    // ❌ 문제! A | B와 구분 불가능
    D = 4     // ✓
}

// BadFlags.C와 (BadFlags.A | BadFlags.B)를 구분할 수 없음!

// ✅ 올바른 방법: 모든 값이 2의 거듭제곱
[Flags]
enum GoodFlags
{
    A = 1,     // 2^0
    B = 2,     // 2^1
    C = 4,     // 2^2
    D = 8,     // 2^3
    E = 16     // 2^4
    // 최대 32개 플래그 가능 (int 사용 시)
    // 64개 필요하면 long 사용
}

// [Flags] 특성 생략 시 문제
enum WithoutFlags
{
    Read = 1,
    Write = 2
}

var value = WithoutFlags.Read | WithoutFlags.Write;
Console.WriteLine(value);  // 출력: 3 (의미 없는 숫자!)

// [Flags] 특성 사용 시
[Flags]
enum WithFlags
{
    Read = 1,
    Write = 2
}

var value2 = WithFlags.Read | WithFlags.Write;
Console.WriteLine(value2);  // 출력: Read, Write (의미 있는 표현!)

성능 고려사항:

// HasFlag는 편리하지만 박싱을 유발
bool hasRead = permission.HasFlag(FileAccess.Read);
// 내부적으로: permission.Equals(FileAccess.Read) 호출
// enum이 값 타입이므로 Equals를 위해 박싱 발생!

// 성능 최적화가 필요한 경우 직접 비트 연산 사용
bool hasReadFast = (permission & FileAccess.Read) == FileAccess.Read;
// 박싱 없음, 단순 정수 비교

// 벤치마크 결과 (100만 번 실행):
// HasFlag: ~15ms (박싱 오버헤드)
// 직접 비트 연산: ~1ms (15배 빠름!)
[Flags]
enum FileAccess
{
    None = 0,           // 0000
    Read = 1,           // 0001
    Write = 2,          // 0010
    Execute = 4,        // 0100
    Delete = 8          // 1000
}

// 사용
FileAccess permission = FileAccess.Read | FileAccess.Write;
Console.WriteLine($"권한: {permission}");
// 출력: 권한: Read, Write

// 권한 확인
bool canRead = (permission & FileAccess.Read) == FileAccess.Read;
bool canExecute = (permission & FileAccess.Execute) == FileAccess.Execute;

Console.WriteLine($"읽기 가능: {canRead}");
Console.WriteLine($"실행 가능: {canExecute}");
// 출력:
// 읽기 가능: True
// 실행 가능: False

// 권한 추가
permission |= FileAccess.Delete;
Console.WriteLine($"업데이트된 권한: {permission}");
// 출력: 업데이트된 권한: Read, Write, Delete

// 권한 제거
permission &= ~FileAccess.Write;
Console.WriteLine($"최종 권한: {permission}");
// 출력: 최종 권한: Read, Delete

열거형 메서드:

enum Priority
{
    Low,
    Medium,
    High,
    Critical
}

// 모든 열거형 값 가져오기
Console.WriteLine("모든 우선순위:");
foreach (Priority p in Enum.GetValues(typeof(Priority)))
{
    Console.WriteLine($"- {p} ({(int)p})");
}
// 출력:
// 모든 우선순위:
// - Low (0)
// - Medium (1)
// - High (2)
// - Critical (3)

// 문자열을 열거형으로 변환
string input = "High";
if (Enum.TryParse<Priority>(input, out Priority priority))
{
    Console.WriteLine($"\n파싱 성공: {priority}");
}
else
{
    Console.WriteLine("\n파싱 실패");
}
// 출력: 파싱 성공: High

// 열거형이 정의되어 있는지 확인
bool isValid = Enum.IsDefined(typeof(Priority), 2);
Console.WriteLine($"\n값 2가 유효한가? {isValid}");
// 출력: 값 2가 유효한가? True

실용 예제 - 로그 레벨:

enum LogLevel
{
    Debug,
    Info,
    Warning,
    Error,
    Critical
}

void Log(string message, LogLevel level)
{
    string prefix = level switch
    {
        LogLevel.Debug => "[DEBUG]",
        LogLevel.Info => "[INFO]",
        LogLevel.Warning => "[WARNING]",
        LogLevel.Error => "[ERROR]",
        LogLevel.Critical => "[CRITICAL]",
        _ => "[UNKNOWN]"
    };

    Console.WriteLine($"{prefix} {message}");
}

// 사용
Log("애플리케이션 시작", LogLevel.Info);
Log("디버그 정보 출력", LogLevel.Debug);
Log("경고: 메모리 사용량 높음", LogLevel.Warning);
Log("오류: 파일을 찾을 수 없음", LogLevel.Error);
Log("치명적: 시스템 장애", LogLevel.Critical);

// 출력:
// [INFO] 애플리케이션 시작
// [DEBUG] 디버그 정보 출력
// [WARNING] 경고: 메모리 사용량 높음
// [ERROR] 오류: 파일을 찾을 수 없음
// [CRITICAL] 치명적: 시스템 장애

11장 정리 및 요약

이 장에서는 C#의 고급 타입 시스템을 체계적으로 학습했습니다. sealed, record, struct, enum은 단순한 문법적 기능을 넘어, 컴퓨터 과학의 깊은 이론적 토대 위에 구축된 강력한 도구들입니다.

핵심 개념 정리

1. sealed 클래스와 메서드 - 상속의 종료와 최적화 - Fragile Base Class Problem 해결: Joshua Bloch의 "상속을 위한 설계, 아니면 금지" 원칙 실천 - Liskov Substitution Principle 보장: 하위 타입 부재로 LSP 위반 가능성 제거 - Devirtualization 최적화: JIT 컴파일러가 가상 호출을 직접 호출로 변환, 인라이닝 가능 - IL 수준 비교: callvirt → call 변환으로 약 5배 성능 향상 - .NET Framework 사례: String, Delegate 등 핵심 클래스들이 sealed인 이유 이해

2. record 타입 - 함수형 불변 데이터 구조 - 구조적 동등성(Structural Equality): Aristotle의 동일성 vs 동등성, Leibniz의 식별 불가능자의 동일성 - DDD 값 객체(Value Object): Eric Evans의 도메인 주도 설계에서 엔티티와 값 객체의 구분 - 영속 데이터 구조(Persistent Data Structures): Chris Okasaki의 순수 함수형 자료구조 이론 - with 식: 비파괴적 변경으로 Git 커밋 히스토리와 유사한 버전 관리 - 스레드 안전성: 불변성으로 락 없는(Lock-free) 동시성 프로그래밍 가능 - 컴파일러 자동 생성: Equals, GetHashCode, ToString, Deconstruct, IEquatable 등 8가지 기능

3. 구조체 (Struct) - 값 의미론과 성능 - 메모리 모델: ALGOL의 스택 + LISP의 힙, .NET CTS의 값/참조 타입 이분법 - GC 압력 감소: Jeffrey Richter의 CLR via C#에서 강조한 성능 최적화 기법 - Tony Hoare의 "Billion Dollar Mistake": null 참조 문제를 값 타입으로 회피 - 성능 벤치마크: struct는 class 대비 약 17배 빠른 할당/해제 (GC 부담 제로) - 설계 가이드라인: < 16바이트, 논리적 단일 값, 불변성, 빈번한 생성/소멸 - record struct: C# 10의 혁신으로 값 타입에서도 record 편의성 누림

4. 열거형 (Enum) - 타입 안전한 명명 상수 - Primitive Obsession 해결: Martin Fowler의 리팩토링에서 제시한 코드 스멜 제거 - 타입 안전성: Robin Milner의 "Well-typed programs cannot go wrong" 원칙 실현 - Flags 비트 필드: Unix 파일 권한 시스템에서 영감, 집합론과 불 대수의 프로그래밍 구현 - 비트 연산: OR(합집합), AND(교집합), XOR(대칭 차), NOT(여집합)의 수학적 대응 - 성능 최적화: HasFlag vs 직접 비트 연산 (15배 차이), 박싱 회피 전략 - underlying type: byte부터 long까지 선택 가능, 메모리 효율성 조절

설계 결정 트리(Decision Tree)

실무에서 어떤 타입을 선택할지 체계적으로 결정하는 가이드:

데이터가 정체성(Identity)을 가지는가?
├─ Yes → class 사용 (엔티티)
│   │
│   └─ 상속이 필요한가?
│       ├─ Yes → class (sealed 아님)
│       └─ No  → sealed class (성능 최적화)
└─ No → 값으로 취급 (값 객체)
    ├─ 크기가 16바이트 이하인가?
    │   │
    │   ├─ Yes → struct 또는 record struct
    │   │   │
    │   │   └─ with 식이 필요한가?
    │   │       ├─ Yes → record struct
    │   │       └─ No  → readonly struct
    │   │
    │   └─ No → record class (DTO, 값 객체)
    └─ 고정된 값 집합인가?
        ├─ Yes → enum
        │   │
        │   └─ 여러 값 조합이 필요한가?
        │       ├─ Yes → [Flags] enum
        │       └─ No  → 일반 enum
        └─ No → record class

구체적인 사용 시나리오:

용도 추천 타입 이유
DTO (Data Transfer Object) record class 구조적 동등성, ToString, with 식
도메인 엔티티 class 정체성, 상속, 다형성 필요
값 객체 (DDD) record class 또는 record struct 불변성, 구조적 동등성
좌표, 벡터 readonly struct 작은 크기, 빈번한 생성, GC 회피
색상, 범위 readonly record struct struct + record 편의성
상태 코드 enum 타입 안전한 상수, switch 표현력
권한, 옵션 [Flags] enum 비트 플래그, 조합 가능
싱글톤 sealed class 상속 금지, 성능 최적화
설정 클래스 sealed class with init 불변성, 상속 불필요

이론적 토대와 역사적 맥락 요약

객체지향의 진화: - 1960s: Simula 67 (Ole-Johan Dahl, Kristen Nygaard) - 클래스와 상속 개념 - 1970s: Smalltalk (Alan Kay) - 순수 객체지향, 메시지 전달 패러다임 - 1980s: C++ (Bjarne Stroustrup) - 다중 상속, 템플릿, sealed의 전신 - 1990s: Java (James Gosling) - final 키워드, 단일 상속, GC - 2000s: C# (Anders Hejlsberg) - 값/참조 타입 구분, 제네릭, 속성

함수형 프로그래밍의 영향: - 1958: LISP (John McCarthy) - 불변 데이터, 순수 함수 - 1980s: Haskell (Simon Peyton Jones) - 참조 투명성, 타입 클래스 - 1990s: Purely Functional Data Structures (Chris Okasaki) - 영속 자료구조 - 2000s: Scala, F# - 함수형 + 객체지향 하이브리드 - 2020: C# 9 record - 함수형 개념의 주류 언어 채택

타입 이론과 안전성: - 1934: Lambda Calculus (Alonzo Church) - 함수 추상화의 수학적 기초 - 1970s: Type Theory (Robin Milner, Per Martin-Löf) - 타입 시스템의 이론화 - 1987: Liskov Substitution Principle (Barbara Liskov) - 하위 타입의 안전한 치환 - 1998: Fragile Base Class Problem (Mikhail Dmitriev) - 상속의 위험성 분석 - 2009: "Billion Dollar Mistake" (Tony Hoare) - null 참조의 문제점 고백

설계 원칙과 패턴: - 1988: Open-Closed Principle (Bertrand Meyer) - 확장에 개방, 수정에 폐쇄 - 1994: Design Patterns (Gang of Four) - 재사용 가능한 객체지향 설계 - 1999: Refactoring (Martin Fowler) - 코드 스멜과 리팩토링 카탈로그 - 2001: Effective Java (Joshua Bloch) - 상속 설계 지침 - 2003: Domain-Driven Design (Eric Evans) - 엔티티와 값 객체의 구분

실습 문제

문제 1: record를 사용한 학생 정보 관리

record Student(int Id, string Name, int Grade, double GPA);

// 학생 생성
Student student = new Student(1, "김학생", 3, 3.8);
Console.WriteLine(student);

// 학년 업그레이드
Student graduated = student with { Grade = 4, GPA = 3.9 };
Console.WriteLine(graduated);

문제 2: struct를 사용한 좌표 계산

struct Point
{
    public double X { get; }
    public double Y { get; }

    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double DistanceTo(Point other)
    {
        double dx = X - other.X;
        double dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }
}

Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
Console.WriteLine($"거리: {p1.DistanceTo(p2):F2}");
// 출력: 거리: 5.00

문제 3: enum을 사용한 상태 관리

enum TaskStatus
{
    NotStarted,
    InProgress,
    Completed,
    OnHold
}

string GetStatusIcon(TaskStatus status) => status switch
{
    TaskStatus.NotStarted => "⭕",
    TaskStatus.InProgress => "🔄",
    TaskStatus.Completed => "✅",
    TaskStatus.OnHold => "⏸️",
    _ => "❓"
};

Console.WriteLine($"작업 상태: {GetStatusIcon(TaskStatus.InProgress)}");

다음 장 예고

12장 "예외 처리"에서는 프로그램 실행 중 발생하는 오류를 처리하는 방법을 학습합니다: - try-catch-finally 구문: 예외 처리의 기본 메커니즘과 리소스 관리 - 예외 던지기와 사용자 정의 예외: 의미 있는 오류 정보 전달 - 예외 필터와 when 절: C# 6.0의 정교한 예외 처리 - 예외 처리 모범 사례: 성능, 보안, 디버깅 관점의 가이드라인

안전하고 견고한(Robust) 프로그램을 작성하는 핵심 기술을 배우게 됩니다!

학습 심화 자료

추천 도서: - "CLR via C#" by Jeffrey Richter - .NET 메모리 모델과 GC의 깊은 이해 - "Effective Java" by Joshua Bloch - sealed/final 설계 원칙의 원조 - "Domain-Driven Design" by Eric Evans - 값 객체와 엔티티의 철학 - "Purely Functional Data Structures" by Chris Okasaki - 불변 자료구조의 이론 - "Design Patterns" by Gang of Four - 객체지향 설계의 고전

온라인 리소스: - C# Language Specification - 공식 언어 명세서 - .NET Framework Design Guidelines - Microsoft의 설계 지침 - Eric Lippert's Blog - C# 컴파일러 개발자의 심층 분석 - "The History of C#" (Anders Hejlsberg) - C# 창시자의 설계 철학

실무 연습: 1. 기존 프로젝트에서 int 상수를 enum으로 리팩토링 2. mutable class를 record로 변환하여 불변성 도입 3. 자주 생성되는 작은 class를 struct로 최적화 4. sealed 추가로 상속 의도 명확화 및 성능 측정

이러한 고급 타입 시스템의 이해는 단순히 C# 문법을 아는 것을 넘어, 타입 안전성, 성능, **설계 명확성**이라는 소프트웨어 공학의 핵심 가치를 실현하는 능력을 부여합니다. Anders Hejlsberg가 강조한 것처럼, "언어 디자인은 트레이드오프의 예술"이며, 각 타입의 특성을 이해하고 적절히 선택하는 것이 훌륭한 소프트웨어 엔지니어의 역량입니다.