DaleSchool

에러 처리 맛보기: panic!과 Result

중급15분

학습 목표

  • panic!과 Result의 차이를 설명할 수 있다
  • unwrap()이 무엇을 하는지 이해한다
  • Phase 1에서 사용했던 unwrap()이 왜 위험할 수 있는지 안다

동작하는 코드

모듈 07에서 숫자 맞추기 게임을 만들 때 unwrap()을 사용했어요. 그때 "에러가 나면 프로그램을 멈춰라"라고만 설명했죠. 이제 그게 정확히 뭔지 알아볼 시간이에요!

fn main() {
    // 파일에서 숫자를 읽는 시나리오
    let input = "42";
    let number: i32 = input.parse().unwrap();
    println!("숫자: {number}");

    let bad_input = "abc";
    let result: Result<i32, _> = bad_input.parse();
    println!("결과: {:?}", result);
}

실행해보세요! "42"는 잘 변환되지만, "abc"Err(...)가 나와요. 만약 bad_input.parse().unwrap()을 했다면 프로그램이 터졌을 거예요.

직접 수정하기

  1. bad_input.parse().unwrap()을 직접 실행해보세요. 어떤 에러 메시지가 나오나요?
  2. input"99999999999999999999"로 바꿔보세요. i32 범위를 넘으면 어떻게 되나요?

"왜?" — 두 가지 에러

Rust는 에러를 두 종류로 나눠요.

| | 복구 불가 (panic!) | 복구 가능 (Result) | | ---- | ------------------------------------------- | -------------------------------------------- | | 비유 | 건물 화재 — 대피! | 택배 배송 실패 — 다시 보내기 | | 언제 | 프로그래밍 실수, 절대 일어나면 안 되는 상황 | 파일 없음, 네트워크 끊김 등 예상 가능한 상황 | | 처리 | 프로그램 즉시 종료 | 에러를 받아서 대응 |

panic! — 복구 불가능한 에러

panic!은 "여기서 더 진행하면 안 된다!"고 프로그램을 멈추는 거예요.

fn main() {
    println!("시작!");
    panic!("심각한 문제 발생!");
    println!("이 줄은 실행되지 않아요");
}

인덱스 범위 초과도 panic!을 일으켜요. 모듈 06에서 본 적 있죠?

fn main() {
    let v = vec![1, 2, 3];
    println!("{}", v[10]); // panic!
}

Result — 택배 상태 조회

Result성공 또는 실패를 담는 열거형이에요. 모듈 13에서 배운 열거형이 여기서도 쓰여요!

enum Result<T, E> {
    Ok(T),    // 성공! 값이 들어있어요
    Err(E),   // 실패! 에러 정보가 들어있어요
}

택배 상태 조회에 비유하면 이해가 쉬워요.

  • Ok(값) — 배송 완료! 물건이 있어요
  • Err(에러) — 배송 실패! 이유가 적혀있어요
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("0으로 나눌 수 없어요"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    let result1 = divide(10.0, 3.0);
    let result2 = divide(10.0, 0.0);

    println!("10 / 3 = {:?}", result1);
    println!("10 / 0 = {:?}", result2);
}

Result를 match로 처리하기

모듈 13에서 배운 matchResult를 처리할 수 있어요!

fn main() {
    let input = "42";
    let result: Result<i32, _> = input.parse();

    match result {
        Ok(number) => println!("변환 성공: {number}"),
        Err(e) => println!("변환 실패: {e}"),
    }
}

모듈 07의 숫자 맞추기 게임에서 바로 이 패턴을 썼어요.

let guess: i32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => {
        println!("숫자를 입력해주세요!");
        continue;
    }
};

그때는 match를 자세히 몰랐지만, 이제는 ResultOkErr를 처리하는 것이라고 정확히 이해할 수 있어요!

unwrap() — 편하지만 위험한 친구

unwrap()Result에서 값을 꺼내는 가장 간단한 방법이에요. 하지만 Err일 때 panic!을 일으켜요.

fn main() {
    let ok_result: Result<i32, String> = Ok(42);
    let value = ok_result.unwrap(); // 42
    println!("{value}");

    // let err_result: Result<i32, String> =
    //     Err(String::from("에러!"));
    // err_result.unwrap(); // panic!
}

unwrap()은 이런 뜻이에요.

  • Ok(값) -> 값을 꺼낸다
  • Err(에러) -> panic!으로 프로그램을 멈춘다

모듈 07에서 io::stdin().read_line(&mut guess).unwrap()을 썼는데, 만약 입력을 읽는 데 실패하면 프로그램이 그냥 죽어버리는 거였어요. 연습용 코드에서는 괜찮지만, 실제 프로그램에서는 위험할 수 있어요.

더 알아보기: expect() — unwrap()의 개선판

expect()unwrap()과 같지만, 에러 메시지를 직접 지정할 수 있어요.

fn main() {
    let input = "abc";
    let number: i32 = input
        .parse()
        .expect("숫자로 변환할 수 없어요");
    // panic 메시지: "숫자로 변환할 수 없어요: ..."
}

어디서 에러가 났는지 알기 쉬워서, unwrap()보다 expect()가 나아요. 하지만 둘 다 panic!을 일으키는 건 같아요.

? 연산자로 에러를 우아하게 전파하는 방법은 Phase 3에서 배울 거예요!

연습 1. 아래 함수를 완성해서 문자열을 i32로 변환하세요. 변환에 실패하면 기본값 0을 반환하세요.

fn parse_or_default(s: &str) -> i32 {
    // 여기를 완성하세요
    // 힌트: match로 s.parse::<i32>()의 결과를 처리하세요
}

fn main() {
    println!("{}", parse_or_default("42"));   // 42
    println!("{}", parse_or_default("abc"));  // 0
    println!("{}", parse_or_default(""));     // 0
}

연습 2. 아래 find_item 함수를 완성하세요. 벡터에서 아이템을 찾으면 Ok(인덱스), 못 찾으면 Err(메시지)를 반환하세요.

fn find_item(items: &[&str], target: &str) -> Result<usize, String> {
    // 여기를 완성하세요
    // 힌트: for와 enumerate()를 사용하세요
}

fn main() {
    let fruits = vec!["사과", "바나나", "포도"];

    match find_item(&fruits, "바나나") {
        Ok(idx) => println!("바나나는 {}번째!", idx),
        Err(msg) => println!("{msg}"),
    }

    match find_item(&fruits, "딸기") {
        Ok(idx) => println!("딸기는 {}번째!", idx),
        Err(msg) => println!("{msg}"),
    }
}

Q1. panic!Result의 가장 큰 차이는?

  • A) panic!은 느리고 Result는 빠르다
  • B) panic!은 프로그램을 즉시 멈추고, Result는 에러를 값으로 반환한다
  • C) panic!은 숫자만, Result는 문자열만 다룬다
  • D) 차이가 없다

Q2. unwrap()의 동작으로 올바른 것은?

  • A) 항상 None을 반환한다
  • B) Ok면 값을 꺼내고, Errpanic!을 일으킨다
  • C) 에러를 무시하고 빈 값을 반환한다
  • D) 에러 메시지를 출력하고 계속 실행한다

Q3. 아래 코드의 출력은?

fn main() {
    let result: Result<i32, &str> = Err("실패");
    let value = match result {
        Ok(n) => n,
        Err(_) => -1,
    };
    println!("{value}");
}
  • A) "실패"
  • B) -1
  • C) 0
  • D) 프로그램이 panic으로 멈춘다