Result로 복구 가능한 오류 처리하기

RustBeginner
지금 연습하기

소개

일부 실패는 프로그래밍 버그이지만, 실행 중에 발생할 것으로 예상되는 실패도 있습니다. 예를 들어 사용자가 잘못된 형식의 텍스트를 입력하거나 허용 범위를 벗어난 값을 입력할 수 있습니다. Rust는 이러한 복구 가능한 결과를 Result<T, E>로 표현하며, 이는 Ok(T) 또는 Err(E) 중 하나입니다.

패닉을 발생시키지 않고 유효하거나 유효하지 않은 텍스트를 파싱하고, 유용한 검증 오류를 반환하며, ? 연산자를 사용해 실패를 호출자에게 전달합니다. 준비된 Cargo 프로젝트에서는 각 수정 작업이 오류 처리에 집중되도록 구성되어 있습니다.

성공한 Result 매칭하기

이 단계에서는 텍스트를 정수로 파싱하고 성공한 결과를 매칭합니다.

프로젝트 경로는 /home/labex/project/result-workshop이며, src/main.rs에는 네 개의 작업용 자리 표시자가 있습니다. 프로젝트 디렉터리로 이동한 후 소스 파일을 엽니다.

cd /home/labex/project/result-workshop
nano src/main.rs

Step 1 주석을 다음 코드로 바꿉니다.

    let valid = "42".parse::<i32>();
    match valid {
        Ok(value) => println!("Parsed value: {value}"),
        Err(error) => println!("Parse error: {error}"),
    }

parse는 서로 다른 출력 타입을 만들 수 있는 재사용 가능한 메서드입니다. ::<i32> 부분은 이 호출의 출력 타입을 지정하며, Rust에 문자열을 부호 있는 32비트 정수로 해석하도록 요청합니다. 이 꺾쇠괄호 타입 인수는 ::를 사용하지만 Message::Text 같은 경로와는 다릅니다. 파싱은 실패할 수 있으므로 반환 타입은 Result입니다. Ok(value)에는 파싱된 정수가 들어 있고, Err(error)에는 파싱에 실패한 이유가 들어 있습니다. match는 두 변형을 모두 처리해야 합니다.

Ctrl+O를 눌러 저장하고, Enter를 누른 다음 Ctrl+X로 종료합니다. 확인하고 실행합니다.

cargo check
cargo run --quiet
Parsed value: 42

"42"가 유효한 정수 텍스트이므로 Ok 분기만 실행됩니다.

잘못된 입력에서 복구하기

이 단계에서는 잘못된 형식의 텍스트를 프로그램을 중단시키는 대신 데이터로 처리합니다.

패닉은 프로그램이 설계상 예상하지 못한 상태에 도달했음을 나타내며, 일반적으로 실행을 중지합니다. 외부에서 들어오는 잘못된 입력은 예상 가능한 상황이므로, 프로그램이 이를 검사하고 보고할 수 있도록 Err로 처리해야 합니다. 소스 파일을 엽니다.

nano src/main.rs

Step 2 주석을 다음 코드로 바꿉니다.

    let invalid = "many".parse::<i32>();
    match invalid {
        Ok(value) => println!("Unexpected value: {value}"),
        Err(error) => println!("Handled invalid input: {error}"),
    }

이 match도 가능한 두 결과를 모두 처리하지만, 준비된 텍스트는 Err 분기를 선택합니다. 저장하고 종료한 후 실행합니다.

cargo run --quiet

두 번째 줄은 다음과 같아야 합니다.

Handled invalid input: invalid digit found in string

프로그램은 정상적으로 계속 실행되었고, 파서의 오류를 유용한 메시지로 변환했습니다. 외부 데이터에는 unwrap()을 사용하지 않는 것이 좋습니다. 같은 Err에서 unwrap()을 사용하면 복구할 기회 없이 패닉이 발생합니다.

물음표 연산자로 파싱하고 검증하기

이 단계에서는 백분율 또는 유용한 오류 메시지를 반환할 수 있는 함수를 작성합니다.

Result<u32, String> 타입은 성공할 경우 부호 없는 정수를 포함하고, 실패할 경우 소유된 오류 텍스트를 포함한다는 뜻입니다. ? 연산자는 Ok에서 값을 가져옵니다. Err를 만나면 해당 오류를 현재 함수에서 즉시 반환합니다.

