콘텐츠로 이동

Rust panic

패닉(panic)은 되돌릴 수 없을 만큼 무언가가 잘못됐을 때 Rust가 프로그램을 즉시 멈추는 방법이에요. 앞 페이지들을 따라 해 왔다면, 사실 이미 모르는 사이에 몇 번 패닉을 일으켜 봤을 거예요. 배열의 끝을 넘어서 인덱스로 접근하거나, None에 .unwrap()을 호출하면 둘 다 패닉이 나거든요.

fn main() {
panic!("뭔가 크게 잘못됐어요");
}
thread 'main' (12345) panicked at src/main.rs:2:5:
뭔가 크게 잘못됐어요
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

괄호 안의 숫자(여기서는 (12345))는 스레드 ID예요. 실행할 때마다 달라지고, 오래된 Rust 버전에서는 표시되지 않아요.

▶ 직접 실행해 보기

이 코드를 실행하면 프로그램은 메시지를 출력하고 멈춰요. panic! 뒤에 있는 코드는 하나도 실행되지 않아요. 첫 줄 thread 'main' (12345) panicked at ...은 “메인 스레드가 src/main.rs의 2번째 줄, 5번째 칸에서 패닉했다”는 뜻이고, 마지막 note:는 “환경 변수 RUST_BACKTRACE=1을 설정하고 실행하면 백트레이스(어디서부터 호출됐는지의 기록)를 보여 준다”는 뜻이에요. 둘 다 Rust가 직접 출력하는 메시지라서 영어로 나와요.

panic!을 직접 부르지 않아도 패닉이 나는 경우

섹션 제목: “panic!을 직접 부르지 않아도 패닉이 나는 경우”

지금까지 배운 몇몇 작업은 뭔가 잘못되면 알아서 패닉을 일으켜요. 아래 예제들은 위험한 값을 코드에 직접 쓰지 않고 함수 매개변수로 받아요. 인덱스나 0을 리터럴로 그 자리에 바로 쓰면, 컴파일러가 그 작업이 항상 실패한다는 걸 컴파일할 때 알아채는 경우가 많아서, 실행해서 패닉이 나기 전에 아예 빌드를 거부하기 때문이에요.

fn get(numbers: &[i32], index: usize) -> i32 {
numbers[index] // 패닉: index out of bounds (인덱스가 범위를 벗어남)
}
fn main() {
let numbers = [1, 2, 3];
println!("{}", get(&numbers, 10));
}
▶ 직접 실행해 보기
fn main() {
let value: Option<i32> = None;
println!("{}", value.unwrap()); // 패닉: called `Option::unwrap()` on a `None` value
}
▶ 직접 실행해 보기
fn divide(a: i32, b: i32) -> i32 {
a / b // 패닉: attempt to divide by zero (0으로 나누려고 함)
}
fn main() {
let a = 10;
let b = 0;
println!("{}", divide(a, b));
}
▶ 직접 실행해 보기

주석 속 영어 문장은 Rust가 실제로 출력하는 패닉 메시지예요. 어느 경우든 Rust는 말이 안 되는 상태로 프로그램이 계속 돌아가게 두지 않아요. 몰래 엉터리 값을 돌려주는 대신 멈추는 거예요.

앞으로 자주 내리게 될 진짜 결정은 이거예요. 이 실패에서 프로그램을 멈춰야 할까요, 아니면 호출자에게 처리할 기회를 줘야 할까요?

  • panic!: 버그, 그리고 코드가 올바르다면 절대 일어나지 않아야 하는 상황에 써요. 제대로 복구할 방법이 없으니, (피해가 더 커지기 전에) 바로 멈추는 게 가장 안전해요.
  • Result(이전 페이지에서 다뤘어요): 잘못된 사용자 입력, 없는 파일, 실패한 네트워크 요청처럼 호출자가 충분히 예상하고 처리하고 싶어 할 만한 실패에 써요.
// 사용자의 잘못된 입력: 복구할 수 있으니 Result를 써요
fn parse_age(input: &str) -> Result<u32, String> {
input.parse().map_err(|_| String::from("올바른 나이가 아니에요"))
}
// 코드가 올바르다면 있을 수 없는 전제 조건: 패닉
fn get_first(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
panic!("get_first가 빈 슬라이스로 호출됐어요");
}
numbers[0]
}

unwrap()과 expect()는 패닉으로 가는 지름길이에요

섹션 제목: “unwrap()과 expect()는 패닉으로 가는 지름길이에요”

Option과 Result에서 .unwrap()을 봤죠. 사실 이건 “값을 줘. 없으면 패닉해”를 줄여 쓴 것일 뿐이에요. .expect("메시지")도 똑같이 동작하지만, 더 쓸모 있는 메시지를 붙일 수 있어요.

fn main() {
let input = "42";
let number: i32 = input.parse().expect("input은 올바른 숫자여야 해요");
println!("{number}");
}
▶ 직접 실행해 보기

변환에 실패하면 패닉 메시지에 여러분이 쓴 문장이 들어가요. .unwrap()이 보여 주는 일반적인 메시지보다 디버깅이 훨씬 쉬워져요.

가정 확인하기: assert!, assert_eq!, unreachable!

섹션 제목: “가정 확인하기: assert!, assert_eq!, unreachable!”

