DaleSchool

여러 가능성을 하나로: 열거형

중급20분

학습 목표

  • enum으로 사용자 정의 타입을 만들 수 있다
  • 열거형의 variant에 데이터를 포함할 수 있다
  • match로 열거형의 모든 variant를 처리할 수 있다
  • if let으로 특정 variant만 처리할 수 있다

동작하는 코드

신호등을 생각해보세요. 빨강, 노랑, 초록 중 하나의 상태만 가능하죠? 이런 "여러 가능성 중 하나"를 표현하는 게 열거형(enum) 이에요.

#[derive(Debug)]
enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn action(light: &TrafficLight) {
    match light {
        TrafficLight::Red => println!("정지!"),
        TrafficLight::Yellow => println!("주의!"),
        TrafficLight::Green => println!("출발!"),
    }
}

fn main() {
    let light = TrafficLight::Red;
    action(&light);
    println!("현재 신호: {:?}", light);
}

match는 값을 보고 어떤 variant인지에 따라 다른 동작을 실행해요. 분류 기계라고 생각하면 돼요 — 값을 넣으면 모양에 따라 다른 칸으로 보내는 기계예요.

직접 수정하기

  1. lightTrafficLight::Green으로 바꿔보세요. 출력이 달라지나요?
  2. match에서 TrafficLight::Yellow 줄을 삭제해보세요. 어떤 에러가 나나요?

"왜?" — 데이터를 가진 열거형

열거형의 진짜 힘은 각 variant에 데이터를 포함할 수 있다는 거예요.

#[derive(Debug)]
enum Shape {
    Circle(f64),              // 반지름
    Rectangle(f64, f64),      // 가로, 세로
    Triangle(f64, f64, f64),  // 세 변의 길이
}

fn describe(shape: &Shape) {
    match shape {
        Shape::Circle(r) => {
            println!("원: 반지름 {r}");
        }
        Shape::Rectangle(w, h) => {
            println!("직사각형: {w} x {h}");
        }
        Shape::Triangle(a, b, c) => {
            println!("삼각형: 변 {a}, {b}, {c}");
        }
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle(5.0),
        Shape::Rectangle(10.0, 3.0),
        Shape::Triangle(3.0, 4.0, 5.0),
    ];

    for s in &shapes {
        describe(s);
    }
}

Shape 하나의 타입으로 원, 직사각형, 삼각형을 모두 담을 수 있어요. 구조체만으로는 이런 표현이 어려워요.

match는 빠짐없이 처리해야 해요

match의 가장 중요한 특징은 모든 가능성을 빠짐없이(exhaustive) 처리해야 한다는 거예요.

error[E0004]: non-exhaustive patterns: `Shape::Triangle(_, _, _)`
              not covered

해석: "삼각형 경우를 처리하지 않았어요. match는 모든 가능성을 다뤄야 해요."

해결: 빠진 variant를 추가하거나, _(와일드카드)로 나머지를 처리하세요.

모든 경우를 일일이 처리하기 번거로울 때는 _로 나머지를 묶을 수 있어요.

fn is_circle(shape: &Shape) -> bool {
    match shape {
        Shape::Circle(_) => true,
        _ => false,  // 나머지는 전부 false
    }
}

if let — 하나만 확인할 때

하나의 variant만 확인하고 나머지는 무시하고 싶을 때는 if let이 깔끔해요.

fn main() {
    let shape = Shape::Circle(5.0);

    // match로 쓰면
    match &shape {
        Shape::Circle(r) => println!("반지름: {r}"),
        _ => {}
    }

    // if let으로 쓰면 — 훨씬 깔끔!
    if let Shape::Circle(r) = &shape {
        println!("반지름: {r}");
    }
}

열거형에도 메서드를 추가할 수 있어요

구조체처럼 impl 블록으로 메서드를 넣을 수 있어요.

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle(w, h) => w * h,
            Shape::Triangle(a, b, c) => {
                // 헤론의 공식
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }
}
더 알아보기: Option — Rust에는 null이 없어요

다른 언어에는 null이나 None이 있어서 "값이 없음"을 표현해요. 하지만 null을 잘못 다루면 프로그램이 터지죠.

Rust에는 null이 없어요! 대신 Option 이라는 열거형을 써요.

enum Option<T> {
    Some(T),   // 값이 있음
    None,      // 값이 없음
}

모듈 06에서 pop()의 결과가 Some(88)이었던 거 기억나세요? 그게 바로 Option이었어요!

fn main() {
    let numbers = vec![1, 2, 3];
    let first = numbers.first();  // Option<&i32>

    match first {
        Some(n) => println!("첫 번째: {n}"),
        None => println!("비어있어요"),
    }
}

Option에 대해서는 나중에 더 자세히 배울 거예요. 지금은 "Rust는 값이 없을 수도 있는 상황을 열거형으로 안전하게 표현한다"고 알아두세요.

연습 1. Coin 열거형을 만들고, 각 동전의 금액을 반환하는 value 메서드를 작성해보세요.

#[derive(Debug)]
enum Coin {
    Ten,        // 10원
    Fifty,      // 50원
    Hundred,    // 100원
    FiveHundred,// 500원
}

impl Coin {
    fn value(&self) -> i32 {
        // 여기를 완성하세요
        // match를 사용해서 각 variant에 맞는 금액을 반환
    }
}

fn main() {
    let coins = vec![
        Coin::Hundred,
        Coin::FiveHundred,
        Coin::Fifty,
    ];

    let total: i32 = coins.iter().map(|c| c.value()).sum();
    println!("총액: {total}원");
    // 출력: 총액: 650원
}

연습 2. 아래 Message 열거형의 process 함수를 완성해보세요.

#[derive(Debug)]
enum Message {
    Quit,
    Echo(String),
    Move { x: i32, y: i32 },
}

fn process(msg: &Message) {
    // 여기를 완성하세요
    // Quit -> "종료합니다" 출력
    // Echo(text) -> text 내용을 출력
    // Move { x, y } -> "({x}, {y})로 이동" 출력
}

fn main() {
    let messages = vec![
        Message::Echo(String::from("안녕!")),
        Message::Move { x: 10, y: 20 },
        Message::Quit,
    ];

    for msg in &messages {
        process(msg);
    }
}

힌트: match에서 구조체 variant는 Message::Move { x, y }처럼 분해해요.

Q1. match에서 모든 variant를 처리하지 않으면?

  • A) 런타임 에러가 발생한다
  • B) 컴파일 에러가 발생한다
  • C) 처리되지 않은 variant는 무시된다
  • D) 프로그램이 무한 루프에 빠진다

Q2. if let은 언제 사용하면 좋을까요?

  • A) 모든 variant를 처리해야 할 때
  • B) 하나의 variant만 확인하고 나머지는 무시할 때
  • C) 열거형을 생성할 때
  • D) 열거형에 메서드를 추가할 때

Q3. 아래 코드의 출력은?

enum Direction {
    Up, Down, Left, Right,
}

fn main() {
    let dir = Direction::Up;
    let msg = match dir {
        Direction::Up => "위",
        Direction::Down => "아래",
        _ => "옆",
    };
    println!("{msg}");
}
  • A) "위"
  • B) "아래"
  • C) "옆"
  • D) 컴파일 에러