Devin.KR

Rust · 심화

트레이트와 동시성으로 깊어지는 Rust

오류 설계 - 사용자 정의 오류와 변환

오류 열거형 설계, Display·Error 구현, From 구현으로 ? 변환, Box<dyn Error> 를 쓰는 자리, 라이브러리와 실행 파일의 오류 처리 차이

개발자KR · 원고 갱신

이 장에서 배우는 것

지금까지 창고 프로그램은 실패를 Option이나 Result<T, String> 정도로 다뤄 왔다. 입출고 명령이 늘고 읽기, 검증, 재고 계산이 서로 다른 이유로 실패하기 시작하면 이 방식은 한계가 온다. 이 장에서는 실패의 종류를 타입으로 표현하는 방법을 정리한다. 오류 열거형을 설계하고, 표준 트레이트 두 개를 구현해 표준 도구와 어울리게 만들고, ? 연산자가 오류 타입을 바꿔 주는 원리를 확인한다. 마지막으로 Box<dyn Error>가 어울리는 자리와 어울리지 않는 자리를 나눈다.

  • 실패의 종류를 열거형 변형으로 나누고, 변형에 호출자가 쓸 정보를 담을 수 있다.
  • Display와 Error를 구현하고, source로 원인 사슬을 연결할 수 있다.
  • From을 구현해 ?가 오류를 자동으로 변환하게 할 수 있다.
  • Box<dyn Error>를 쓸 자리와 열거형을 쓸 자리를 구분할 수 있다.

문제 상황

창고 담당자가 텍스트 명령을 한 줄씩 넘긴다. in bolt 10은 입고, out bolt 3은 출고다. 이 줄을 처리하는 동안 실패할 수 있는 지점은 서로 성격이 다르다.

  • 명령 형식이 틀렸다. 첫 단어가 move이거나 인자 수가 맞지 않는다.
  • 수량 자리에 숫자가 아닌 글자가 왔다. 표준 라이브러리의 ParseIntError가 발생한다.
  • 등록된 적 없는 품목을 출고하려 했다.
  • 보유 수량보다 많이 출고하려 했다.

이 실패를 전부 String으로 돌려주면 호출자는 메시지 문자열을 들여다보고 분기해야 한다. 문구를 고치는 순간 분기가 조용히 깨진다. 반대로 어떤 실패는 재주문 대상이고, 어떤 실패는 입력자에게 되묻는 것이 맞다. 호출자가 실패의 종류를 구별해야 하므로 종류가 타입에 드러나야 한다. 한편 최상위 main은 종류를 구별할 필요 없이 메시지를 출력하고 끝내면 된다. 한 프로그램 안에서도 계층마다 오류에 요구하는 것이 다르다. 이 장은 그 차이를 코드로 보여 준다.

오류 열거형 설계

변형은 호출자의 분기 기준이다

오류 열거형의 변형은 호출자가 서로 다르게 대응할 실패 단위로 나눈다. 로그에 찍는 문구가 다르다는 이유만으로 변형을 나누면 변형이 늘기만 한다. 반대로 대응이 같다는 이유로 합치면 필요한 정보가 사라진다. 변형에 담는 값은 호출자가 대응에 쓸 것으로 한정한다. 재고 부족이라면 품목, 보유 수량, 요청 수량이 그런 값이다.

#[derive(Debug)]
enum StockError {
    UnknownItem(String),
    Insufficient { sku: String, have: u32, want: u32 },
    BadQuantity(ParseIntError),
    BadCommand(String),
}

BadQuantity는 원인이 된 ParseIntError를 그대로 품는다. 우리 오류가 하위 오류를 감싸는 구조이고, 뒤에서 source로 이 관계를 노출한다. Debug는 derive로 얻는다. Error 트레이트가 Debug와 Display를 슈퍼트레이트로 요구하기 때문이다.

Display 와 Error 구현

Display는 사람이 읽는 한 줄 메시지를 만든다. 규칙을 하나 정하면 뒤가 편해진다. Display는 자기 층의 설명만 쓰고, 하위 오류의 문장은 넣지 않는다. 하위 오류는 source로 따로 제공한다. 이렇게 하면 원인 사슬을 출력하는 쪽이 같은 문장을 두 번 찍지 않는다. 이 규칙을 어긴 사례는 뒤의 “자주 틀리는 것”에서 다시 본다.

