learn

Rust의 Option<T> — null의 대안으로서의 enum

출발점

대부분의 OOP 언어는 모든 reference 타입에 묵시적으로 null이 끼어들어간다. Java의 String은 사실 "string 또는 null"이고, Python의 모든 변수도 마찬가지다. Tony Hoare(null의 발명자)는 2009년에 이를 "billion-dollar mistake" 라고 불렀다 — null을 reference 시스템에 넣을 수 있어서 넣었을 뿐이고, 그 결정이 40년간 셀 수 없는 버그·취약점·시스템 충돌을 만들었다는 자기 비판.

문제의 본질은 "값이 없을 수 있다"는 개념 자체가 아니라, 그것이 타입 시스템 외부에서 묵시적으로 처리된다는 점이다. 컴파일러가 "이 값은 null일 수 있다 / 아니다"를 추적하지 않으니 NullPointerException이 런타임까지 살아남는다.

Rust의 답 — null 자체를 없애고 enum으로 표현

Rust 표준 라이브러리는 prelude에 다음 enum을 박아둔다.

enum Option<T> {
    None,
    Some(T),
}

<T>는 generic 파라미터(10장에서 자세히)다. Some 안에 어떤 타입이든 담을 수 있어 Option<i32>, Option<String>, Option<Rectangle> 등이 모두 가능하다.

핵심 설계 결정은 일반 타입은 절대 null이 아니다라는 것이다. "값이 없을 수도 있다"는 가능성을 표현하려면 명시적으로 Option<T>로 감싸야 한다.

let x: i32 = 5;                  // 절대 null 아님
let y: Option<i32> = Some(5);
let z: Option<i32> = None;

let sum = x + y;  // ❌ 컴파일 에러 — i32와 Option<i32>는 다른 타입

왜 이게 더 나은가

Option<T>T는 컴파일러 입장에서 완전히 다른 타입이다. 그래서 다음과 같은 보장이 따라온다.

함수가 Rectangle을 리턴하면 그건 진짜 Rectangle이다. null 체크를 할 필요도 없고, NullPointerException 같은 런타임 폭발도 없다.

Option<Rectangle>을 리턴하는 함수는 호출자가 None 케이스를 반드시 명시적으로 처리해야 한다. 그러지 않으면 컴파일이 안 된다.

fn find_user(id: u32) -> Option<User> { /* ... */ }

let u = find_user(42);
// u.name();  ← 컴파일 에러. u는 Option<User>이지 User가 아님.

이걸 풀려면 match로 variant를 분기하거나, unwrap()/expect()로 강제 추출하거나, ? 연산자나 map/and_then 같은 메서드 체인을 거쳐야 한다.

다른 언어의 같은 개념

언어표현
Java 8+Optional<T> — Rust Option의 직접 모방. 단 null이 동시에 존재해서 효과가 반감
KotlinT? — null safety가 타입 시스템에 박힘
SwiftOptional<T> 또는 T?
HaskellMaybe T — 직계 조상
TypeScript`T

null 자체를 제거하지는 못해도(legacy 호환), 새 언어들이 컴파일러 차원에서 null을 강제로 다루게 하는 방향으로 모두 수렴했다 — 2010년대 언어 디자인의 가장 명확한 합의 중 하나다.

컴파일러가 강제하는 것

Java의 Optional<T>는 라이브러리 타입일 뿐이라 호출자가 무시하고 그냥 null을 던질 수 있다. 게다가 Optional<T> 변수 자체가 null일 수도 있다는 코미디. Kotlin과 Rust는 컴파일러가 nullable과 non-nullable을 타입 시스템에서 구분하기에 무시할 수 없다.

Rust의 Option<T>는 이 점에서 Kotlin T?와 동급의 강제력을 가지며, ML/Haskell의 Maybe처럼 표준 라이브러리 enum이라는 형태로 구현된 점이 특징.

다음 단계

Option<T> 값에서 진짜 T를 꺼내려면 variant를 분기 처리해야 한다. Rust가 이걸 풀어주는 도구가 match(다음 절)와 if let. 이때 컴파일러가 모든 variant가 처리됐는지 검사해서, "None 케이스를 깜빡했다"는 종류의 버그를 컴파일 타임에 잡아준다.

결론

Option는 enum이라는 sum type 메커니즘을 활용해 null의 묵시성을 타입 시스템 안으로 끌어온 것이다. "값이 없을 수 있음"을 명시적으로 표현하고, 컴파일러가 처리 누락을 강제로 검사한다. NullPointerException이라는 카테고리의 버그가 이 디자인 안에서는 컴파일 단계에서 걸러진다. 단순히 라이브러리 타입을 추가한 게 아니라 언어 차원의 null 거부 결정이라는 게 핵심이다.

관련: what-is-rust-enum