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 크레잇으로 이걸 한 줄로 줄이는 방법까지 다룹니다.
이 강의에서 배우는 것
- 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
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 등 정상적으로 실패할 수 있는 상황
핵심 예제
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 빌드에서는 제거됨:
fn process(v: &[i32]) {
debug_assert!(!v.is_empty(), "빈 슬라이스 들어옴");
// ... 실제 처리
}Cargo.toml 로 panic 전략 변경:
[profile.release]
panic = "abort" # 바이너리 크기 줄이기, Drop 호출 안 함스택 트레이스 보기:
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 로 스택 트레이스
과제
- 배열 인덱스 초과를 일부러 일으켜 panic 메시지와 스택 트레이스 확인
- assert_eq! 로 작성한 간단한 테스트 함수가 의도대로 panic 을 일으키는지 확인
- Cargo.toml 의 release 프로필을 abort 로 바꾸고 빌드 크기 비교
18. 커스텀 에러 타입 (thiserror·From)
실무 라이브러리·서비스에서는 Box<dyn Error> 보다 구체적인 에러 타입을 정의해 호출자가 종류별로 분기할 수 있게 합니다. 이 강의에서 enum 으로 자체 에러 타입을 만들고, From 트레잇·Display·Error 트레잇을 손으로 구현해 보고, thiserror 크레잇으로 이걸 한 줄로 줄이는 방법까지 다룹니다.
소개
라이브러리는 어떤 종류의 실패가 일어났는지 호출자에게 정확히 알려야 합니다 — `MyError::NotFound`, `MyError::PermissionDenied` 같이 variant 로 분류해야 호출자가 분기 처리 가능. 이 강의에서 그 패턴을 손으로 한 번 만들고, thiserror 로 짧게 줄이는 흐름을 봅니다.
핵심 개념
1) 손으로 작성하는 커스텀 에러
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 에 의존성 추가:
[dependencies]
thiserror = "1"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 로 만든 에러로 ? 자동 변환:
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 별로 분기:
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 트레잇에 의존
과제
- 파일 IO 와 정수 파싱을 함께 다루는 함수의 커스텀 에러를 손으로 작성 (Display·Error·From)
- 같은 에러를 thiserror 로 재작성하고 코드 줄 수 비교
- 에러 variant 에 #[from] 을 붙이고 ? 가 자동 변환되는지 확인
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.
댓글은 회원만 쓸 수 있습니다.
광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침