← C# 강의 목록으로
모던 C#
모던 C# · 선수: 제어문/클래스

21. 패턴 매칭

값의 모양에 따라 분기하는 강력한 도구가 패턴 매칭입니다. is·switch 식·속성 패턴·관계 패턴·논리 패턴·튜플 패턴까지 정리합니다.

C#.NET 8패턴 매칭switch
소요 시간
약 1~1.5시간
난이도
📊 중급
선수 조건
🎯 제어문 + 클래스
결과물
값의 모양에 따라 분기하는 강력한 도구가 패턴 매칭입니다. is·switch 식·속성 패턴·관계 패턴·논리 패턴·튜플 패턴까지 정리합니다.

이 강의에서 배우는 것

  • 1`is` 패턴으로 **타입 검사 + 변수 선언** 을 한 번에 한다
  • 2`switch` 식의 **타입 패턴 / 속성 패턴 / 튜플 패턴 / 리스트 패턴** 을 자유롭게 쓴다
  • 3`when` 절로 추가 조건을 단다
  • 4패턴 매칭이 어떻게 가독성과 안전성을 같이 끌어올리는지 본다

소개

C# 의 **패턴 매칭(pattern matching)** 은 단순한 `if/else` 사다리를 압축해 주는 동시에, 데이터의 **모양(shape)** 을 코드에 그대로 드러나게 합니다. .NET 8 에는 타입·속성·튜플·위치·리스트까지 거의 모든 형태의 패턴이 있습니다.

핵심 개념

1) `is` 패턴 — 타입 + 변수

csharp
if (obj is string s)
{
    Console.WriteLine(s.Length);   // s 는 이미 string 으로 캐스팅되어 있음
}

이전 스타일 `if (obj is string) { string s = (string)obj; ... }` 의 보일러플레이트가 사라집니다. `is not`, `and`, `or` 와 결합할 수도 있습니다.

csharp
if (n is int x and > 0) ...
if (c is 'a' or 'A') ...

2) `switch` 식 — 값을 돌려주는 분기

표현식이라 변수에 그대로 대입할 수 있습니다. `default` 자리는 `_` (discard).

csharp
string label = day switch
{
    1 => "월", 2 => "화", 3 => "수",
    _ => "기타",
};

3) 타입 패턴 — 상속/인터페이스 분기

csharp
string sound = animal switch
{
    Dog d  => $"{d.Name} 멍멍",
    Cat c  => $"{c.Name} 야옹",
    _      => "조용...",
};

4) 속성 패턴 — 객체 속성 검사

csharp
string zone = point switch
{
    { X: 0, Y: 0 }     => "원점",
    { X: 0 }           => "Y축 위",
    { Y: 0 }           => "X축 위",
    { X: > 0, Y: > 0 } => "1사분면",
    _                  => "기타",
};

비교 연산자 (`>`, `<=` ...) 도 그대로 쓸 수 있습니다.

5) 튜플 패턴 — 여러 값 동시에

csharp
(int x, int y) p = (1, 2);
string s = (x, y) switch
{
    (0, 0)                     => "원점",
    var (a, b) when a == b     => "대각선",
    _                          => "기타",
};

6) 리스트 패턴 (.NET 7+) — 길이 + 위치

csharp
int[] arr = [1, 2, 3];
string s = arr switch
{
    []                       => "비어 있음",
    [var only]               => $"하나만: {only}",
    [1, .., 3]               => "1로 시작 3으로 끝",
    [_, _, _]                => "정확히 세 개",
    [var first, .. var rest] => $"맨 앞 {first}, 나머지 {rest.Length}개",
};

`..` 은 "그 사이는 뭐든 OK", `var name` 은 슬라이스/요소를 변수로 받기.

7) `when` 절 — 추가 조건

csharp
string s = age switch
{
    int a when a < 0   => "잘못된 값",
    int a when a < 20  => "청소년",
    _                  => "성인",
};

핵심 예제

예제 1 — `IsPattern` : `is` 패턴

csharp
foreach (object item in items)
{
    if (item is string s)        Console.WriteLine($"문자열: {s}");
    else if (item is int n and > 0) Console.WriteLine($"양의 정수: {n}");
    else if (item is double d)   Console.WriteLine($"실수: {d}");
    else                         Console.WriteLine($"기타: {item}");
}

**실행 결과**

text
문자열(5자): hello
양의 정수: 42
실수: 3.14
기타: True
문자열(5자): world

**메모:** `is int n and > 0` 처럼 타입과 값 조건을 결합할 수 있어서 한 줄에 많은 의미를 담습니다.

