Step 00: 고급 에러 처리 (thiserror, anyhow)
0 studying now
prep 60 min
고급 에러 처리 (thiserror, anyhow)
커스텀 에러 타입을 만들고 체계적으로 에러를 처리합니다.
Execute this step
Run from project root:
cargo runStep 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 데이터베이스에 연결합니다.