impl Error for StockError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            StockError::BadQuantity(e) => Some(e),
            _ => None,
        }
    }
}

Error에는 필수 메서드가 없다. 비어 있는 impl Error for StockError {}만으로도 컴파일된다. source는 기본 구현이 None이고, 원인을 가진 변형이 있을 때만 재정의한다. 반환 타입의 'static이 의미하는 바는 수명을 깊이 다루는 장에서 따로 설명한다. 여기서는 “참조를 담지 않은 오류 타입이면 그대로 쓸 수 있다”고만 알아 두면 된다.

From 구현과 ? 변환

? 연산자는 Err(e)를 만나면 함수에서 일찍 반환하는데, 이때 From::from(e)를 한 번 거친다. 함수의 반환 오류 타입이 StockError이고 안에서 ParseIntError가 나오면, 변환 방법을 알려 주는 From 구현이 있어야 한다. 구현이 없으면 컴파일이 멈춘다.

fn parse_qty(text: &str) -> Result<u32, StockError> {
    let qty: u32 = text.parse()?;
    Ok(qty)
}

이 코드는 error[E0277]: `?` couldn't convert the error to `StockError`로 시작하는 오류를 낸다. 요지는 From<ParseIntError> 트레이트가 StockError에 구현돼 있지 않다는 것이다. 구현을 더하면 해결된다.

impl From<ParseIntError> for StockError {
    fn from(e: ParseIntError) -> Self {
        StockError::BadQuantity(e)
    }
}

이후로 text.parse()?는 실패 시 StockError::BadQuantity로 감싸져 반환된다. 호출 코드에서 변환 로직이 사라지고 성공 경로만 남는다.

? 연산자는 From 구현을 거쳐 하위 오류를 우리 오류로, 다시 Box 로 바꿔 준다.

그림은 변환이 한 번에 끝나지 않을 수 있음을 보여 준다. 라이브러리 층에서 ParseIntError가 StockError가 되고, 응용 층에서 StockError가 Box<dyn Error>가 된다. 각 단계의 ?가 그 단계의 From을 호출할 뿐이다.

주의할 점이 하나 있다. From 구현은 “이 하위 오류는 항상 이 변형을 뜻한다”는 선언이다. 같은 ParseIntError가 수량 파싱과 가격 파싱에서 모두 나온다면 하나의 From으로는 둘을 구별할 수 없다. 그런 경우에는 From을 쓰지 않고 map_err로 변형을 직접 고른다.

Box<dyn Error> 를 쓰는 자리

무엇을 해 주는가

Box<dyn Error>는 Error를 구현한 어떤 타입이든 힙에 담아 한 타입으로 다루는 트레이트 객체다. 앞 장에서 본 Box가 값을 힙에 두는 도구이고, 트레이트 객체를 다룬 장에서 본 dyn이 구체 타입을 지우는 도구다. 둘을 합친 것이 이 타입이다. 이 타입은 표준 라이브러리가 두 가지 변환을 제공해서 쓰기 쉽다. Error를 구현한 값은 ?로 자동으로 담기고, &str과 String은 .into()로 담긴다.

fn run_batch(wh: &mut Warehouse, script: &str, limit_text: &str)
    -> Result<u32, Box<dyn Error>>
{
    let limit: u32 = limit_text.trim().parse()?;       // ParseIntError
    if limit == 0 {
        return Err("한도는 1 이상이어야 한다".into());  // &str
    }
    for line in script.split(';') {
        handle(wh, line)?;                              // StockError
    }
    ...
}

한 함수 안에서 세 종류의 실패가 섞여도 반환 타입을 하나로 둘 수 있다. 대신 구체 타입 정보는 타입에서 사라진다. 꼭 필요하면 downcast_ref::<StockError>()로 되살릴 수 있지만, 이것은 호출자가 분기해야 할 때마다 쓰는 용도가 아니다.

라이브러리와 실행 파일의 차이

호출자가 누구인지가 선택을 가른다. 라이브러리의 호출자는 다른 코드이고, 그 코드는 실패에 따라 다르게 행동하려 한다. 따라서 라이브러리는 열거형처럼 종류가 드러나는 타입을 돌려줘야 한다. 실행 파일의 최상위는 실패를 사람에게 보여 주고 종료 코드를 정하는 곳이다. 여기서는 종류보다 무엇이든 받아 주는 것이 유용하다.

