[2026] Rust 에러 처리 | Result, Option, ? 연산자

[2026] Rust 에러 처리 | Result, Option, ? 연산자

이 글의 핵심

Rust 에러 처리: Result, Option, ? 연산자. Result·Option.

들어가며

Rust에는 try/catch 스타일의 예외가 없습니다. 실패 가능한 연산은 Result로, 값이 없을 수 있음은 Option으로 타입에 적어 두고 처리합니다. 복구 가능한 오류를 값으로 돌려주는 방식이라, 제어 흐름이 추적하기 쉽습니다.

Rust와의 첫 만남

“빌려주기 검사기(Borrow Checker)와 싸우는 게 프로그래밍의 반”이라는 농담이 있을 정도로, Rust는 처음에 정말 어렵습니다. 저도 첫 프로젝트에서 컴파일러 에러와 씨름하며 “이게 정말 생산성이 높은 언어인가?” 의심했습니다. 하지만 몇 주간 고생 끝에 컴파일이 통과된 코드는 런타임 에러가 거의 없다는 걸 깨달았습니다. C++에서는 세그멘테이션 폴트가 프로덕션에서 터지는 악몽을 자주 겪었는데, Rust는 그런 걱정이 없습니다. 컴파일러가 미리 잡아주니까요. 특히 멀티스레드 코드를 작성할 때 이 차이가 극명합니다. C++에서는 데이터 레이스를 찾느라 디버거와 씨름했지만, Rust는 컴파일 단계에서 “이 코드는 스레드 안전하지 않아”라고 알려줍니다. 처음엔 답답했지만, 지금은 이 엄격함이 감사합니다.

1. Result<T, E>

기본 Result

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("0으로 나눌 수 없음"))
    } else {
        Ok(a / b)
    }
}
fn main() {
    match divide(10, 2) {
        Ok(result) => println!("결과: {}", result),
        Err(e) => println!("에러: {}", e),
    }
}

Result 메서드

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

fn main() {
    let result = divide(10, 2);
    
    // unwrap: Ok면 값, Err면 panic
    let value = result.unwrap();  // 5
    
    // unwrap_or: Err면 기본값
    let value2 = divide(10, 0).unwrap_or(0);  // 0
    
    // unwrap_or_else: Err면 클로저 실행
    let value3 = divide(10, 0).unwrap_or_else(|e| {
        println!("에러: {}", e);
    });
    
    // expect: unwrap + 커스텀 메시지
    let value4 = divide(10, 2).expect("나눗셈 실패");
}

Result 체이닝

fn parse_and_double(s: &str) -> Result<i32, String> {
    s.parse::<i32>()
        .map(|n| n * 2)
        .map_err(|e| format!("파싱 실패: {}", e))
}
fn main() {
    match parse_and_double("42") {
        Ok(n) => println!("결과: {}", n),  // 84
        Err(e) => println!("{}", e),
    }
}

2. Option

기본 Option

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("홍길동"))
    } else {
        None
    }
}
fn main() {
    match find_user(1) {
        Some(name) => println!("사용자: {}", name),
        None => println!("사용자 없음"),
    }
}

Option 메서드

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

fn main() {
    let x: Option<i32> = Some(5);
    
    // map: 값이 있으면 변환
    let y = x.map(|v| v * 2);  // Some(10)
    
    // and_then: 값이 있으면 함수 실행
    let z = x.and_then(|v| Some(v + 1));  // Some(6)
    
    // filter: 조건 만족하면 Some, 아니면 None
    let w = x.filter(|v| v % 2 == 0);  // None (5는 홀수)
    
    // unwrap_or: None이면 기본값
    let value = x.unwrap_or(0);  // 5
    
    // is_some, is_none
    if x.is_some() {
        println!("값 있음");
    }
}

Option과 Result 변환

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

fn main() {
    let opt: Option<i32> = Some(5);
    
    // Option → Result
    let res: Result<i32, &str> = opt.ok_or("값 없음");
    
    let res2: Result<i32, String> = opt.ok_or_else(|| {
        String::from("값이 없습니다")
    });
}

3. ? 연산자

기본 사용

