포스트

에러 핸들링 모델 — 예외 vs 에러 값 vs Result

실패를 호출자에게 전달하는 예외·에러 값·Result 계열을 비교하고, 전달 방식과 실패 타입 표현, 처리 강제성, panic 같은 비복구 흐름을 서로 다른 축으로 구분한다.

에러 핸들링 모델 — 예외 vs 에러 값 vs Result

난이도 입문 · 선행 없음

🗺️ 프로그래밍 언어 개념 로드맵의 한 편

한 줄 요약

에러 핸들링은 실패를 어떻게 표현하고 호출자에게 전달하며 처리하게 하는가의 문제다. 예외, 에러 값, Result는 대표적인 선택이지만 단순히 세 언어의 일대일 대응으로 보면 안 된다.

1
2
3
4
5
6
7
8
실패 표현
→ 값인가, 타입의 variant인가, 예외 객체인가

전달 경로
→ 일반 반환인가, 별도 예외 흐름인가

처리 강제성
→ 타입·컴파일러·lint가 얼마나 드러내고 요구하는가

이 축들을 나누면 C++·Go·Rust뿐 아니라 다른 언어의 모델도 비교하기 쉽다.

1. 예외 — 정상 반환과 다른 제어 흐름

C++·Java·Python 등의 예외는 실패를 일반 반환값과 다른 경로로 전파한다.

1
2
3
4
5
6
7
8
9
10
11
12
std::string readFile(const std::string& path) {
    if (!exists(path)) {
        throw std::runtime_error("no such file");
    }
    return "...";
}

try {
    auto content = readFile("x");
} catch (const std::exception& e) {
    std::cerr << e.what();
}

예외의 핵심은 실패가 일반 반환 경로와 분리되어 호출 스택을 따라 전파될 수 있다는 것이다.

다만 예외 = 시그니처에 안 보인다도 모든 언어에 그대로 적용되는 법칙은 아니다. Java checked exception처럼 일부 예외를 선언하고 처리하도록 요구하는 언어도 있다. C++의 일반적인 예외 타입은 함수 시그니처만 보고 완전히 알기 어렵다는 식으로 언어별 차이를 구분해야 한다.

예외 비용도 항상 느리다보다 정상 경로와 실제 throw 경로의 비용 모델이 구현에 따라 다르다고 이해하는 편이 정확하다. C++ 구현에서는 실제 throw·stack unwinding이 상대적으로 비싼 경우가 일반적이지만 정상 경로 비용은 ABI 전략에 따라 달라진다.

2. 에러 값 — 실패도 일반 데이터 흐름으로 반환한다

Go는 error를 일반 값으로 반환한다.

1
2
3
4
content, err := readFile("x")
if err != nil {
    return err
}

실패가 일반 반환 경로에 있으므로 호출 지점에서 눈에 잘 보인다. 반면 반환된 error_로 버리거나 반환값 자체를 사용하지 않는 식으로 무시할 수도 있다.

Go의 특징을 예외가 없다라고만 기억하기보다 복구 가능한 실패를 값으로 명시적으로 전달하는 관용구를 언어와 표준 라이브러리가 중심에 둔다고 이해하는 편이 좋다. Go에도 panic/recover라는 별도 비정상 제어 흐름이 있지만 일반적인 에러 반환을 대신하는 용도는 아니다.

3. Result — 성공과 실패를 하나의 합타입으로 표현한다

Rust의 Result<T, E>는 성공과 실패를 타입의 두 variant로 표현한다.

1
2
3
4
5
fn read_file(path: &str) -> Result<String, io::Error> {
    // ...
}

let content = read_file("x")?;

Result<T, E>의 중요한 점은 실패 가능성과 에러 타입 E가 반환 타입에 드러난다는 것이다. ?는 실패를 마법처럼 없애는 문법이 아니라, 현재 함수의 반환 타입에 맞게 실패를 조기에 전파하는 연산이다.

Rust의 Result에는 #[must_use]가 적용되어 방치하면 기본적으로 경고가 발생한다. 다만 이것을 언어가 반드시 처리하지 않으면 컴파일을 금지한다고 과장하면 안 된다. lint 정책에 따라 경고를 허용할 수 있고, 명시적으로 값을 버릴 수도 있다.

4. 세 모델을 한 줄의 강제성 순위로 만들지 않는다

기존 설명처럼

1
2
3
예외 = 편의
Go = 명시성
Rust = 강제성

