본문 바로가기
IT/Rust 기초 완전 정복

Rust 옵션과 결과 타입 활용하기 (10)

by 지식 발전소 2024. 4. 24.
728x90
반응형

1

 

옵션 타입 이해하기

옵션(Option) 타입은 Rust에서 값이 있거나 없음을 표현하는 열거형 타입입니다. 이는 null 참조 에러를 방지하기 위해 도입되었습니다. 옵션 타입에는 두 가지 값이 있습니다. Some(T)과 None입니다. Some(T)은 T 타입의 값을 가지고 있고, None은 값이 없음을 나타냅니다.

예를 들어, 사용자로부터 입력받은 문자열이 비어있을 수 있다면 다음과 같이 표현할 수 있습니다.

let user_input: Option<String> = get_user_input();

get_user_input() 함수는 Option<String> 타입을 반환합니다. 만약 사용자가 입력을 했다면 Some(input_string)을, 그렇지 않다면 None을 반환합니다. 이후 user_input에 대해 패턴 매칭을 수행하여 값의 존재 여부를 확인할 수 있습니다.

match user_input {
    Some(input_str) => {
        println!("사용자 입력: {}", input_str);
        // 입력 문자열을 처리하는 로직
    }
    None => println!("사용자 입력이 없습니다."),
}

이렇게 함으로써 개발자는 명시적으로 None 케이스를 처리해야 합니다. 이를 통해 실수로 null을 참조하는 상황을 방지할 수 있습니다. 또한 옵션 타입에는 유용한 메서드들이 제공됩니다.

  • is_some(): 옵션이 Some 값을 가지고 있는지 확인합니다.
  • is_none(): 옵션이 None인지 확인합니다.
  • unwrap(): Some 값이 있다면 그 값을 반환하고, 없다면 패닉(panic)을 발생시킵니다.
  • unwrap_or(default): Some 값이 있다면 그 값을 반환하고, 없다면 default 값을 반환합니다.
  • map(f): 옵션 내부의 값에 함수 f를 적용하여 새로운 옵션 값을 생성합니다.

옵션 타입은 null에 대한 부담 없이 안전하게 값의 존재 여부를 처리할 수 있게 해줍니다. 이를 활용하면 null 참조 문제를 방지할 수 있어 견고한 코드를 작성할 수 있습니다.

실전 예제: 옵션 타입 활용하기

옵션 타입의 개념을 실제 예제를 통해 살펴보겠습니다. 가정해 봅시다. 사용자로부터 나이를 입력받고, 그 나이가 성인인지 아닌지를 판단하는 프로그램을 작성해야 한다고 합시다.

use std::io;

fn main() {
    println!("나이를 입력해주세요:");

    let mut age_input = String::new();
    io::stdin()
        .read_line(&mut age_input)
        .expect("입력을 읽는 중 오류가 발생했습니다.");

    let age: Option<u8> = age_input.trim().parse().ok();

    match age {
        Some(age) if age >= 18 => println!("성인입니다."),
        Some(age) => println!("미성년자입니다. 나이는 {}세입니다.", age),
        None => println!("유효한 나이가 입력되지 않았습니다."),
    }
}

먼저 read_line 함수를 사용하여 사용자로부터 입력을 받습니다. trim()을 호출하여 앞뒤 공백을 제거한 뒤, parse().ok()를 호출하여 u8 타입으로 변환을 시도합니다. parse()가 성공하면 Some(u8) 값을, 실패하면 None을 반환하므로 이를 age 변수에 할당합니다.

그 후 match 구문을 사용하여 age의 값에 따라 다른 동작을 수행합니다.

  • Some(age) if age >= 18인 경우, 성인이므로 "성인입니다."를 출력합니다.
  • Some(age) 이지만 나이가 18세 미만인 경우, "미성년자입니다. 나이는 {}세입니다."를 출력합니다.
  • None인 경우, 올바른 나이가 입력되지 않았으므로 "유효한 나이가 입력되지 않았습니다."를 출력합니다.

