실패 가능성(Fallibility)
이전 연습 문제에서 작성한 Ticket::new 함수를 다시 한번 살펴봅시다.
impl Ticket {
pub fn new(
title: String,
description: String,
status: Status
) -> Ticket {
if title.is_empty() {
panic!("제목은 비어 있을 수 없습니다.");
}
if title.len() > 50 {
panic!("제목은 50바이트를 초과할 수 없습니다.");
}
if description.is_empty() {
panic!("설명은 비어 있을 수 없습니다.");
}
if description.len() > 500 {
panic!("설명은 500바이트를 초과할 수 없습니다.");
}
Ticket {
title,
description,
status,
}
}
}
이 함수는 유효성 검사 중 하나라도 통과하지 못하면 바로 패닉을 일으킵니다. 이는 호출한 쪽에서 오류를 직접 처리할 기회를 주지 않기 때문에 그리 바람직한 방식은 아닙니다.
이제 Rust의 핵심 오류 처리 메커니즘인 Result 타입을 배워볼 차례입니다.
Result 타입
Result 타입은 표준 라이브러리에 정의된 열거형으로, 다음과 같은 구조를 가집니다.
enum Result<T, E> {
Ok(T),
Err(E),
}
두 가지 베리언트가 있습니다.
Ok(T): 작업이 성공했음을 나타내며, 결과값인T를 담고 있습니다.Err(E): 작업이 실패했음을 나타내며, 발생한 오류 정보인E를 담고 있습니다.
Ok와 Err 모두 제네릭(Generic) 타입을 사용하므로, 성공했을 때와 실패했을 때 반환할 타입을 자유롭게 지정할 수 있습니다.
예외(Exceptions)는 없습니다
Rust에서 복구 가능한 오류는 **값(Value)**으로 취급됩니다. 즉, 오류도 다른 값들과 마찬가지로 함수의 인자로 전달되거나 변수에 저장될 수 있는 평범한 타입의 인스턴스일 뿐입니다.
이는 Python이나 C# 같은 언어들과는 매우 큰 차이점입니다. 그런 언어들은 오류를 알리기 위해 **예외(Exceptions)**를 던지는 방식을 사용하죠. 하지만 예외 방식은 몇 가지 단점이 있습니다.
- 함수의 선언부(Signature)만 보고는 이 함수가 예외를 던지는지 알 수 없습니다.
- 예외를 던진다면 정확히 어떤 종류의 예외가 발생하는지도 알기 어렵습니다.
- 이를 확인하려면 문서를 일일이 읽거나 함수 내부의 구현 코드를 직접 확인해야 합니다.
- 또한 예외 처리 로직이 예외 발생 지점과 멀리 떨어져 있는 경우가 많아 코드의 흐름을 파악하기 어렵게 만듭니다.
실패 가능성을 타입 시스템에 담다
반면 Rust는 Result를 통해 함수의 선언부에 실패 가능성을 명시하도록 강제합니다. 함수가 실패할 가능성이 있고 호출자가 이를 처리해야 한다면, 반드시 Result를 반환해야 합니다.
// 선언부만 보고도 이 함수가 실패할 수 있다는 것을 즉시 알 수 있습니다.
// 또한 에러 타입인 `ParseIntError`를 보고 어떤 실패 상황이 발생할지 예측할 수 있죠.
fn parse_int(s: &str) -> Result<i32, ParseIntError> {
// ...
}
이것이 바로 Result의 가장 큰 장점입니다. 실패 가능성을 ’명시적’으로 만들어 줍니다.
물론 Rust에도 패닉(Panic)은 존재합니다. 다른 언어의 예외처럼 타입 시스템이 추적해주지 않죠. 하지만 패닉은 오직 복구 불가능한 치명적인 오류를 위해서만 아껴서 사용해야 합니다.
Exercise
The exercise for this section is located in 05_ticket_v2/06_fallibility