[RUST] 9. 에러 처리

    시작하며

    러스트는 에러를 크게 회복 가능한(recoverable) 에러와 회복이 불가능한(unrecoverable) 에러 두가지로 구분한다. 러스트에는 예외라는 개념이 없고, 회복 가능한 에러를 표현하는 Result<T,E> 타입과 회복 불가능한 에러가 프로그램의 실행을 종료하는 panic! 매크로를 지원한다.

    panic! 매크로를 이용한 회복 불가능한 에러 처리

    panic! 매크로를 실행하면 프로그램은 실패 메시지를 출력하고 스택을 모두 정리한 다음 종료한다.

    fn main() {
        panic = 'abort'
    }

    에러 메시지에는 패닉 메시지와 패닉이 발생한 소스 코드의 위치가 출력된다.

    panic! 역추적 사용하기

    범위를 벗어난 인덱스를 이용해 panic! 매크로 호출해보자

    책에서는 작성한 파일이 아닌 mod.rs 파일을 가리킨다고 적혀있지만 intelliJ 환경에서는 main.rs 파일을 가리키고 있다. 역추적을 위해 RUST_BACKTRACE 환경 변수를 설정해야한다. 책에서는 1로 설정하라고 되어있지만, 에러메시지에는 full로 설정하라고 되어있어 full로 설정하였다. intelliJ 에서의 환경 변수 설정은 run 버튼옆 드롭다운 메뉴의 'Edit Configurations' 버튼을 눌러 환경 변수를 추가하였다. 환경 변수를 추가하고 실행을 했을때 stack backtrace 내용을 확인 해 볼수 있다.

    Result 타입으로 에러 처리하기

    Result 열거자는 Ok와 Err 열것값을 정의하고 있다. 파일여는 코드를 통해 컴파일러에게 리턴 타입을 확인해보자

    use std::fs::File;
    
    fn main()
    {
        let f: u32 = File::open("hello.txt");
    }

    에러 메시지를 통해 File::open 함수의 리턴 타입이 Result<T,E> 라는 것을 알 수 있다. 리턴 결과에 따라 동작을 할 수 있도록 코드를 작성해보자

    use std::fs::File;
    
    fn main()
    {
        let f = File::open("hello.txt");
    
        let f = match f {
            Ok(file) => file,
            Err(error) => {
                panic!("파일 열기 실패: {:?}", error);
            }
        };
    }

    위 예제는 결과값이 Ok 이면 변수 f에 파일 핸들을 리턴해서 파일의 내용을 읽거나 쓸 수 있다. hello.txt 파일이 존재하지 않을 경우 panic! 매크로의 출력결과를 볼 수 있다.

    match 표현식으로 여러 종류의 에러 처리하기

    중첩된 match 표현식을 이용해 실패 원인에 따라 다르게 동작하도록 할 수 있다.

    use std::fs::File;
    
    fn main()
    {
        let f = File::open("hello.txt");
    
        let f = match f {
            Ok(file) => file,
            Err(ref error) => match error.kind() {
                ErrorKind::NotFound => match File::create("hello.txt") {
                    Ok(fc) => fc,
                    Err(e) => panic!("파일을 생성하지 못했습니다: {:?}", e),
                 },
                 other_error => panic!("파일을 열지 못했습니다: {:?}", other_error),
            },
        };
    }

    에러 발생 시 패닉을 발생하는 더 빠른 방법: unwarp과 expect

    match 표현식과 같은 동작을 하는 단축(shortcut)메서드인 unwrap 메서드가 있다.

    use std::fs::File;
    
    fn main()
    {
        let f = File::open("hello.txt").unwrap();
    }

    hello.txt 파일이 없는 상태에서 실행하면 다음과 같은 에러 메시지를 볼 수 있다.

    expect 메서드는 unwrap 메서드와 유사하지만 panic! 매크로에 에러 메시지를 전달할 수 있다.

    use std::fs::File;
    
    fn main()
    {
        let f = File::open("hello.txt").expect("파일을 열 수 없습니다.");
    }

    에러 전파하기

    실패 가능성이 있는 함수를 호출할 때는 에러를 함수 안에서 처리하지 않고 호출하는 코드에 에러를 리턴해서 호출자가 에러를 처리하게 할 수 있다.

    use std::io;
    use std::io::Read;
    use std::fs::File;
    
    fn read_username_from_file() -> Result<String, io::Error> {
        let f = File::open("hello.txt");
    
        let mut f = match f {
            Ok(file) => file,
            Err(e) => return Err(e),
        };
    
        let mut s = String::new();
    
        match f.read_to_string(&mut s) {
            Ok(_) => Ok(s),
            Err(e) => Err(e),
        }
    }

    ? 연산자를 이용해 에러를 더 쉽게 전파하기

    앞선 예제를 ? 연산자를 이용해 구현한 코드이다. ? 연산자의 경우 에러값은 from 함수를 이용해 전달 된다. 에러 타입은 현재 함수의 리턴 타입에 정의된 에러 타입으로 변환된다.

    use std::io;
    use std::io::Read;
    use std::fs::File;
    
    fn read_username_from_file() -> Result<String, io::Error> {
        let mut f = File::open("hello.txt")?;
        let mut s = String::new();
    
        f.read_to_string(&mut s)?;
        Ok(s)
    }
    
    //더 짧게 다음과 같이 코드를 줄일 수 있다.
    fn read_username_from_file() -> Result<String, io::Error> {
        let mut s = String::new();
    
        File::open("hello.txt")?.read_to_string(&mut s)?;
        Ok(s)
    }
    
    //러스트에서는 이미 fs::read_to_string 함수를 제공하고 있다.
    fn read_username_from_file() -> Result<String, io::Error> {
        fs::read_to_string("hello.txt")
    }

    ? 연산자는 Result 타입을 리턴하는 함수에서만 사용할 수 있다.

    () 타입을 리턴하는 main 함수에 ? 연산자를 사용하면 어떤 일이 벌어지는지 살펴보자

    use std::fs::File;
    
    fn main() {
        let f = File::open("hello.txt")?;
    }

    위 에러 메시지는 Result 타입을 리턴하지 않는 함수에 ? 연산자를 사용할 수 없다는 점을 설명하고 있다. main 함수는 특별한 함수여서 이 함수가 리턴할 수 있는 값의 타입에 제한이 있다. 다음과 같이 Result<T,E> 타입을 리턴할 수도 있다.

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

    Box 타입은 트레이트 객체라고 부르는 타입이며 '모든 종류의 에러'를 의미한다고 알아두자

    패닉에 빠질 것인가? 말 것인가?

    실패할 가능성이 있는 함수를 작성할 때는 대체로 Result 타입을 리턴할 것을 권한다.

    예제, 프로토타입 코드, 그리고 테스트

    unwrap과 expect 메서드는 실제로 에러를 어떻게 처리할 것인지 결정하기에 앞서 프로토타이핑을 해보는 상황에 유용하다. 테스트 코드를 실행하는 중에 메서드 호출이 실패하면 설령 그 메서드가 실제로 테스트하는 메서드가 아니라 할지라도 전체 테스트를 실패처리 해야할 것이다.

    컴파일러보다 개발자가 더 많은 정보를 가진 경우

    Result 타입을 리턴하는 로직의 결과가 Ok 값을 리턴할 것이 확실하더라도, 그 로직이 컴파일러가 이해할 수 없는 것이라면 unwrap 메서드를 함께 호출해 주는 것이 좋다.

    use std::net::IpAddr;
    let home: IpAddr = "127.0.0.1".parse().unwrap();

    127.0.0.1 은 유효한 IP 주소여서 파싱이 실패할 리 없지만 그래도 unwrap 메서드를 호출할 수 있다.

    에러 처리를 위한 조언

    만일 누군가 우리가 작성한 코드를 호출하면서 적절하지 않은 값을 전달했다면 가장 좋은 선택은 panic! 매크로를 호출해서 버그가 있음을 알여야 한다. 하지만 어떤 이유로 작업이 실패할 수 있다면 panic! 매크로를 호출하는 것보다는 Result 타입을 리턴하는 편이 낫다. 코드가 어떤 값에 대한 작업을 수행할 때는 그 값이 유효한지를 먼저 검사한 후 그렇지 않으면 패닉을 발생시켜야 한다.

    유효성 검사를 위한 커스텀 타입

    러스트의 타입 시스템이 유효한 값의 절달을 보장한다는 사실을 활용하기 위해 유효성 검사를 위한 커스텀 타입을 생성하는 방법에 대해 알아보자

    loop {
        // 생략
    
        let guss: i32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };
    
        if guess < 1 || guess > 100 {
            println!("1에서 100 사이의 값을 입력해 주세요");
            continue;
        }
    
        match guess.cmp(&secret_number);
        // 생략
    }

    이 코드의 if 구문은 입력된 값이 범위를 벗어나는지 확인하고 사용자에게 입력값에 문제가 있음을 알린 후 continue 구문을 호출해서 사용자가 다시 값을 입력할 수 있도록 한다. 더 나은 방법은 새로운 타입을 생성하고 이 타입의 인스턴스를 생성하는 함수에 유효성 검사 코드를 넣는 것이다.

    pub struct Guess{
        value: i32.
    }
    
    impl Guess{
        pub fn new(value: i32) -> Guess{
            if value < 1 || value > 100 {
                panic!("유추한 값은 반드시 1에서 100 사이의 값이어야 합니다. 입력한 값:{}", value);
            }
    
            Guess {
                value
            }
        }
    
        pub fn value(&self) -> i32 {
            self.value
        }
    }

    모듈 외부의 코드가 반드시 Guess::new 함수를 이용해 Guess 타입의 인스턴스를 생성하게 함으로써 엉뚱한 값을 가진 Guess 타입은 절대 생성할 수 없게 된다. 이제 Guess 타입을 시그너처에 추가해서 별도의 유효성 검사 없이도 원하는 값을 처리할 수 잇다.

    요약

    • panic! 매크로는 프로그램이 제대로 처리할 수 없는 비정상적인 상태에 놓였다는 것을 알려주고 유효하지 않거나 잘못된 값을 계속 사용하지 않도록 프로세스를 종료한다.
    • Result 열거자는 러스트의 타입 시스템을 이용해 작업이 실패할 수도 있음을 명시하고, 실패한 경우 프로그램을 회복할 기회를 제공한다.
    • Result 타입을 리턴하면 호출 코드가 작업이 성공한 경우와 실패한 경우를 모두 처리할 수 있도록 유도할 수 있다.
    반응형

    댓글

    Designed by JB FACTORY