본문 바로가기

반박 가능성: 패턴이 매칭에 실패할지의 여부

모든 값에 성공하는 반박 불가능 패턴과 실패할 수 있는 반박 가능 패턴을 구분해 각 구문에 허용되는 형태를 판단합니다.

패턴에는 반박 가능한 패턴과 반박 불가능한 패턴의 두 가지 형태가 있습니다.

넘겨진 모든 가능한 값에 대해 매칭되는 패턴을 반박 불가능한(irrefutable) 패턴이라고 합니다.

일례로 let x = 5; 구문에서 x는 무엇이든 매칭되므로 매칭에 실패할 수 없습니다.

일부 가능한 값에 대해 매칭에 실패할 수 있는 패턴은 반박 가능(refutable)합니다.

if let Some(x) = a_value 표현식의 Some(x)를 예로 들 수 있는데, a_value 변수의 값이 Some이 아니라 None이면 Some(x) 패턴이 매칭되지 않기 때문입니다.

함수 매개변수, else가 없는 단순 let 구문, for 루프의 항목 패턴에는 반박 불가능한 패턴이 필요합니다. 이 자리에는 패턴 불일치 후 무엇을 할지 정하는 별도 경로가 없습니다.

if let·while let 표현식과 let ... else 구문은 반박 가능한 패턴을 허용합니다. 반박 불가능한 패턴도 쓸 수 있지만, 불일치 경로가 불필요하므로 컴파일러는 기본 설정에서 경고합니다.

조건문은 성공 또는 실패 여부에 따라 서로 다른 동작을 수행하니까요.

Option<i32>의 값이 현재 Some(7)이어도 그 타입에는 None이라는 다른 형태가 있습니다. 반박 가능성은 현재 초기값 하나가 아니라 타입이 허용하는 값 전체를 기준으로 판단합니다. 이어 어떤 구문이 불일치 경로를 제공하는지를 따로 확인하세요.

RUST · 패턴과 코드

반박 가능성은 현재 타입의 모든 값에 맞는지로 판단한다

일부 값에서 실패하는 패턴은 그 불일치를 처리할 수 있는 구문에 놓습니다. 단순 let과 let-else는 계약이 다릅니다.

같은 Option 패턴을 세 구문에 놓을 때의 판정 Some(x)는 Option의 일부 값에만 일치합니다. 화살표는 이 패턴과 단순 let, if let, let-else의 비교 관계 및 각 결과를 연결합니다. 단순 let은 E0005, if let은 Some에서 블록 실행과 None에서 건너뜀, let-else는 Some 뒤의 바인딩과 None에서 발산하는 else를 보여 줍니다. 세 갈래는 같은 패턴을 서로 다른 구문에 대입한 비교입니다. Some(x) Option<T>의 패턴 일부 값에 불일치 단순 let else 없는 구문 불일치 경로 없음 if let 일치 시 블록 블록 안의 x let-else 불일치 시 else else는 반드시 발산 E0005 Option에 None 가능 현재 값이 Some여도 단순 let은 오류 Some / None Some: 블록 실행 None: 블록 건너뜀 x는 블록 안의 값 Some / None Some: 이후 x 사용 None: else로 이동 else: 반드시 발산 return: 함수 종료
같은 Option 패턴을 세 구문에 놓을 때의 판정 Some(x)는 Option의 일부 값에만 일치합니다. 화살표는 이 패턴과 단순 let, if let, let-else의 비교 관계 및 각 결과를 연결합니다. 단순 let은 E0005, if let은 Some에서 블록 실행과 None에서 건너뜀, let-else는 Some 뒤의 바인딩과 None에서 발산하는 else를 보여 줍니다. 세 갈래는 같은 패턴을 서로 다른 구문에 대입한 비교입니다. Some(x) Option<T>의 패턴 일부 값에 불일치 단순 let else 없는 구문 불일치 경로 없음 if let 일치 시 블록 블록 안의 x let-else 불일치 시 else else는 반드시 발산 E0005 Option에 None 가능 현재 값이 Some여도 단순 let은 오류 Some / None Some: 블록 실행 None: 블록 건너뜀 x는 블록 안의 값 Some / None Some: 이후 x 사용 None: else로 이동 else: 반드시 발산 return: 함수 종료