코드의 어느 지점에서 무언가가 반드시 참이라는 걸 알고 있을 때가 있어요. Rust에는 그런 믿음을 검사로 바꿔 주는 작은 매크로가 몇 가지 있어요. 검사에 실패하면 알기 쉬운 메시지와 함께 패닉을 일으켜요. 그래서 버그가 나중에 이상한 결과를 일으키는 대신, 버그가 시작된 바로 그 자리에서 드러나요.

  • assert!(condition, "message"): 조건이 false이면 패닉해요.
  • assert_eq!(left, right): 두 값이 같지 않으면 패닉하고, 두 값을 모두 출력해요.
  • unreachable!("message"): 절대 실행되면 안 되는 코드를 표시해요. 나올 수 없는 값에 대한 match 갈래 같은 곳이요.
fn average(numbers: &[i32]) -> i32 {
assert!(!numbers.is_empty(), "average에는 숫자가 하나 이상 필요해요");
let mut total = 0;
for n in numbers {
total += n;
}
total / numbers.len() as i32
}
fn day_name(day: u8) -> &'static str {
match day {
1 => "월요일",
2 => "화요일",
3 => "수요일",
4 => "목요일",
5 => "금요일",
6 => "토요일",
7 => "일요일",
_ => unreachable!("day는 1부터 7 사이여야 하는데 {day}이(가) 들어왔어요"),
}
}
fn main() {
let avg = average(&[70, 80, 90]);
assert_eq!(avg, 80);
println!("평균: {avg}");
println!("3번째 요일: {}", day_name(3));
}

실행 결과:

평균: 80
3번째 요일: 수요일
▶ 직접 실행해 보기

여기서는 모든 검사를 통과하니까 프로그램이 평소처럼 실행돼요. 이제 Playground에서 목록을 [70, 80, 95]로 바꿔 보세요. 평균이 81이 되고, assert_eq!가 두 값을 나란히 보여 주면서 프로그램을 멈춰요.

thread 'main' (12345) panicked at src/main.rs:26:5:
assertion `left == right` failed
left: 81
right: 80

“left == right 단언이 실패했다”는 뜻이에요. left는 avg(81), right는 기대한 값(80)이에요. average(&[])나 day_name(9)도 시도해 보세요. 여러분이 직접 쓴 메시지가 패닉에 나오는 걸 볼 수 있어요.

이 검사들은 릴리스 빌드에서도 항상 실행되니까, 가볍게 유지하세요. assert_eq!는 테스트 페이지에서 다시 만나요. 테스트가 “이건 저것과 같아야 해”라고 말하는 주된 방법이거든요. todo!()도 있어요. “not yet implemented”(아직 구현되지 않음)라는 메시지와 함께 패닉하는 매크로예요. 나중에 완성할 함수의 뼈대를 잡을 때, 함수 본문 자리를 임시로 채우는 데 편리해요.

설정값 let port_text = "8080";이 있어요. .parse()와 .expect(...)로 이걸 u16으로 바꾸고, 무엇이 잘못됐는지 설명하는 메시지를 쓰세요. 그런 다음 8080번 포트에서 대기 중을 출력하세요. 힌트: parse가 무엇을 만들어야 할지 알 수 있도록 변수에 타입이 필요해요. let port: u16 = ...처럼요. 다 했으면 텍스트를 "80a0"으로 바꿔서 패닉 속에 여러분의 메시지가 나오는지 확인해 보세요.

정답 보기
fn main() {
let port_text = "8080";
let port: u16 = port_text
.parse()
.expect("설정 파일의 포트는 0부터 65535 사이의 숫자여야 해요");
println!("{port}번 포트에서 대기 중");
}

실행 결과:

8080번 포트에서 대기 중
▶ 직접 실행해 보기

각자 낼 몫을 반환하는 fn split_bill(total: u32, people: u32) -> u32를 작성해 보세요. 0명으로 나누는 건 호출하는 코드의 버그이니, 알기 쉬운 메시지와 함께 assert!로 잡아내세요. 총액 120을 4명이 나눌 때의 몫을 출력하세요. 힌트: 나누기 전에, 함수의 첫 줄에 assert!를 두세요.

정답 보기
fn split_bill(total: u32, people: u32) -> u32 {
assert!(people > 0, "split_bill에는 한 명 이상이 필요해요");
total / people
}
fn main() {
let share = split_bill(120, 4);
println!("한 사람당 {share}씩 내면 돼요");
}

실행 결과:

한 사람당 30씩 내면 돼요
▶ 직접 실행해 보기
  • panic!은 에러 메시지와 함께 프로그램을 즉시 멈춰요.
  • 표준 라이브러리의 일부 작업(잘못된 배열 인덱스, None에 .unwrap(), 0으로 나누기)은 자동으로 패닉을 일으켜요.
  • 있을 수 없어야 하는 버그나 전제 조건에는 panic!을, 호출자가 충분히 처리할 수 있는 실패에는 Result를 쓰세요.
  • .expect("메시지")는 .unwrap()과 같지만, 패닉을 디버깅하기 쉽게 해 주는 메시지를 붙일 수 있어요.
  • assert!, assert_eq!, unreachable!은 참이라고 믿었던 게 거짓으로 밝혀지면 일부러 패닉을 일으켜요.

이해도 점검

1. panic!을 호출하면 어떻게 되나요?

2. 이 페이지의 기본 원칙에 따르면, 언제 Result 대신 panic!을 써야 하나요?

3. .unwrap()과 .expect('메시지')의 차이는 무엇인가요?

점수: 0 / 3