패턴의 실패 가능성: 패턴이 매칭되지 않을 수 있는 경우

패턴은 두 가지 형태로 나뉜다: 실패할 수 없는 패턴(irrefutable)과 실패할 수 있는 패턴(refutable). 어떤 값이든 매칭되는 패턴은 _실패할 수 없는 패턴_이다. 예를 들어 let x = 5; 문에서 x는 어떤 값이든 매칭되므로 실패할 수 없다. 반면, 특정 값에 대해 매칭에 실패할 수 있는 패턴은 _실패할 수 있는 패턴_이다. 예를 들어 if let Some(x) = a_value 표현식에서 Some(x)a_value 변수의 값이 Some이 아닌 None일 경우 매칭에 실패한다.

함수 매개변수, let 문, for 루프는 오직 실패할 수 없는 패턴만 허용한다. 이는 값이 매칭되지 않을 경우 프로그램이 의미 있는 동작을 할 수 없기 때문이다. if letwhile let 표현식, 그리고 let...else 문은 실패할 수 있는 패턴과 실패할 수 없는 패턴을 모두 허용하지만, 컴파일러는 실패할 수 없는 패턴에 대해 경고를 발생시킨다. 이는 조건문의 기능이 성공 또는 실패에 따라 다르게 동작할 수 있도록 설계되었기 때문이다.

일반적으로 실패할 수 있는 패턴과 실패할 수 없는 패턴의 구분에 대해 크게 걱정할 필요는 없지만, 오류 메시지에서 이 개념을 접했을 때 대응할 수 있도록 기본적인 이해는 필요하다. 이 경우, 코드의 의도에 따라 패턴을 변경하거나 패턴을 사용하는 구문을 수정해야 한다.

Rust에서 실패할 수 없는 패턴이 필요한 곳에 실패할 수 있는 패턴을 사용하거나 그 반대의 경우 어떤 일이 발생하는지 예제를 통해 살펴보자. 리스팅 19-8은 let 문을 보여주지만, 패턴으로 실패할 수 있는 패턴인 Some(x)를 지정했다. 예상대로 이 코드는 컴파일되지 않는다.

fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value;
}
Listing 19-8: let 문에서 실패할 수 있는 패턴 사용 시도

만약 some_option_valueNone 값이라면 Some(x) 패턴과 매칭되지 않아 패턴이 실패할 수 있음을 의미한다. 그러나 let 문은 실패할 수 없는 패턴만 허용한다. 왜냐하면 None 값에 대해 코드가 유효한 동작을 할 수 없기 때문이다. 컴파일 시 Rust는 실패할 수 없는 패턴이 필요한 곳에 실패할 수 있는 패턴을 사용하려 했다는 오류를 발생시킨다:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
 --> src/main.rs:3:9
  |
3 |     let Some(x) = some_option_value;
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
  |
3 |     let Some(x) = some_option_value else { todo!() };
  |                                     ++++++++++++++++

For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

Some(x) 패턴이 모든 유효한 값을 커버하지 못했기 때문에 Rust는 정당하게 컴파일 오류를 발생시킨다.

실패할 수 없는 패턴이 필요한 곳에 실패할 수 있는 패턴을 사용했다면, 패턴을 사용하는 코드를 변경해 문제를 해결할 수 있다. let 대신 if let을 사용하면 된다. 이렇게 하면 패턴이 매칭되지 않을 경우 중괄호 안의 코드를 건너뛰어 유효하게 동작할 수 있다. 리스팅 19-9는 리스팅 19-8의 코드를 어떻게 수정하는지 보여준다.

fn main() {
    let some_option_value: Option<i32> = None;
    let Some(x) = some_option_value else {
        return;
    };
}
Listing 19-9: let 대신 let...else와 실패할 수 있는 패턴 사용

이제 코드는 완벽하게 유효하다. 그러나 if let에 실패할 수 없는 패턴(항상 매칭되는 패턴)을 사용하면, 리스팅 19-10과 같이 x를 사용할 경우 컴파일러는 경고를 발생시킨다.

fn main() {
    let x = 5 else {
        return;
    };
}
Listing 19-10: if let에 실패할 수 없는 패턴 사용 시도

Rust는 if let에 실패할 수 없는 패턴을 사용하는 것이 의미가 없다고 경고한다:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `let...else` pattern
 --> src/main.rs:2:5
  |
2 |     let x = 5 else {
  |     ^^^^^^^^^
  |
  = note: this pattern will always match, so the `else` clause is useless
  = help: consider removing the `else` clause
  = note: `#[warn(irrefutable_let_patterns)]` on by default

warning: `patterns` (bin "patterns") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/patterns`

이러한 이유로, match의 각 분기(arm)는 실패할 수 있는 패턴을 사용해야 한다. 단, 마지막 분기는 남은 모든 값을 매칭할 수 있는 실패할 수 없는 패턴을 사용해야 한다. Rust는 match에 단일 분기만 있을 경우 실패할 수 없는 패턴을 허용하지만, 이 구문은 특별히 유용하지 않으며 더 간단한 let 문으로 대체할 수 있다.

이제 패턴을 사용할 위치와 실패할 수 있는 패턴과 실패할 수 없는 패턴의 차이를 알았으니, 패턴을 생성하는 데 사용할 수 있는 모든 구문을 살펴보자.