? 연산자는 에러를 자동으로 전파하는 간결한 문법입니다: 다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use std::fs;
use std::io;
// ? 연산자 사용 (간결)
fn read_file() -> Result<String, io::Error> {
    // fs::read_to_string(): Result<String, io::Error> 반환
    let content = fs::read_to_string("file.txt")?;
    // ? 연산자의 동작:
    // - Ok(content)면: content를 추출하여 변수에 할당
    // - Err(e)면: 즉시 함수를 종료하고 Err(e)를 반환 (조기 반환)
    
    Ok(content)
    // 성공 시 content를 Ok로 감싸서 반환
}
// ? 없이 작성하면 (장황)
fn read_file_verbose() -> Result<String, io::Error> {
    // match로 명시적으로 처리
    match fs::read_to_string("file.txt") {
        Ok(content) => {
            // 성공 시: content를 Ok로 감싸서 반환
            Ok(content)
        },
        Err(e) => {
            // 실패 시: 에러를 Err로 감싸서 반환
            Err(e)
        },
    }
    // ? 연산자 한 줄이 이 match 전체를 대체
}
// 사용 예제
fn main() {
    match read_file() {
        Ok(content) => {
            println!("파일 내용:");
            println!("{}", content);
        },
        Err(e) => {
            println!("파일 읽기 실패: {}", e);
        },
    }
}

? 연산자의 장점:

  1. 간결성: match 블록 대신 한 줄로 처리
  2. 가독성: 에러 처리 로직이 명확
  3. 체이닝: 여러 ? 연산자를 연속으로 사용 가능
  4. 타입 안전: 컴파일 타임에 에러 타입 검증

여러 ? 연산자

여러 단계의 에러 처리를 간결하게 체이닝할 수 있습니다:

use std::fs;
use std::io;
fn read_and_parse() -> Result<i32, Box<dyn std::error::Error>> {
    // Box<dyn std::error::Error>: 모든 에러 타입을 담을 수 있는 박스
    // dyn: 동적 디스패치 (런타임에 타입 결정)
    
    // 1단계: 파일 읽기
    let content = fs::read_to_string("number.txt")?;
    // Result<String, io::Error> 반환
    // ? : Err이면 즉시 반환, Ok면 String 추출
    
    // 2단계: 공백 제거 및 파싱
    let number: i32 = content.trim().parse()?;
    // trim(): 앞뒤 공백 제거
    // parse::<i32>(): 문자열 → i32 변환
    // Result<i32, ParseIntError> 반환
    // ? : Err이면 즉시 반환, Ok면 i32 추출
    
    // 3단계: 결과 계산
    Ok(number * 2)
    // 모든 단계가 성공하면 최종 결과 반환
}
// 사용 예제
fn main() {
    match read_and_parse() {
        Ok(n) => println!("결과: {}", n),
        Err(e) => println!("에러: {}", e),
    }
}
// ? 없이 작성하면 (매우 장황)
fn read_and_parse_verbose() -> Result<i32, Box<dyn std::error::Error>> {
    // 1단계: 파일 읽기
    let content = match fs::read_to_string("number.txt") {
        Ok(c) => c,
        Err(e) => return Err(Box::new(e)),
    };
    
    // 2단계: 파싱
    let number: i32 = match content.trim().parse() {
        Ok(n) => n,
        Err(e) => return Err(Box::new(e)),
    };
    
    // 3단계: 결과 계산
    Ok(number * 2)
}

? 연산자의 동작 원리: 아래 코드는 rust를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 이 코드:
let x = some_function()?;
// 는 다음과 같이 확장됨:
let x = match some_function() {
    Ok(value) => value,
    Err(e) => return Err(e.into()),  // 에러 타입 변환 후 반환
};

실전 예시: 파일 처리 파이프라인: 다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use std::fs;
use std::io;
fn process_file(path: &str) -> Result<Vec<i32>, Box<dyn std::error::Error>> {
    // 파일 읽기 → 줄 분리 → 파싱 → 필터링
    let content = fs::read_to_string(path)?;  // 1. 파일 읽기
    
    let numbers: Result<Vec<i32>, _> = content
        .lines()                              // 2. 줄 분리
        .map(|line| line.trim().parse())      // 3. 각 줄 파싱
        .collect();                           // 4. 결과 수집
    
    let numbers = numbers?;  // 파싱 에러 전파
    
    // 5. 양수만 필터링
    let positive: Vec<i32> = numbers.into_iter()
        .filter(|&n| n > 0)
        .collect();
    
    Ok(positive)
}

