만자의 개발일지

[Rust] 패턴 매칭 본문

Rust

[Rust] 패턴 매칭

박만자 2025. 5. 31. 23:43

저번 포스팅에서는 열거형에 대해 알아보았습니다. 이번 포스팅에서는 패턴 매칭에 대해 알아보도록 하겠습니다.

패턴 매칭(Pattern Matching)

패턴 매칭이란 값의 구조에 따라 코드를 분기하는 기능입니다. 패턴 매칭은 구조체, 열거형, 튜플, 배열 등 다양한 데이터 타입에서 사용할 수 있으며, 각 상황에 맞는 표현식을 사용하여 패턴을 처리할 수 있습니다.

Rust에서 패턴을 처리하는 표현식match, if let, while let, for, let, function parameter(함수 매개변수), matches! 등이 있습니다.

각각이 무엇이고 어떻게 사용하는지 알아보도록 하겠습니다.

 

match

match 표현식은 패턴 매칭에서 가장 중요한 부분 중 하나입니다. match값을 여러 패턴과 비교하여 첫 번째로 일치하는 패턴에 대한 코드를 실행합니다. 다른 언어의 switch문과 비슷하지만 그 이상의 기능을 제공하며, match 키워드의 강력한 기능들에 대해 살펴보도록 하겠습니다.

 

다음은 match 키워드 사용 예시입니다.

fn main() {
    let a = Some(5);
    
    match a {
        Some(x) => println!("The value is {x}"),
        None => println!("No value")
    }
}

패턴 매칭의 대상이 되는 값scrutinee라고 합니다. 위 예제에서는 match뒤에 오는 ascrutinee입니다.

aSome(5)라는 값을 가지고 있고, match의 첫 번째 패턴과 일치합니다. 따라서 Some내부의 5는 패턴 변수인 x에 할당되고 해당 분기에서 x를 사용하여 값을 출력하는 것을 보실 수 있습니다.

 

match값에 대한 모든 경우를 반드시 처리하도록 강제합니다. 다음 코드는 에러를 발생 시킵니다.

fn main() {
    let a = Some(5);
    
    match a {
        Some(x) => println!("The value is {x}"),
    }
}

Option<T>는 두 개의 variant를 가지고 있지만, match 내부에서 한 개의 variant에 대해서만 처리하였기 때문입니다.

이처럼 컴파일러는 match에서 발생할 수 있는 모든 패턴에 대한 처리여부를 확인하여 오류 가능성을 줄여줍니다.

 

match에서는 일부 패턴만 처리하고 나머지 모든 패턴을 처리하면서 값을 무시하고 싶을 때, wildcard(_) 패턴을 사용하여 처리할 수 있습니다.

fn main() {
    let number = 5;
    
    match number {
        3 => println!("three"),
        6 => println!("six"),
        _ => println!("any number")
    }
}

 

또한 match 표현식에서 패턴만으로 표현하기 어려운 경우 if 문을 사용해 조건을 주어 더 정밀하게 패턴을 제어할 수 있으며, 이를 매치 가드(match guard)라고 부릅니다.

fn main() {
    let a = Some(5);
    
    match a {
        Some(x) if x > 10 => println!("The value is more than 10: {x}"),
        Some(x) => println!("The value is {x}"),
        None => println!("No value")
    }
}

매치 가드는 패턴이 매칭된 후 해당 조건이 참일 때만 해당 분기가 실행됩니다. 따라서 위 예시는 패턴은 매칭이 됬지만 

조건이 참이 아니기 때문에 첫 번째 분기가 실행되지 않을 것입니다.

 

if let

if let 표현식은 match의 간결한 형태로, 단일 패턴에 대해서만 처리할 때 사용합니다. 

fn main() {
    let a = Some(5);
    
    if let Some(x) = a {
        println!("The value is {x}");
    }
}

if let값이 특정 패턴과 일치할 때만 코드를 실행합니다.

 

if letmatch와 달리 모든 패턴에 대해 처리할 필요가 없지만, 필요한 경우 else if let 또는 else사용하여 다중 패턴에 대해 처리할 수 있습니다.

enum Status {
    Ready,
    Waiting,
    Error,
    Done
}

fn main() {
    let status = Status::Ready;

    if let Status::Ready = status {
        println!("Status is ready");
    } else if let Status::Waiting = status {
        println!("Status is waiting");
    } else {
        println!("Status is either in error or done");
    }
}

모든 패턴에 대한 처리가 필요한 경우에는 if let보다는 match를 사용하는 것이 더 유용합니다.

 

