← Rust 강의 목록으로
🦀
에러 처리
에러 처리 · 선수: 16강 · 에러 처리 · 선수: 17강

17–18. panic! 과 unrecoverable 에러 · 커스텀 에러 타입

panic 은 "여기서부터 회복은 불가능하니 즉시 멈춤" 을 알리는 메커니즘입니다. 배열 범위 초과·integer division by zero·unwrap on None 등이 자동으로 panic 을 일으킵니다. 이 강의에서 panic 의 동작 원리, unwind vs abort, 그리고 panic 을 언제 명시적으로 호출해야 하는지 정리합니다. 실무 라이브러리·서비스에서는 Box<dyn Error> 보다 구체적인 에러 타입을 정의해 호출자가 종류별로 분기할 수 있게 합니다. 이 강의에서 enum 으로 자체 에러 타입을 만들고, From 트레잇·Display·Error 트레잇을 손으로 구현해 보고, thiserror 크레잇으로 이걸 한 줄로 줄이는 방법까지 다룹니다.

Rustpanicunwrapassert에러 처리thiserror에러 타입FromError trait
소요 시간
⏱ 약 1시간 + 약 1.5시간
난이도
📊 중급
선수 조건
🎯 16강
결과물
panic 은 "여기서부터 회복은 불가능하니 즉시 멈춤" 을 알리는 메커니즘입니다. 배열 범위 초과·integer division by zero·unwrap on None 등이 자동으로 panic 을 일으킵니다. 이 강의에서 panic 의 동작 원리, unwind vs abort, 그리고 panic 을 언제 명시적으로 호출해야 하는지 정리합니다. / 실무 라이브러리·서비스에서는 Box<dyn Error> 보다 구체적인 에러 타입을 정의해 호출자가 종류별로 분기할 수 있게 합니다. 이 강의에서 enum 으로 자체 에러 타입을 만들고, From 트레잇·Display·Error 트레잇을 손으로 구현해 보고, thiserror 크레잇으로 이걸 한 줄로 줄이는 방법까지 다룹니다.

이 강의에서 배우는 것

  • 1panic 이 일어나는 자동 상황들을 안다
  • 2panic!("msg") 매크로로 명시적 패닉을 일으킨다
  • 3unwind vs abort 의 차이를 안다
  • 4assert!·assert_eq!·debug_assert! 매크로를 사용한다
  • 5Result 처리 vs panic 사용 기준을 안다
  • 6enum 으로 도메인별 에러 variant 를 정의한다
  • 7Display·Error 트레잇을 직접 구현한다
  • 8From<T> 로 다른 에러 자동 변환을 설정한다
  • 9thiserror 매크로로 보일러플레이트를 줄인다
  • 10anyhow 와 thiserror 의 역할 차이를 안다

17. panic! 과 unrecoverable 에러

panic 은 "여기서부터 회복은 불가능하니 즉시 멈춤" 을 알리는 메커니즘입니다. 배열 범위 초과·integer division by zero·unwrap on None 등이 자동으로 panic 을 일으킵니다. 이 강의에서 panic 의 동작 원리, unwind vs abort, 그리고 panic 을 언제 명시적으로 호출해야 하는지 정리합니다.

소개

panic 은 다른 언어의 unchecked 예외와 비슷하지만 메시지가 명확하고 스택 트레이스가 깔끔합니다. 대부분의 panic 은 **버그를 즉시 드러내는 안전망** 입니다 — 디버그 빌드의 정수 오버플로우, .unwrap() 의 None 등.

핵심 개념

1) panic 이 자동 발생하는 상황

  • 배열·슬라이스 범위 초과 인덱싱
  • 정수 overflow (debug 빌드)
  • 0 으로 정수 나누기
  • Option::unwrap() on None / Result::unwrap() on Err
  • RefCell 빌림 규칙 런타임 위반

2) 명시적 panic

