← Rust 강의 목록으로
🦀
에러 처리
에러 처리 · 선수: 타입 1~4강 · 에러 처리 · 선수: 15강

15–16. Result<T,E> 와 Option<T> 다루기 · ? 연산자와 에러 전파

Rust 의 에러 처리는 두 가지 enum 으로 압축됩니다 — Option<T> 는 "값이 없을 수 있다", Result<T,E> 는 "실패할 수 있다". try/catch 가 없는 대신 타입으로 모든 실패를 표현하기 때문에 호출자가 처리하지 않으면 컴파일러가 잡아냅니다. 이 강의에서 두 enum 의 변환·헬퍼 메서드를 한 번에 정리합니다. ? 연산자는 Result 또는 Option 표현식 뒤에 붙여 "실패면 즉시 함수에서 반환" 을 한 글자로 줄여줍니다. 그래서 여러 단계의 IO·파싱이 깔끔하게 일자로 늘어집니다. From 트레잇을 통해 다른 종류의 에러 타입이 자동 변환되어 전파되는 메커니즘도 함께 살펴봅니다.

RustResultOption에러 처리match?에러 전파From
소요 시간
⏱ 약 1.5시간 + 약 1.5시간
난이도
📊 중급
선수 조건
🎯 타입 1~4강
결과물
Rust 의 에러 처리는 두 가지 enum 으로 압축됩니다 — Option<T> 는 "값이 없을 수 있다", Result<T,E> 는 "실패할 수 있다". try/catch 가 없는 대신 타입으로 모든 실패를 표현하기 때문에 호출자가 처리하지 않으면 컴파일러가 잡아냅니다. 이 강의에서 두 enum 의 변환·헬퍼 메서드를 한 번에 정리합니다. / ? 연산자는 Result 또는 Option 표현식 뒤에 붙여 "실패면 즉시 함수에서 반환" 을 한 글자로 줄여줍니다. 그래서 여러 단계의 IO·파싱이 깔끔하게 일자로 늘어집니다. From 트레잇을 통해 다른 종류의 에러 타입이 자동 변환되어 전파되는 메커니즘도 함께 살펴봅니다.

이 강의에서 배우는 것

  • 1Result<T,E> 와 Option<T> 의 차이를 안다
  • 2match 와 헬퍼 메서드로 분기 처리한다
  • 3.ok() / .ok_or() 로 두 타입을 변환한다
  • 4함수 시그니처에 Result 반환을 명시한다
  • 5panic 과 Result 의 사용 기준을 안다
  • 6? 연산자로 Result/Option 의 실패를 즉시 반환한다
  • 7main 함수 시그니처를 Result 반환으로 바꿔 ? 를 쓴다
  • 8From 트레잇으로 서로 다른 에러 타입을 자동 변환·전파한다
  • 9? 가 Option 에도 적용되는 조건을 안다
  • 10Box<dyn Error> 로 간편한 에러 누적을 활용한다

15. Result<T,E> 와 Option<T> 다루기

Rust 의 에러 처리는 두 가지 enum 으로 압축됩니다 — Option<T> 는 "값이 없을 수 있다", Result<T,E> 는 "실패할 수 있다". try/catch 가 없는 대신 타입으로 모든 실패를 표현하기 때문에 호출자가 처리하지 않으면 컴파일러가 잡아냅니다. 이 강의에서 두 enum 의 변환·헬퍼 메서드를 한 번에 정리합니다.

소개

다른 언어는 예외(exception) 가 함수 시그니처에 보이지 않거나(unchecked) 따로 throws 절을 적습니다. Rust 는 반환 타입에 Result/Option 으로 명시 — 호출자가 어떻게 실패하는지 한눈에 보고, 처리 누락은 컴파일 에러가 됩니다.

핵심 개념

1) Result<T, E>

rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}

성공값 타입 T 와 실패값 타입 E 를 모두 시그니처에 명시. 둘 다 데이터를 가집니다.

2) Option<T> vs Result<T,E>

