Rust 기초 강좌 #6 에러 처리: Result와 ? 연산자, panic·unwrap 사용 기준
Rust에는 try/catch가 없습니다. 예외라는 개념 자체가 없기 때문입니다. 대신 지난 편의 Option과 같은 원리로, 실패할 수 있다는 사실을 반환 타입에 드러내고 컴파일러가 처리를 강제합니다. 처음에는 번거로워 보이지만, “이 함수가 어떤 상황에서 실패하는지"가 시그니처만 봐도 전부 드러나는 코드가 됩니다.
Result: 성공 또는 실패 #
enum Result<T, E> {
Ok(T), // 성공. 결과값을 담는다
Err(E), // 실패. 에러값을 담는다
}파일 읽기, 네트워크 요청, 파싱처럼 실패할 수 있는 함수는 전부 Result를 반환합니다. 2편부터 쓰던 parse가 그렇습니다.
let input = "42";
let n: i32 = match input.parse() {
Ok(value) => value,
Err(e) => {
println!("숫자가 아닙니다: {e}");
return;
}
};예외 방식과의 차이가 여기서 드러납니다. 예외는 처리를 잊어도 컴파일되고, 런타임에 어디선가 터집니다. Result는 i32가 아니라 Result<i32, ParseIntError>라는 다른 타입이라서, 실패 가능성을 벗겨 내기 전에는 값을 사용할 수 없습니다. 호출한 쪽이 처리를 잊는 것이 컴파일 단계에서 불가능합니다.
? 연산자: 에러는 위로 전달한다 #
모든 호출마다 match를 쓰면 코드가 금방 계단이 됩니다. 실무 에러 처리의 대부분은 “여기서 처리하지 않고 호출한 쪽에 넘기기"인데, 그 전파를 한 글자로 줄인 것이 ? 연산자입니다.
use std::fs;
fn read_config() -> Result<Config, ConfigError> {
let text = fs::read_to_string("config.toml")?; // Err이면 여기서 즉시 반환
let config = parse_config(&text)?; // Err이면 여기서 즉시 반환
Ok(config)
}?는 결과가 Ok면 안의 값을 꺼내 계속 진행하고, Err이면 그 에러를 함수의 반환값으로 즉시 되돌려 보냅니다. 위 함수를 match로 풀어 쓰면 십수 줄이 되는데, ? 두 개로 같은 의미가 됩니다. 이때 함수의 반환 타입이 Result여야 한다는 조건이 있습니다. 에러를 넘겨받을 곳이 시그니처에 선언되어 있어야 하기 때문입니다. 일반 함수에서 ?를 쓰면 “이 함수는 Result를 반환하지 않는다"는 컴파일 에러(E0277)가 나고, help 줄이 반환 타입 변경을 제안합니다.
전파는 main까지 이어질 수 있습니다. main도 Result를 반환할 수 있어서, 작은 프로그램은 이 형태가 실용적인 기본형입니다.
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let config = read_config()?;
run(config)?;
Ok(())
}Box<dyn Error>는 “어떤 종류든 에러면 된다"는 실용적 타입입니다(dyn의 정체는 8편의 트레이트에서 다룹니다). 서로 다른 에러 타입이 섞이는 프로그램의 진입점에서 흔히 쓰는 형태이고, 에러가 main까지 올라오면 프로세스가 메시지를 출력하고 실패 코드로 종료합니다.
unwrap과 expect: 지름길의 사용 조건 #
let n: i32 = "42".parse().unwrap(); // Err이면 패닉
let n: i32 = "42".parse().expect("포트 번호 파싱 실패"); // 메시지를 남기는 unwrap
unwrap은 실패 처리를 포기하고 “실패하면 그냥 죽어라"를 선택하는 것입니다. 무책임해 보이지만 정당한 자리가 있습니다.
- 예제, 프로토타입, 테스트: 에러 처리가 글의 본질이 아닐 때. 이 강좌의 앞 편들이 그랬습니다.
- 실패가 곧 프로그램 버그일 때: 방금 넣은 키를 바로 꺼내는 경우처럼, 실패했다면 로직 자체가 잘못된 상황. 이때는
expect("방금 넣은 키가 없음")처럼 이유를 남기는 쪽이 낫습니다.
반대로 사용자 입력, 파일, 네트워크처럼 정상 운영 중에도 실패할 수 있는 지점의 unwrap은 시한폭탄입니다. 언젠가 형식이 어긋난 입력 하나에 프로세스 전체가 내려갑니다. 이런 곳은 ?로 전파하거나 match로 복구하는 것이 정석입니다.
panic: 복구가 아니라 중단 #
panic!은 프로그램을 즉시 중단시키는 장치이고, unwrap의 실패가 바로 이것입니다. 배열 범위 밖 인덱스 접근(2편)도 패닉이었습니다. Rust의 구분 기준은 명확합니다.
- 예상 가능한 실패(없는 파일, 잘못된 입력, 끊긴 연결) →
Result로 표현하고 처리를 강제합니다. - 일어나면 안 되는 상태(불변 조건 위반, 논리적으로 불가능한 분기) →
panic!으로 즉시 중단합니다. 잘못된 상태로 계속 도는 것보다 죽는 것이 안전하기 때문입니다.
라이브러리를 만든다면 이 구분이 더 중요해집니다. 호출자가 복구할 수 있어야 하는 실패를 패닉으로 처리하면, 호출자에게서 선택권을 빼앗는 설계가 됩니다.
정리 #
- Rust에는 예외가 없습니다. 실패 가능성은
Result<T, E>라는 반환 타입으로 드러나고, 처리하지 않으면 값 자체를 사용할 수 없습니다. ?연산자는Ok면 값을 꺼내고Err이면 즉시 반환하는 전파 장치입니다. 반환 타입이Result인 함수 안에서만 쓸 수 있습니다.- 작은 프로그램은
main() -> Result<(), Box<dyn Error>>가 실용적인 기본형입니다. unwrap·expect는 예제·테스트이거나 실패가 곧 버그인 지점에서만 정당합니다. 입력·파일·네트워크에는?와match가 정석입니다.- 예상 가능한 실패는
Result, 일어나면 안 되는 상태는panic이라는 경계가 Rust 에러 설계의 축입니다. - 다음 편에서는 실무 코드의 몸통인 컬렉션과 이터레이터를 다룹니다.
Vec,String,HashMap, 그리고 for 반복문을 대체하는 체인 스타일까지 살펴보겠습니다.