Rust 오류 처리 심층 탐구: Result 및 Option을 넘어서
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Rust에서 에러를 처리하는 것은 단순히 Result
와 Option
을 사용하는 것만큼 간단하지 않습니다. 초보자에게 Rust의 에러 처리는 꽤 불친절할 수 있습니다. 여러 번 어려움을 겪은 후, 저는 이 주제에 대한 제 지식을 정리하기로 했습니다. 이 가이드는 두 가지 주요 부분으로 구성되어 있습니다.
Result
작업을 위해 제공되는 공식 메서드- 사용자 정의 에러를 정의하고 처리하는 방법.
이러한 개념을 숙달하면 Rust의 에러 처리 문제를 극복하는 데 도움이 될 것입니다.
에러 처리를 위한 메서드
에러를 효과적으로 처리하려면 Rust에 내장된 메서드를 활용해야 합니다. 이렇게 하면 작업이 훨씬 쉬워집니다.
유용한 메서드는 다음과 같습니다.
or()
and()
or_else()
and_then()
map()
map_err()
map_or()
map_or_else()
ok_or()
ok_or_else()
- ...
아래에서는 이러한 메서드를 언제 사용해야 하는지, 어떻게 사용해야 하는지, 궁극적으로 코드를 작성할 때 Err
타입을 어떻게 설계해야 하는지 설명하겠습니다.
or()
및 and()
이러한 메서드를 사용하면 논리적 OR 및 AND와 유사하게 두 가지 옵션 중에서 선택할 수 있습니다.
or()
: 표현식을 순서대로 평가합니다. 표현식 중 하나라도Some
또는Ok
가 되면 해당 값이 즉시 반환됩니다.and()
: 두 표현식 모두Some
또는Ok
인 경우 두 번째 표현식의 값을 반환합니다. 결과 중 하나라도None
또는Err
이면 대신 해당 값을 반환합니다.
let s1 = Some("some1"); let s2 = Some("some2"); let n: Option<&str> = None; let o1: Result<&str, &str> = Ok("ok1"); let o2: Result<&str, &str> = Ok("ok2"); let e1: Result<&str, &str> = Err("error1"); let e2: Result<&str, &str> = Err("error2"); assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1 assert_eq!(s1.or(n), s1); // Some or None = Some assert_eq!(n.or(s1), s1); // None or Some = Some assert_eq!(n.or(n), n); // None1 or None2 = None2 assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1 assert_eq!(o1.or(e1), o1); // Ok or Err = Ok assert_eq!(e1.or(o1), o1); // Err or Ok = Ok assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2 assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2 assert_eq!(s1.and(n), n); // Some and None = None assert_eq!(n.and(s1), n); // None and Some = None assert_eq!(n.and(n), n); // None1 and None2 = None1 assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2 assert_eq!(o1.and(e1), e1); // Ok and Err = Err assert_eq!(e1.and(o1), e1); // Err and Ok = Err assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1
or_else()
및 and_then()
or()
및 and()
메서드는 값을 수정하지 않고 두 값 중에서만 선택합니다. 더 복잡한 로직을 적용해야 하는 경우 클로저와 함께 or_else()
및 and_then()
을 사용해야 합니다.
// Option과 함께 or_else() 사용 let s1 = Some("some1"); let s2 = Some("some2"); let fn_some = || Some("some2"); // 다음 코드와 동일: let fn_some = || -> Option<&str> { Some("some2") }; let n: Option<&str> = None; let fn_none = || None; assert_eq!(s1.or_else(fn_some), s1); // Some1 or_else Some2 = Some1 assert_eq!(s1.or_else(fn_none), s1); // Some or_else None = Some assert_eq!(n.or_else(fn_some), s2); // None or_else Some = Some assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2 // Result와 함께 or_else() 사용 let o1: Result<&str, &str> = Ok("ok1"); let o2: Result<&str, &str> = Ok("ok2"); let fn_ok = |_| Ok("ok2"); let e1: Result<&str, &str> = Err("error1"); let e2: Result<&str, &str> = Err("error2"); let fn_err = |_| Err("error2"); assert_eq!(o1.or_else(fn_ok), o1); // Ok1 or_else Ok2 = Ok1 assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok assert_eq!(e1.or_else(fn_ok), o2); // Err or_else Ok = Ok assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2
map()
Result
또는 Option
내부의 값을 수정하려면 map()
을 사용합니다.
let s1 = Some("abcde"); let s2 = Some(5); let n1: Option<&str> = None; let n2: Option<usize> = None; let o1: Result<&str, &str> = Ok("abcde"); let o2: Result<usize, &str> = Ok(5); let e1: Result<&str, &str> = Err("abcde"); let e2: Result<usize, &str> = Err("abcde"); let fn_character_count = |s: &str| s.chars().count(); assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2 assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2 assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2 assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2
map_err()
Result
에서 Err
값을 수정해야 하는 경우 map_err()
을 사용합니다.
let o1: Result<&str, &str> = Ok("abcde"); let o2: Result<&str, isize> = Ok("abcde"); let e1: Result<&str, &str> = Err("404"); let e2: Result<&str, isize> = Err(404); let fn_character_count = |s: &str| -> isize { s.parse().unwrap() }; assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2 assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2
map_or()
Err
가 없을 것이라고 확신하는 경우 map_or()
을 사용하여 대신 기본값을 반환할 수 있습니다.
const V_DEFAULT: u32 = 1; let s: Result<u32, ()> = Ok(10); let n: Option<u32> = None; let fn_closure = |v: u32| v + 2; assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12); assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);
map_or_else()
map_or()
는 기본값 반환만 허용하지만, 클로저가 기본값을 제공해야 하는 경우 map_or_else()
를 사용합니다.
let s = Some(10); let n: Option<i8> = None; let fn_closure = |v: i8| v + 2; let fn_default = || 1; assert_eq!(s.map_or_else(fn_default, fn_closure), 12); assert_eq!(n.map_or_else(fn_default, fn_closure), 1);
ok_or()
Option
을 Result
로 변환하려면 ok_or()
를 사용할 수 있습니다.
const ERR_DEFAULT: &str = "error message"; let s = Some("abcde"); let n: Option<&str> = None; let o: Result<&str, &str> = Ok("abcde"); let e: Result<&str, &str> = Err(ERR_DEFAULT); assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T) assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)
ok_or_else()
잠재적인 Err
케이스를 처리하고 클로저를 사용하여 동일한 타입의 에러를 반환하려는 경우 ok_or_else()
를 사용합니다.
let s = Some("abcde"); let n: Option<&str> = None; let fn_err_message = || "error message"; let o: Result<&str, &str> = Ok("abcde"); let e: Result<&str, &str> = Err("error message"); assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T) assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default)
에러를 설계하는 방법
초보자는 Rust의 엄격한 에러 타입 때문에 좌절감을 느끼는 경우가 많습니다. 특히 여러 Result
반환에서 타입 불일치에 직면했을 때 그렇습니다. Result
타입을 더 깊이 이해하면 이러한 좌절감을 피할 수 있습니다.
간단한 사용자 정의 에러 정의
프로그램을 작성할 때 사용자 정의 에러를 정의하는 것이 일반적입니다. 다음은 간단한 사용자 정의 Result
의 예입니다.
use std::fmt; // CustomError는 사용자 정의 에러 타입입니다. #[derive(Debug)] struct CustomError; // 사용자에게 보이는 에러 메시지를 위해 Display 트레이트 구현. impl fmt::Display for CustomError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "에러가 발생했습니다. 다시 시도해주세요!") } } // CustomError를 생성하는 예제 함수입니다. fn make_error() -> Result<(), CustomError> { Err(CustomError) } fn main(){ match make_error() { Err(e) => eprintln!("{}", e), _ => println!("에러 없음"), } eprintln!("{:?}", make_error()); }
참고:
eprintln!
매크로는 에러 출력에 사용되지만 출력이 리디렉션되지 않는 한println!
과 동일하게 작동합니다.
Debug
및 Display
를 구현하면 오류를 표시하기 위해 포맷할 수 있을 뿐만 아니라 사용자 정의 오류를 Box<dyn std::error::Error>
트레이트 객체로 변환할 수도 있습니다.
더 복잡한 에러 정의
실제 시나리오에서는 에러 코드와 메시지를 할당하는 경우가 많습니다.
use std::fmt; struct CustomError { code: usize, message: String, } // 코드에 따라 다른 오류 메시지 표시. impl fmt::Display for CustomError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let err_msg = match self.code { 404 => "죄송합니다, 페이지를 찾을 수 없습니다!", _ => "죄송합니다, 문제가 발생했습니다! 다시 시도해주세요!", }; write!(f, "{}", err_msg) } } impl fmt::Debug for CustomError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "CustomError {{ code: {}, message: {} }}", self.code, self.message ) } } fn make_error() -> Result<(), CustomError> { Err(CustomError { code: 404, message: String::from("페이지를 찾을 수 없음"), }) } fn main() { match make_error() { Err(e) => eprintln!("{}", e), _ => println!("에러 없음"), } eprintln!("{:?}", make_error()); }
Display
및Debug
를 수동으로 구현하면#[derive(Debug)]
를 사용하는 것보다 더 사용자 정의된 출력을 얻을 수 있습니다.
에러 변환
타사 라이브러리를 사용하고 있으며 각 라이브러리가 자체 오류 타입을 정의하는 경우 어떻게 해야 할까요? Rust는 오류 변환을 위해 std::convert::From
트레이트를 제공합니다.
use std::fs::File; use std::io; #[derive(Debug)] struct CustomError { kind: String, message: String, } // `io::Error`를 `CustomError`로 변환합니다. impl From<io::Error> for CustomError { fn from(error: io::Error) -> Self { CustomError { kind: String::from("io"), message: error.to_string(), } } } fn main() -> Result<(), CustomError> { let _file = File::open("nonexistent_file.txt")?; Ok(()) }
?
연산자는 자동으로 std::io::Error
를 CustomError
로 변환합니다. 이 접근 방식은 오류 처리를 상당히 간소화합니다.
여러 에러 타입 처리
함수가 여러 에러 타입을 처리하는 경우 어떻게 해야 할까요?
use std::fs::File; use std::io::{self, Read}; use std::num; #[derive(Debug)] struct CustomError { kind: String, message: String, } impl From<io::Error> for CustomError { fn from(error: io::Error) -> Self { CustomError { kind: String::from("io"), message: error.to_string(), } } } impl From<num::ParseIntError> for CustomError { fn from(error: num::ParseIntError) -> Self { CustomError { kind: String::from("parse"), message: error.to_string(), } } } fn main() -> Result<(), CustomError> { let mut file = File::open("hello_world.txt")?; let mut content = String::new(); file.read_to_string(&mut content)?; let _number: usize = content.parse()?; Ok(()) }
고급 에러 처리 전략
함수가 다른 에러 타입을 반환할 때, 다음은 네 가지 일반적인 접근 방식입니다.
1. Box<dyn Error>
사용
이 메서드는 모든 에러 타입을 트레이트 객체로 변환합니다.
use std::fs::read_to_string; use std::error::Error; fn main() -> Result<(), Box<dyn Error>> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String, Box<dyn Error>> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) }
장점: 코드 간소화.
단점: 약간의 성능 손실 및 에러 타입 정보 손실 가능성.
2. 사용자 정의 에러 타입
enum
을 정의하여 모든 에러 타입을 나타냅니다.
#[derive(Debug)] enum MyError { EnvironmentVariableNotFound, IOError(std::io::Error), } impl From<std::env::VarError> for MyError { fn from(_: std::env::VarError) -> Self { Self::EnvironmentVariableNotFound } } impl From<std::io::Error> for MyError { fn from(value: std::io::Error) -> Self { Self::IOError(value) } }
단점: 장황하지만 정확한 에러 제어를 제공합니다.
3. thiserror
사용
어노테이션을 사용하여 사용자 정의 에러 정의를 간소화합니다.
#[derive(thiserror::Error, Debug)] enum MyError { #[error("환경 변수를 찾을 수 없습니다")] EnvironmentVariableNotFound(#[from] std::env::VarError), #[error(transparent)] IOError(#[from] std::io::Error), }
단순성과 제어의 균형을 위해 적극 권장됩니다.
4. anyhow
사용
모든 에러 타입을 캡슐화하여 성능 저하를 감수하면서 편의성을 제공합니다.
use anyhow::Result; fn main() -> Result<()> { let html = render()?; println!("{}", html); Ok(()) } fn render() -> Result<String> { let file = std::env::var("MARKDOWN")?; let source = read_to_string(file)?; Ok(source) }
마지막 생각: 그냥 하세요!
이러한 기술을 통해 이제 Rust에서 오류 관리를 처리할 수 있습니다. 단순함을 선호하든 미세한 제어를 선호하든 Rust의 오류 처리 메커니즘은 사용자의 요구에 맞게 조정할 수 있습니다.
저희 Leapcell은 Rust 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불 — 요청 없음, 요금 없음.
타의 추종을 불허하는 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 쉬운 동시성 처리를 위한 자동 확장.
- 운영 오버헤드가 없으므로 구축에만 집중하십시오.
문서에서 더 자세히 알아보세요!
X에서 우리를 팔로우하세요: @LeapcellHQ