이처럼 옵션 타입을 활용하면 null 값에 대한 걱정 없이 값의 존재 여부를 안전하게 처리할 수 있습니다. 패턴 매칭과 함께 사용하면 더욱 강력한 제어 흐름을 구현할 수 있습니다.

결과 타입 이해하기

결과(Result) 타입은 Rust에서 에러 처리를 위한 열거형 타입입니다. 이는 함수 호출이나 연산이 성공했는지 실패했는지를 나타내며, 실패한 경우에는 그 이유도 제공합니다.

결과 타입은 다음과 같은 두 가지 값을 가질 수 있습니다.

  • Ok(value): 연산이 성공했을 때 반환되는 값입니다. value는 성공한 연산의 결과값입니다.
  • Err(err): 연산이 실패했을 때 반환되는 값입니다. err는 실패 이유를 나타내는 에러 값입니다.

예를 들어, std::fs 모듈의 read_to_string 함수는 파일의 내용을 문자열로 읽어오는 작업을 수행합니다. 이 함수는 Result<String, io::Error> 타입을 반환합니다. 파일 읽기에 성공하면 Ok(file_contents)를, 실패하면 Err(err)를 반환합니다.

use std::fs;

fn main() {
    let file_result = fs::read_to_string("example.txt");

    match file_result {
        Ok(contents) => println!("파일 내용: {}", contents),
        Err(err) => println!("파일 읽기 실패: {}", err),
    }
}

이렇게 함으로써 개발자는 에러 상황을 명시적으로 처리해야 합니다. 결과 타입 또한 옵션 타입과 마찬가지로 유용한 메서드들이 제공됩니다.

  • is_ok(): 결과가 Ok 값인지 확인합니다.
  • is_err(): 결과가 Err 값인지 확인합니다.
  • unwrap(): Ok 값이 있다면 그 값을 반환하고, Err라면 패닉을 발생시킵니다.
  • unwrap_or(default): Ok 값이 있다면 그 값을 반환하고, Err라면 default 값을 반환합니다.
  • map(f): Ok 값에 함수 f를 적용하여 새로운 결과 값을 생성합니다.
  • and_then(f): Ok 값에 함수 f를 적용하여 새로운 결과 타입을 반환합니다. f는 Result를 반환해야 합니다.

결과 타입을 활용하면 에러 처리를 안전하고 명시적으로 수행할 수 있습니다. 이를 통해 견고하고 예외 상황에 대비한 코드를 작성할 수 있습니다.

실전 예제: 결과 타입 활용하기

결과 타입의 개념을 실제 예제를 통해 살펴보겠습니다. 가정해 봅시다. 파일에서 특정 단어의 개수를 세는 프로그램을 작성해야 한다고 합시다.

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn main() {
    let file_result = File::open("example.txt");

    let mut word_count = 0;
    let search_word = "the";

    match file_result {
        Ok(file) => {
            let reader = BufReader::new(file);
            for line in reader.lines() {
                let line_result = line;
                match line_result {
                    Ok(line) => {
                        let words: Vec<_> = line.split_whitespace().collect();
                        word_count += words
                            .iter()
                            .filter(|w| w.to_lowercase() == search_word)
                            .count();
                    }
                    Err(err) => {
                        println!("파일 읽기 중 오류 발생: {}", err);
                        return;
                    }
                }
            }
            println!("'{}' 단어의 개수: {}", search_word, word_count);
        }
        Err(err) => {
            println!("파일 열기 실패: {}", err);
        }
    }
}

먼저 File::open 함수를 호출하여 "example.txt" 파일을 열려고 시도합니다. 이 함수는 Result<File, io::Error> 타입을 반환합니다.

