복구 가능 vs 복구 불가능 에러

러스트에서 에러 처리는 크게 복구 가능한 에러와 복구 불가능한 에러로 나눌 수 있다. 복구 가능한 에러는 적절한 로직이나 대안 처리를 통해 프로그램을 계속 정상 동작 상태로 유지할 수 있는 반면, 복구 불가능한 에러는 프로그램이 더 이상 정상 동작을 유지할 수 없다고 판단하고 예외적으로 종료해 버리는 방식으로 대응한다. Rust에서는 이러한 개념을 언어 차원에서 비교적 명확하게 구분하며, 각각에 대해 다른 메커니즘을 제공한다.

복구 가능한 에러는 대표적으로 외부 자원이나 I/O 작업에서 발생할 수 있다. 예를 들어 파일을 열거나 네트워크 자원에 접근할 때, 해당 자원이 존재하지 않거나 접근 권한 문제가 발생할 수 있다. 이런 상황은 치명적이지만 꼭 프로그램 전체를 종료해야만 하는 것은 아니다. 파일 경로를 수정하거나 네트워크 연결을 재시도함으로써 문제를 극복할 수도 있기 때문이다. Rust에서는 이러한 복구 가능한 에러를 일반적으로 Result<T, E> 타입으로 표현한다. Result 타입은 Ok(T) 또는 Err(E) 두 가지 분기로 나뉘며, Ok는 정상 동작, Err는 에러 상황을 의미한다. 사용자는 match 표현식, ? 연산자, unwrap_or_else 등의 다양한 방식으로 Err를 처리하거나 대체할 수 있다.

복구 불가능한 에러는 프로그램이 오류를 인지한 시점에서 더 이상 정상적인 동작을 기대하기 어렵거나, 해석할 수 없을 정도의 예외 상황이 발생했음을 의미한다. 예컨대 메모리를 더 이상 할당할 수 없는 상황이 발생하거나 논리적으로 절대 일어나서는 안 되는 상태가 감지된 경우를 생각해볼 수 있다. Rust는 복구 불가능한 에러 상황에서 주로 panic! 매크로를 통해 즉각 프로그램을 중단하게 한다. panic!이 호출되면 런타임은 스택을 타고 올라가면서 모든 자원을 해제하고 종료 시점에 에러 메시지를 출력한다. 러스트는 기본적으로 메모리 안전성을 보장하기 위해 이러한 치명적 상태를 조기에 중단함으로써, 더 심각한 문제로 이어지지 않게 보호하는 전략을 취한다.

Rust에서 Result 타입으로 표현되는 복구 가능한 에러는 사용자가 직접 정의한 Error 트레이트를 구현하거나 표준 라이브러리에서 제공하는 에러 타입을 조합해 사용할 수 있다. 예컨대 파일을 처리하는 std::fs 모듈에서 반환되는 Result<File, io::Error> 타입을 match로 분기 처리하여 파일 존재 여부를 검사하거나, ? 연산자로 에러를 빠르게 전파할 수 있다. 아래 예시를 살펴보자.

fn open_file(path: &str) -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string(path)?;
    Ok(content)
}

fn main() {
    match open_file("example.txt") {
        Ok(text) => {
            println!("파일 내용을 성공적으로 읽었다:\n{}", text);
        }
        Err(e) => {
            eprintln!("파일을 열 수 없다: {}", e);
        }
    }
}

open_file 함수는 파일 경로가 유효하지 않거나 권한이 없을 경우 std::io::Error를 반환한다. main 함수에서는 match를 통해 Err를 잡아내고 적절한 대처 방안을 적용할 수 있다. 이렇게 복구가 가능한 에러 시나리오에서는 Result를 사용해 에러를 호출자에게 알리고, 호출자는 이를 어떻게 처리할지 결정한다.

반면 복구 불가능한 에러 상황이라면 panic! 매크로를 사용할 수 있다. 논리적으로 절대 일어나서는 안 되는 값이 발생했거나, 내부 불변성이 깨졌을 때 등 “이 시점에서는 더 이상 안전하게 진행할 수 없다”고 판단되면 즉시 프로그램을 멈추고 에러 메시지를 출력한다.

이 예시에서 0으로 나누는 것은 수학적으로 정의되지 않은 상황이므로 프로그램을 계속 진행하려고 시도하기보다 즉시 오류를 보고 중지시키는 편이 합리적이다. Rust에서는 이렇게 복구가 불가능하다고 판단되는 오류에 대해서는 panic!을 통해 의도를 명확하게 드러내도록 권장한다.

복구 불가능한 에러를 처리할 때는, Rust가 스택 언와인딩(stack unwinding)을 수행할 수 있다는 점에 유의해야 한다. 스택 언와인딩 과정에서, 각 함수가 반납해야 할 자원(파일 핸들, 메모리 등)이 안전하게 정리된다. 기본적으로 Cargo 설정에서 panic 발생 시 스택 언와인딩이 일어나도록 설정되어 있지만, 성능상 이점이나 특정 플랫폼 특성을 위해 abort로 빌드할 수도 있다. abort로 설정하면 언와인딩 없이 프로그램이 바로 종료되어 버리므로, 대형 프로젝트에서는 의도에 맞춰 빌드 설정을 주의 깊게 살펴봐야 한다.

정리하자면, 러스트에서 복구 가능한 에러는 Result 타입으로 표현되고, 호출자 또는 상위 로직에서 이를 어떻게 처리할지 책임진다. 반면 복구 불가능하다고 판단되는 에러 상황은 panic!을 사용해 프로그램을 중단함으로써 더욱 치명적인 결과나 예측 불가능한 상태를 방지한다. 이렇게 언어 차원에서 복구 가능한 에러와 불가능한 에러를 분명히 구분해놓음으로써, 코드가 의도를 분명히 드러내고 안정적으로 동작하게 만든다.

Last updated