All posts

Rust 초급 8편: Option·Result로 빈 값과 오류 처리하기

학습 시간을 숫자 문자열로 받는다고 해 봅시다. "30"은 정수로 바꿀 수 있지만 "abc"는 바꿀 수 없습니다. 아직 입력하지 않은 값도 있을 수 있습니다. 이번에는 부재와 실패를 구분해 프로그램이 예상 가능한 문구로 응답하게 만듭니다.

정상 결과와 수정이 필요한 결과를 확인하는 게로 오류 처리를 표현한 일러스트

학습 시간을 숫자 문자열로 받는다고 해 봅시다. "30"은 정수로 바꿀 수 있지만 "abc"는 바꿀 수 없습니다. 아직 입력하지 않은 값도 있을 수 있습니다. 이번에는 부재와 실패를 구분해 프로그램이 예상 가능한 문구로 응답하게 만듭니다.

준비와 목표

7편의 enum과 match를 사용합니다. 목표는 Option과 Result의 변형을 처리하고, ?가 오류를 호출자에게 전달하는 방식을 읽는 것입니다.

cargo new study_errors
cd study_errors

이번에는 콘솔 입력 대신 여러 샘플 문자열을 코드에 적어 테스트합니다. 키보드 입력 처리 없이 변환과 검증에 집중하기 위해서입니다.

핵심 실습: 학습 시간 변환하기

src/main.rs 전체를 아래로 바꾸세요.

fn parse_minutes(input: &str) -> Result<u32, String> {
    let minutes = input.trim().parse::<u32>()
        .map_err(|_| String::from("0 이상의 정수를 입력하세요"))?;

    if minutes == 0 {
        return Err(String::from("학습 시간은 1분 이상이어야 합니다"));
    }
    Ok(minutes)
}

fn main() {
    let nickname: Option<&str> = None;
    println!("학습자: {}", nickname.unwrap_or("익명"));

    for input in [" 30 ", "abc", "0"] {
        match parse_minutes(input) {
            Ok(minutes) => println!("저장 가능: {minutes}분"),
            Err(message) => println!("입력 오류: {message}"),
        }
    }
}
cargo run
학습자: 익명
저장 가능: 30분
입력 오류: 0 이상의 정수를 입력하세요
입력 오류: 학습 시간은 1분 이상이어야 합니다

값이 없을 때는 Option

Option<T>는 Some(T) 또는 None입니다. 여기서는 닉네임이 없으므로 None입니다. Some("민수")로 바꾸면 실제 이름이 표시됩니다. <&str>은 이 Option에 값이 있다면 문자열 참조라는 뜻입니다.

unwrap_or("익명")은 값이 있으면 그 값을, 없으면 기본값을 사용합니다. 부재가 정상적인 상황일 때 사용할 수 있습니다. 값이 없다는 것 자체를 오류로 취급할지는 프로그램의 요구사항에 따라 정합니다. Option 공식 API

실패할 수 있는 계산에는 Result

Result<T, E>는 성공 값 Ok(T) 또는 오류 Err(E)입니다. 이 함수의 성공 타입은 u32, 오류 타입은 String입니다. 변환 실패뿐 아니라 0분이라는 업무 규칙 위반도 같은 반환 형식으로 알립니다.

trim()은 문자열 양끝의 공백을 제거한 문자열 슬라이스를 반환합니다. parse::<u32>()는 그 문자열을 u32로 해석합니다. ::<u32>는 결과 타입을 명시하는 표기입니다. 음수와 u32 범위를 넘는 숫자도 변환에 실패합니다.

map_err와 ?를 천천히 읽기

parse가 반환하는 원래 오류는 정수 변환 오류 타입입니다. maperr는 오류인 경우에만 우리가 정한 String으로 바꿉니다. ||는 인자를 받지만 이번에는 그 값이 필요 없다는 뜻의 짧은 클로저입니다. 성공한 정수는 그대로 통과합니다.