관점OptionResult
용도값이 있을 수도 / 없을 수도성공 / 실패 + 실패 정보
없을 때None (정보 없음)Err(E) (실패 이유 포함)
언제 쓰나리스트에서 찾기 실패 등 정보 없는 부재파일 IO·네트워크·파싱 등 실패 이유 중요

3) 자주 쓰는 헬퍼

메서드동작
.unwrap()Ok/Some 이면 값, 아니면 panic
.expect("msg")panic 시 메시지 명시
.unwrap_or(d)실패 시 기본값
.unwrap_or_else(|e| ...)실패 시 컴퓨트한 기본값
.ok()Result → Option (Err 정보 버림)
.map(|x| ...)Ok/Some 일 때만 변환
.map_err(|e| ...)Err 만 변환

핵심 예제

문자열을 정수로 — Result 반환:

rust
fn parse_age(s: &str) -> Result<u32, String> {
    match s.trim().parse::<u32>() {
        Ok(n) if n > 150 => Err(format!("비현실적 나이: {}", n)),
        Ok(n)            => Ok(n),
        Err(e)           => Err(format!("파싱 실패: {}", e)),
    }
}

fn main() {
    for s in ["30", "abc", "999"] {
        match parse_age(s) {
            Ok(n)  => println!("{} OK -> {}", s, n),
            Err(e) => println!("{} ERR -> {}", s, e),
        }
    }
}

Option ↔ Result 변환:

rust
fn find(v: &[i32], target: i32) -> Option<usize> {
    v.iter().position(|&x| x == target)
}

fn find_or_err(v: &[i32], target: i32) -> Result<usize, String> {
    find(v, target).ok_or(format!("{} not found", target))
}

fn main() {
    let v = [10, 20, 30];
    println!("{:?}", find_or_err(&v, 20));  // Ok(1)
    println!("{:?}", find_or_err(&v, 99));  // Err("99 not found")
}

헬퍼 메서드 체이닝:

rust
fn main() {
    let s = "  42  ";
    let n = s.trim().parse::<i32>()
        .map(|x| x * 2)
        .unwrap_or(-1);
    println!("{}", n); // 84
}

자주 하는 실수

Q. 그럼 매번 match 를 다 써야 하나요?

A. 아닙니다. 다음 강의의 `?` 연산자가 "실패면 즉시 반환" 을 한 글자로 줄여줍니다. .map / .and_then 으로 체이닝하는 패턴도 흔히 씁니다.

Q. .unwrap() 을 쓰면 안 되나요?

A. 프로토타이핑·예제·테스트엔 OK. 실무 코드는 **"이건 절대 None/Err 일 수 없다" 가 코드 구조로 명백할 때만** 쓰는 게 안전. 사용자 입력·외부 IO 결과엔 절대 .unwrap() 금지.

Q. Option 과 Result 중 뭘 쓰나요?

A. **실패 이유가 의미 있으면 Result, 단순히 부재면 Option**. 예: 리스트 검색은 Option, 파일 열기는 Result.

정리

  • Option = 부재 표현, Result = 실패 이유까지 표현
  • 실패가 가능한 함수는 반환 타입에 명시 → 처리 누락이 컴파일 에러
  • match / .unwrap_or / .map / .ok_or 등 다양한 분기 방법
  • .unwrap() 은 신중히

과제

  1. fn divide(a: i32, b: i32) -> Result<i32, String> 작성 (b=0 이면 Err)
  2. 사용자가 입력한 문자열을 받아 u32 로 파싱하고, 1~120 범위면 Ok 아니면 Err 반환하는 함수
  3. Vec<Result<i32, String>> 에서 Ok 값만 합치는 함수 (.filter_map / .iter 활용)

16. ? 연산자와 에러 전파

? 연산자는 Result 또는 Option 표현식 뒤에 붙여 "실패면 즉시 함수에서 반환" 을 한 글자로 줄여줍니다. 그래서 여러 단계의 IO·파싱이 깔끔하게 일자로 늘어집니다. From 트레잇을 통해 다른 종류의 에러 타입이 자동 변환되어 전파되는 메커니즘도 함께 살펴봅니다.

소개

