6장: 에러 처리 & 디자인 패턴

6.1 thiserror - 커스텀 에러 타입

thiserror는 커스텀 에러 타입을 쉽게 만들 수 있게 해줍니다.

6.1.1 기본 사용법

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("데이터를 찾을 수 없습니다: {0}")]
    NotFound(String),

    #[error("접근 권한이 없습니다")]
    PermissionDenied,

    #[error("I/O 에러")]
    Io(#[from] std::io::Error),

    #[error("파싱 에러: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("알 수 없는 에러")]
    Unknown,
}

fn read_user(id: &str) -> Result<String, DataStoreError> {
    if id.is_empty() {
        return Err(DataStoreError::NotFound("빈 ID".to_string()));
    }

    // I/O 에러가 자동으로 변환됨
    let content = std::fs::read_to_string(format!("users/{}.txt", id))?;

    // Parse 에러가 자동으로 변환됨
    let _age: u32 = content.trim().parse()?;

    Ok(content)
}

fn main() {
    match read_user("123") {
        Ok(data) => println!("Data: {}", data),
        Err(e) => eprintln!("Error: {}", e),
    }
}

6.2 anyhow - 애플리케이션 에러 처리

anyhow는 애플리케이션에서 에러를 쉽게 전파하고 컨텍스트를 추가할 수 있게 해줍니다.

use anyhow::{Context, Result, bail, ensure};

fn process_config(path: &str) -> Result<String> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("설정 파일을 읽을 수 없습니다: {}", path))?;

    ensure!(!content.is_empty(), "설정 파일이 비어있습니다");

    if content.contains("invalid") {
        bail!("유효하지 않은 설정입니다");
    }

    Ok(content)
}

fn main() -> Result<()> {
    let config = process_config("config.toml")?;
    println!("Config loaded: {}", config);
    Ok(())
}

6.2.1 에러 체이닝

use anyhow::{Context, Result};

fn read_file(path: &str) -> Result<String> {
    std::fs::read_to_string(path)
        .context("파일 읽기 실패")
}

fn parse_number(s: &str) -> Result<i32> {
    s.parse::<i32>()
        .context("숫자 파싱 실패")
}

fn process() -> Result<i32> {
    let content = read_file("number.txt")
        .context("설정 로드 실패")?;

    let number = parse_number(&content)
        .context("설정 파싱 실패")?;

    Ok(number * 2)
}

fn main() {
    match process() {
        Ok(result) => println!("Result: {}", result),
        Err(e) => {
            eprintln!("Error: {:?}", e);
            // 에러 체인 출력
            for cause in e.chain() {
                eprintln!("  Caused by: {}", cause);
            }
        }
    }
}

6.3 Builder 패턴

복잡한 객체를 단계적으로 생성합니다.

// derive_builder 크레이트 사용
use derive_builder::Builder;

#[derive(Builder, Debug)]
#[builder(setter(into))]
struct Request {
    url: String,

    #[builder(default)]
    method: String,

    #[builder(default)]
    headers: Vec<(String, String)>,

    #[builder(default)]
    body: Option<String>,

    #[builder(default = "30")]
    timeout: u64,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let request = RequestBuilder::default()
        .url("https://api.example.com/users")
        .method("POST")
        .headers(vec![
            ("Content-Type".to_string(), "application/json".to_string()),
            ("Authorization".to_string(), "Bearer token".to_string()),
        ])
        .body(r#"{"name": "Alice"}"#)
        .timeout(60)
        .build()?;

    println!("{:?}", request);
    Ok(())
}

6.3.1 수동 Builder 구현

struct Database {
    host: String,
    port: u16,
    username: String,
    password: String,
    pool_size: usize,
}

struct DatabaseBuilder {
    host: Option<String>,
    port: Option<u16>,
    username: Option<String>,
    password: Option<String>,
    pool_size: Option<usize>,
}

impl DatabaseBuilder {
    fn new() -> Self {
        DatabaseBuilder {
            host: None,
            port: None,
            username: None,
            password: None,
            pool_size: None,
        }
    }

    fn host(mut self, host: impl Into<String>) -> Self {
        self.host = Some(host.into());
        self
    }

    fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }

    fn username(mut self, username: impl Into<String>) -> Self {
        self.username = Some(username.into());
        self
    }

    fn password(mut self, password: impl Into<String>) -> Self {
        self.password = Some(password.into());
        self
    }

    fn pool_size(mut self, size: usize) -> Self {
        self.pool_size = Some(size);
        self
    }

    fn build(self) -> Result<Database, String> {
        Ok(Database {
            host: self.host.ok_or("host is required")?,
            port: self.port.unwrap_or(5432),
            username: self.username.ok_or("username is required")?,
            password: self.password.ok_or("password is required")?,
            pool_size: self.pool_size.unwrap_or(10),
        })
    }
}

fn main() -> Result<(), String> {
    let db = DatabaseBuilder::new()
        .host("localhost")
        .username("admin")
        .password("secret")
        .build()?;

    println!("Connected to {}:{}", db.host, db.port);
    Ok(())
}

6.4 Newtype 패턴

타입 안전성을 높이고 의미를 명확히 합니다.

// 이메일 검증
use std::fmt;

#[derive(Debug, Clone)]
pub struct Email(String);