예제 2 — `SwitchType` : 타입 패턴 + `switch` 식

csharp
static string Describe(Animal a) => a switch
{
    Dog d  => $"{d.Name}: 멍멍",
    Cat c  => $"{c.Name}: 야옹",
    Bird b => $"{b.Name}: 짹짹",
    _      => "알 수 없는 동물",
};

**실행 결과**

text
바둑이: 멍멍
나비: 야옹
짹짹: 짹짹
쫑: 멍멍

**메모:** 옛 다형성 `virtual Sound()` 와 비교해 보세요. 동물 종류 추가가 적고 분기 로직이 한 곳에 모이면 패턴 매칭이 더 깔끔합니다.

예제 3 — `PropertyPattern` : 속성 패턴

csharp
string zone = p switch
{
    { X: 0, Y: 0 }     => "원점",
    { X: 0 }           => "Y축 위",
    { Y: 0 }           => "X축 위",
    { X: > 0, Y: > 0 } => "1사분면",
    { X: < 0, Y: < 0 } => "3사분면",
    _                  => "기타 사분면",
};

**실행 결과**

text
(0, 0) → 원점
(0, 5) → Y축 위
(7, 0) → X축 위
(3, 4) → 1사분면
(-1, -2) → 3사분면

**메모:** `switch` 식의 분기는 **위에서 아래로** 먼저 매칭되는 항목을 고릅니다. 더 구체적인 조건을 위에 두세요.

예제 4 — `TuplePattern` : 가위바위보

csharp
string result = (p1, p2) switch
{
    ("rock",     "scissors") => "P1 승",
    ("paper",    "rock")     => "P1 승",
    ("scissors", "paper")    => "P1 승",
    var (a, b) when a == b   => "비김",
    _                        => "P2 승",
};

**실행 결과**

text
rock     vs scissors : P1 승
paper    vs rock     : P1 승
scissors vs scissors : 비김
rock     vs paper    : P2 승

**메모:** 룰 테이블을 코드로 그대로 표현할 수 있습니다. `var (a, b) when ...` 으로 변수 분해 + 조건 부착.

예제 5 — `ListPattern` : 리스트 패턴

csharp
string desc = arr switch
{
    []              => "비어 있음",
    [var only]      => $"원소 한 개 ({only})",
    [1, .., 3]      => "1로 시작 3으로 끝",
    [_, _, _]       => "정확히 세 개",
    [var first, .. var rest] => $"맨 앞 {first}, 나머지 {rest.Length}개",
};

**실행 결과**

text
[] → 비어 있음
[42] → 원소 한 개 (42)
[1,2,3] → 1로 시작 3으로 끝
[1,99,3] → 1로 시작 3으로 끝
[1,2,3,4,5] → 1로 시작 3으로 끝
[9,8,7] → 정확히 세 개

**메모:** `[1, .., 3]` 가 `[_, _, _]` 보다 위에 있으므로 `[1,2,3]` 도 첫 번째 분기에서 잡힙니다. 우선순위를 항상 의식하세요.

전체 예제 코드 (src/)

src/IsPattern/IsPattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/IsPattern/Program.cs

csharp
// is 패턴 — 타입 체크 + 변수 선언을 한 번에.
object[] items = ["hello", 42, 3.14, true, "world"];

foreach (object item in items)
{
    // declaration pattern : 타입에 맞으면 변수 s 에 캐스팅된 채로 담아 준다.
    if (item is string s)
    {
        Console.WriteLine($"문자열({s.Length}자): {s}");
    }
    // 결합도 가능
    else if (item is int n and > 0)
    {
        Console.WriteLine($"양의 정수: {n}");
    }
    else if (item is double d)
    {
        Console.WriteLine($"실수: {d}");
    }
    else
    {
        Console.WriteLine($"기타: {item}");
    }
}

src/ListPattern/ListPattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/ListPattern/Program.cs

csharp
// 리스트/배열 패턴 (.NET 7+) — 길이와 위치를 함께 검사.
int[][] arrays =
[
    [],
    [42],
    [1, 2, 3],
    [1, 99, 3],
    [1, 2, 3, 4, 5],
    [9, 8, 7],
];

foreach (int[] arr in arrays)
{
    string desc = arr switch
    {
        []              => "비어 있음",
        [var only]      => $"원소 한 개 ({only})",
        [1, .., 3]      => "1로 시작 3으로 끝",
        [_, _, _]       => "정확히 세 개",
        [var first, .. var rest] => $"맨 앞 {first}, 나머지 {rest.Length}개",
    };
    Console.WriteLine($"[{string.Join(",", arr)}] → {desc}");
}

