DaleSchool

Option 완벽 이해: 값이 없을 수도 있다면

중급20분

학습 목표

  • Option<T>가 Some(T)와 None으로 구성됨을 이해한다
  • match/if let/unwrap_or()/map() 등으로 Option을 처리할 수 있다
  • unwrap() 대신 안전한 대안을 선택할 수 있다
  • 함수 반환값으로 Option을 활용할 수 있다

동작하는 코드

다른 언어에서는 값이 없을 때 null을 사용해요. 그리고 NullPointerException이라는 악명 높은 버그에 시달리죠. Rust는 null이 없어요. 대신 Option<T> 으로 "값이 있을 수도, 없을 수도 있다"를 표현해요.

fn main() {
    let fruits = vec!["사과", "바나나", "포도"];

    let first = fruits.get(0);   // Some("사과")
    let fourth = fruits.get(10); // None

    println!("첫 번째: {:?}", first);
    println!("네 번째: {:?}", fourth);
}

vec.get()Option<&T>를 반환해요. 인덱스가 범위 안이면 Some(값), 밖이면 None이에요. fruits[10]처럼 직접 접근하면 panic이 나지만, .get(10)은 안전하게 None을 돌려줘요.

직접 해보기

  1. fruits.get(2)의 결과를 출력해보세요.
  2. fruits[10]으로 바꿔서 실행해보세요. 어떤 에러가 나오나요?
  3. fruits.get(10)과 비교해보세요 — None이 panic보다 나은 이유가 느껴지나요?

"왜?" — 쇼핑몰 재고 조회

Option쇼핑몰 재고 조회로 생각하면 쉬워요.

  • Some(상품) — 재고 있음! 상품이 들어있어요
  • None — 품절! 상품이 없어요
enum Option<T> {
    Some(T),  // 값이 있어요
    None,     // 값이 없어요
}

Option<T>는 사실 모듈 13에서 배운 열거형이에요! Rust 표준 라이브러리에 이미 정의되어 있고, SomeNone은 별도 import 없이 바로 쓸 수 있어요.

match로 처리하기

가장 기본적인 방법이에요. 모든 경우를 빠짐없이 처리해야 하니까 안전해요.

fn find_discount(item: &str) -> Option<u32> {
    match item {
        "사과" => Some(10),
        "바나나" => Some(20),
        _ => None,
    }
}

fn main() {
    let item = "사과";

    match find_discount(item) {
        Some(percent) => println!("{item}: {percent}% 할인!"),
        None => println!("{item}: 할인 없음"),
    }
}

if let — 하나만 관심 있을 때

Some인 경우만 처리하고 싶다면 if let이 간결해요.

fn main() {
    let name: Option<&str> = Some("지수");

    if let Some(n) = name {
        println!("안녕, {n}!");
    }
    // None이면 아무 일도 안 해요
}

unwrap_or() — 기본값 제공

None일 때 대신 쓸 값을 지정할 수 있어요.

fn main() {
    let port: Option<u16> = None;
    let actual_port = port.unwrap_or(8080);
    println!("포트: {actual_port}"); // 포트: 8080

    let custom: Option<u16> = Some(3000);
    let actual = custom.unwrap_or(8080);
    println!("포트: {actual}"); // 포트: 3000
}

unwrap_or_default()도 있어요. 타입의 기본값(0, "", false 등)을 사용해요.

fn main() {
    let count: Option<i32> = None;
    println!("개수: {}", count.unwrap_or_default()); // 개수: 0

    let name: Option<String> = None;
    println!("이름: '{}'", name.unwrap_or_default()); // 이름: ''
}

map() — 값이 있을 때만 변환

Some 안의 값을 변환하고 싶을 때 map()을 써요. None이면 그대로 None이에요.

fn main() {
    let name: Option<&str> = Some("rust");
    let upper = name.map(|n| n.to_uppercase());
    println!("{:?}", upper); // Some("RUST")

    let empty: Option<&str> = None;
    let upper = empty.map(|n| n.to_uppercase());
    println!("{:?}", upper); // None
}

and_then() — Option을 반환하는 체이닝

map()은 변환 함수가 일반 값을 반환할 때 써요. 변환 함수 자체가 Option을 반환한다면 and_then()을 쓰세요.

fn parse_port(s: &str) -> Option<u16> {
    s.parse::<u16>().ok()
}

fn main() {
    let input: Option<&str> = Some("8080");
    let port = input.and_then(parse_port);
    println!("{:?}", port); // Some(8080)

    let bad: Option<&str> = Some("abc");
    let port = bad.and_then(parse_port);
    println!("{:?}", port); // None
}

