열거형 (Enums)

Rust는 복잡한 데이터와 다양한 상태를 안전하게 표현할 수 있는 강력한 도구인 열거형(enum)을 제공합니다.
enum은 단순한 값 목록이 아닌, 각 변형(variant)에 고유한 데이터를 담을 수 있어 패턴 매칭(match)과 결합해 매우 유용하게 사용됩니다.


1. 기본 열거형의 정의와 사용

열거형은 여러 개의 이름 있는 변형을 정의하는 타입입니다.
enum 다음에 이름을 입력하고 중괄호 안에 variant(변형)를 입력합니다.

enum Direction {
    North,
    South,
    East,
    West,
}

아래와 같이 함수에 열거형을 사용할 때는 함수명을 적고 인수를 입력하는데, 인수의 형식은 열거형이 됩니다. 그리고, match 흐름 제어 연산자를 사용하는데, match 다음에 인수명을 기재하고, 분기(arm)를 정의하는데 열거형의 이름 다음에 ::을 추가하며, =>을 사용해 실행 코드를 지정합니다.

fn move_to(dir: Direction) {
    match dir {
        Direction::North => println!("북쪽으로 이동"),
        Direction::South => println!("남쪽으로 이동"),
        Direction::East  => println!("동쪽으로 이동"),
        Direction::West  => println!("서쪽으로 이동"),
    }
}

main 함수는 아래와 같이 let을 이용해 direction 변수에 열거형 이름::변형을 입력하고, 위 함수 move_to의 인수로 direction 변수를 입력하면 “북쪽으로 이동”이란 글자가 화면에 표시됩니다.

fn main () {
    let direction = Direction::North;
    move_to(direction); 
}

North를 달리하면 출력 결과가 달라지며, 변형에 없는 값, 아래에서는 North2를 입력하면 “Direction에서 (North2) variant가 발견되지 않는다”라는 에러 메시지가 표시되므로 정확한 입력을 보장할 수 있습니다.


2. 열거형 변형에 데이터 저장

열거형은 각 변형마다 다른 타입의 데이터를 가질 수 있습니다. 이 점이 Rust enum의 강력한 특징입니다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

각 변형은 정해진 구조의 데이터를 저장할 수 있습니다:

  • Quit은 데이터 없음
  • Move는 구조체 형태(중괄호 사용)
  • Write는 문자열
  • ChangeColor는 튜플 형태(소괄호 사용)
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("종료"),
        Message::Move { x, y } => println!("이동: ({}, {})", x, y),
        Message::Write(text) => println!("메시지: {}", text),
        Message::ChangeColor(r, g, b) => println!("색상 변경: {}, {}, {}", r, g, b),
    }
}

fn main() {
    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::Write(String::from("안녕하세요"));
    let msg4 = Message::ChangeColor(255, 0, 0);

    process(msg1);
    process(msg2);
    process(msg3);
    process(msg4);
}

위 코드를 실행하면 아래와 같이 분기에 따라 실행 코드가 화면에 출력됩니다.

종료
이동: (10, 20)
메시지: 안녕하세요
색상 변경: 255, 0, 0

3. match 표현식

enum과 함께 가장 강력하게 사용되는 문법이 match입니다. 모든 경우를 exhaustively(빠짐없이) 처리하도록 강제되어, 안전한 분기 로직을 작성할 수 있습니다.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String),
}

fn main() {
    
    let coin = Coin::Quarter("New York".to_string());

    match coin {
        Coin::Penny => println!("1원!"),
        Coin::Nickel => println!("5원!"),
        Coin::Dime => println!("10원!"),
        Coin::Quarter(state) => println!("25원 from {:?}", state),
    }
}

  • enum Coin이 네 가지가 있으므로 match 연산자는 네 가지 분기를 모두 작성해야 합니다. 위 코드에서 match 분기의 하나를 주석 처리하고 실행하면

“모든 경우를 소진하지 않았다(none-exaustive patterns)”고 하면서 ‘Coin:Dime” not covered라고 “Dime 코인을 커버하지 않았다”고 합니다.

  • 모든 경우를 망라하기 어렵다면 _를 이용해 위를 제외한 다른 경우는 모두 이에 해당하는 실행 코드를 적용하도록 할 수 있습니다.
    match coin {
        Coin::Penny => println!("1원!"),
        Coin::Nickel => println!("5원!"),
        // Coin::Dime => println!("10원!"),
        // Coin::Quarter(state) => println!("25원 from {:?}", state),
        _ => println!("기타 동전!"),
    }

4. if let 표현식

단일 패턴만 확인하고 나머지는 무시하고 싶을 때는 if let 구문을 이용해 더 간결하게 처리할 수 있습니다.

    let coin = Coin::Penny;
    if let Coin::Penny = coin {
        println!("1원!");
    }

if 문의 내용이 “같다면”인데 let 문이므로 ==이 아니라 =를 사용했다는 것, 그리고 Coin::Penny가 앞에 왔다는 점을 주의해야 합니다. 위 코드를 실행하면 “1원!”가 화면에 출력됩니다.

