panic!과 복구 불가능한 에러

한 줄 요약

panic!은 **“더 이상 진행할 수 없다”**는 프로그램의 비상 탈출이다.

러스트의 에러 두 종류

종류도구용도
복구 가능Result<T, E>파일 없음, 네트워크 실패, 파싱 에러
복구 불가능panic!배열 범위 초과, 논리적 버그, 불변 조건 위반

Result는 “실패할 수 있으니 처리해라”. panic!은 “버그다, 여기서 죽어라”.

panic! 기본

fn main() {
    panic!("치명적 에러 발생!");
}
thread 'main' panicked at '치명적 에러 발생!', src/main.rs:2:5

암묵적 panic

직접 panic!을 호출하지 않아도 패닉이 발생하는 경우:

// 1. 배열 범위 초과
let v = vec![1, 2, 3];
v[99];  // 패닉!
 
// 2. unwrap()에 None/Err
let x: Option<i32> = None;
x.unwrap();  // 패닉!
 
// 3. 정수 오버플로 (디버그 모드)
let x: u8 = 255;
let y = x + 1;  // 디버그 모드에서 패닉!

unwrap() 계열 정리

메서드동작패닉 메시지
unwrap()None/Err이면 패닉기본 메시지
expect("msg")None/Err이면 패닉커스텀 메시지
unwrap_or(default)None/Err이면 기본값패닉 없음
unwrap_or_else(f)None/Err이면 클로저 실행패닉 없음
unwrap_or_default()None/Err이면 Default 값패닉 없음
// 프로덕션 코드에서 unwrap()을 쓰면 안 되는 이유
let port: u16 = env::var("PORT")
    .unwrap()            // 환경변수 없으면 패닉 → 서버 다운
    .parse()
    .unwrap();           // 파싱 실패하면 패닉 → 서버 다운
 
// 이렇게 쓰자
let port: u16 = env::var("PORT")
    .unwrap_or_else(|_| "8080".to_string())
    .parse()
    .unwrap_or(8080);

expect()는 언제 쓰는가

“논리적으로 절대 실패할 수 없는” 경우에 이유를 명시하며 쓴다.

// 하드코딩된 정규식 — 컴파일 타임에 검증된 것과 마찬가지
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$")
    .expect("정규식 리터럴이므로 항상 유효");
 
// 방금 push한 벡터의 마지막 원소
let mut v = vec![1, 2, 3];
v.push(4);
let last = v.last().expect("방금 push했으므로 비어있을 수 없음");

패닉 동작 방식

Unwind (기본)

패닉이 발생하면 스택을 되감으며(unwind) 각 프레임의 Drop을 호출한다.

struct Resource(String);
 
impl Drop for Resource {
    fn drop(&mut self) {
        println!("정리: {}", self.0);
    }
}
 
fn main() {
    let _a = Resource("A".into());
    let _b = Resource("B".into());
    panic!("에러!");
}
// 출력:
// 정리: B
// 정리: A
// thread 'main' panicked at '에러!'

리소스가 안전하게 정리된다.

Abort

Cargo.toml에서 패닉 시 즉시 종료하도록 설정할 수 있다.

[profile.release]
panic = "abort"

스택 되감기를 하지 않아 바이너리가 작아지고 종료가 빠르다. 임베디드나 바이너리 크기가 중요한 환경에서 쓴다.

catch_unwind - 패닉 포착

일반적으로 패닉은 잡지 않지만, 특수한 상황에서 포착할 수 있다.

use std::panic;
 
let result = panic::catch_unwind(|| {
    println!("정상 실행");
    42
});
println!("{:?}", result);  // Ok(42)
 
let result = panic::catch_unwind(|| {
    panic!("에러!");
});
println!("패닉 잡음: {:?}", result);  // Err(Any)

사용하는 경우

// 서버: 하나의 요청 처리 중 패닉이 전체 서버를 죽이면 안 됨
fn handle_request(req: Request) -> Response {
    match panic::catch_unwind(|| process(req)) {
        Ok(response) => response,
        Err(_) => Response::internal_server_error(),
    }
}
 
// FFI: C 코드로 패닉이 전파되면 정의되지 않은 동작
extern "C" fn callback() {
    let _ = panic::catch_unwind(|| {
        risky_operation();
    });
}

catch_unwind는 일반적인 에러 핸들링 도구가 아니다. 그건 Result의 역할이다.

panic! vs Result 선택 기준

panic!을 쓸 때

// 1. 프로그래머의 실수 (버그)
fn divide(a: i32, b: i32) -> i32 {
    assert!(b != 0, "0으로 나눌 수 없음");  // 호출자의 버그
    a / b
}
 
// 2. 불변 조건(invariant) 위반
impl BoundedQueue {
    fn pop(&mut self) -> T {
        assert!(!self.is_empty(), "빈 큐에서 pop 시도");
        // ...
    }
}
 
// 3. 프로토타이핑 / 예제 코드
fn main() {
    let data = fetch_data().unwrap();  // TODO: 나중에 제대로 처리
}

Result를 쓸 때

// 1. 외부 요인에 의한 실패
fn read_file(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path)  // 파일이 없을 수 있음
}
 
// 2. 사용자 입력
fn parse_age(input: &str) -> Result<u32, ParseIntError> {
    input.parse()  // 사용자가 뭘 입력할지 모름
}
 
// 3. 네트워크, DB 등 외부 시스템
fn fetch_user(id: u32) -> Result<User, ApiError> {
    // 네트워크가 끊길 수 있음
}

판단 기준

질문panic!Result
호출자가 이 실패를 처리할 수 있는가?XO
이 실패가 프로그래머의 버그인가?OX
외부 요인(I/O, 네트워크, 사용자)인가?XO
프로토타입/테스트 코드인가?O-

assert! 매크로 계열

// 조건 확인
assert!(x > 0);
assert!(x > 0, "x는 양수여야 함, 실제값: {}", x);
 
// 같음 확인
assert_eq!(result, expected);
assert_eq!(result, expected, "결과가 다름");
 
// 다름 확인
assert_ne!(a, b);
 
// 디버그 모드에서만 실행
debug_assert!(expensive_check());
debug_assert_eq!(a, b);

assert!는 릴리즈 모드에서도 실행된다. debug_assert!는 디버그 모드에서만 실행된다.

todo!(), unimplemented!(), unreachable!()

fn not_yet() -> i32 {
    todo!()  // 아직 구현 안 됨. 패닉 발생.
}
 
fn legacy_feature() {
    unimplemented!("이 기능은 지원하지 않음")
}
 
fn process(value: i32) {
    match value {
        1 => println!("하나"),
        2 => println!("둘"),
        _ => unreachable!("1 또는 2만 들어온다고 확신"),
    }
}
매크로의미
todo!()아직 안 했음 (나중에 구현)
unimplemented!()의도적으로 구현하지 않음
unreachable!()이 코드에 도달하면 버그

셋 다 패닉을 발생시키지만, 의도를 명확히 전달하는 데 의미가 있다.

정리

  • panic! = 복구 불가능한 에러. 버그, 불변 조건 위반
  • Result = 복구 가능한 에러. 외부 요인, 사용자 입력
  • unwrap()/expect()는 프로토타입에서만. 프로덕션에서는 ? 또는 unwrap_or
  • catch_unwind는 서버/FFI 같은 특수 상황에서만
  • assert!는 불변 조건 검증, todo!/unreachable!은 의도 명시

다음 글에서는 thiserror, anyhow 크레이트를 사용한 실전 에러 핸들링 패턴을 다룬다.