오류 표현 방식별 특성
방식호출자의 분기작성 비용어울리는 자리
Result<T, String>문구 비교뿐가장 낮다짧은 스크립트, 임시 코드
사용자 정의 열거형match로 변형 분기열거형, 트레이트 구현, From라이브러리 공개 API, 도메인 계층
Box<dyn Error>downcast_ref로 우회낮다실행 파일 최상위, 응용 계층
라이브러리 층은 열거형으로 종류를 보존하고, 응용 층부터 Box 에 담아 출력만 한다.

두 방식을 섞는 것도 자연스럽다. 이 장의 코드가 그렇다. 하위 계층의 parse_command와 apply는 StockError를 돌려준다. 그 위의 run_batch는 Box<dyn Error>를 돌려주고 main은 메시지와 원인 사슬만 출력한다. fn main() -> Result<(), Box<dyn Error>>로 선언해 오류를 그대로 반환하는 방법도 있다. 이 경우 런타임은 오류를 Debug 형식으로 출력하고 종료 코드 1로 끝낸다. 문구를 다듬어 출력해야 한다면 main에서 직접 처리하는 편이 낫다.

완성 코드

src/main.rs 한 파일이다. 외부 크레이트는 쓰지 않는다.

use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum StockError {
    UnknownItem(String),
    Insufficient { sku: String, have: u32, want: u32 },
    BadQuantity(ParseIntError),
    BadCommand(String),
}

impl fmt::Display for StockError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            StockError::UnknownItem(sku) => write!(f, "등록되지 않은 품목: {sku}"),
            StockError::Insufficient { sku, have, want } => {
                write!(f, "재고 부족: {sku} 보유 {have}, 요청 {want}")
            }
            StockError::BadQuantity(_) => write!(f, "수량을 읽을 수 없다"),
            StockError::BadCommand(line) => write!(f, "알 수 없는 명령: {line}"),
        }
    }
}

impl Error for StockError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            StockError::BadQuantity(e) => Some(e),
            _ => None,
        }
    }
}

impl From<ParseIntError> for StockError {
    fn from(e: ParseIntError) -> Self {
        StockError::BadQuantity(e)
    }
}

enum Command {
    In(String, u32),
    Out(String, u32),
}

struct Warehouse {
    stock: HashMap<String, u32>,
}

impl Warehouse {
    fn new() -> Self {
        Warehouse { stock: HashMap::new() }
    }

    fn total(&self) -> u32 {
        self.stock.values().sum()
    }

    fn apply(&mut self, cmd: &Command) -> Result<u32, StockError> {
        match cmd {
            Command::In(sku, qty) => {
                let slot = self.stock.entry(sku.clone()).or_insert(0);
                *slot += qty;
                Ok(*slot)
            }
            Command::Out(sku, qty) => {
                let slot = self
                    .stock
                    .get_mut(sku)
                    .ok_or_else(|| StockError::UnknownItem(sku.clone()))?;
                if *slot < *qty {
                    return Err(StockError::Insufficient {
                        sku: sku.clone(),
                        have: *slot,
                        want: *qty,
                    });
                }
                *slot -= qty;
                Ok(*slot)
            }
        }
    }
}

fn parse_command(line: &str) -> Result<Command, StockError> {
    let parts: Vec<&str> = line.split_whitespace().collect();
    match parts.as_slice() {
        ["in", sku, qty] => Ok(Command::In(sku.to_string(), qty.parse()?)),
        ["out", sku, qty] => Ok(Command::Out(sku.to_string(), qty.parse()?)),
        _ => Err(StockError::BadCommand(line.to_string())),
    }
}

fn handle(wh: &mut Warehouse, line: &str) -> Result<u32, StockError> {
    let cmd = parse_command(line)?;
    wh.apply(&cmd)
}

fn run_batch(
    wh: &mut Warehouse,
    script: &str,
    limit_text: &str,
) -> Result<u32, Box<dyn Error>> {
    let limit: u32 = limit_text.trim().parse()?;
    if limit == 0 {
        return Err("한도는 1 이상이어야 한다".into());
    }
    for line in script.split(';') {
        handle(wh, line)?;
    }
    let total = wh.total();
    if total > limit {
        return Err(format!("총 재고 {total} 이 한도 {limit} 을 넘었다").into());
    }
    Ok(total)
}

fn describe(err: &dyn Error) -> String {
    let mut text = err.to_string();
    let mut cause = err.source();
    while let Some(inner) = cause {
        text.push_str(" <- ");
        text.push_str(&inner.to_string());
        cause = inner.source();
    }
    text
}

