learn

struct vs enum — 차이, 장단점, 사용 가이드

개념의 갈림길

structenum
의미여러 구성요소를 동시에 가짐가능한 형태 중 정확히 하나
type theoryproduct typesum type
자연어 단서"...와 ...와 ...""...이거나 ...이거나 ..."
struct User { name: String, age: u32, email: String }
//             ^^^ name AND age AND email

enum NetworkState { Connected(String), Connecting, Disconnected }
//                  ^^^ 셋 중 정확히 하나

도메인을 자연어로 설명했을 때 어느 패턴인지를 들으면 선택이 명확해진다.

메모리 레이아웃 비교

struct — 필드 크기의 합 + alignment padding.

struct Point { x: i32, y: i32, z: i32 }

┌────────┬────────┬────────┐
│   x    │   y    │   z    │   12 bytes
└────────┴────────┴────────┘

모든 인스턴스에 모든 필드가 메모리를 차지한다. C struct와 동일한 모델.

enummax(variants) + tag(discriminant).

enum Shape {
    Circle(f64),               // 8 bytes
    Rectangle(f64, f64),       // 16 bytes ← 가장 큼
    Point,                     // 0 bytes
}

┌──────┬──────────────────────────────┐
│ tag  │  data (16 bytes로 padded)    │   ~24 bytes
└──────┴──────────────────────────────┘
   ↑              ↑
 어떤 variant인지   가장 큰 variant 크기로 자리잡음

C에서 union + 별도 tag 필드로 만들던 tagged union과 같은 구조. Rust enum은 컴파일러가 안전하게 감싸준 tagged union이라고 봐도 정확하다.

메모리 함정과 최적화

variant 크기 편차 문제 — 한 variant가 크면 작은 variant들도 그 크기로 부풀려진다. 작은 variant 인스턴스를 100만 개 만들어도 각각 큰 variant 크기를 차지. 해결: 큰 variant를 Box<T>로 heap에 빼서 enum은 포인터만 갖게 한다.

enum Shape {
    Circle(f64),
    BigPolygon(Box<[f64; 1000]>),
}

niche optimization — 컴파일러가 "유효하지 않은 비트 패턴"을 활용해 tag를 별도로 두지 않는 최적화. 대표 예: Option<&T>&T와 같은 크기다. reference가 0이 될 수 없다는 사실을 활용해 None을 0 비트 패턴으로 표현. Java Optional이 결코 못 하는 최적화.

결정 가이드

상황선택
여러 데이터가 항상 함께 존재struct
가능한 형태가 유한하고 분리enum
상태 머신 (idle/loading/error)enum
"있을 수도, 없을 수도"Option<T> (struct 필드로)
성공/실패 결과Result<T, E> (enum)
도메인 객체 (User, Product)struct
메시지/이벤트/명령의 종류enum

둘은 거의 항상 함께 쓴다

실제 도메인 모델링은 struct과 enum의 중첩으로 표현된다.

struct User {
    id: u64,
    name: String,
    role: UserRole,
    contact: ContactInfo,
}

enum UserRole {
    Admin,
    Member { permissions: Vec<String> },
    Guest { expires_at: u64 },
}

enum ContactInfo {
    Email(String),
    Phone(String),
    Both { email: String, phone: String },
}

User는 struct(여러 필드 동시), UserRole은 enum(셋 중 하나), UserRole::Member는 struct스러운 내부 필드. ADT의 본질적 활용은 sum과 product의 자유로운 중첩이다.

장단점

struct

장점

  • 직관적·단순. 필드 접근(.field) 가볍고 명료.
  • 메모리 레이아웃 예측 가능. FFI에 그대로 노출 가능.
  • 부분 업데이트(u.email = ...) 자연스러움.

단점

  • 모든 필드를 항상 가져야 함. "있을 수도 없을 수도"는 Option<T> 필드로 별도 처리.
  • 필드 추가 시 모든 인스턴스 메모리가 늘어남.
  • 한 객체가 여러 상태를 가지면 어색 — 상태 머신은 enum이 자연스럽다.

enum

장점

  • exhaustiveness 검사match에서 variant 빠뜨리면 컴파일 에러. variant 추가 시 처리 안 한 곳을 컴파일러가 모두 잡아준다. OOP 상속 계층 대비 enum의 가장 큰 실질적 이점.
  • 가능한 상태를 type에 표현. 잘못된 조합이 애초에 표현 불가능. ("로딩 중인데 데이터가 있다" 같은 모순 상태를 type으로 막을 수 있다.)
  • 메모리 효율 — 다양한 형태를 한 type으로 표현하면서 인스턴스는 하나만큼만 차지.

단점

  • 매번 variant 분기 처리 필요 — 단순 데이터엔 오버헤드.
  • 가장 큰 variant 크기에 맞춰지므로 variant 편차 크면 메모리 낭비. → Box<T>로 우회.
  • variant 추가 시 모든 match를 수정해야 함 — 장점이자 단점. 닫힌 시스템엔 좋지만, 외부 확장에 열린 시스템엔 trait가 더 적합.

기억할 패턴

상태 머신

enum HttpResponse {
    Pending,
    Loading { progress: f32 },
    Success(Body),
    Failed { code: u16, message: String },
}

OOP 상속 계층의 환원

// Java: abstract Shape + Circle/Square/Triangle 자식
enum Shape {
    Circle { radius: f64 },
    Square { side: f64 },
    Triangle { base: f64, height: f64 },
}

옵셔널 필드

struct Profile {
    name: String,
    bio: Option<String>,
    avatar_url: Option<String>,
}

성공/실패 — 표준 라이브러리 enum.

enum Result<T, E> { Ok(T), Err(E) }

결론

struct는 "모든 구성요소를 동시에 가지는 묶음", enum은 "가능한 형태 중 정확히 하나". 메모리상 struct는 필드 합, enum은 가장 큰 variant + tag. 도메인을 자연어로 풀어 어느 쪽인지 판단하는 휴리스틱이 가장 빠르고, 실무에선 거의 항상 둘을 중첩해서 쓴다 — struct 안에 enum 필드, enum variant 안에 struct스러운 필드.

관련: what-is-rust-enum, option-as-null-alternative