또한 if letmatch의 매치 가드같은 기능을 지원하지 않기 때문에 값에 조건을 부여하여 처리하고 싶은 경우 패턴 변수를 활용하여 내부에서 직접 처리해야 합니다.

fn main() {
    let a = Some(20);
    
    if let Some(x) = a {
        if x > 10 {
            println!("This value is more than 10: {x}");
        }
    }
}

 

while let

while let 표현식은 패턴이 일치하는 동안 계속해서 코드 반복 실행합니다. 

fn main() {
    let mut a = Some(5);
    
    while let Some(i) = a {
        if i <= 0 {
            a = None;
        } else {
            println!("{i}");
            a = Some(i-1);    
        }
    }
}

코드 실행 후 값을 재평가하고, 패턴이 일치하지 않으면 루프는 종료됩니다.

while let역시 단일 패턴에 대해서만 처리하며, if let과 같이 다중 패턴을 처리하는 기능을 지원하지 않습니다.

 

만약 반복문내에서 여러 패턴을 처리해야거나, 패턴이 일치하지 않을 때의 동작을 정의해야한다면, 다음과 같이 loopmatch를 사용하는 것이 좋습니다.

fn main() {
    let mut a = Some(5);
    
    loop {
        match a {
            Some(i) if i <= 0 => a = None,
            Some(i) => {
                println!("{i}");
                a = Some(i-1);
            },
            None => break
        }
    }
}

 

for

for 표현식은 배열과 벡터와 같은 이터레이터 요소를 순회할 때 자주 사용하는 반복문입니다. Rust의 for문에서도 패턴 매칭이 사용됩니다.

fn main() {
    let tups = [(1, 2), (3, 4, 5), (6, 7)];
    
    for (x, y) in tups { // error: mismatched types
        println!("x: {x}, y: {y}");
    }
}

이터레이터 요소에 패턴과 일치하지 않는 값이 있다면 에러가 발생합니다..

 

let

변수를 선언할 때 사용하는 let 구문에서도 패턴 매칭이 사용됩니다. let 구문은 할당하는 값의 형태를 패턴으로 지정하여 특정 구조를 가진 값만 변수에 바인딩할 수 있습니다.

fn main() {
    let point = (3, 5, 7);
    let (x, y) = point; // error: mismatched types
    println!("x = {x}, y = {y}");
}

let 구문 역시 패턴이 일치하지 않으면 에러를 발생시킵니다. 

 

let-else 구문을 사용하여 let 구문에서 패턴이 일치하지 않는 상황을 처리할 수 있습니다.

fn print_point(point: (i32, i32)) {
    let (x, y) = point else { // let-else
        println!("invalid point");
        return;
    };

    println!("x: {x}, y: {y}");
}

fn main() {
    let valid_point = (10, 20);
    print_point(valid_point);
}

let-else 구문을 사용할 때 주의할 점은 else 블록에서 반드시 스코프를 빠져나가는 표현식(diverging expression)을 작성해야 합니다. diverging expression에는 panic!, return, break, continue 등이 있습니다.

 

diverging expression을 반드시 작성해야 하는 이유는 else 블록이 실행되었을 때 변수가 어떤 값을 가지는지 명확히 알 수 없기 때문입니다.

fn main() {
    let some_option = None;
    let Some(value) = some_option else {
        println!("No value");
        // value가 초기화 되지 않았을 수 있음
    };
    println!("value: {}", value); // 초기화 되지 않은 변수에 접근
}

 

function parameter

함수 파라미터에서도 패턴 매칭이 사용됩니다. 함수 파라미터는 let 구문과 마찬가지로 특정 구조를 가진 인수만 파라미터에 바인딩할 수 있습니다.

struct Point {
    x: i32,
    y: i32
}

fn print_point(Point { x: a, y: b }: Point) {
    println!("x: {a}, y: {b}");
}

fn main() {
    let p = Point{ x: 10, y: 20 };
    print_point(p);
}

 

matches!

matches! 매크로는 주어진 값이 특정 패턴과 일치하는지 비교하고, 일치하는지 여부를 불리언 값으로 반환합니다.

matches!(EXPRESSION, PATTERN)

 

다음은 matches! 매크로 사용예시입니다.

fn main() {
    let option_value = Some(10);
    if matches!(option_value, Some(_)) {
        println!("The values is Some");
    }
}