if let coin = Coin::Penny 이라고 순서를 바꿔 표시하면 에러는 나지 않는데 coin 변수를 사용하지 않았으므로 _coin으로 변수명을 바꾸라는 제안을 합니다.

match보다 간단하지만, 나머지 경우는 무시되므로 사용에 주의가 필요합니다.


5. 열거형은 메서드도 가질 수 있다

열거형도 구조체처럼 impl 블록을 통해 메서드를 정의할 수 있습니다.

enum Status {
    Ready,
    Waiting,
    Error(i32),
}

impl Status {
    fn print(&self) {
        match self {
            Status::Ready => println!("준비 완료"),
            Status::Waiting => println!("대기 중"),
            Status::Error(code) => println!("에러 코드: {}", code),
        }
    }
}
fn main() {
    let status1 = Status::Ready;
    let status2 = Status::Waiting;
    let status3 = Status::Error(404);

    status1.print();
    status2.print();
    status3.print();
}

위 코드를 실행하면 아래와 같이 화면에 표시됩니다.

준비 완료
대기 중
에러 코드: 404

6. Option 열거형

Rust 표준 라이브러리에는 매우 자주 쓰이는 열거형인 Option이있습니다. 이는 값이 있을 수도 있고, 없을 수도 있다는 개념을 타입 시스템으로 안전하게 표현합니다.

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

예시:

let some_number = Some(5);
let no_number: Option<i32> = None;

이 방식은 null을 사용하지 않고도 안전하게 결측 값을 표현할 수 있게 해줍니다.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(n) => Some(n + 1),
        None => None,
    }
}

위 함수 plus_one은 인수 x가 정수(i32)라면 +1을 하고, None이라면 None을 반환하라는 의미인데, Option Enum이라 정수를 그냥 입력하면 안되고, Some 괄호 안에 입력해야 합니다.

fn main() {
    let num1 = Some(5);
    let num2 = None;

    println!("Result 1: {:?}", plus_one(num1)); // Result 1: Some(6)
    println!("Result 2: {:?}", plus_one(num2)); // Result 2: None
}

위와 같이 main 함수를 만들어 실행하면 숫자 5가 Some(5)라고 입력되면 Some(6)이 반환되고, None이 입력되면 None이 반환됩니다.

Rust는 기존의 사고 방식을 모두 바꿔버리니 적응하기 어렵습니다.


마무리

Rust의 열거형은 단순한 열거 상수를 넘어서 다양한 형태의 상태를 표현하는 강력한 수단입니다.특히 Option, Result, match와 결합하면 null, 에러, 상태 관리 등의 문제를 컴파일 타임에 안전하게 해결할 수 있습니다.

구조체 (Structs)

Rust에서 복잡한 데이터를 다루기 위해 사용하는 기본 단위가 바로 구조체(struct)입니다. 구조체는 여러 개의 관련된 데이터를 하나의 타입으로 묶어 표현할 수 있도록 해줍니다. Rust는 구조체와 메서드(impl 블록)를 통해 모듈화, 캡슐화, 데이터 모델링이 가능합니다.


1. 기본 구조체 정의 및 사용

가장 기본적인 구조체는 struct 다음에 구조체 이름을 쓰고, 중괄호 안에 필드의 이름과 타입을 :으로 연결해 선언합니다.

Rust의 구조체 이름 규칙은 대문자 카멜 케이스(Camel case)입니다. 예를 들어 User, MySruct와 같이 단어 시작을 대문자로 하고, _를 사용할 수 있으며 숫자로 시작할 수 없고, 공백이 포함되면 안됩니다.

struct User {
    username: String,
    email: String,
    active: bool,
}

사용할땐 struct의 인스턴스를 생성합니다.

일반 변수 선언할 때와 마찬가지로 let 키워드를 사용하고, 그 다음에 인스턴스 이름을 적고, = 구조체 이름을 적은 다음 중괄호안에 필드의 이름과 값을, key: value 쌍으로 아래와 같이 입력합니다. 구조체는 모든 필드의 타입이 명확해야 합니다.

fn main() {
    let user1 = User {
        username: String::from("alice"),
        email: String::from("alice@example.com"),
        active: true,
    };

    println!("username: {}", user1.username);
}
  • 문자열(String)은 “alice”와 같이 큰따옴표 안에 입력한다고 되는 것이 아니며, String::from(“alice”)라고 하거나, “alice”.to_string()으로 입력해야 합니다.
  • bool(논리값)도 true, false와 같이 모두 소문자로 표기합니다.
  • 필드 값을 끝까지 입력하고, 쉼표가 있어도 문제가 없습니다.
  • 구조체 인스턴스는 tuple과 마찬가지로 . 연산자(notation)로 접근할 수 있습니다.
  • Rust는 사용하는 기호도 여러가지가 사용돼서 복잡합니다.