소스 파일을 엽니다.

nano src/main.rs

main 위에 다음 함수를 추가합니다.

fn parse_percentage(text: &str) -> Result<u32, String> {
    let value = text
        .parse::<u32>()
        .map_err(|_| format!("not a whole number: {text}"))?;

    if value <= 100 {
        Ok(value)
    } else {
        Err(format!("outside 0..=100: {value}"))
    }
}

이 함수를 짧은 순서로 나누어 읽어 보겠습니다. 먼저 parse::<u32>()가 텍스트를 정수로 변환하려고 시도합니다. 그런 다음 map_err(...)가 파싱 실패만 처리하여, 원래 입력을 포함하는 더 명확한 String으로 바꿉니다.

다음으로 ?는 성공한 숫자를 value에 넣거나, 새 오류를 parse_percentage에서 즉시 반환합니다. 파싱에 성공하면 if가 숫자를 검증하고 Ok(value) 또는 범위를 벗어났다는 내용의 Err를 반환합니다.

클로저 매개변수 _는 새 메시지가 원래 파서 오류를 대신하므로 원래 오류 값을 의도적으로 무시한다는 뜻입니다. 여기서 _는 무시되는 매개변수입니다. 앞에서 사용한 _ =>는 와일드카드 매칭 분기였고, let _ =는 전체 결과를 버렸습니다.

main 안의 Step 3 주석을 다음 코드로 바꿉니다.

    println!("85 => {:?}", parse_percentage("85"));
    println!("150 => {:?}", parse_percentage("150"));
    println!("many => {:?}", parse_percentage("many"));

:? 포매터는 이 실습에서 Ok 또는 Err 변형을 표시합니다. 저장하고 종료한 후 확인하고 실행합니다.

cargo check
cargo run --quiet

새로 추가한 줄은 다음과 같아야 합니다.

85 => Ok(85)
150 => Err("outside 0..=100: 150")
many => Err("not a whole number: many")

이제 함수는 유효한 값, 파싱에는 성공했지만 범위를 벗어난 값, 잘못된 형식의 텍스트를 구분합니다.

다른 함수를 통해 오류 전파하기

이 단계에서는 두 번째 함수 경계에서 ?를 사용하고, main에서 최종 결과를 처리합니다.

헬퍼 함수가 애플리케이션에서 오류를 어떻게 표시하거나 복구해야 하는지 항상 알고 있는 것은 아닙니다. 헬퍼 함수는 Err를 상위로 전파하고, 호출자가 처리 방법을 결정하도록 할 수 있습니다. 소스 파일을 엽니다.

nano src/main.rs

parse_percentage와 main 사이에 다음 함수를 추가합니다.

fn acceptance_message(text: &str) -> Result<String, String> {
    let value = parse_percentage(text)?;
    Ok(format!("accepted {value}%"))
}

파싱 또는 검증에 실패하면 ?가 기존 String 오류를 즉시 반환합니다. 성공하면 함수는 메시지를 Ok로 감쌉니다.

main 안의 Step 4 주석을 다음 코드로 바꿉니다.

    for text in ["73", "bad"] {
        match acceptance_message(text) {
            Ok(message) => println!("{text} => {message}"),
            Err(message) => println!("{text} => error: {message}"),
        }
    }

반복문은 성공하는 입력 하나와 실패하는 입력 하나를 제공합니다. 각 결과를 사용자에게 보여 주는 출력으로 바꾸는 경계는 main입니다. 저장하고 종료한 후 확인하고 실행합니다.

cargo check
cargo run --quiet

마지막 줄은 다음과 같아야 합니다.

73 => accepted 73%
bad => error: not a whole number: bad

parse_percentage에서 만든 동일한 오류 컨텍스트가 acceptance_message를 거쳐 전파되었습니다.

요약

성공하거나 실패한 Result 값을 매칭하고, 잘못된 형식의 입력을 복구 가능한 조건으로 처리했습니다. map_err로 컨텍스트가 포함된 String 오류를 만들었으며, unwrap으로 패닉을 발생시키는 대신 ?를 사용해 함수 경계를 넘어 실패를 전파했습니다.