화살표는 같은 패턴과 세 구문, 각 구문에서의 판정·실행 결과를 연결합니다. 한 갈래를 따라 해당 구문의 실패 처리와 x의 사용 범위를 함께 확인하세요.

반박 불가능 · 모든 값에 일치

let x = 5;
let (a, b) = (1, 2);

이름 x는 어떤 i32에도, (a,b)는 두 항목 튜플의 모든 값에 맞습니다. 단순 let·for 항목·매개변수에 사용할 수 있습니다.

반박 가능 · 일부 값에 불일치

Some(x) // Option<T>의 패턴

Some에는 맞지만 None에는 맞지 않습니다. 현재 Option<T> 타입에 None이라는 다른 가능한 값이 있기 때문입니다.

단순 let · 불일치가 처리되지 않음

let Some(x) = value; // E0005

value: Option<T>이면 None에서 실패할 수 있으므로 컴파일 오류입니다. 런타임에 현재 값이 Some이어도 이 단순 let은 허용되지 않습니다.

if let / let-else · 실패 경로가 있음

if let Some(x) = value {
    // 일치할 때만 x 사용
}
let Some(x) = value else { return };

if let은 건너뛰고, let-else는 발산하는 else로 벗어납니다. while let은 불일치 시 반복을 끝냅니다.

경고와 match의 완전성은 별도로 확인

단독 if let x = 5처럼 항상 맞는 조건은 기본 설정에서 경고합니다. match는 Some(x)와 None 두 반박 가능 갈래만으로도 모든 Option 값을 처리할 수 있습니다. 마지막 갈래가 반드시 와일드카드일 필요는 없습니다.

반박 가능성은 패턴과 타입의 관계이고, 허용 여부는 구문의 실패 처리 계약까지 함께 판단합니다.

패턴의 반박 가능성은 문법 선택을 결정합니다.

실패할 수 있는 패턴을 반드시 성공해야 하는 자리에 두면 컴파일러가 바로 막아줍니다.

일반적으로는 반박 가능한 패턴과 반박 불가능한 패턴을 구분할 필요는 없습니다; 하지만 반박 가능성의 개념에 익숙해야 에러 메시지에서 이것이 나왔을 때 대응이 가능합니다.

이러한 경우 코드에서 의도한 동작에 따라 패턴 혹은 패턴을 사용하는 구문을 변경할 필요가 있을 것입니다.

러스트가 반박 불가능한 패턴을 요구하는 곳에서 반박 가능한 패턴의 사용을 시도하는 경우와 그 반대의 경우 어떤 일이 발생하는지 예제를 살펴봅시다.

아래 예제 17-8과 17-9는 fn main() 안의 짧은 코드 조각입니다. some_option_value는 Option<i32> 값이라고 가정합니다. 예제 17-8은 값이 Some(7)이든 None이든 단순 let에 반박 가능한 패턴을 놓았으므로 컴파일되지 않습니다.

예제 17-8은 let 구문을 보여주는데, 여기에는 반박 가능한 패턴인 Some(x)를 지정했습니다.

예상할 수 있듯 이 코드는 컴파일되지 않습니다.

예제 17-8: let에서 반박 가능한 패턴의 사용 시도하기
    let Some(x) = some_option_value;

some_option_value가 None 값이면 Some(x) 패턴과 매칭되지 않으므로 이 패턴은 반박 가능합니다.

이 예처럼 else가 없는 단순 let에는 None을 처리할 경로가 없으므로 반박 불가능한 패턴이 필요합니다. let ... else로 실패 경로를 제공하는 경우는 아래에서 구별합니다.

