Result와 에러 처리 — begin/rescue가 사라진 자리
예외 대신 반환값으로 에러를 처리하는 Rust의 방식
Ruby의 에러 처리는 예외 기반이다. 뭔가 잘못되면 raise하고, 어딘가에서 rescue로 잡는다. 잡지 않으면 프로그램이 죽는다.
# Ruby
def read_config
File.read("config.toml")
rescue Errno::ENOENT => e
puts "파일 없음: #{e.message}"
nil
end
Rust에는 예외가 없다. 에러가 발생할 수 있는 함수는 Result<T, E>를 반환한다.
// Rust
fn read_config() -> Result<String, std::io::Error> {
std::fs::read_to_string("config.toml")
}
Result 사용하기
match read_config() {
Ok(content) => println!("{}", content),
Err(e) => println!("파일 없음: {}", e),
}
Option과 마찬가지로 match로 분기한다. Ok(T)는 성공, Err(E)는 실패.
? 연산자 — Ruby의 raise 전파에 대응
Ruby에서 rescue 안 하면 예외가 상위로 전파된다. Rust에서는 ?를 붙이면 에러가 상위 함수로 전파된다.
# Ruby — 예외 자동 전파
def load_app
config = File.read("config.toml") # 실패하면 예외가 올라감
parse(config)
end
// Rust — ? 로 명시적 전파
fn load_app() -> Result<App, Box<dyn std::error::Error>> {
let config = std::fs::read_to_string("config.toml")?; // Err면 여기서 반환
let app = parse(&config)?;
Ok(app)
}
?는 "Err면 바로 반환, Ok면 값을 꺼내서 계속 진행"이다. Ruby의 자동 전파를 명시적으로 쓴 것.
unwrap과 expect
let config = read_config().unwrap(); // Err면 panic
let config = read_config().expect("설정 파일 필수"); // 메시지 포함 panic
프로토타이핑에서는 unwrap을 쓰고, 나중에 제대로 된 에러 처리로 바꾸는 패턴이 일반적이다. Ruby에서 일단 rescue => e; raise e로 넘기는 것과 비슷한 임시 처리.
map_err — 에러 변환
fn load_config() -> Result<Config, AppError> {
let content = std::fs::read_to_string("config.toml")
.map_err(|e| AppError::Io(e))?; // io::Error → AppError로 변환
parse_config(&content)
.map_err(|e| AppError::Parse(e))?;
Ok(config)
}
Ruby에서 rescue IOError => e; raise AppError, e.message로 에러를 감싸는 패턴.
Ruby와의 결정적 차이
에러 가능성이 타입에 드러난다. Result<T, E>를 반환하는 함수는 "이 함수는 실패할 수 있다"고 시그니처에 명시한다. Ruby에서는 어떤 메서드가 예외를 던질지 문서를 읽지 않으면 모른다.
핵심 포인트
Result<T, E>는 Ok(값) 또는 Err(에러) — 예외의 타입 안전한 대체
? 연산자로 에러 전파 — Ruby의 자동 예외 전파를 명시적으로
unwrap은 프로토타이핑용 — 프로덕션에서는 match/? 사용
에러 가능성이 함수 시그니처에 드러난다
장점
- ✓ 어떤 함수가 실패할 수 있는지 타입만 보고 알 수 있다
- ✓ rescue를 깜빡해서 프로덕션에서 죽는 일이 없다
단점
- ✗ 에러 타입 정의가 번거롭다 — thiserror/anyhow crate로 해결
- ✗ ? 남발하면 에러가 어디서 발생했는지 추적이 어려워질 수 있다