[Rust 시리즈 11편] 에러 처리 기초

English version

Rust에서 에러 처리는 "컴파일러가 눈 감고 넘어가는 상황을 없애 보자"는 철학의 연장선에 있습니다. 이번 편에서는 [[panic|panic!]], [[result|Result<T, E>]], [[question-mark-operator|? 연산자]]가 각각 어떤 역할을 하는지, 그리고 언제 어떤 도구를 골라야 실전 프로그램이 더 안전해지는지를 다룹니다.

왜 에러 처리가 중요한가

  • 시스템 언어의 특성: 파일, 네트워크, 사용자 입력처럼 실패가 흔한 작업을 다룰수록 예측 가능한 에러 흐름이 필요합니다.
  • Ownership 규칙과 연계: 에러가 나더라도 자원이 자동으로 정리되려면, 에러 표현도 타입 시스템 안에 들어와 있어야 합니다.
  • 런타임 패닉 최소화: panic!은 프로그램을 즉시 중단시키기 때문에, 복구 가능한 상황에서는 Result로 실패를 표현하는 습관이 중요합니다.

[[panic|panic!]]으로 프로그램을 중단시키기

[[panic|panic!]]은 "이 지점 이후로 실행을 이어가면 안 된다"고 판단될 때 쓰는 매크로입니다.

fn main() {
    let config = std::env::var("APP_CONFIG")
        .unwrap_or_else(|_| panic!("환경 변수가 필요합니다"));

    println!("config = {}", config);
}
  • panic!이 발생하면 스택 언와인딩(unwinding)이 진행되고, 현재 스레드는 즉시 중단됩니다.
  • 디버깅 중에 "절대 일어나면 안 되는 상태"를 확인하거나, 샘플 프로젝트에서 빠르게 실패 지점을 찾을 때 유용합니다.
  • 하지만 CLI나 서버처럼 계속 실행돼야 하는 프로그램에서는 남용하지 말고, 복구 가능한 실패를 Result로 표현하세요.

[[result|Result<T, E>]]로 실패를 값으로 다루기

[[result|Result]]는 enum Result<T, E> { Ok(T), Err(E) } 형태이며, 성공과 실패를 모두 타입 시스템 안에 넣습니다.

fn read_config() -> Result<String, std::io::Error> {
    std::fs::read_to_string("./config.txt")
}

fn main() {
    match read_config() {
        Ok(content) => println!("config: {}", content),
        Err(err) => eprintln!("파일을 읽지 못했습니다: {}", err),
    }
}
  • Ok(T)는 정상 결과를, Err(E)는 실패 이유를 담습니다.
  • E 타입을 통해 어떤 에러인지 구체적으로 표현할 수 있고, matchif let으로 분기합니다.
  • Result를 반환하는 함수는 호출자에게 실패 가능성을 명시적으로 전달하므로, 함수 이름만 봐도 안전성을 추론하기 쉬워집니다.

헬퍼 메서드 활용하기

  • unwrap, expect: 빠른 프로토타입 단계에서 ResultT로 강제로 꺼낼 때 사용하되, 최종 코드에서는 의미 있는 메시지를 제공하거나 안전한 흐름으로 교체하세요.
  • map, map_err, and_then: 함수형 스타일로 성공·실패값을 가공할 수 있습니다.

[[question-mark-operator|? 연산자]]로 에러 전파 단축하기

[[question-mark-operator|?]]는 [[result|Result]] (또는 [[option|Option]])가 Err일 때 현재 함수에서 즉시 반환하도록 만들어 줍니다.

fn load_user(id: u64) -> Result<String, Box<dyn std::error::Error>> {
    let path = format!("./users/{}.json", id);
    let raw = std::fs::read_to_string(&path)?;
    let parsed: serde_json::Value = serde_json::from_str(&raw)?;
    Ok(parsed["name"].as_str().unwrap_or("unknown").to_string())
}
  • ?Err를 만나면 함수의 반환 타입에 맞춰 그대로 전달하고, Ok면 내부 값을 꺼내 다음 줄로 넘깁니다.
  • ?를 사용하려면 현재 함수도 Result 또는 Option을 반환해야 합니다. 그리고 Result를 다룰 때는 함수 반환 타입의 Err가 현재 에러를 받아들일 수 있어야 합니다.
  • 중첩된 match 대신 직선적인 흐름을 만들 수 있어 가독성이 크게 올라갑니다.

언제 무엇을 고를까

상황 권장 도구 이유
복구 불가능한 버그, 디버깅 보조 panic!, unreachable! 문제를 즉시 표면화해 빠르게 수정
파일/네트워크 등 실패가 자연스러운 작업 Result 호출자에게 선택권을 넘기고 실패 이유를 보존
간단한 실습·프로토타입 단계 unwrap, expect 흐름을 단순화하되 나중에 안전한 처리로 교체
여러 에러를 같은 타입으로 묶기 Result<T, Box<dyn Error>> 또는 커스텀 enum 다양한 에러 원인을 한 채널로 전달

입문 단계에서는 아래처럼 기억하면 판단이 쉬워집니다.

  • 학습용 짧은 예제: unwrap, expect를 잠깐 허용
  • 실제로 실패가 자연스러운 작업: Result
  • 정말 계속 실행하면 안 되는 버그: panic!

CodeSandbox로 이어서 실습하기

아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo checkcargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. starter를 fork한 뒤 src/main.rs를 연다
  2. 본문 예제를 붙여 넣고 cargo check와 cargo run을 차례로 실행한다
  3. 타입, 값, 참조 흐름을 바꿔 컴파일 피드백과 출력 차이를 비교한다

Rust 실습은 브라우저 미리보기보다 터미널 피드백이 더 중요합니다. 여러 파일 구조나 추가 crate가 필요한 예제는 파일 배치를 조금 더 손봐야 할 수 있습니다.

실습 과제

  1. std::fs::File::openread_to_string을 조합해 파일 내용을 읽는 함수를 작성하고, ? 연산자를 적용해 보세요.
  2. 환경 변수가 없을 때만 panic!을 호출하고, 있을 때는 Result를 반환하도록 분기하는 함수를 만들어 보세요.
  3. expect 메시지에 "왜 여기서 panic을 허용했는지" 이유를 포함해, 나중에 로그를 봐도 의도를 파악할 수 있게 해 보세요.

완료 기준

  • panic!Result의 차이와 쓰임새를 한 문장으로 설명할 수 있다.
  • ? 연산자를 사용해 Result가 연쇄된 함수를 작성해 봤다.
  • unwrap이나 expect를 어디까지 허용할지 스스로 기준을 세웠다.

다음 편에서는 프로젝트가 커질 때 필수로 필요한 모듈·패키지 구조를 다뤄, 에러 처리 코드가 더 명확한 경로로 흘러가도록 준비하겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요