fn print_stock(wh: &Warehouse) {
    let mut items: Vec<_> = wh.stock.iter().collect();
    items.sort();
    for (sku, qty) in items {
        println!("  {sku}: {qty}");
    }
}

fn main() {
    let mut wh = Warehouse::new();
    let lines = [
        "in bolt 10",
        "in nut 4",
        "out bolt 3",
        "out nut 9",
        "out gear 1",
        "in bolt x1",
        "move bolt 2",
    ];
    for line in lines {
        match handle(&mut wh, line) {
            Ok(left) => println!("[{line}] 남은 수량 {left}"),
            Err(e) => println!("[{line}] 오류: {}", describe(&e)),
        }
    }
    println!("재고 현황");
    print_stock(&wh);

    let cases = [
        ("in bolt 5;out bolt 2", "100"),
        ("in bolt 5", "abc"),
        ("in bolt 5", "0"),
        ("in bolt 50", "10"),
        ("in bolt 2;out bolt 5", "10"),
        ("in bolt x", "10"),
    ];
    for (i, (script, limit)) in cases.into_iter().enumerate() {
        let n = i + 1;
        let mut wh = Warehouse::new();
        match run_batch(&mut wh, script, limit) {
            Ok(total) => println!("[배치 {n}] 성공: 총 재고 {total}"),
            Err(e) => {
                println!("[배치 {n}] 실패: {}", describe(&*e));
                if matches!(
                    e.downcast_ref::<StockError>(),
                    Some(StockError::Insufficient { .. })
                ) {
                    println!("  재발주 대상: 재고 부족 오류로 식별됐다");
                }
            }
        }
    }
}

줄별 해설

오류 타입 부분

StockError의 네 변형은 문제 상황에서 꼽은 네 실패에 대응한다. Insufficient만 이름 있는 필드를 쓴다. 값이 셋이라 위치만으로는 어느 것이 보유이고 어느 것이 요청인지 읽기 어렵기 때문이다. Display 구현은 match self로 변형마다 한 줄 문장을 만든다. BadQuantity(_)는 하위 오류를 문장에 넣지 않는다. 앞에서 정한 규칙대로 그 내용은 source가 담당한다.

impl Error의 source는 BadQuantity일 때만 Some(e)를 돌려준다. 여기서 e는 &ParseIntError이고, Some(e)는 &dyn Error로 자동 변환된다. From<ParseIntError> 구현은 ?가 변환에 사용한다.

명령 처리 부분

parse_command는 줄을 공백 기준으로 나눠 Vec<&str>에 모으고, as_slice()의 슬라이스 패턴으로 모양을 판별한다. ["in", sku, qty]는 첫 원소가 "in"이고 원소가 정확히 셋일 때만 맞는다. qty.parse()?의 대상 타입은 Command::In의 두 번째 인자인 u32로 정해진다. 실패하면 ParseIntError가 From을 거쳐 StockError::BadQuantity로 반환된다. 어느 패턴에도 맞지 않으면 BadCommand를 만든다.

apply의 출고 분기에서 ok_or_else는 Option을 Result로 바꾼다. 품목이 없을 때만 UnknownItem을 만드는 클로저가 실행되므로, 성공 경로에서 String 복제 비용이 들지 않는다. slot이 가변 참조이므로 *slot < *qty처럼 역참조해서 비교한다. 오류 값에 *slot을 복사해 담은 뒤에야 반환하므로, 참조가 오류 안으로 새어 나가지 않는다.

응용 계층과 출력

run_batch는 한 함수에 세 종류의 실패를 섞는다. 한도 문자열의 ?는 ParseIntError를, .into()는 문자열 슬라이스를, handle(wh, line)?는 StockError를 각각 Box<dyn Error>로 바꾼다. format!으로 만든 String도 .into()로 담긴다.

describe는 &dyn Error를 받아 메시지를 만든 뒤, source()를 따라가며 <-로 이어 붙인다. StockError 참조와 Box에서 꺼낸 &*e가 같은 함수에 들어갈 수 있다. 두 타입 모두 &dyn Error로 변환되기 때문이다. main의 마지막 반복에서는 matches!와 downcast_ref로 상자 속 타입을 확인한다. 이 방식은 응용 계층에서 예외적으로 쓰는 우회로임을 보이려는 용도다. print_stock은 HashMap 순회 순서가 일정하지 않으므로 벡터로 모아 정렬한 뒤 출력한다.