matches! 내부 값을 추출할 필요가 없고, 단순히 패턴이 일치하는지 여부만 확인하고 싶을 때 사용하면 훨씬 간결하게 코드를 작성할 수 있습니다. 

 

matches! 매크로 역시 match 표현식처럼 가드를 사용할 수 있습니다.

fn main() {
    let option_value = Some(20);
    if matches!(option_value, Some(x) if x > 10) {
        println!("The values is Some and more than 10");
    }
}

 

패턴(Patterns)

앞선 예제에서 다양한 패턴들을 예시로 만나봤습니다. 패턴 매칭에서 패턴은 값의 구조를 나타내는 형식입니다. Rust는 다양한 형식의 패턴을 제공합니다. 패턴에서 사용되는 중요한 개념들과 유용하게 쓸 수 있는 몇 가지 패턴들에 대해 알아보도록 하겠습니다.

 

구조 분해(Destructuring)

구조 분해란 복합 타입의 값을 구성하는 요소를 추출하여 개별 변수에 바인딩하는 강력한 기능입니다. 구조 분해를 통해 코드의 가독성을 높이고, 특정 데이터 구조의 일부만 사용하는 등 효율적인 코드를 작성할 수 있도록 돕습니다..

 

다음과 같이 각 복합 타입에 대해 구조 분해를 할 수 있습니다.

struct Point {
    x: i32,
    y: i32,
}

enum Color {
    RGB(u8, u8, u8),
    HSL(f32, f32, f32)
}

fn main() {
    let arr = [1, 2, 3];
    let [a, b, c] = arr; // array destructuring
    println!("[0]: {a}, [1]: {b}, [2]: {c}");

    let p = (10, 20); 
    let (x, y) = p; // tuple destructuring
    println!("x: {x}, y: {y}");
    
    let p = Point { x: 30, y: 40 };
    let Point {x, y} = p; // struct destructuring
    println!("x: {x}, y: {y}");
    
    let rgb = Color::RGB(0, 255, 123);
    let Color::RGB(r, g, b) = rgb else { return }; // enum destructuring
    println!("r: {r}, g: {g}, b:{b}");
}

구조 분해는 각 요소에 맞는 변수를 나열하여 값을 추출합니다. 앞선 예제에서 자주 다룬 패턴 변수 역시 구조 분해가 사용된 코드입니다.

 

반박 가능성(Refutability)

Refutability패턴이 특정 값에 대해 매칭의 성공/실패 여부를 나타내는 개념입니다.

 

크게 두 가지 패턴이 있습니다.

  • 반박 불가능한 패턴(Irrefutable Patterns): 주어진 값이 어떤 값이든 항상 매칭되는 패턴입니다.
  • 반박 가능한 패턴(Refutable Patterns): 주어진 값 중 일부에는 매칭이될 수 있지만, 다른 일부에는 매칭 되지 않을 수도 있는 패턴입니다.
let (x, y) = (1, 2);               // "(x, y)" is an irrefutable pattern

if let (a, 3) = (1, 2) {           // "(a, 3)" is refutable, and will not match
    panic!("Shouldn't reach here");
} else if let (a, 4) = (3, 4) {    // "(a, 4)" is refutable, and will match
    println!("Matched ({}, 4)", a);
}

위 코드에서 (x, y)는 어떠한 값이 오더라도 매칭에 성공하지만, (a, 3)의 경우 튜플의 두번째 값이 3인 경우에만 매칭이 성공합니다.

 

Rust는 코드의 안정성과 명확성을 보장하기 위해 특정 문법에서 Refutability를 강제합니다.

 

1. let & let-else

letirrefutable 패턴을 사용해야 합니다. 

fn main() {
    let Some(x) = Some(10);
    // error[E0005]: refutable pattern in local binding: `None` not covered
}

컴파일러는 let어떤 값이든 변수를 항상 초기화시킬 것이라고 기대하기 때문입니다. refutable 패턴을 사용한다면 변수가 초기화되지 않을 우려가 있어 위험합니다.

 

반대로 let-elserefutable 패턴을 사용해야 합니다.

fn main() {
    let x = Some(10) else { // warning: irrefutable `let...else` pattern
        panic!("never execute this code");
    };
}

let-else에서 irrefutable 패턴을 사용할 경우 절대 패턴 매칭이 실패할 일이 없기 때문에 else 블록 내부에 코드가 실행되지 않습니다.

 

2. if let & while let

if letwhile letrefutable 패턴을 사용해야 합니다.

