thiserror는 커스텀 에러 타입을 쉽게 만들 수 있게 해줍니다.
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),
}
}
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(())
}
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);
}
}
}
}
복잡한 객체를 단계적으로 생성합니다.
// 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(())
}
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(())
}
타입 안전성을 높이고 의미를 명확히 합니다.
// 이메일 검증
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(())
}
데이터 구조와 알고리즘을 분리합니다.
// 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));
}
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);
}