map()을 썼다면 Option<Option<u16>>이 되어 버려요. and_then()은 이걸 평탄화해서 Option<u16>으로 만들어줘요.

실전 예제: 사용자 검색

여러 메서드를 조합하면 이렇게 깔끔해져요.

struct User {
    name: String,
    email: Option<String>,
}

fn find_user(users: &[User], name: &str) -> Option<&User> {
    users.iter().find(|u| u.name == name)
}

fn main() {
    let users = vec![
        User { name: "지수".into(), email: Some("jisu@example.com".into()) },
        User { name: "민수".into(), email: None },
    ];

    // 사용자를 찾고, 이메일이 있으면 출력
    let email = find_user(&users, "지수")
        .and_then(|u| u.email.as_deref())
        .unwrap_or("이메일 없음");

    println!("지수의 이메일: {email}");

    let email = find_user(&users, "영희")
        .and_then(|u| u.email.as_deref())
        .unwrap_or("이메일 없음");

    println!("영희의 이메일: {email}");
}

unwrap()은 왜 위험한가?

unwrap()None일 때 panic을 일으켜요. 모듈 16에서 Resultunwrap()이 위험하다고 배웠죠? Option도 마찬가지예요.

fn main() {
    let value: Option<i32> = None;
    // value.unwrap(); // panic: called `Option::unwrap()` on a `None` value
}

| 메서드 | None일 때 | 용도 | | --------------------- | ------------------- | ---------------------------------- | | unwrap() | panic! | 테스트 코드, 절대 None이 아닌 경우 | | unwrap_or(기본값) | 기본값 반환 | 대체값이 있을 때 | | unwrap_or_default() | 타입 기본값 | 0, "" 등으로 충분할 때 | | expect("메시지") | 메시지와 함께 panic | 디버깅용 |

익숙해지면 map(), and_then(), unwrap_or() 체이닝이 정말 편해져요. 처음엔 어색하지만, Rust 코드를 읽다 보면 자연스러워질 거예요!

연습 1 (쉬움). 아래 함수를 완성하세요. 벡터에서 첫 번째 짝수를 찾아 반환해요.

fn first_even(numbers: &[i32]) -> Option<i32> {
    // 여기를 완성하세요
    // 힌트: for 루프와 if로 짝수를 찾아 Some(n)을 반환하세요
    // 못 찾으면 None을 반환하세요
}

fn main() {
    let nums = vec![1, 3, 4, 7, 8];
    println!("{:?}", first_even(&nums)); // Some(4)

    let odds = vec![1, 3, 5];
    println!("{:?}", first_even(&odds)); // None
}

연습 2 (보통). 아래 코드에서 unwrap()을 제거하고 안전한 대안으로 바꿔보세요.

fn main() {
    let config = vec![
        ("host", "localhost"),
        ("port", "8080"),
    ];

    // unwrap()을 쓰지 않고 고쳐보세요!
    let host = config.iter()
        .find(|(k, _)| *k == "host")
        .unwrap()
        .1;

    let timeout = config.iter()
        .find(|(k, _)| *k == "timeout")
        .unwrap() // 여기서 panic!
        .1;

    println!("host: {host}, timeout: {timeout}");
}

힌트: map()unwrap_or()를 조합해보세요.

연습 3 (도전). and_then()을 사용해서 중첩된 Option을 처리하는 함수를 작성하세요.

fn get_area_code(phone: Option<&str>) -> Option<&str> {
    // phone이 Some이고, "-"를 포함하면 "-" 앞부분(지역번호)을 반환
    // 그 외에는 None 반환
    // 힌트: and_then()과 split_once()를 사용하세요
}

fn main() {
    println!("{:?}", get_area_code(Some("02-1234-5678"))); // Some("02")
    println!("{:?}", get_area_code(Some("01012345678")));   // None
    println!("{:?}", get_area_code(None));                  // None
}

Q1. Option<T>에 대한 설명으로 올바른 것은?

  • A) 값이 없으면 null을 반환한다
  • B) Some(T)과 None 두 가지로 값의 유무를 나타낸다
  • C) 에러가 발생하면 프로그램을 종료한다
  • D) Result<T, E>와 완전히 같다

Q2. 다음 코드의 출력은?

fn main() {
    let v = vec![10, 20, 30];
    let value = v.get(5).unwrap_or(&0);
    println!("{value}");
}
  • A) 30
  • B) None
  • C) 0
  • D) 프로그램이 panic으로 멈춘다

Q3. map()and_then()의 차이는?

  • A) 차이가 없다
  • B) map()은 일반 값을 반환하는 함수에, and_then()은 Option을 반환하는 함수에 사용한다
  • C) map()은 None에서만 동작한다
  • D) and_then()은 항상 panic을 일으킨다