실행 결과

$ cargo run
[in bolt 10] 남은 수량 10
[in nut 4] 남은 수량 4
[out bolt 3] 남은 수량 7
[out nut 9] 오류: 재고 부족: nut 보유 4, 요청 9
[out gear 1] 오류: 등록되지 않은 품목: gear
[in bolt x1] 오류: 수량을 읽을 수 없다 <- invalid digit found in string
[move bolt 2] 오류: 알 수 없는 명령: move bolt 2
재고 현황
  bolt: 7
  nut: 4
[배치 1] 성공: 총 재고 3
[배치 2] 실패: invalid digit found in string
[배치 3] 실패: 한도는 1 이상이어야 한다
[배치 4] 실패: 총 재고 50 이 한도 10 을 넘었다
[배치 5] 실패: 재고 부족: bolt 보유 2, 요청 5
  재발주 대상: 재고 부족 오류로 식별됐다
[배치 6] 실패: 수량을 읽을 수 없다 <- invalid digit found in string

배치 2의 메시지가 영어인 것은 표준 라이브러리의 ParseIntError가 자기 문구를 그대로 내보내기 때문이다. 우리 오류로 감싸지 않고 상자에 바로 담았으므로 문맥 설명이 붙지 않는다. 배치 6은 같은 원인을 StockError로 감싼 경우이고, 설명과 원인이 사슬로 함께 출력된다.

실무에서 자주 틀리는 것

Display 에 하위 오류를 넣고 source 로도 노출한다

// 틀린 코드
StockError::BadQuantity(e) => write!(f, "수량을 읽을 수 없다: {e}"),
// source() 도 Some(e) 를 돌려준다

사슬을 따라 출력하는 코드는 수량을 읽을 수 없다: invalid digit found in string <- invalid digit found in string처럼 같은 문장을 두 번 찍는다. 한 층은 한 가지 방법으로만 원인을 알린다.

// 고친 코드
StockError::BadQuantity(_) => write!(f, "수량을 읽을 수 없다"),

오류를 String 으로 돌려주고 문구로 분기한다

// 틀린 코드
fn ship(...) -> Result<u32, String> { Err(format!("재고 부족: {sku}")) }

if err.contains("재고 부족") { reorder(); }

문구를 다듬거나 번역하면 분기가 어떤 경고도 없이 깨진다. 컴파일러는 문자열 비교의 의미를 검사하지 못한다. 열거형으로 바꾸면 변형을 추가했을 때 match가 빠진 곳을 컴파일러가 알려 준다.

// 고친 코드
match err {
    StockError::Insufficient { .. } => reorder(),
    other => report(other),
}

같은 하위 오류에 From 하나만 두고 의미를 섞는다

// 틀린 코드
fn parse_pair(p: &str, q: &str) -> Result<(u32, u32), StockError> {
    Ok((p.parse()?, q.parse()?))   // 가격 오류도 BadQuantity 가 된다
}

가격 문자열이 틀려도 “수량을 읽을 수 없다”로 보고된다. 입력자는 엉뚱한 칸을 고친다. From은 하위 오류와 변형이 일대일일 때만 쓰고, 그렇지 않으면 변형을 직접 고른다.

// 고친 코드 (StockError 에 BadPrice(ParseIntError) 변형을 더한다)
fn parse_pair(p: &str, q: &str) -> Result<(u32, u32), StockError> {
    let price = p.parse().map_err(StockError::BadPrice)?;
    let qty = q.parse().map_err(StockError::BadQuantity)?;
    Ok((price, qty))
}

라이브러리의 공개 함수가 Box<dyn Error> 를 돌려준다

// 틀린 코드
pub fn ship(wh: &mut Warehouse, sku: &str, qty: u32) -> Result<u32, Box<dyn Error>> { ... }

호출자는 어떤 실패가 가능한지 시그니처만으로 알 수 없다. 재고 부족만 따로 처리하려면 downcast_ref를 쓰는데, 이것은 구현 세부에 기대는 코드다. 라이브러리는 이후 버전에서 오류 타입을 바꿀 수 있고, 그러면 호출자의 분기가 조용히 빗나간다.

// 고친 코드
pub fn ship(wh: &mut Warehouse, sku: &str, qty: u32) -> Result<u32, StockError> { ... }