rust
panic!("의도된 종료 — TODO: implement");
assert!(n > 0, "n 은 양수여야 함");
assert_eq!(actual, expected, "값이 다릅니다");

3) unwind vs abort

전략동작기본
unwind스택 풀며 Drop 호출, 자원 해제dev/release 기본
abort즉시 종료, Drop 호출 안 함Cargo.toml panic = "abort" 로 변경 가능 (바이너리 작아짐)

4) 사용 기준 — panic vs Result

  • **panic** — 회복 불가능한 버그·invariant 위반 (계속 실행하면 더 큰 문제)
  • **Result** — 사용자 입력·외부 IO 등 정상적으로 실패할 수 있는 상황

핵심 예제

rust
fn divide(a: i32, b: i32) -> i32 {
    assert!(b != 0, "0 으로 나눌 수 없음");
    a / b
}

fn main() {
    println!("{}", divide(10, 2)); // 5
    println!("{}", divide(10, 0)); // thread 'main' panicked at '0 으로 나눌 수 없음'
}

디버그용 assertion — release 빌드에서는 제거됨:

rust
fn process(v: &[i32]) {
    debug_assert!(!v.is_empty(), "빈 슬라이스 들어옴");
    // ... 실제 처리
}

Cargo.toml 로 panic 전략 변경:

toml
[profile.release]
panic = "abort"     # 바이너리 크기 줄이기, Drop 호출 안 함

스택 트레이스 보기:

bash
RUST_BACKTRACE=1 cargo run
# panic 위치까지의 호출 스택이 자세히 출력

자주 하는 실수

Q. .unwrap() 도 panic 인가요?

A. 네, None/Err 이면 panic. 그래서 사용자 입력·IO 결과엔 .unwrap() 대신 ? 나 .unwrap_or() 가 좋습니다.

Q. panic 을 catch 할 수 있나요?

A. `std::panic::catch_unwind` 가 있지만 일반적인 에러 처리 도구가 아닙니다 — FFI 경계나 스레드 격리 같은 특수 상황용. 일상 코드는 Result 로.

Q. debug_assert! 가 release 에서 사라지는데 신뢰해도 되나요?

A. 개발 단계 검증용입니다. **실제로 영향을 미쳐야 하는 검증은 assert!** 를 쓰세요 — release 에서도 살아있습니다.

정리

  • panic = 회복 불가, 즉시 멈춤
  • 배열 초과·overflow·.unwrap on None 등이 자동 발생
  • 명시적 panic 은 panic!·assert!·assert_eq! 매크로
  • 버그/invariant 위반엔 panic, 정상 실패 가능엔 Result
  • RUST_BACKTRACE=1 로 스택 트레이스

과제

  1. 배열 인덱스 초과를 일부러 일으켜 panic 메시지와 스택 트레이스 확인
  2. assert_eq! 로 작성한 간단한 테스트 함수가 의도대로 panic 을 일으키는지 확인
  3. Cargo.toml 의 release 프로필을 abort 로 바꾸고 빌드 크기 비교

18. 커스텀 에러 타입 (thiserror·From)

실무 라이브러리·서비스에서는 Box<dyn Error> 보다 구체적인 에러 타입을 정의해 호출자가 종류별로 분기할 수 있게 합니다. 이 강의에서 enum 으로 자체 에러 타입을 만들고, From 트레잇·Display·Error 트레잇을 손으로 구현해 보고, thiserror 크레잇으로 이걸 한 줄로 줄이는 방법까지 다룹니다.

소개

라이브러리는 어떤 종류의 실패가 일어났는지 호출자에게 정확히 알려야 합니다 — `MyError::NotFound`, `MyError::PermissionDenied` 같이 variant 로 분류해야 호출자가 분기 처리 가능. 이 강의에서 그 패턴을 손으로 한 번 만들고, thiserror 로 짧게 줄이는 흐름을 봅니다.

핵심 개념