src/PropertyPattern/Program.cs

csharp
// property 패턴 — 객체의 속성 값을 바로 매칭한다.
Point[] points = [new(0, 0), new(0, 5), new(7, 0), new(3, 4), new(-1, -2)];

foreach (Point p in points)
{
    string zone = p switch
    {
        { X: 0, Y: 0 }            => "원점",
        { X: 0 }                  => "Y축 위",
        { Y: 0 }                  => "X축 위",
        { X: > 0, Y: > 0 }        => "1사분면",
        { X: < 0, Y: < 0 }        => "3사분면",
        _                         => "기타 사분면",
    };
    Console.WriteLine($"({p.X}, {p.Y}) → {zone}");
}

internal sealed record Point(int X, int Y);

src/PropertyPattern/PropertyPattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/SwitchType/Program.cs

csharp
// switch 식 + 타입 패턴 — 상속 트리에 한 번씩 분기하는 데 적합.
Animal[] zoo = [new Dog("바둑이"), new Cat("나비"), new Bird("짹짹"), new Dog("쫑")];

foreach (Animal a in zoo)
{
    string sound = Describe(a);
    Console.WriteLine(sound);
}

static string Describe(Animal a) => a switch
{
    Dog d  => $"{d.Name}: 멍멍",
    Cat c  => $"{c.Name}: 야옹",
    Bird b => $"{b.Name}: 짹짹",
    _      => "알 수 없는 동물",   // discard 패턴 — 그 외 전부
};

internal abstract record Animal(string Name);
internal sealed record Dog(string Name) : Animal(Name);
internal sealed record Cat(string Name) : Animal(Name);
internal sealed record Bird(string Name) : Animal(Name);

src/SwitchType/SwitchType.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

src/TuplePattern/Program.cs

csharp
// 튜플 패턴 — 여러 값을 동시에 매칭. 가위바위보 룰을 한눈에.
(string, string)[] games =
[
    ("rock", "scissors"),
    ("paper", "rock"),
    ("scissors", "scissors"),
    ("rock", "paper"),
];

foreach ((string p1, string p2) in games)
{
    string result = (p1, p2) switch
    {
        ("rock",     "scissors") => "P1 승",
        ("paper",    "rock")     => "P1 승",
        ("scissors", "paper")    => "P1 승",
        var (a, b) when a == b   => "비김",        // when 절로 추가 조건
        _                        => "P2 승",
    };
    Console.WriteLine($"{p1,-8} vs {p2,-8} : {result}");
}

src/TuplePattern/TuplePattern.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

자주 하는 실수

  1. `switch` 식에서 **모든 경로 커버 안 함** — `default` (`_`) 가 없으면 `MatchFailureException` 가능. 컴파일러도 경고.
  2. 더 일반적인 패턴을 위에 두는 바람에 아래 더 구체적인 패턴이 영영 안 닿는 코드.
  3. 속성 패턴에서 null 체크 빠뜨림 — `obj switch { { Prop: ... } => ... }` 는 obj 가 null 이면 매칭 안 됨. 의도라면 OK, 아니면 별도 분기.
  4. `when` 절을 남용해 분기가 사실상 `if/else if` 가 되어 가독성이 떨어짐 — 그럴 땐 그냥 `if` 가 낫습니다.
  5. 리스트 패턴을 큰 컬렉션에 사용 — 패턴은 길이를 통해 인덱싱하므로 `IEnumerable` 보다는 배열/리스트에 적합.

정리

  • `is` 패턴 = 타입 검사 + 변수 선언 + 추가 조건
  • `switch` 식은 값을 돌려주므로 `var x = obj switch { ... };` 형태가 자연스럽다
  • 타입/속성/튜플/리스트 패턴을 조합하면 분기 로직이 데이터 모양을 그대로 그린다
  • 분기 순서가 의미를 가진다 — 더 구체적인 패턴을 위에
  • `when` 절은 마지막 보루, 너무 많이 쓰면 차라리 `if` 가 가독성 좋다

과제

**과제 - 21. 패턴 매칭**

문제 1 — 도형 넓이 계산기

  • 프로젝트 폴더: `Homework01/`
  • 핵심 개념: 타입 패턴, `switch` 식, `record`

요구사항

  • 다음 도형을 `record` 로 정의한다.
  • `Circle(double Radius)`
  • `Rectangle(double Width, double Height)`
  • `Triangle(double Base, double Height)`
  • 공통 추상: `abstract record Shape;`
  • `static double Area(Shape s)` 메서드를 **`switch` 식 + 타입 패턴** 으로 작성한다.
  • 다음 배열에 대해 결과를 출력한다.