컴파일 타임에 러스트는 반박 불가능한 패턴이 필요한 곳에 반박 가능한 패턴을 사용하려고 시도했다고 불평할 것입니다.

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding: `None` not covered
 --> 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/ch18-02-refutability.html
note: `Option<i32>` defined here
 --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1
  |
  = note.
/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: not covered
  = note: the matched value is of type `Option<i32>`
help: you might want to use `if let` to ignore the variant that isn't matched
  |
3 |     let x = if let Some(x) = some_option_value { x } else { todo!() };
  |     ++++++++++                                 ++++++++++++++++++++++
help: alternatively, 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` due to previous error

패턴 Some(x)으로 모든 유효한 값을 포함하지 않았으므로(정확히는 포함할 수도 없으므로) 러스트는 당연히 컴파일 에러를 냅니다.

반박 불가능한 패턴이 필요한 곳에서 반박 가능한 패턴을 사용한 경우, 패턴을 사용하는 코드를 변경하여 문제를 해결할 수 있습니다.

let을 사용하는 대신 if let을 사용하면 됩니다.

그러면 패턴이 매칭되지 않을 때 중괄호 안의 코드를 건너뛴 뒤 프로그램이 계속 실행될 수 있습니다.

예제 17-9는 예제 17-8의 코드를 수정하는 방법을 보여줍니다.

예제 17-9: let 대신 반박 가능한 패턴과 함께 if let과 코드 블록 사용하기
    if let Some(x) = some_option_value {
        println!("{}", x);
    }

코드에 탈출구를 만들어 주었습니다!

패턴이 매칭되지 않으면 본문을 건너뛸 수 있으므로, 이 if let에서는 반박 가능한 Some(x) 패턴을 유효하게 사용할 수 있습니다.

같은 Option에서 실패 경로와 바인딩 범위 비교하기

컴파일 오류의 도움말이 제안한 let ... else도 같은 Some(x) 패턴에 실패 경로를 제공합니다. if let은 일치하면 그 블록 안에서 x를 사용하고, 불일치하면 블록을 건너뛴 뒤 계속합니다. let ... else는 일치하면 같은 바깥 블록의 뒤쪽에서도 x를 사용할 수 있습니다.

그 대신 let ... else의 else는 반드시 발산(diverge)해야 합니다. 여기서 발산은 그 else가 끝난 뒤 let 다음 문장으로 정상 진행하지 않는다는 뜻입니다. 현재 함수에서 나가는 return, 적절한 반복문 문맥의 break·continue, 정상 반환하지 않는 panic! 등이 그 경로를 만듭니다. 아래에서는 return으로 with_let_else 함수만 종료합니다. 그 함수를 호출한 main은 계속 실행됩니다.

다음은 두 구문을 Some(7)과 None에 각각 적용하는 전체 프로그램입니다.

src/main.rs (Option 실패 경로와 바인딩 범위 비교)
fn with_if_let(value: Option<i32>) {
    if let Some(x) = value {
        println!("if-let value={x}");
    }
    println!("if-let after");
}

fn with_let_else(value: Option<i32>) {
    let Some(x) = value else {
        println!("let-else missing");
        return;
    };
    println!("let-else value={x}");
    println!("let-else after");
}

fn main() {
    with_if_let(Some(7));
    with_let_else(Some(7));
    with_if_let(None);
    with_let_else(None);
    println!("main after");
}

호출 순서대로 따라가면 예상 출력은 다음과 같습니다.

if-let value=7
if-let after
let-else value=7
let-else after
if-let after
let-else missing
main after

첫 두 호출은 Some(7)에 일치하므로 값을 출력한 다음 각 함수의 after까지 진행합니다. 세 번째 호출은 None이어서 if let 블록만 건너뛰고 if-let after를 출력합니다. 네 번째 호출에서는 let-else missing을 출력한 뒤 return으로 함수에서 나오므로 let-else after는 출력하지 않습니다. 이후 호출자인 main의 마지막 출력은 실행됩니다.

바인딩의 범위도 구분하세요. with_if_let의 마지막 println!에서 x를 사용하면 그 이름은 이미 if let 블록 밖이므로 컴파일 오류입니다. 반면 with_let_else의 마지막 두 문장은 let과 같은 블록 안에 있어서 성공 경로의 x를 사용할 수 있습니다. else가 출력만 하고 정상 종료하면 x가 없는 상태로 뒤 문장에 도달할 수 있으므로 허용되지 않습니다.

이 조건과 바인딩 범위는 Rust Reference의 let 구문과 if 표현식에서 확인할 수 있습니다. 위 그림의 세 갈래는 같은 Some(x)를 각 구문에 놓았을 때의 판정과 실행 결과입니다. 현재 값에 일치하는지 확인한 뒤, 불일치 경로와 x를 사용할 범위까지 연결해 읽으세요.

직접 판단하기

  1. main의 첫 두 호출만 Some(3)으로 바꾸면 출력 중 어느 줄이 달라지나요? 나머지 호출은 그대로 둡니다.
  2. with_let_else의 else에서 return;만 삭제하고 println!을 남기면 왜 컴파일되지 않나요?
  3. with_if_let의 마지막 문장을 println!("if-let after {x}");로 바꾸면 컴파일되나요? 호출을 모두 Some으로 바꾸면 판단이 달라지나요?
정답과 풀이
  1. if-let value=7과 let-else value=7만 각각 value=3으로 바뀝니다. 나머지 after·missing 출력과 마지막 main after는 같습니다.
  2. 불일치 시 출력한 뒤 정상 진행하면 x가 없는 채로 뒤 문장에 도달합니다. let ... else는 이 경로가 발산해야 하므로 return 같은 제어 이동이 필요합니다. 호출에 Some만 넣더라도 구문의 None 경로에 대한 조건은 없어지지 않습니다.
  3. 컴파일되지 않습니다. x는 if let 블록 안에서만 보이므로 호출값을 바꿔도 블록 밖에서 그 이름을 사용할 수 없습니다. 성공한 값을 뒤쪽에서도 써야 한다면 실패 경로가 발산하는 let ... else를 쓰거나 match 결과를 바깥 변수에 바인딩할 수 있습니다.

예제 17-10에 나오는 x처럼 if let에 항상 매칭되는 패턴을 사용하면, 컴파일러는 경고를 줄 것입니다.

예제 17-10: if let에 반박 불가능한 패턴 사용 시도하기
    if let x = 5 {
        println!("{}", x);
    };

러스트는 반박 불가능한 패턴에 if let을 사용하는 것은 이치에 맞지 않는다고 불평합니다.

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

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

match는 가드 없는 갈래들의 집합이 모든 가능한 값을 포함해야 합니다. 마지막 갈래가 반드시 반박 불가능 패턴일 필요는 없습니다. 예를 들어 Some(x)와 None 두 갈래는 각각 반박 가능하지만 함께 모든 Option 값을 처리합니다. 와일드카드는 남은 경우를 한꺼번에 처리할 때 쓰는 선택입니다.

러스트에서는 하나의 갈래만 있는 match에 반박 불가능한 패턴 사용이 허용되지만, 이 문법은 특별히 유용하지 않으며 더 간단한 let 구문으로 대체할 수 있습니다.

이제 패턴을 사용하는 위치, 그리고 반박 가능한 패턴과 반박 불가능한 패턴 간의 차이점을 알았으니, 패턴을 만드는 데 사용할 수 있는 모든 문법을 다뤄봅시다.


반박 가능성: 패턴이 매칭에 실패할지의 여부 학습 판단 기준표

패턴이 매칭에 실패할 수 있는지 확인하는 마지막 점검은 실제 호출 모양과 실패 처리 경계가 자연스럽게 이어지는지에 맞춥니다.