1) 손으로 작성하는 커스텀 에러

rust
use std::fmt;

#[derive(Debug)]
enum AppError {
    NotFound(String),
    Invalid(String),
    Io(std::io::Error),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::NotFound(s) => write!(f, "not found: {}", s),
            AppError::Invalid(s)  => write!(f, "invalid: {}", s),
            AppError::Io(e)       => write!(f, "io: {}", e),
        }
    }
}

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

2) thiserror — 한 줄로 줄이기

Cargo.toml 에 의존성 추가:

toml
[dependencies]
thiserror = "1"
rust
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("not found: {0}")]
    NotFound(String),
    #[error("invalid: {0}")]
    Invalid(String),
    #[error("io: {0}")]
    Io(#[from] std::io::Error),  // From 자동 생성
}

3) thiserror vs anyhow

크레잇역할추천 사용처
thiserror라이브러리용 구체 에러 enum 매크로재사용 라이브러리 작성
anyhow애플리케이션용 Box<dyn Error> wrapper + 컨텍스트바이너리·서비스 코드

핵심 예제

thiserror 로 만든 에러로 ? 자동 변환:

rust
use thiserror::Error;
use std::fs;
use std::num::ParseIntError;

#[derive(Debug, Error)]
enum AppError {
    #[error("file: {0}")]
    Io(#[from] std::io::Error),
    #[error("parse: {0}")]
    Parse(#[from] ParseIntError),
    #[error("empty file")]
    Empty,
}

fn first_number(path: &str) -> Result<i32, AppError> {
    let s = fs::read_to_string(path)?;           // io::Error → AppError
    let line = s.lines().next().ok_or(AppError::Empty)?;
    Ok(line.trim().parse::<i32>()?)              // ParseIntError → AppError
}

fn main() -> Result<(), AppError> {
    println!("{}", first_number("data.txt")?);
    Ok(())
}

호출자가 variant 별로 분기:

rust
match first_number("data.txt") {
    Ok(n)                       => println!("ok: {}", n),
    Err(AppError::Empty)        => eprintln!("파일이 비었음"),
    Err(AppError::Io(e))        => eprintln!("IO 실패: {}", e),
    Err(AppError::Parse(e))     => eprintln!("숫자 아님: {}", e),
}

자주 하는 실수

Q. Box<dyn Error> 만 쓰면 안 되나요?

A. 빠른 프로토타이핑이나 main 함수엔 OK. 라이브러리에서 그렇게 하면 호출자가 종류별 분기를 못 합니다 — 항상 다운캐스팅해야 해서 사용성이 떨어집니다.

Q. thiserror 와 anyhow 동시에?

A. 흔히 함께 씁니다. 라이브러리 모듈은 thiserror 로 구체 에러를 정의하고, 그것들을 사용하는 바이너리/main 에서는 anyhow::Result 로 받아 컨텍스트(.context) 를 덧붙이는 식.

Q. impl Error 가 꼭 필요한가요?

A. 다른 라이브러리·프레임워크와 상호운용하려면 필수. Error 트레잇을 구현해야 `Box<dyn Error>` 같은 통합 타입에 들어갈 수 있고 source() 체인을 따라갈 수 있습니다.

정리

  • enum 으로 도메인 에러를 variant 로 정의 → 호출자가 분기 가능
  • Display·Error·From 트레잇을 직접 구현하거나 thiserror 매크로로 한 줄
  • 라이브러리 = thiserror, 애플리케이션 = anyhow (혼용 가능)
  • ?의 자동 변환은 From 트레잇에 의존

과제

  1. 파일 IO 와 정수 파싱을 함께 다루는 함수의 커스텀 에러를 손으로 작성 (Display·Error·From)
  2. 같은 에러를 thiserror 로 재작성하고 코드 줄 수 비교
  3. 에러 variant 에 #[from] 을 붙이고 ? 가 자동 변환되는지 확인
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침