메커니즘은 struct와 동일
impl 블록을 enum 이름 위에 두면 method를 정의할 수 있다. &self/&mut self/self, automatic referencing, associated function의 :: 호출, Self alias — 모두 what-are-methods-and-associated-functions|struct method 메커니즘을 그대로 따른다.
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
fn unit_circle() -> Self {
Shape::Circle { radius: 1.0 }
}
}
유일하게 새로운 점 — method 본문에서 self가 어떤 variant인지 결정해야 한다는 것. struct는 항상 같은 모양이라 self.width 식으로 직접 접근할 수 있었지만, enum은 variant마다 들고 있는 데이터가 다르므로 match로 분기한다.
메모리와 dispatch — 정적
s.area()는 컴파일 타임에 Shape::area(&s)로 풀린다. &s는 enum 인스턴스(tag + data)의 메모리 주소이며, 함수 코드는 text segment에 한 번 박혀있고 호출 비용은 C 함수 호출과 같다. vtable을 거치지 않는 정적 dispatch다 — how-method-dispatch-works의 내용이 enum에도 그대로 적용된다.
method 본문의 match self는 어셈블리 레벨에서 tag(discriminant) 값을 읽어 jump table 또는 if-else로 분기하는 가벼운 연산. enum + match 조합이 OOP의 virtual method 호출보다 빠른 경우가 많은 이유.
OOP 상속과의 비교
같은 도메인을 Java로 풀면:
abstract class Shape {
abstract double area();
}
class Circle extends Shape {
double radius;
double area() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape { /* ... */ }
class Triangle extends Shape { /* ... */ }
각 자식 클래스가 자기 area를 오버라이드하고, JVM의 vtable을 통한 동적 dispatch로 호출이 풀린다.
Rust enum은 정반대 디자인 — 한 곳에 메서드를 정의하고 본문에서 variant 분기. variant마다 구현을 따로 두지 않고 한 메서드 안에서 match로 처리한다. 같은 도메인을 표현하면서 dispatch는 정적, 코드는 한 곳에 모인다.
Expression Problem — 두 디자인의 trade-off
컴퓨터 과학의 유명한 이슈. "데이터의 새 형태를 추가하기 쉬운가 vs 새 연산을 추가하기 쉬운가"의 두 축이 충돌한다.
| 추가 방향 | OOP 상속 | Rust enum |
|---|---|---|
| 새 데이터 형태 (자식 클래스 / variant) | 쉬움 | 어려움 |
| 새 연산 (메서드) | 어려움 | 쉬움 |
OOP 상속에선 Hexagon 자식을 추가하고 area를 오버라이드하면 끝 — 기존 코드는 건드리지 않는다. 반면 perimeter 메서드를 새로 추가하려면 모든 자식 클래스를 수정해야 한다.
Rust enum에선 impl 블록에 fn perimeter를 추가하면 끝 — 기존 variant는 건드리지 않는다. 반면 Hexagon variant를 추가하면 모든 메서드의 match를 수정해야 한다. 대신 컴파일러가 처리 안 한 모든 곳을 잡아주므로 빠뜨릴 수가 없다 — OOP 대비 큰 안전성 이점.
도메인에 따른 선택
- variant가 자주 늘어나는 시스템 (플러그인이 새 도형을 추가하는 경우 등) → trait 기반이 적합
- 연산이 자주 늘어나는 닫힌 시스템 (컴파일러의 AST 처리, 상태 머신 등) → enum이 적합
Rust는 이걸 인지하고 둘 다 제공한다. 닫힌 sum type엔 enum, 열린 확장엔 trait. 도메인 특성에 맞춰 골라 쓴다. is-rust-object-oriented에서 본 "trait + enum이 OOP의 일부 역할을 한다"의 구체적 의미가 이 trade-off다.
결론
enum의 method는 struct와 메커니즘이 동일하고(impl 블록, self, 정적 dispatch), 차이는 본문에서 variant 분기를 match로 처리한다는 점뿐이다. OOP 상속으로 풀던 도메인이 Rust에선 한 enum + impl 블록으로 환원되며, trade-off는 Expression Problem — variant가 자주 늘면 trait/OOP, 연산이 자주 늘면 enum이 맞다.
관련: what-is-rust-enum, what-are-methods-and-associated-functions, how-method-dispatch-works