match 구문을 사용하여 파일 열기 결과를 처리합니다.

  • Ok(file)인 경우, BufReader를 생성하고 파일의 각 줄을 순회하며 검색 단어의 개수를 셉니다. 각 줄을 읽을 때마다 lines() 메서드가 Result<String, io::Error> 타입을 반환하므로, 이를 다시 match하여 에러 상황을 처리합니다.
  • Err(err)인 경우, 파일 열기에 실패했으므로 에러 메시지를 출력합니다.

파일의 각 줄에서 단어를 분리한 뒤, 검색 단어와 일치하는 단어의 개수를 셉니다. 마지막으로 총 검색 단어 개수를 출력합니다.

이처럼 결과 타입을 활용하면 에러 상황을 안전하고 명시적으로 처리할 수 있습니다. 함수 호출이나 연산의 성공 여부를 체크하고, 실패한 경우에는 적절한 에러 처리를 수행할 수 있습니다.

옵션과 결과 타입의 조합 활용

옵션과 결과 타입은 서로 조합하여 사용할 수 있습니다. 이를 통해 더욱 복잡한 상황을 안전하게 처리할 수 있습니다.

예를 들어, 파일에서 특정 단어를 검색하되, 파일이 존재하지 않으면 기본 문자열을 사용하는 프로그램을 작성해 봅시다.

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn search_word_in_file(filename: &str, search_word: &str) -> Option<usize> {
    let file_result = File::open(filename);

    match file_result {
        Ok(file) => {
            let reader = BufReader::new(file);
            let mut word_count = 0;
            for line in reader.lines() {
                let line_result = line;
                match line_result {
                    Ok(line) => {
                        let words: Vec<_> = line.split_whitespace().collect();
                        word_count += words
                            .iter()
                            .filter(|w| w.to_lowercase() == search_word)
                            .count();
                    }
                    Err(err) => {
                        println!("파일 읽기 중 오류 발생: {}", err);
                        return None;
                    }
                }
            }
            Some(word_count)
        }
        Err(err) => {
            println!("파일 열기 실패: {}", err);
            None
        }
    }
}

fn main() {
    let filename = "example.txt";
    let search_word = "the";

    let word_count = search_word_in_file(filename, search_word).unwrap_or_else(|| {
        println!("기본 문자열에서 '{}' 단어 검색", search_word);
        let default_string = "The quick brown fox jumps over the lazy dog.";
        default_string
            .split_whitespace()
            .filter(|w| w.to_lowercase() == search_word)
            .count()
    });

    println!("'{}' 단어의 개수: {}", search_word, word_count);
}

search_word_in_file 함수는 파일 이름과 검색 단어를물론, 자세히 설명해 드리겠습니다.

search_word_in_file 함수는 파일 이름과 검색 단어를 인자로 받아 Option<usize> 타입을 반환합니다. 이 함수 내부에서는 파일 열기와 단어 검색 로직을 수행합니다.

  1. File::open을 사용하여 주어진 파일을 열려고 시도합니다. 이 함수는 Result<File, io::Error> 타입을 반환합니다.
  2. 파일 열기에 성공하면(Ok(file)), BufReader를 생성하고 파일의 각 줄에서 검색 단어의 개수를 세어 word_count에 누적합니다. 단어 개수를 셀 때마다 에러 처리를 수행합니다.
  3. 파일 열기에 실패하거나(Err(err)), 파일 읽기 중 에러가 발생하면(Err(err)), 에러 메시지를 출력하고 None을 반환합니다.
  4. 모든 처리가 성공적으로 완료되면 Some(word_count)를 반환합니다.

main 함수에서는 search_word_in_file 함수를 호출하고, 그 결과를 word_count에 할당합니다. 만약 None이 반환되면 unwrap_or_else 클로저를 실행합니다.

  1. 클로저에서는 기본 문자열에서 검색 단어의 개수를 구합니다.
  2. 클로저의 결과가 word_count에 할당됩니다.

따라서 파일이 존재하면 해당 파일에서 검색 단어의 개수를 반환하고, 그렇지 않으면 기본 문자열에서 검색 단어의 개수를 반환합니다.