마지막 ?는 성공이면 안쪽 정수를 꺼내고, 오류이면 이 함수에서 바로 Err를 반환합니다. 오류를 없애거나 무조건 성공으로 만드는 기호가 아닙니다. 오류가 호출자에게 전달되고 main의 match가 문구를 표시합니다. Result와 오류 전파

parse_minutes가 Result를 반환하기 때문에 이 ?를 사용할 수 있습니다. 아무 반환값도 없는 보통의 fn main() 본문에 그대로 옮기면 반환 타입 조건이 맞지 않습니다. ?를 만났다면 그 함수의 반환 타입도 함께 읽으세요.

unwrap으로 덮어두지 않기

"abc".parse::<u32>().unwrap()처럼 작성하면 실패 시 panic이 발생합니다. 잘못된 사용자 입력은 충분히 예상할 수 있으므로 match로 알리는 편이 적합합니다. unwrap이 언제나 금지라는 뜻은 아닙니다. 실패를 정말 허용할 수 없는 전제인지 판단해야 합니다.

입력 샘플을 ["30", "40"]으로만 테스트하면 실패 경로를 놓칩니다. 숫자가 아닌 문자열, 0, 음수, 앞뒤 공백이 있는 숫자를 함께 넣어 보세요. 성공 경로만 실행되는 것과 오류 처리가 맞는 것은 별개입니다.

자주 만나는 오류

함수의 반환 타입이 Result<u32, String>인데 Err("문구")를 쓰면 &str과 String이 맞지 않습니다. String::from("문구")로 맞추세요. Ok(minutes) 끝에는 세미콜론을 붙이지 않습니다. 이 식이 최종 반환값입니다.

Option과 Result도 섞어 쓰지 않습니다. 값이 없는 None에는 실패 원인이 없고, Err는 오류 정보를 담습니다. 닉네임이 없다는 것과 파일을 읽다 실패했다는 것은 서로 다른 상황입니다.

연습과 해설

하루 학습 시간을 최대 1,440분으로 제한하세요. 입력 "1440"은 성공하고 "1441"은 오류여야 합니다.

해설: 0분 검사 아래에 if minutes > 1440 { return Err(String::from("하루는 1440분을 넘을 수 없습니다")); }를 추가합니다. main의 샘플 배열에도 두 문자열을 넣습니다. 경계 바로 위와 아래를 시험하면 >와 >=를 잘못 선택한 경우를 찾기 쉽습니다.

다음 9편에서는 여러 기록을 Vec에 모으고 HashMap으로 주제별 시간을 합칩니다. 값 조회에서 Option이 실제로 어떻게 나오는지도 확인합니다.

실행 확인

이 글의 핵심 실습은 Rust 1.99.1, 2024 Edition의 Linux 환경에서 확인했습니다. 의도적으로 실패하는 예제는 설명한 오류가 발생하는지 구분해 검사했습니다. 운영체제별 오류 문구는 다를 수 있습니다.

Rust 초급 시리즈 전체 목차

  1. 설치와 Cargo로 첫 프로그램 만들기
  2. 변수·자료형·문자열로 학습 기록 만들기
  3. 함수·조건문·반복문으로 학습 기록 계산하기
  4. String으로 이해하는 소유권과 이동
  5. 참조와 빌림으로 문자열 읽고 수정하기
  6. 구조체와 메서드로 학습 할 일 만들기
  7. 열거형과 패턴 매칭으로 학습 상태 표현하기
  8. Option·Result로 빈 값과 오류 처리하기
  9. 벡터·해시맵·문자열로 학습 기록 모으기
  10. 모듈과 외부 크레이트로 코드 정리하기
  11. 테스트와 디버깅으로 목표 계산 검증하기
  12. 파일을 검색하는 CLI 프로젝트 완성하기

시리즈 모아보기

Discussion

No comments yet