fn main() {
    let a = 5;
    if let x = a { // warning: irrefutable `if let` pattern
        println!("{}", x);
    }
}

irrefutable 패턴도 사용 가능하긴 하지만 컴파일러가 경고를 발생시키며, if letwhile let에서 irrefutable 패턴을 사용하는 것은 if truewhile true와 똑같기 때문에 사용 목적에 부합하지 않습니다.

 

3. match

match는 모든 가능한 경우를 반드시 처리해야 합니다. 

enum Color {
    RGB(u8, u8, u8),
    HSL(f32, f32, f32)
}

fn main() {
    let rgb = Color::RGB(0, 0, 0);
    match rgb {
        Color::RGB(0, 0, 0) => println!("black"), // refutable
        Color::RGB(255, 255, 255) => println!("white"), // refutable
        Color::RGB(r, g, b) => println!("Red: {r}, Green: {g}, Blue: {b}"), //irrefutable
        _ => println!("The value is not rgb")
    }
}

match 내부에서는 refutable 패턴, irrefutable 패턴 모두 사용가능하지만 결과적으로 match 표현식 전체가 irrefutable 해야 합니다.

 

Binding(@)

@ 연산자를 통해 값이 특정 패턴과 일치하는지 검사함과 동시에 새로운 변수에 값을 바인딩할 수 있습니다.

 

다음은 매치 가드를 사용한 예제입니다.

fn main() {
    let value = 10;
    match value {
        n if n >= 10 && n <= 20 => println!("{} is in range", n),
        _ => println!("Out of range"),
    }
}

 

위 코드를 @ 연산자를 사용하여 다음과 같이 처리할 수 있습니다.

fn main() {
    let value = 10;
    match value {
        n @ 10..=20 => println!("{} is in range", n),
        _ => println!("Out of range"),
    }
}

@ 연산자는 매치 가드와 유사하지만 코드의 가독성을 높이고 특정 조건에 따른 값을 활용하려할 때 코드를 간결하게 만들어 줍니다. 특히 리터럴과 범위 패턴에 자주 사용됩니다.

 

또한 @ 연산자는 매치 가드와 함께 사용할 수도 있습니다.

fn main() {
    let num = 15;
    match num {
        n @ 0 => println!("{} is the number 0.", n),
        n @ 100..=i32::MAX => println!("{} is a number 100 or greater.", n),
        n @ 10..=20 if n % 2 != 0 => println!("{} is an odd number between 10 and 20.", n),
        other_num => println!("{} is another type of number.", other_num)
    }
}

이처럼 범위 패턴이나 리터럴 같은 간단한 패턴은 @ 연산자로 처리하고 복잡한 논리 조건은 매치 가드로 처리하면서 패턴 매칭을 더욱 유연하게 활용할 수 있습니다.

 

Wildcard pattern( _ ) 

와일드카드 패턴은 값이 사용되지 않음을 의미합니다. 패턴 매칭에서 특정 값을 무시하고 싶을 때 와일드카드 패턴을 사용해서 처리할 수 있습니다.

 

다음 예시를 보겠습니다.

fn main() {
    let a = 2;

    match a {
        1 => println!("one"),
        x => println!("another number") // warning: unused variable: `x`
    }
}

x 라는 패턴 변수 하나를 선언하여 나머지 모든 경우에 대해서 처리하고 있지만 정작 해당 값은 사용하지 않고 있습니다.

이 경우 컴파일러는 경고를 발생시킵니다.

 

위 코드를 와일드카드 패턴을 사용하여 다음과 같이 바꿀 수 있습니다.

fn main() {
    let a = 2;

    match a {
        1 => println!("one"),
        _ => println!("another number")
    }
}

와일드카드 패턴을 사용하면 컴파일러는 해당 변수를 사용하지 않는 변수로 처리합니다.

 

또한 다음과 같이 와일드카드 패턴 뒤에 변수명을 추가로 작성할 수 있습니다.

fn main() {
    let a = 2;

    match a {
        1 => println!("one"),
        _number => println!("another number")
    }
}

마찬가지로 사용되지 않는 변수이지만, 디버깅 목적으로 어떤 값이 오는지 명시적으로 표현하고 싶을 때 위와 같이 사용합니다.

 

Rest Pattern(..)

Rest 패턴은 배열, 튜플, 구조체와 같은 복합타입에 대한 패턴 매칭에서 특정 요소들을 제외한 나머지 부분에 대한 값을 무시할 때 사용합니다.

 

다음 예시를 보겠습니다.