```csharp Shape[] shapes = [new Circle(2), new Rectangle(3, 4), new Triangle(5, 6)]; ```

예상 출력

text
Circle(2) 넓이: 12.57
Rectangle(3, 4) 넓이: 12.00
Triangle(5, 6) 넓이: 15.00

힌트

  • 원 넓이 = `Math.PI * r * r`.
  • 삼각형 = `0.5 * base * height`.
  • 출력은 `넓이.ToString("F2")` 또는 `$"{넓이:F2}"`.

문제 2 — 좌표 분류기

  • 프로젝트 폴더: `Homework02/`
  • 핵심 개념: 속성 패턴, `when` 절

요구사항

  • `record Point(int X, int Y);` 를 정의한다.
  • `static string Classify(Point p)` 를 `switch` 식 + 속성 패턴으로 작성한다.
  • 분류 규칙:
  • `(0, 0)` → `"원점"`
  • `X == 0` 이고 `Y != 0` → `"Y축"`
  • `Y == 0` 이고 `X != 0` → `"X축"`
  • `X == Y` (그리고 0,0 아님) → `"y=x 직선"`
  • 그 외 양수만 → `"1사분면"`
  • 그 외 음수만 → `"3사분면"`
  • 그 외 → `"기타 사분면"`
  • 다음 배열로 호출:

```csharp Point[] points = [new(0,0), new(0,5), new(7,0), new(3,3), new(2,8), new(-1,-5), new(-3,4)]; ```

예상 출력

text
(0,0)  → 원점
(0,5)  → Y축
(7,0)  → X축
(3,3)  → y=x 직선
(2,8)  → 1사분면
(-1,-5) → 3사분면
(-3,4) → 기타 사분면

힌트

  • 분기 순서가 중요합니다 — `(0,0)` 을 가장 먼저.
  • `{ X: var x, Y: var y } when x == y` 처럼 변수를 끄집어내 비교.
  • `{ X: > 0, Y: > 0 }` 으로 사분면 한 번에.

정답 확인

직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.

정답 (answer/)

homework/answer/Homework01/Homework01.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework01/Program.cs

csharp
Shape[] shapes = [new Circle(2), new Rectangle(3, 4), new Triangle(5, 6)];

foreach (Shape s in shapes)
{
    string label = s switch
    {
        Circle c    => $"Circle({c.Radius})",
        Rectangle r => $"Rectangle({r.Width}, {r.Height})",
        Triangle t  => $"Triangle({t.Base}, {t.Height})",
        _           => "Unknown",
    };
    Console.WriteLine($"{label} 넓이: {Area(s):F2}");
}

// 타입 패턴 + switch 식 — 새 도형이 늘어도 한 곳만 고치면 된다.
static double Area(Shape s) => s switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    Triangle t  => 0.5 * t.Base * t.Height,
    _           => throw new InvalidOperationException($"지원 안 함: {s.GetType().Name}"),
};

internal abstract record Shape;
internal sealed record Circle(double Radius) : Shape;
internal sealed record Rectangle(double Width, double Height) : Shape;
internal sealed record Triangle(double Base, double Height) : Shape;

homework/answer/Homework02/Homework02.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>CodingNow.Lecture.Modern21</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

homework/answer/Homework02/Program.cs

csharp
Point[] points =
[
    new(0, 0), new(0, 5), new(7, 0), new(3, 3),
    new(2, 8), new(-1, -5), new(-3, 4),
];

foreach (Point p in points)
{
    Console.WriteLine($"({p.X},{p.Y}) → {Classify(p)}");
}

// 분기 순서가 핵심 — 가장 구체적인 (0,0) 을 맨 위에 둔다.
static string Classify(Point p) => p switch
{
    { X: 0, Y: 0 }                          => "원점",
    { X: 0 }                                => "Y축",
    { Y: 0 }                                => "X축",
    { X: var x, Y: var y } when x == y      => "y=x 직선",
    { X: > 0, Y: > 0 }                      => "1사분면",
    { X: < 0, Y: < 0 }                      => "3사분면",
    _                                       => "기타 사분면",
};

internal sealed record Point(int X, int Y);

직접 해 보기

bash
cd src/IsPattern && dotnet run
cd ../SwitchType && dotnet run
cd ../PropertyPattern && dotnet run
cd ../TuplePattern && dotnet run
cd ../ListPattern && dotnet run

다음 단원

[22_Record와_init](../22_Record와_init/) — 패턴 매칭과 가장 잘 어울리는 `record` 타입을 익힙니다.

예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