으로 요약하면 방향은 잡히지만 실제 차이를 너무 많이 지운다.

더 정확하게는 다음 질문을 본다.

질문예외에러 값Result 계열
실패가 일반 반환 타입에 드러나는가언어마다 다름보통 반환값에 보임명시적으로 드러남
별도 제어 흐름을 쓰는가아니오아니오
실패 타입을 정적으로 표현하는가언어마다 다름error interface 등E 타입 매개변수
무시를 막는가언어/도구 정책에 따라 다름쉽게 무시 가능must-use/lint 등으로 경고 가능
전파 문법throwreturn err?

즉 에러 모델은 하나의 좋음/나쁨 순위가 아니라 표현·전달·강제성의 조합이다.

5. 복구 가능한 실패와 프로그램 불변식 위반은 별도 축이다

파일 없음, 네트워크 오류, 잘못된 사용자 입력처럼 정상 운영 중 발생할 수 있는 실패는 호출자가 대응할 수 있는 복구 가능한 오류다.

반면 내부 불변식이 깨졌거나 프로그램이 계속 안전하게 진행할 수 없다고 판단한 경우에는 panic, assertion, terminate 같은 별도 메커니즘을 사용할 수 있다.

하지만

1
2
복구 가능 = Result/error
버그 = panic

도 절대 규칙은 아니다. 라이브러리 경계, 실행 환경, 복구 정책에 따라 선택이 달라진다. 예를 들어 C++ exception은 복구 가능한 오류에도 널리 사용되며, Rust panic!도 API 계약 위반이나 복구 불가능하다고 판단한 상황에 쓰인다.

중요한 질문은 호출자가 이 실패를 정상적으로 처리할 수 있고 처리해야 하는가다.

6. Option은 Result와 관련 있지만 같은 에러 모델은 아니다

1
2
3
4
5
Option<T>
→ 값이 있거나 없음

Result<T, E>
→ 성공값이거나 실패 정보 E

둘 다 합타입(sum type)으로 볼 수 있고 비슷한 조합 연산을 제공하지만 의미가 다르다. None은 반드시 오류를 뜻하지 않는다.

예를 들어 검색 결과가 없는 것은 정상적인 부재일 수 있고, 파일을 읽지 못한 것은 원인을 전달해야 하는 실패일 수 있다.

이 차이는 Null 안전성에서 이어진다.

7. 언어별로 보면

C++

예외를 제공하지만 모든 코드베이스가 예외를 같은 방식으로 사용하지 않는다. RAII와 stack unwinding이 예외 안전성의 핵심이고, C++23 std::expected를 사용하면 값 기반 Result 스타일 API도 만들 수 있다.

Go

(value, error) 패턴이 중심이다. 에러는 fmt.Errorf("...: %w", err)로 문맥을 더하고 errors.Is/errors.As로 원인을 검사할 수 있다. panic은 일반적인 에러 반환을 대체하는 흐름으로 사용하지 않는 것이 관용적이다.

Rust

Result<T, E>?가 복구 가능한 오류 처리의 중심이다. panic!은 별도 흐름이며 Result와 역할을 구분한다.

한눈에 정리

1
2
3
4
5
6
7
에러 핸들링을 볼 때

1. 실패를 무엇으로 표현하는가
2. 함수 시그니처에 얼마나 드러나는가
3. 어떤 경로로 전파하는가
4. 호출자가 무시할 수 있는가
5. 복구 불가능한 상황은 별도 메커니즘으로 다루는가

이 다섯 질문으로 보면 언어별 문법을 외우는 것보다 모델 차이가 잘 보인다.

스스로 점검

1. 예외와 Result의 가장 근본적인 차이는?

예외는 일반 반환과 다른 제어 흐름으로 실패를 전파하고, Result는 성공/실패를 일반 반환 타입의 variant로 표현한다.

2. Rust Result는 반드시 처리하지 않으면 컴파일이 실패하는가?

항상 그런 것은 아니다. Result는 must-use라 방치하면 기본적으로 경고가 발생하고 프로젝트가 해당 lint를 deny하면 빌드 실패가 된다. 중요한 차이는 실패 가능성이 반환 타입에 명시된다는 점이다.

3. Option과 Result가 같은 합타입 계열인데도 구분해야 하는 이유는?

Option은 값의 존재/부재를 표현하고 Result는 성공/실패와 실패 정보의 전달을 표현한다. 부재가 항상 오류인 것은 아니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.