한눈에 보기

오류 설계에서 쓰는 요소와 역할
요소역할구현 위치핵심 규칙
오류 열거형실패 종류를 타입으로 구분도메인 계층호출자의 분기 단위로 변형을 나눈다
Display사람이 읽는 한 줄 설명오류 타입자기 층의 설명만 쓴다
Error::source원인 사슬 연결원인을 품은 변형원인이 없으면 기본값 None
From? 의 오류 변환오류 타입하위 오류와 변형이 일대일일 때만
Box<dyn Error>서로 다른 오류를 하나로 수용응용 계층, main공개 API 의 반환 타입으로는 쓰지 않는다
계층별 오류 처리 방침
계층반환 오류 타입호출자가 하는 일변환 수단
라이브러리사용자 정의 열거형변형별 matchFrom, map_err
응용 함수Box<dyn Error>그대로 위로 전달?, .into()
실행 파일 최상위없음 (출력과 종료)메시지와 사슬 출력source() 순회

연습 문제

  1. StockError에 단종 품목을 뜻하는 Discontinued(String) 변형을 추가한다고 하자. 코드의 어느 곳에서 컴파일 오류가 나는지, 그 이유가 무엇인지 설명하라.
  2. parse_command의 qty.parse()?를 From에 기대지 않고 map_err로 바꿔 보라. 이때 From 구현을 지워도 컴파일되는가?
  3. 오류와 그 원인을 모두 세어 사슬의 길이를 돌려주는 chain_len(err: &dyn Error) -> usize를 작성하라. 이 장의 StockError::BadQuantity에 적용하면 얼마가 되는가?
  4. 라이브러리 함수가 Box<dyn Error>를 돌려줄 때 호출자가 재고 부족만 구별하는 방법과 그 단점을 설명하고, 이를 피하는 시그니처를 제시하라.

정답과 해설

  1. Display 구현의 match self에서 오류가 난다. match는 모든 변형을 다뤄야 하는데 새 변형이 빠졌기 때문이다(non-exhaustive patterns). source는 _ => None이 있으므로 그대로 컴파일된다. 이 차이를 알아 두면 _ 와일드카드를 쓸 때 변형 추가 시의 안전망을 스스로 줄인다는 점도 보인다. 호출자가 match로 변형을 나눠 처리하는 곳이 있다면 그곳에서도 같은 오류가 난다. 이것이 열거형을 쓰는 이유다.
  2. 다음처럼 쓴다.
    ["in", sku, qty] => {
        let qty = qty.parse().map_err(StockError::BadQuantity)?;
        Ok(Command::In(sku.to_string(), qty))
    }
    이 줄만 보면 ?가 오류 타입 StockError를 그대로 받으므로 From이 필요 없다. 그러나 ?는 같은 타입이라도 From::from을 거치고, 이때는 자기 자신으로의 항등 변환이 쓰인다. 두 번째 패턴도 같은 방식으로 바꿔야 하며, 바꾸고 나면 From이 어디서도 쓰이지 않으므로 지워도 컴파일된다. 쓰지 않는 구현이 남는 것은 경고 대상이 아니므로 남겨 두어도 무방하다.
  3. 다음처럼 쓴다.
    fn chain_len(err: &dyn Error) -> usize {
        let mut n = 1;
        let mut cause = err.source();
        while let Some(inner) = cause {
            n += 1;
            cause = inner.source();
        }
        n
    }
    BadQuantity는 자기 자신과 ParseIntError가 있으므로 2다. 원인이 없는 다른 변형은 1이다. describe와 같은 순회 구조이며, 원인이 없을 때 source()가 None을 돌려주는 점이 종료 조건이다.
  4. 호출자는 e.downcast_ref::<StockError>()가 Some(StockError::Insufficient { .. })인지 검사한다. 단점은 두 가지다. 시그니처가 어떤 오류가 올 수 있는지 알려 주지 않고, 라이브러리가 내부 오류 타입을 바꾸면 컴파일은 되지만 분기가 한 번도 맞지 않게 된다. 이를 피하려면 시그니처에 열거형을 직접 쓴다. 예를 들면 pub fn ship(...) -> Result<u32, StockError>다. 그러면 호출자는 match로 변형을 나눌 수 있고, 변형이 바뀌면 컴파일러가 알려 준다.
오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

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

댓글을 남기려면 로그인이 필요합니다.