이렇게 옵션과 결과 타입을 조합하여 사용하면 더욱 복잡한 제어 흐름을 안전하게 구현할 수 있습니다. 에러 처리와 값의 존재 여부 확인을 명시적으로 수행할 수 있어 견고한 코드를 작성할 수 있습니다.

옵션과 결과 타입의 활용 팁

이제 옵션과 결과 타입을 보다 효과적으로 활용하는 방법에 대해 알아보겠습니다.

1. 패턴 매칭 (Pattern Matching)

옵션과 결과 타입의 값을 match 구문으로 패턴 매칭하는 것이 가장 일반적인 방법입니다. 하지만 매번 match 구문을 작성하는 것은 번거로울 수 있습니다. 이를 해결하기 위해 다음과 같은 유용한 메서드들이 제공됩니다.

  • unwrap(): Ok 또는 Some 값이 있다면 그 값을 반환하고, 그렇지 않으면 패닉을 발생시킵니다.
  • unwrap_or(default): Ok 또는 Some 값이 있다면 그 값을 반환하고, 그렇지 않으면 default 값을 반환합니다.
  • unwrap_or_else(f): Ok 또는 Some 값이 있다면 그 값을 반환하고, 그렇지 않으면 클로저 f를 호출하여 그 결과를 반환합니다.
  • expect(msg): unwrap()과 비슷하지만, 패닉 시 사용자 정의 메시지를 출력합니다.

이러한 메서드들을 활용하면 코드를 간결하게 작성할 수 있습니다. 하지만 프로그램의 중요한 부분에서는 여전히 match 구문을 사용하여 에러 처리를 명시적으로 수행하는 것이 좋습니다.

2. 결과 타입 조합하기 (Combining Results)

여러 개의 결과 타입을 조합할 때는 다음과 같은 메서드들을 사용할 수 있습니다.

  • and_then(f): Ok 값이 있다면 클로저 f를 호출하고, 그 결과를 반환합니다. f는 Result를 반환해야 합니다.
  • or_else(f): Err 값이 있다면 클로저 f를 호출하고, 그 결과를 반환합니다. f는 Result를 반환해야 합니다.

이를 활용하면 에러 처리 로직을 간결하게 표현할 수 있습니다.

let result = File::open("example.txt")
    .map_err(|err| {
        eprintln!("파일 열기 실패: {}", err);
        err
    })
    .and_then(|file| {
        let reader = BufReader::new(file);
        // 파일 처리 로직
        Ok(processed_data)
    })
    .or_else(|err| {
        eprintln!("파일 처리 중 오류 발생: {}", err);
        Err(err)
    });

위 예제에서는 파일 열기, 파일 처리 등의 작업을 and_then과 or_else를 사용하여 연결했습니다. 각 단계에서 에러가 발생하면 에러 처리 로직이 실행되고, 그렇지 않으면 다음 단계로 진행됩니다.

3. 결과와 옵션 타입 변환하기

때로는 결과 타입과 옵션 타입 간의 변환이 필요할 수 있습니다. 이를 위해 다음과 같은 메서드들이 제공됩니다.

  • ok_or(err): Some 값이 있다면 Ok(value)를 반환하고, None이라면 Err(err)를 반환합니다.
  • ok_or_else(f): Some 값이 있다면 Ok(value)를 반환하고, None이라면 클로저 f를 호출하여 그 결과를 Err로 반환합니다.
  • transpose(): Result<Option<T>, E> 타입을 Option<Result<T, E>> 타입으로 변환합니다.

이러한 메서드들을 활용하면 옵션과 결과 타입을 자유롭게 변환할 수 있어 더욱 유연한 코드를 작성할 수 있습니다.

4. 쉬운 에러 전파 (Easy Error Propagation)

? 연산자를 사용하면 에러를 전파하기 매우 쉽습니다. ? 연산자는 Result 값에 대해 작동하며, Ok 값이라면 그 값을 반환하고, Err 값이라면 현재 함수에서 해당 에러를 반환합니다.

