Rust에서 에러 처리는 "컴파일러가 눈 감고 넘어가는 상황을 없애 보자"는 철학의 연장선에 있습니다. 이번 편에서는 [[panic|panic!]], [[result|Result<T, E>]], [[question-mark-operator|? 연산자]]가 각각 어떤 역할을 하는지, 그리고 언제 어떤 도구를 골라야 실전 프로그램이 더 안전해지는지를 다룹니다.
왜 에러 처리가 중요한가
- 시스템 언어의 특성: 파일, 네트워크, 사용자 입력처럼 실패가 흔한 작업을 다룰수록 예측 가능한 에러 흐름이 필요합니다.
- Ownership 규칙과 연계: 에러가 나더라도 자원이 자동으로 정리되려면, 에러 표현도 타입 시스템 안에 들어와 있어야 합니다.
- 런타임 패닉 최소화:
panic!은 프로그램을 즉시 중단시키기 때문에, 복구 가능한 상황에서는Result로 실패를 표현하는 습관이 중요합니다.
[[panic|panic!]]으로 프로그램을 중단시키기
[[panic|panic!]]은 "이 지점 이후로 실행을 이어가면 안 된다"고 판단될 때 쓰는 매크로입니다.
fn main() {
let config = std::env::var("APP_CONFIG")
.unwrap_or_else(|_| panic!("환경 변수가 필요합니다"));
println!("config = {}", config);
}
panic!이 발생하면 스택 언와인딩(unwinding)이 진행되고, 현재 스레드는 즉시 중단됩니다.- 디버깅 중에 "절대 일어나면 안 되는 상태"를 확인하거나, 샘플 프로젝트에서 빠르게 실패 지점을 찾을 때 유용합니다.
- 하지만 CLI나 서버처럼 계속 실행돼야 하는 프로그램에서는 남용하지 말고, 복구 가능한 실패를
Result로 표현하세요.
[[result|Result<T, E>]]로 실패를 값으로 다루기
[[result|Result]]는 enum Result<T, E> { Ok(T), Err(E) } 형태이며, 성공과 실패를 모두 타입 시스템 안에 넣습니다.
fn read_config() -> Result<String, std::io::Error> {
std::fs::read_to_string("./config.txt")
}
fn main() {
match read_config() {
Ok(content) => println!("config: {}", content),
Err(err) => eprintln!("파일을 읽지 못했습니다: {}", err),
}
}
Ok(T)는 정상 결과를,Err(E)는 실패 이유를 담습니다.E타입을 통해 어떤 에러인지 구체적으로 표현할 수 있고,match나if let으로 분기합니다.Result를 반환하는 함수는 호출자에게 실패 가능성을 명시적으로 전달하므로, 함수 이름만 봐도 안전성을 추론하기 쉬워집니다.
헬퍼 메서드 활용하기
- unwrap, expect: 빠른 프로토타입 단계에서
Result를T로 강제로 꺼낼 때 사용하되, 최종 코드에서는 의미 있는 메시지를 제공하거나 안전한 흐름으로 교체하세요. - map, map_err, and_then: 함수형 스타일로 성공·실패값을 가공할 수 있습니다.
[[question-mark-operator|? 연산자]]로 에러 전파 단축하기
[[question-mark-operator|?]]는 [[result|Result]] (또는 [[option|Option]])가 Err일 때 현재 함수에서 즉시 반환하도록 만들어 줍니다.
fn load_user(id: u64) -> Result<String, Box<dyn std::error::Error>> {
let path = format!("./users/{}.json", id);
let raw = std::fs::read_to_string(&path)?;
let parsed: serde_json::Value = serde_json::from_str(&raw)?;
Ok(parsed["name"].as_str().unwrap_or("unknown").to_string())
}
?는Err를 만나면 함수의 반환 타입에 맞춰 그대로 전달하고,Ok면 내부 값을 꺼내 다음 줄로 넘깁니다.?를 사용하려면 현재 함수도Result또는Option을 반환해야 합니다. 그리고Result를 다룰 때는 함수 반환 타입의Err가 현재 에러를 받아들일 수 있어야 합니다.- 중첩된
match대신 직선적인 흐름을 만들 수 있어 가독성이 크게 올라갑니다.
언제 무엇을 고를까
| 상황 | 권장 도구 | 이유 |
|---|---|---|
| 복구 불가능한 버그, 디버깅 보조 | panic!, unreachable! |
문제를 즉시 표면화해 빠르게 수정 |
| 파일/네트워크 등 실패가 자연스러운 작업 | Result |
호출자에게 선택권을 넘기고 실패 이유를 보존 |
| 간단한 실습·프로토타입 단계 | unwrap, expect |
흐름을 단순화하되 나중에 안전한 처리로 교체 |
| 여러 에러를 같은 타입으로 묶기 | Result<T, Box<dyn Error>> 또는 커스텀 enum |
다양한 에러 원인을 한 채널로 전달 |
입문 단계에서는 아래처럼 기억하면 판단이 쉬워집니다.
- 학습용 짧은 예제:
unwrap,expect를 잠깐 허용 - 실제로 실패가 자연스러운 작업:
Result - 정말 계속 실행하면 안 되는 버그:
panic!
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo check와 cargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요