Result 매 단계마다 match 를 쓰면 코드가 들여쓰기 절벽으로 가게 됩니다. `?` 한 글자는 그걸 한 줄로 줄여줍니다. "이 단계에서 실패하면 그냥 그대로 위로 반환" 의 의미.

핵심 개념

1) ? — Result 에 붙이면

`expr?` 는 다음과 동치:

rust
// expr?
// 의미:
match expr {
    Ok(v)  => v,
    Err(e) => return Err(e.into()), // From 으로 변환
}

2) ? — Option 에 붙이면

함수 반환 타입이 Option 인 경우, None 이면 즉시 None 반환.

3) From 트레잇으로 에러 자동 변환

함수가 반환하는 에러 타입 `MyError` 가 `From<io::Error>` 를 구현하면, io 작업의 ? 연산자 결과가 자동으로 `MyError` 로 변환돼 전파됩니다. 여러 종류 에러를 한 가지로 통합.

4) main 도 Result 반환 가능

rust
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
    let n = "42".parse::<i32>()?;
    println!("{}", n);
    Ok(())
}

핵심 예제

파일 읽고 첫 줄 파싱 — ? 가 없을 때 vs 있을 때:

rust
use std::fs::File;
use std::io::{self, Read};

// ? 없는 버전 — 들여쓰기 절벽
fn read_first_number_v1(path: &str) -> Result<i32, String> {
    let mut f = match File::open(path) {
        Ok(f) => f,
        Err(e) => return Err(format!("open: {}", e)),
    };
    let mut s = String::new();
    if let Err(e) = f.read_to_string(&mut s) {
        return Err(format!("read: {}", e));
    }
    let first = s.lines().next().ok_or("empty")?;
    first.trim().parse::<i32>().map_err(|e| format!("parse: {}", e))
}
rust
// ? 와 Box<dyn Error> 버전 — 한 줄씩 깨끗하게
use std::error::Error;

fn read_first_number_v2(path: &str) -> Result<i32, Box<dyn Error>> {
    let mut s = String::new();
    File::open(path)?.read_to_string(&mut s)?;
    let first = s.lines().next().ok_or("empty")?;
    let n = first.trim().parse::<i32>()?;
    Ok(n)
}

Option 과 함께 쓰기:

rust
fn first_char_uppercased(s: &str) -> Option<char> {
    let c = s.chars().next()?;          // None 이면 즉시 None
    c.to_uppercase().next()
}

fn main() {
    println!("{:?}", first_char_uppercased("hello")); // Some('H')
    println!("{:?}", first_char_uppercased(""));      // None
}

자주 하는 실수

Q. ? 가 함수 안에서만 동작하나요?

A. 네, 그 함수의 반환 타입이 Result 또는 Option 이어야 합니다. main 도 Result 반환이면 ? 사용 가능. 그렇지 않은 함수에선 컴파일 에러.

Q. 에러 종류가 다른데 ? 가 통과되나요?

A. From 트레잇이 구현돼 있으면 자동 변환됩니다. Box<dyn Error> 는 거의 모든 에러를 받아주는 "전능" 타입이라 간편하지만, 라이브러리에선 구체 에러 타입(thiserror 등) 이 권장됩니다 — 다음 강의에서.

Q. ? 를 식 중간에 못 쓰나요?

A. 씁니다. `let n = parse()? + 1;` 처럼. 단 Result/Option 인 위치에만.

정리

  • ? = "실패면 즉시 반환" 한 글자 매크로
  • 함수 시그니처가 Result/Option 이어야 사용 가능
  • From 트레잇으로 다른 에러 타입 자동 변환
  • main 도 Result 반환이면 ? 사용 가능 — 프로그램 진입점 단순화

과제

  1. ? 연산자로 두 단계 파싱(문자열 → 정수 → 양수 검증) 을 작성
  2. fn first_two_uppercased(s: &str) -> Option<(char, char)> 를 ? 로 작성
  3. main 을 -> Result<(), Box<dyn Error>> 로 바꿔 std 파일 IO 와 파싱을 ? 로 처리
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침