impl Email {
    pub fn new(email: impl Into<String>) -> Result<Self, String> {
        let email = email.into();

        if email.contains('@') && email.contains('.') {
            Ok(Email(email))
        } else {
            Err(format!("유효하지 않은 이메일: {}", email))
        }
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for Email {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

// 사용자 구조체
struct User {
    name: String,
    email: Email, // String이 아닌 Email 타입!
}

fn send_email(to: &Email, message: &str) {
    println!("Sending to {}: {}", to, message);
}

fn main() -> Result<(), String> {
    let email = Email::new("user@example.com")?;
    let user = User {
        name: "Alice".to_string(),
        email: email.clone(),
    };

    send_email(&user.email, "Welcome!");

    // 컴파일 에러!
    // send_email("invalid", "Test");

    Ok(())
}

6.5 Visitor 패턴

데이터 구조와 알고리즘을 분리합니다.

// AST 노드
enum Expr {
    Number(i32),
    Add(Box<Expr>, Box<Expr>),
    Multiply(Box<Expr>, Box<Expr>),
}

// Visitor 트레이트
trait ExprVisitor {
    type Output;

    fn visit_number(&mut self, n: i32) -> Self::Output;
    fn visit_add(&mut self, left: &Expr, right: &Expr) -> Self::Output;
    fn visit_multiply(&mut self, left: &Expr, right: &Expr) -> Self::Output;
}

impl Expr {
    fn accept<V: ExprVisitor>(&self, visitor: &mut V) -> V::Output {
        match self {
            Expr::Number(n) => visitor.visit_number(*n),
            Expr::Add(l, r) => visitor.visit_add(l, r),
            Expr::Multiply(l, r) => visitor.visit_multiply(l, r),
        }
    }
}

// 평가 방문자
struct Evaluator;

impl ExprVisitor for Evaluator {
    type Output = i32;

    fn visit_number(&mut self, n: i32) -> i32 {
        n
    }

    fn visit_add(&mut self, left: &Expr, right: &Expr) -> i32 {
        left.accept(self) + right.accept(self)
    }

    fn visit_multiply(&mut self, left: &Expr, right: &Expr) -> i32 {
        left.accept(self) * right.accept(self)
    }
}

// 출력 방문자
struct Printer;

impl ExprVisitor for Printer {
    type Output = String;

    fn visit_number(&mut self, n: i32) -> String {
        n.to_string()
    }

    fn visit_add(&mut self, left: &Expr, right: &Expr) -> String {
        format!("({} + {})", left.accept(self), right.accept(self))
    }

    fn visit_multiply(&mut self, left: &Expr, right: &Expr) -> String {
        format!("({} * {})", left.accept(self), right.accept(self))
    }
}

fn main() {
    // (2 + 3) * 4
    let expr = Expr::Multiply(
        Box::new(Expr::Add(
            Box::new(Expr::Number(2)),
            Box::new(Expr::Number(3)),
        )),
        Box::new(Expr::Number(4)),
    );

    let mut evaluator = Evaluator;
    println!("Result: {}", expr.accept(&mut evaluator));

    let mut printer = Printer;
    println!("Expression: {}", expr.accept(&mut printer));
}

6.6 Strategy 패턴

trait CompressionStrategy {
    fn compress(&self, data: &[u8]) -> Vec<u8>;
    fn decompress(&self, data: &[u8]) -> Vec<u8>;
}

struct GzipCompression;
struct ZstdCompression;
struct NoCompression;

impl CompressionStrategy for GzipCompression {
    fn compress(&self, data: &[u8]) -> Vec<u8> {
        println!("Compressing with GZIP");
        data.to_vec() // 실제로는 압축
    }

    fn decompress(&self, data: &[u8]) -> Vec<u8> {
        println!("Decompressing with GZIP");
        data.to_vec()
    }
}

impl CompressionStrategy for ZstdCompression {
    fn compress(&self, data: &[u8]) -> Vec<u8> {
        println!("Compressing with Zstd");
        data.to_vec()
    }

    fn decompress(&self, data: &[u8]) -> Vec<u8> {
        println!("Decompressing with Zstd");
        data.to_vec()
    }
}

impl CompressionStrategy for NoCompression {
    fn compress(&self, data: &[u8]) -> Vec<u8> {
        data.to_vec()
    }

    fn decompress(&self, data: &[u8]) -> Vec<u8> {
        data.to_vec()
    }
}

struct FileManager {
    strategy: Box<dyn CompressionStrategy>,
}

impl FileManager {
    fn new(strategy: Box<dyn CompressionStrategy>) -> Self {
        FileManager { strategy }
    }

    fn set_strategy(&mut self, strategy: Box<dyn CompressionStrategy>) {
        self.strategy = strategy;
    }

    fn save(&self, data: &[u8]) {
        let compressed = self.strategy.compress(data);
        println!("Saved {} bytes", compressed.len());
    }

    fn load(&self, data: &[u8]) -> Vec<u8> {
        self.strategy.decompress(data)
    }
}

fn main() {
    let data = b"Hello, World!";

    let mut manager = FileManager::new(Box::new(GzipCompression));
    manager.save(data);

    manager.set_strategy(Box::new(ZstdCompression));
    manager.save(data);

    manager.set_strategy(Box::new(NoCompression));
    manager.save(data);
}
🎯 핵심 요약: