출시·고도화 중
Rust 안내서 · 5/6
Rust는 예외(exception)가 없습니다. 대신 오류를 두 종류로 나눕니다. 복구할 수 없는 버그는 panic!으로 프로그램을 멈추고, 실패할 수 있는 작업은 Result 값으로 돌려줘 호출한 쪽이 처리하게 합니다. 이 장의 뒷부분에서는 Rust에 내장된 테스트 도구를 다룹니다.
panic!은 "여기까지 오면 프로그램에 버그가 있다"는 뜻입니다. 배열 범위를 벗어나거나, None에 unwrap()을 호출해도 패닉이 납니다. 파일이 없거나 입력 형식이 틀린 것처럼 예상할 수 있는 실패에는 Result<T, E>를 씁니다.
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
let text = fs::read_to_string(path)?; // 실패하면 여기서 오류를 돌려줍니다
Ok(text.trim().to_string())
}
fn main() {
match read_config("config.txt") {
Ok(text) => println!("설정: {text}"),
Err(e) => eprintln!("설정을 읽지 못했습니다: {e}"),
}
}unwrap()과 expect("이유")는 오류면 패닉을 일으킵니다. 예제나 테스트, "절대 실패하지 않는다"는 것이 확실한 곳에서만 쓰고, 그때도 이유를 적는 expect가 낫습니다.
?는 Result가 Ok면 안의 값을 꺼내고, Err면 그 오류를 즉시 함수 밖으로 돌려줍니다. 오류 타입이 다르면 From 변환을 거쳐 함수의 오류 타입으로 바꿔 줍니다. Option을 돌려주는 함수 안에서는 Option에도 쓸 수 있습니다.
use std::error::Error;
use std::num::ParseIntError;
fn sum_strs(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x: i32 = a.trim().parse()?;
let y: i32 = b.trim().parse()?;
Ok(x + y)
}
fn first_char_upper(s: &str) -> Option<char> {
let c = s.chars().next()?; // 빈 문자열이면 None
Some(c.to_ascii_uppercase())
}
// main 도 Result 를 돌려줄 수 있습니다. Err 이면 오류를 출력하고 실패 코드로 끝납니다
fn main() -> Result<(), Box<dyn Error>> {
println!("{}", sum_strs("2", " 40 ")?); // 42
println!("{:?}", first_char_upper("rust")); // Some('R')
println!("{:?}", sum_strs("2", "x")); // Err(ParseIntError { .. })
Ok(())
}Box<dyn Error>는 어떤 오류 타입이든 담을 수 있어 작은 프로그램의 main에 편리합니다.
라이브러리에서는 실패 원인을 열거형으로 정의하면 호출한 쪽이 match로 구분할 수 있습니다. Display와 std::error::Error를 구현하고, From을 구현해 두면 ?가 자동으로 변환합니다.
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
Negative(i64),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Parse(e) => write!(f, "숫자가 아닙니다: {e}"),
AppError::Negative(n) => write!(f, "음수는 받을 수 없습니다: {n}"),
}
}
}
impl std::error::Error for AppError {}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn parse_positive(s: &str) -> Result<i64, AppError> {
let n: i64 = s.parse()?; // ParseIntError 가 AppError 로 바뀝니다
if n < 0 {
return Err(AppError::Negative(n));
}
Ok(n)
}
fn main() {
for input in ["42", "-7", "abc"] {
match parse_positive(input) {
Ok(n) => println!("성공: {n}"),
Err(e) => println!("실패: {e}"),
}
}
}이 반복 코드를 줄여 주는 크레이트도 널리 쓰입니다. 라이브러리에서는 thiserror로 오류 열거형을 간단히 정의하고, 애플리케이션에서는 anyhow로 여러 오류를 하나로 모아 문맥을 덧붙이는 방식이 흔합니다.
테스트는 #[test]를 붙인 함수입니다. 보통 같은 파일 아래쪽에 #[cfg(test)] 모듈을 두어 테스트할 때만 컴파일되게 합니다. assert!, assert_eq!, assert_ne!로 결과를 확인합니다.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("divide by zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn panics_on_zero() {
divide(1, 0);
}
#[test]
fn parses_number() -> Result<(), std::num::ParseIntError> {
let n: i32 = "10".parse()?; // 테스트도 Result 를 돌려줄 수 있습니다
assert_eq!(n, 10);
Ok(())
}
}cargo test는 단위 테스트, tests/ 폴더의 통합 테스트, 문서 주석 속 예제(문서 테스트)를 모두 실행합니다.
cargo test # 전부 실행
cargo test adds # 이름에 adds 가 들어간 테스트만
cargo test -- --nocapture # 테스트 중 println! 출력 보기
cargo test -- --ignored # #[ignore] 를 붙인 느린 테스트 실행
cargo test --doc # 문서 테스트만/// 문서 주석 안의 코드 블록은 문서가 되면서 동시에 테스트로 실행되므로, 예제가 실제 코드와 어긋나지 않습니다. 패키지 이름이 my-crate라면 코드에서는 my_crate로 씁니다.
/// 두 수를 더합니다.
///
/// ```
/// let sum = my_crate::add(2, 3);
/// assert_eq!(sum, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}panic!, 예상할 수 있는 실패는 Result로 표현합니다.?는 오류를 바로 돌려주며 From으로 오류 타입을 변환합니다.Display, Error, From을 구현하면 쓰기 좋은 오류 타입이 됩니다.#[test]와 cargo test로 단위 · 통합 · 문서 테스트를 한 번에 돌립니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.