Option에서 ? 사용

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

fn get_first_char(s: &str) -> Option<char> {
    s.chars().next()
}
fn get_first_uppercase(s: &str) -> Option<char> {
    let first = get_first_char(s)?;
    if first.is_uppercase() {
        Some(first)
    } else {
        None
    }
}

4. panic!

panic! 사용

fn main() {
    panic!("프로그램 중단!");
}

unwrap과 expect

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

fn main() {
    let result: Result<i32, &str> = Err("에러 발생");
    
    // unwrap: Err면 panic
    // let value = result.unwrap();  // panic!
    
    // expect: panic 메시지 커스텀
    // let value = result.expect("값을 가져올 수 없음");  // panic!
}

5. 커스텀 에러 타입

기본 커스텀 에러

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use std::fmt;
#[derive(Debug)]
enum MathError {
    DivisionByZero,
    NegativeNumber,
}
impl fmt::Display for MathError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MathError::DivisionByZero => write!(f, "0으로 나눌 수 없음"),
            MathError::NegativeNumber => write!(f, "음수는 허용되지 않음"),
        }
    }
}
impl std::error::Error for MathError {}
fn divide(a: i32, b: i32) -> Result<i32, MathError> {
    if b == 0 {
        Err(MathError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}
fn sqrt(n: i32) -> Result<f64, MathError> {
    if n < 0 {
        Err(MathError::NegativeNumber)
    } else {
        Ok((n as f64).sqrt())
    }
}

6. 실전 예제

예제: 파일 읽기 및 파싱

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use std::fs;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
}
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}
impl From<ParseIntError> for AppError {
    fn from(err: ParseIntError) -> Self {
        AppError::Parse(err)
    }
}
fn read_number_from_file(path: &str) -> Result<i32, AppError> {
    let content = fs::read_to_string(path)?;
    let number = content.trim().parse::<i32>()?;
    Ok(number)
}
fn main() {
    match read_number_from_file("number.txt") {
        Ok(n) => println!("숫자: {}", n),
        Err(AppError::Io(e)) => println!("파일 에러: {}", e),
        Err(AppError::Parse(e)) => println!("파싱 에러: {}", e),
    }
}

예제: 사용자 입력 검증

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#[derive(Debug)]
enum ValidationError {
    TooShort,
    TooLong,
    InvalidChar,
}
fn validate_username(name: &str) -> Result<(), ValidationError> {
    if name.len() < 3 {
        return Err(ValidationError::TooShort);
    }
    if name.len() > 20 {
        return Err(ValidationError::TooLong);
    }
    if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
        return Err(ValidationError::InvalidChar);
    }
    Ok(())
}
fn main() {
    let usernames = vec!["ab", "valid_user123", "invalid-user", "a".repeat(25)];
    
    for name in usernames {
        match validate_username(&name) {
            Ok(_) => println!("✓ '{}' 유효함", name),
            Err(ValidationError::TooShort) => println!("✗ '{}' 너무 짧음", name),
            Err(ValidationError::TooLong) => println!("✗ '{}' 너무 김", name),
            Err(ValidationError::InvalidChar) => println!("✗ '{}' 잘못된 문자", name),
        }
    }
}

7. 에러 처리 패턴

패턴 1: 조기 반환

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

fn process_data(data: &str) -> Result<i32, String> {
    if data.is_empty() {
        return Err(String::from("데이터 비어있음"));
    }
    
    let number = match data.parse::<i32>() {
        Ok(n) => n,
        Err(_) => return Err(String::from("파싱 실패")),
    };
    
    if number < 0 {
        return Err(String::from("음수 불가"));
    }
    
    Ok(number * 2)
}

패턴 2: 에러 변환

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

use std::fs;
use std::io;
fn read_and_process() -> Result<String, Box<dyn std::error::Error>> {
    let content = fs::read_to_string("data.txt")?;
    let number: i32 = content.trim().parse()?;
    Ok(format!("처리된 값: {}", number * 2))
}

정리

핵심 요약

  1. Result<T, E>: 성공(Ok) 또는 실패(Err)
  2. Option: 값 있음(Some) 또는 없음(None)
  3. ? 연산자: 에러 자동 전파
  4. panic!: 복구 불가능한 에러
  5. 커스텀 에러: enum + Display + Error 트레이트

다음 단계


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3