12. 열거형 + Option<T> 입문
Rust 의 enum 은 C 의 enum 과 차원이 다릅니다 — 각 variant 가 **다른 타입의 데이터를 가질 수 있는** sum type 입니다. 그리고 가장 자주 쓰이는 enum 인 Option<T> (Some(T) 또는 None) 가 어떻게 null pointer 의 1조 달러 실수를 풀어내는지 살펴봅니다.
이 강의에서 배우는 것
- 1enum 각 variant 에 서로 다른 데이터를 담는다
- 2Option<T> 로 "값이 없을 수도 있음" 을 타입으로 표현한다
- 3match / if let 으로 enum 을 분해한다
- 4Option 의 .unwrap() / .unwrap_or() / .map() 사용법을 안다
- 5null 이 없는 언어가 어떻게 안전성을 끌어올리는지 설명한다
소개
Tony Hoare 가 1965년 null pointer 를 발명한 것을 "1조 달러짜리 실수" 라 회고했습니다. Rust 는 null 이라는 개념을 언어 차원에서 없애고 `Option<T>` 로 대체했습니다 — 값이 없을 수도 있다는 것이 **타입에 명시**되어 컴파일러가 처리 누락을 잡아냅니다.
핵심 개념
1) Rust 의 enum 은 sum type
enum Message {
Quit, // 데이터 없음
Move { x: i32, y: i32 }, // named struct 형태
Write(String), // tuple 형태 1개
ChangeColor(i32, i32, i32), // tuple 형태 3개
}각 variant 가 사실상 서로 다른 타입의 "한 가지 경우". C 의 enum 처럼 정수 별칭이 아닙니다.
2) Option<T> — 빌트인
enum Option<T> {
None,
Some(T),
}표준 라이브러리에 포함돼 있고 어디서나 바로 쓸 수 있습니다 (use std::option::Option 불필요).
3) 분해 — match / if let
- **match** — 모든 variant 처리 강제. 누락 시 컴파일 에러
- **if let** — 한 variant 만 관심 있을 때 짧게
4) Option 헬퍼 메서드
| 메서드 | 의미 |
|---|---|
| .unwrap() | Some 이면 안의 값, None 이면 panic |
| .unwrap_or(default) | Some 이면 값, None 이면 default |
| .map(|x| ...) | Some 인 경우만 변환 |
| .and_then(|x| ...) | Some 인 경우만 변환 (반환도 Option) |
| .is_some() / .is_none() | boolean 검사 |
핵심 예제
match 로 모든 variant 처리:
enum Coin { Penny, Nickel, Dime, Quarter }
fn value(c: Coin) -> u32 {
match c {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() { println!("{}", value(Coin::Quarter)); }Option<T> 다루기:
fn find_first_even(v: &[i32]) -> Option<i32> {
for &x in v {
if x % 2 == 0 { return Some(x); }
}
None
}
fn main() {
let v = [1, 3, 4, 7, 8];
match find_first_even(&v) {
Some(x) => println!("found {}", x),
None => println!("not found"),
}
// if let — 한쪽만 관심
if let Some(x) = find_first_even(&v) { println!("got {}", x); }
// helper 메서드
let v2: [i32; 0] = [];
let result = find_first_even(&v2).unwrap_or(-1);
println!("{}", result); // -1
}data-carrying variant:
enum Shape {
Circle(f64),
Rectangle { w: f64, h: f64 },
}
fn area(s: Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle { w, h } => w * h,
}
}
fn main() {
println!("{}", area(Shape::Circle(2.0)));
println!("{}", area(Shape::Rectangle { w: 3.0, h: 4.0 }));
}자주 하는 실수
Q. .unwrap() 을 그냥 써도 되나요?
A. 프로토타이핑·테스트엔 OK. 실무 코드에선 "None 일 수 있다" 는 상황에서 panic 위험이 커지므로 .unwrap_or(...) / .map / ? 연산자(다음 강의들) 가 더 안전합니다.
Q. match 의 모든 variant 를 쓰기 귀찮은데 와일드카드는?
A. `_ =>` 로 나머지 모두 매칭. 하지만 가능하면 명시적으로 쓰는 게 좋습니다 — 나중에 variant 추가될 때 컴파일러가 알려주는 안전망이 사라지니까요.
Q. null 대신 Option 을 쓰는 게 그렇게 큰 차이인가요?
A. Java/Python 에서는 메서드를 호출하다가 NullPointerException 이 런타임에 터지지만, Rust 는 `T` 자리에 `None` 이 들어갈 수 없습니다. "이 함수는 실패할 수 있다" 가 타입에서 강제되니 처리 누락이 컴파일 에러.
정리
- enum 각 variant 가 서로 다른 데이터를 가질 수 있음 (sum type)
- Option<T> 가 null 을 대체 — 부재를 타입에 표현
- match / if let 으로 분해, match 는 모든 variant 처리 강제
- Option 의 .unwrap / .unwrap_or / .map 헬퍼 사용
과제
- fn find(v: &[i32], target: i32) -> Option<usize> 작성
- enum Event { Login(String), Logout, Click { x: i32, y: i32 } } 의 각 variant 를 match 로 다른 메시지 출력
- Option<i32> 두 개를 더하는 함수 — 둘 다 Some 이면 합, 하나라도 None 이면 None 반환