지금까지 나온 것이
변수의 형식은 : 다음에 표시하고,
println다음에 !를 붙여야 하며,
match 패턴의 경우 => 을 사용해서 실행 코드를 지정하고, else를 _로 표시하며,
숫자 입력시 천단위 구분 기호로 _를 사용하고,
char를 입력할 때는 작은 따옴표, String을 입력할 때는 큰따옴표,
반환 값의 타입을 지정할 때는 ->,
loop label은 ‘로 시작하며,
참조를 표시할 때는 &를 사용하고,
튜플과 구조체의 값을 지정할 때는 .을 사용합니다.


2. 구조체는 소유권을 가진다

Rust에서 구조체는 일반 변수처럼 소유권을 가집니다. 즉, 구조체를 다른 변수로 이동시키면 원래 변수는 더 이상 사용할 수 없습니다.

let user2 = user1; // user1의 소유권이 user2로 이동
// println!("{}", user1.email); // 오류!

필드 하나만 이동하는 경우도 마찬가지입니다.

    let username = user1.username; // 소유권 이동 (user1.username에 대한 소유권은 종료됨)

    // user1.username은 더 이상 유효하지 않음, username 변수가 소유권을 갖게 됨

    println!("username: {}", username);

일부 필드를 참조로 처리하거나 클론(clone)을 사용해야 합니다.

let username = &user1.username;
또는
let username = user1.username.clone();

3. 기존 구조체 인스턴스로 새 구조체 인스턴스 생성하기

구조체 인스턴스를 만들 때 기존 구조체를 기반으로 일부 필드만 바꾸고 싶은 경우, 다음과 같이 .. 문법을 사용하여 나머지는 (user2와) 동일하다고 할 수 있습니다:

let user3 = User {
    email: String::from("bob@example.com"),
    ..user2
};

단, user2는 이후 더 이상 사용할 수 없습니다. 그 이유는 username, email과 active 필드의 소유권이 user3에게로 넘어갔기 때문입니다.

또한 ..user2라고 나머지 필드는 똑같다고 할 때 맨 뒤에 ,를 붙이면 안됩니다. 구조체 정의할 때는 ,로 끝나도 되는 것과 구분됩니다.


4. 튜플 구조체 (Tuple Struct)

필드의 이름이 없고 형식만 있는 구조체도 정의할 수 있습니다. 이를 튜플 구조체라고 하며, 단순한 데이터 묶음에 유용합니다. 구조체 이름 다음이 중괄호가 아니라 소괄호인 것도 다릅니다.

struct Color(i32, i32, i32);

fn main() {
    let red = Color(255, 0, 0);
    println!("Red: {}, {}, {}", red.0, red.1, red.2);
}


5. 유사 유닛 구조체 (Unit-like Struct)

필드가 없는 구조체도 정의할 수 있습니다. 이를 유닛 구조체라고 하며, 마치 빈 enum처럼 동작하거나 타입 태깅 등에 사용됩니다.

struct Marker;

fn main() {
    let _m = Marker;
}

이런 구조체는 메모리를 차지하지 않으며, 값 자체보다 타입에 의미를 둘 때 사용됩니다.


6. 구조체에 메서드 구현

Rust는 구조체에 메서드(method)를 추가할 수 있습니다. impl 블록을 통해 구조체에 동작을 부여할 수 있습니다.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

&self는 해당 메서드가 구조체 인스턴스를 참조로 빌려서 사용한다는 뜻입니다.

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("면적: {}", rect.area());
}

impl 블록 안에는 여러 메서드(함수)를 정의할 수 있으며, 정적 메서드(fn new, 생성자 역할)는 다음처럼 작성합니다:

impl Rectangle {
    fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }
}

위와 같이 생성자를 선언한 경우 아래와 같이 Rectangle::new 다음의 괄호 안에 필드 이름을 입력할 필요 없이 너비와 높이만을 입력해서 인스턴스를 만들 수 있으며 , 면적을 계산하는 것은 같습니다.

    let rect1 = Rectangle::new(10, 20);
    println!("rect1 면적: {}", rect1.area());

7. 디버깅을위한 #[derive(Debug)]

구조체를 println!으로 출력하려면 Debug 트레이트를 구현해야 합니다.

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 3, y: 7 };
    println!("{:?}", p);
}

위에서 {:?} 포맷은 Debug 형식 출력을 의미하며, 결과는 Point { x: 3, y: 7 }처럼 구조체의 필드 이름과 값을 포함한 형태로 출력됩니다.

그러나, 아래와 같이 #[derive(Debug)]를 주석 처리하고 실행하면 “Point가 {:?}를 사용해서 포맷될 수 없다”는 에러 메시지가 표시됩니다.


마무리

Rust의 구조체는 단순한 데이터 묶음을 넘어서, 로직과 상태를 함께 표현할 수 있는 강력한 도구입니다. 구조체를 메서드와 함께 사용하면 객체지향적 모델도 자연스럽게 구현할 수 있으며, 안전하고 구조화된 데이터 설계가 가능합니다.