use std::fs::File;
use std::io;

fn read_file(filename: &str) -> io::Result<String> {
    let file = File::open(filename)?; // 에러 발생 시 이 함수에서 에러를 반환
    let mut contents = String::new();
    file.read_to_string(&mut contents)?; // 에러 발생 시 이 함수에서 에러를 반환
    Ok(contents)
}

위 예제에서 File::open이나 read_to_string에서 에러가 발생하면, 해당 에러가 read_file 함수 밖으로 전파됩니다. 이렇게 하면 중첩된 match 구문을 작성할 필요 없이 깔끔한 에러 처리 코드를 작성할 수 있습니다.

5. 공장 패턴 (Factory Pattern)

옵션과 결과 타입을 반환하는 함수를 작성할 때는 공장 패턴을 사용하면 좋습니다. 이 패턴은 생성자 함수를 분리하여 객체 생성 로직과 에러 처리 로직을 분리하는 방식입니다.

struct Person {
    name: String,
    age: u8,
}

impl Person {
    fn new(name: &str, age: u8) -> Result<Person, String> {
        if age < 1 || age > 130 {
            return Err(String::from("잘못된 나이입니다."));
        }
        Ok(Person {
            name: name.to_string(),
            age,
        })
    }
}

위 예제에서 Person::new 함수는 Result<Person, String> 타입을 반환합니다. 이 함수 내부에서 나이의 유효성을 검사하고, 유효하지 않으면 에러를 반환합니다. 이렇게 하면 객체 생성과 에러 처리 로직을 깔끔하게 분리할 수 있습니다.

이러한 방식을 사용하면 보다 견고하고 유지보수 가능한 코드를 작성할 수 있습니다.

마무리

옵션과 결과 타입은 Rust에서 안전하고 견고한 코드를 작성하는 데 있어 필수적인 개념입니다. 이러한 타입을 효과적으로 활용하면 null 참조 에러와 예외 상황을 피할 수 있습니다. 패턴 매칭, 조합 메서드, 타입 변환 등의 기능을 잘 이해하고 사용하면 더욱 깔끔하고 명시적인 코드를 작성할 수 있습니다.

또한, 실제 예제를 통해 옵션과 결과 타입이 어떻게 활용되는지 살펴봄으로써 이해도를 높일 수 있었을 것입니다. 이제 여러분도 옵션과 결과 타입을 활용하여 더욱 안전하고 견고한 Rust 코드를 작성할 수 있게 되었기를 바랍니다.

핵심 요약

  • 옵션 타입은 값의 존재 여부를 안전하게 표현하는 열거형 타입입니다.
  • 결과 타입은 함수 호출이나 연산의 성공/실패 여부와 실패 이유를 나타내는 열거형 타입입니다.
  • 패턴 매칭과 유용한 메서드를 활용하면 옵션과 결과 타입을 더욱 효과적으로 다룰 수 있습니다.
  • 옵션과 결과 타입을 조합하여 더욱 복잡한 제어 흐름을 안전하게 구현할 수 있습니다.
  • ? 연산자와 공장 패턴을 활용하면 에러 처리 코드를 깔끔하게 작성할 수 있습니다.

참고 자료

[1] The Rust Programming Language Book, Steve Klabnik and Carol Nichols, https://doc.rust-lang.org/book/

[2] Rust by Example, https://doc.rust-lang.org/rust-by-example/

[3] "Rust의 Option과 Result 타입 활용하기", https://www.ted.com/talks/aaron_turon_rust_s_option_and_result_types

[4] "Using Rust's Option and Result Types Effectively", https://www.jamesbarlowdotcom.com/posts/using-rusts-option-and-result-types-effectively/

[5] "Rust: Error Handling and Option/Result Types", https://hermanradtke.com/2022/06/09/rust-error-handling-and-optionresult-types.html

 

 

한 고대 문서 이야기

여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오

gospel79.tistory.com

 

유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법

올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을

stock79.tistory.com

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

댓글