Step 00: 고급 에러 처리 (thiserror, anyhow)
prep 60 min

고급 에러 처리 (thiserror, anyhow)

커스텀 에러 타입을 만들고 체계적으로 에러를 처리합니다.

Execute this step

Run from project root:
cargo run

Step 0: 고급 에러 처리

학습 목표

  • 커스텀 에러 타입으로 명확한 에러 처리하기
  • thiserror로 에러 타입 자동 구현하기
  • anyhow로 빠른 프로토타이핑하기

Cargo.toml에 추가

[dependencies]
thiserror = "2.0"
anyhow = "1.0"

핵심 개념

1. 기존 에러 처리의 문제

1// String 에러는 타입 정보가 없음
2fn find_book(id: u32) -> Result<Book, String> {
3    Err("책을 찾을 수 없습니다".to_string())
4}
5
6// 호출하는 쪽에서 에러 종류를 알 수 없음
7match find_book(1) {
8    Err(e) => {
9        // 이게 NotFound인지, PermissionDenied인지 알 수 없음!
10        println!("에러: {}", e);
11    }
12    _ => {}
13}

2. thiserror로 커스텀 에러

1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum LibraryError {
5    #[error("책 ID {0}를 찾을 수 없습니다")]
6    BookNotFound(u32),
7    
8    #[error("책 '{0}'은(는) 이미 대여 중입니다")]
9    AlreadyBorrowed(String),
10    
11    #[error("책 '{0}'은(는) 대여 중이 아닙니다")]
12    NotBorrowed(String),
13    
14    #[error("데이터베이스 오류: {0}")]
15    DatabaseError(#[from] sqlx::Error),
16    
17    #[error("JSON 직렬화 오류: {0}")]
18    SerializationError(#[from] serde_json::Error),
19}
20
21pub type Result<T> = std::result::Result<T, LibraryError>;

사용:

1fn find_book(&self, id: u32) -> Result<&Book> {
2    self.books.get(&id)
3        .ok_or(LibraryError::BookNotFound(id))
4}
5
6fn borrow_book(&mut self, id: u32) -> Result<()> {
7    let book = self.find_book_mut(id)?; // ? 연산자로 에러 전파
8    
9    if !matches!(book.status, BookStatus::Available) {
10        return Err(LibraryError::AlreadyBorrowed(book.title.clone()));
11    }
12    
13    book.status = BookStatus::Borrowed { /* ... */ };
14    Ok(())
15}

3. anyhow로 간단하게

프로토타이핑이나 애플리케이션 코드에서 사용:

1use anyhow::{Context, Result};
2
3fn load_config() -> Result<Config> {
4    let config = std::fs::read_to_string("config.json")
5        .context("설정 파일을 읽을 수 없습니다")?;
6    
7    let config: Config = serde_json::from_str(&config)
8        .context("JSON 파싱 실패")?;
9    
10    Ok(config)
11}

4. ? 연산자

1// 이 코드는
2let result = operation();
3let value = match result {
4    Ok(v) => v,
5    Err(e) => return Err(e.into()),
6};
7
8// 이렇게 간단하게
9let value = operation()?;

전체 에러 타입 구현

src/error.rs 생성:

1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum LibraryError {
5    #[error("책 ID {0}를 찾을 수 없습니다")]
6    BookNotFound(u32),
7    
8    #[error("책 '{title}'은(는) 이미 대여 중입니다 (대여자: {borrower})")]
9    AlreadyBorrowed {
10        title: String,
11        borrower: String,
12    },
13    
14    #[error("책 '{0}'은(는) 대여 중이 아닙니다")]
15    NotBorrowed(String),
16    
17    #[error("잘못된 카테고리: {0}")]
18    InvalidCategory(String),
19    
20    #[error("권한이 없습니다")]
21    Unauthorized,
22    
23    #[error("잘못된 날짜 형식: {0}")]
24    InvalidDate(String),
25}
26
27pub type Result<T> = std::result::Result<T, LibraryError>;

src/lib.rs 수정:

1mod error;
2pub use error::{LibraryError, Result};
3
4impl Library {
5    pub fn find_book(&self, id: u32) -> Result<&Book> {
6        self.books.get(&id)
7            .ok_or(LibraryError::BookNotFound(id))
8    }
9    
10    pub fn find_book_mut(&mut self, id: u32) -> Result<&mut Book> {
11        self.books.get_mut(&id)
12            .ok_or(LibraryError::BookNotFound(id))
13    }
14    
15    pub fn borrow_book(&mut self, id: u32, borrower: String, due_date: String) -> Result<()> {
16        let book = self.find_book_mut(id)?;
17        
18        match &book.status {
19            BookStatus::Available => {
20                book.status = BookStatus::Borrowed {
21                    by: borrower,
22                    due_date,
23                };
24                Ok(())
25            }
26            BookStatus::Borrowed { by, .. } => {
27                Err(LibraryError::AlreadyBorrowed {
28                    title: book.title.clone(),
29                    borrower: by.clone(),
30                })
31            }
32            _ => Err(LibraryError::NotBorrowed(book.title.clone())),
33        }
34    }
35    
36    pub fn return_book(&mut self, id: u32) -> Result<()> {
37        let book = self.find_book_mut(id)?;
38        
39        match book.status {
40            BookStatus::Borrowed { .. } => {
41                book.status = BookStatus::Available;
42                Ok(())
43            }
44            _ => Err(LibraryError::NotBorrowed(book.title.clone())),
45        }
46    }
47}

웹 핸들러에서 에러 처리

src/bin/web.rs:

1use actix_web::{error::ResponseError, http::StatusCode, HttpResponse};
2use book_manager::LibraryError;
3
4// Actix-web용 에러 변환
5impl ResponseError for LibraryError {
6    fn status_code(&self) -> StatusCode {
7        match self {
8            LibraryError::BookNotFound(_) => StatusCode::NOT_FOUND,
9            LibraryError::AlreadyBorrowed { .. } => StatusCode::CONFLICT,
10            LibraryError::NotBorrowed(_) => StatusCode::BAD_REQUEST,
11            LibraryError::Unauthorized => StatusCode::UNAUTHORIZED,
12            LibraryError::InvalidCategory(_) => StatusCode::BAD_REQUEST,
13            LibraryError::InvalidDate(_) => StatusCode::BAD_REQUEST,
14        }
15    }
16    
17    fn error_response(&self) -> HttpResponse {
18        HttpResponse::build(self.status_code()).json(ApiResponse::<()>::error(
19            self.to_string()
20        ))
21    }
22}
23
24// 핸들러가 간단해짐
25async fn get_book(
26    data: web::Data<AppState>,
27    path: web::Path<u32>,
28) -> Result<HttpResponse, LibraryError> {
29    let id = path.into_inner();
30    let book = data.library.find_book(id).await?; // ? 연산자로 간단하게!
31    Ok(HttpResponse::Ok().json(ApiResponse::success(book)))
32}
33
34async fn borrow_book(
35    data: web::Data<AppState>,
36    path: web::Path<u32>,
37    req: web::Json<BorrowRequest>,
38) -> Result<HttpResponse, LibraryError> {
39    let id = path.into_inner();
40    data.library.borrow_book(id, req.borrower.clone(), req.due_date.clone()).await?;
41    Ok(HttpResponse::Ok().json(ApiResponse::success("대여 완료")))
42}

에러 매칭

1match library.borrow_book(1, "김철수".to_string(), "2026-02-20".to_string()) {
2    Ok(_) => println!("대여 성공"),
3    Err(LibraryError::BookNotFound(id)) => {
4        println!("책 ID {}를 찾을 수 없습니다", id);
5    }
6    Err(LibraryError::AlreadyBorrowed { title, borrower }) => {
7        println!("'{}'은(는) 이미 {}님이 대여 중입니다", title, borrower);
8    }
9    Err(e) => println!("기타 에러: {}", e),
10}

체크리스트

  • [ ] thiserror로 커스텀 에러 타입을 만들었습니다
  • [ ] #[error] 속성으로 에러 메시지를 정의했습니다
  • [ ] ? 연산자로 에러를 전파했습니다
  • [ ] ResponseError를 구현하여 웹 에러로 변환했습니다
  • [ ] 에러 타입으로 패턴 매칭했습니다

다음 단계

Step 1에서는 SQLx를 사용하여 PostgreSQL 데이터베이스에 연결합니다.

Did you find this helpful? Give it a cheer!