struct Point {
    x: i32,
    y: i32,
    z: i32
}

fn main() {
    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, y: _, z: _ } => println!("x is {}", x)
	}
}

구조체 필드에서 x 값만 사용하고 싶어 와일드카드 패턴을 사용하여  나머지 필드를 무시하였습니다. 하지만 와일드카드 패턴은 단일 요소에 대해서만 처리할 수 있기 때문에 요소가 많아질 수록 코드가 복잡해집니다.

 

위 경우 Rest 패턴을 사용하여 간결하게 처리할 수 있습니다.

struct Point {
    x: i32,
    y: i32,
    z: i32
}

fn main() {
    let origin = Point { x: 0, y: 0, z: 0 };

    match origin {
        Point { x, .. } => println!("x is {}", x)
	}
}

Rest 패턴을 사용하면 특정 요소를 제외한 나머지 모든 요소를 무시할 수 있습니다. 위 코드에서는 x 뒤에 오는 모든 필드들을 무시하였습니다.

 

또는 다음과 같이 다양한 방법으로 사용할 수 있습니다.

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
    	// 첫번째 요소와 마지막 요소만 사용하고 나머지는 무시
        (first, .., last) => println!("Some numbers: {first}, {last}"),
        // 마지막 요소만 사용하고 나머지는 무시
        (.., last) => println!("Some numbers: {last}"),
        // 모든 요소를 무시
        (..) => println!("None")
    }
}

 

또한 슬라이스 값에 대한 패턴 매칭에서 @ 연산자와 함게 사용하면 나머지 요소들을 패턴 변수에 바인딩하여 사용할 수 있습니다.

fn main() {
    let numbers = &[1, 2, 3, 4, 5];

    match numbers {
        &[first, middle @ .., last] => {
            println!("First: {}", first);
            println!("Middle elements: {:?}", middle);
            println!("Last: {}", last);
        }
    }
}

이처럼 Rest 패턴은 특정 요소만 사용하거나 가변적인 길이를 가진 값에서 나머지 부분을 효율적으로 처리하고 싶을 때 유용하게 사용할 수 있습니다.

 

OR Pattern( | )

OR 패턴은 두개 이상의 패턴을 조건으로 줄 때 사용합니다. 

 

다음 코드를 보겠습니다. 

enum Color {
    Red,
    Green,
    Blue,
    Yellow,
}

fn main() {
    let color = Color::Blue;
    
    match color {
        Color::Red => println!("This is a primary color."),
        Color::Green => println!("This is a primary color."),
        Color::Blue => println!("This is a primary color."),
        Color::Yellow => println!("This is a secondary color."),
    }
}

모든 패턴을 개별적으로 나열하여 처리하였습니다. 

 

동일한 처리가 필요한 패턴들을 OR 패턴을 사용하여 묶어줄 수 있습니다.

enum Color {
    Red,
    Green,
    Blue,
    Yellow,
}

fn main() {
    let color = Color::Blue;
    
    match color {
        Color::Red | Color::Green | Color::Blue => println!("This is a primary color."),
        Color::Yellow => println!("This is a secondary color."),
    }
}

관련된 패턴들을 한 곳에 모아둠으로써 코드를 간결하게 작성할 수 있습니다.

 

다음과 같은 상황에서는 OR 패턴을 사용할 수 없습니다.

enum MyEnum {
    A(i32),
    B(u32),
    C(i32),
}

fn main() {
    let a = MyEnum::B(10);
    
    match a {
        MyEnum::A(x) | MyEnum::B(x) => println!("A or B value is: {x}"), // error: mismatched types
        _ => println!("C")
    }
}

OR 패턴을 사용할 때 각 패턴에서 바인딩하는 값들은 모두 같은 타입이여야 합니다. 위 경우 MyEnum::A(x)에는 i32 값이, MyEnum::B(x)에는 u32 값이 바인딩 되기 때문에 OR 패턴을 사용할 수 없습니다.


지금까지 패턴 매칭에 대해 알아보았습니다. 다음 포스팅에서는 Rust의 소유권과 메모리모델에 대해서 알아보도록 하겠습니다.

 

참고

 

'Rust' 카테고리의 다른 글

[Rust] 열거형  (0) 2025.05.18
[Rust] 구조체와 메서드 & 연관 함수  (0) 2025.05.04
[Rust] 조건문과 반복문  (0) 2025.05.02
[Rust] 함수와 표현식  (0) 2025.04.29
[Rust] 변수와 타입  (0) 2025.04.28
Comments