Rust에서의 오류 처리와 모듈 리팩터링

이전 장에서는 명령행 인터페이스를 갖는 간단한 텍스트 검색 도구인 미니 버전의 grep을 구현하기 시작했습니다. 이 프로그램은 주어진 파일 내에서 특정 문자열을 찾는 기능을 수행합니다. 현재까지 작성된 코드는 기본적인 동작은 하지만, 여전히 예외 상황에 대한 처리가 부족한 상태입니다.

문제점: 잘못된 입력 처리

현재 프로그램은 사용자가 정확히 세 개 이상의 인수를 제공한다고 가정하고 있습니다. 그러나 인수가 부족할 경우 다음과 같은 런타임 패닉이 발생합니다:

thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1

이러한 메시지는 개발자에게는 의미가 있지만 일반 사용자에게는 혼란스럽습니다. 따라서 더 명확하고 사용자 친화적인 오류 처리 방식이 필요합니다.

방법 개선: panic! 대신 Result 사용

Rust에서는 복구 가능한 오류의 경우 panic!보다는 Result<T, E> 타입을 사용하는 것이 바람직합니다. 이를 통해 오류를 호출자에게 전달하고 적절히 처리할 수 있습니다. 먼저 Config 구조체의 생성 로직을 수정합니다:

use std::process;

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn from_args(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("입력 인수가 부족합니다: 쿼리와 파일 경로를 모두 지정해야 합니다.");
        }
        let search_term = args[1].clone();
        let path = args[2].clone();

        Ok(Config {
            query: search_term,
            file_path: path,
        })
    }
}

여기서 반환 타입은 Result<Config, &'static str>이며, 실패 시 정적 생명주기를 가진 문자열 리터럴을 반환합니다. 이렇게 하면 컴파일러가 오류 메시지의 수명을 안전하게 관리할 수 있습니다.

오류 처리: unwrap_or_else 활용

main 함수에서는 이 Result 값을 적절히 처리해야 합니다. unwrap_or_else 메서드를 사용하면 성공 시에는 값을 추출하고, 실패 시에는 클로저를 통해 사용자 정의 동작을 수행할 수 있습니다:

fn main() {
    let cli_args: Vec<String> = env::args().collect();

    let config = Config::from_args(&cli_args)
        .unwrap_or_else(|error_msg| {
            eprintln!("인수 해석 중 문제 발생: {}", error_msg);
            process::exit(1);
        });

    match fs::read_to_string(&config.file_path) {
        Ok(content) => {
            println!("검색 결과:\n{}", content);
        }
        Err(e) => {
            eprintln!("파일 읽기 실패: {}", e);
            process::exit(1);
        }
    }
}

여기서 eprintln! 매크로를 사용하여 오류 메시지를 표준 에러 스트림(stderr)으로 출력함으로써, 표준 출력(stdout)과의 혼동을 방지합니다. 또한 process::exit(1)을 호출하여 비정상 종료 상태 코드를 반환합니다.

최종 코드 요약

위 변경사항을 반영한 전체 코드는 다음과 같습니다:

use std::env;
use std::fs;
use std::process;

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn from_args(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("입력 인수가 부족합니다: 쿼리와 파일 경로를 모두 지정해야 합니다.");
        }
        let search_term = args[1].clone();
        let path = args[2].clone();

        Ok(Config { query: search_term, file_path: path })
    }
}

fn main() {
    let cli_args: Vec<String> = env::args().collect();

    let config = Config::from_args(&cli_args)
        .unwrap_or_else(|error_msg| {
            eprintln!("인수 해석 중 문제 발생: {}", error_msg);
            process::exit(1);
        });

    match fs::read_to_string(&config.file_path) {
        Ok(content) => println!("검색 결과:\n{}", content),
        Err(e) => {
            eprintln!("파일 읽기 실패: {}", e);
            process::exit(1);
        }
    }
}

이렇게 리팩터링된 코드는 더 견고하며, 예상치 못한 입력이나 파일 접근 오류에도 명확한 피드백을 제공합니다. 또한 Rust의 타입 시스템과 오류 처리 철학을 따르므로 유지보수성이 높아집니다.

태그: Rust 오류처리 result 클로저 명령행도구

6월 10일 18:21에 게시됨