개념의 갈림길
| struct | enum | |
|---|---|---|
| 의미 | 여러 구성요소를 동시에 가짐 | 가능한 형태 중 정확히 하나 |
| type theory | product type | sum 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와 동일한 모델.
enum — max(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스러운 필드.