Result로 복구 가능한 오류 다루기

Result로 복구 가능한 오류 다루기

프로그램이 돌다 보면 파일이 없거나, 네트워크가 끊기는 등 "복구할 수 있는" 실패가 생겨요. 이런 상황을 프로그램을 죽여 버리는 패닉(panic)으로 처리하기보다는, 값을 돌려주면서 호출한 쪽에서 대응하게 하고 싶을 때가 많아요. Rust는 이런 복구 가능한 오류를 Result enum으로 표현해요.

출처: The Rust Programming Language - Recoverable Errors with Result

Result는 성공 값과 오류 값을 담는 두 개의 variant OkErr로 이루어져 있어요.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

여기서 T는 성공했을 때 반환할 값의 타입이고, E는 실패했을 때 오류 정보의 타입이에요. 예를 들어 File::open은 성공하면 파일 핸들을 Ok에 담아 주고, 실패하면 오류 정보를 Err에 담아 줘요.

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(e) => panic!("Problem opening the file: {e:?}"),
    };
}

match는 가장 기본적인 처리 방법이지만 조금 장황하죠. 그래서 Result에는 여러 헬퍼 메서드가 준비되어 있어요. 성공이면 값을 꺼내고 실패면 패닉을 일으키는 unwrap이 대표적이에요.

let greeting_file = File::open("hello.txt").unwrap();

오류를 호출한 쪽으로 전파하기

모든 오류를 바로 처리할 필요는 없어요. 함수 안에서 처리하지 못하는 오류는 return으로 호출한 쪽에 넘겨 줄 수 있어요. 이를 오류 전파(propagating)라고 해요.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();
    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

이런 match 패턴은 너무 반복적이라서 Rust는 ? 연산자를 제공해요. ?Ok면 안의 값을 꺼내주고, Err면 그 오류를 그대로 함수 밖으로 조기 반환(early return)해 줘요.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

?는 함수의 반환 타입이 Result처럼 이 오류 타입과 호환되어야 사용할 수 있어요. 오류를 조기 반환하기 때문이죠. main 함수 자체를 Result를 반환하게 만들 수도 있어요. 그러면 main 안에서도 ?를 쓸 수 있어요.

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;
    Ok(())
}

여기서 Box<dyn Error>는 대략 "어떤 종류의 오류든"이라는 뜻의 트레이트 객체예요. 아직은 그저 어떤 오류를 다 받아줄 수 있는 타입이라고 이해하면 충분해요.

더 알아보기