14. 트레잇 (정의·구현·기본 메서드·trait object)
트레잇(trait) 은 "이 타입이 이런 동작을 한다" 는 약속입니다. Java/Kotlin 의 interface, Haskell 의 typeclass 와 비슷한 역할. 기본 메서드 구현, 트레잇 바운드, dyn Trait 의 동적 디스패치까지 한 번에 짚습니다. 표준 라이브러리의 Display·Debug·Iterator 같은 트레잇이 어떻게 다양한 메서드 체이닝을 가능하게 하는지도 살펴봅니다.
이 강의에서 배우는 것
- 1trait 을 정의하고 여러 타입에 impl 한다
- 2기본 메서드 구현으로 공통 동작을 한 곳에 둔다
- 3트레잇 바운드로 제네릭 함수의 동작을 제한한다
- 4dyn Trait vs impl Trait 의 디스패치 차이를 안다
- 5표준 라이브러리의 Debug / Display / Clone / Default 등을 derive 또는 직접 구현한다
소개
Java 의 interface 가 익숙하다면 trait 는 그것의 강화판입니다 — interface 가 약속만 하고 끝나는 것과 달리, trait 은 기본 구현·연관 타입·연관 상수까지 묶을 수 있어 표준 라이브러리의 풍부한 메서드 체이닝(Iterator 등) 이 가능합니다.
핵심 개념
1) trait 정의와 구현
trait Greet {
fn hello(&self) -> String;
// 기본 메서드 — 호출하는 타입에서 override 안 해도 됨
fn shout(&self) -> String {
format!("{}!!!", self.hello())
}
}
struct Korean;
impl Greet for Korean {
fn hello(&self) -> String { "안녕".into() }
}2) 트레잇 바운드 — 제네릭 함수 제한
fn print_hello<T: Greet>(t: &T) {
println!("{}", t.hello());
}
// 같은 의미의 where 절
fn print_hello2<T>(t: &T) where T: Greet { /* ... */ }3) impl Trait — 익명 반환 타입
함수 반환 타입에 `impl Trait` 을 쓰면 "이 트레잇을 구현하는 어떤 타입" 을 반환한다는 의미. 구체 타입은 컴파일러가 추론.
4) dyn Trait — 동적 디스패치
| 관점 | impl Trait / 제네릭 | dyn Trait |
|---|---|---|
| 디스패치 | 정적 (compile-time) | 동적 (runtime vtable) |
| 성능 | 빠름 (인라인 가능) | 함수 호출 비용 약간 |
| 다양한 타입 컬렉션 | ✗ (모든 원소 같은 타입) | ✓ (Vec<Box<dyn Trait>>) |
| 바이너리 크기 | 단형화로 커질 수 있음 | 더 작음 |
5) 흔히 쓰는 표준 트레잇
- **Debug** — `{:?}` 포매팅, `#[derive(Debug)]` 으로 자동 생성
- **Display** — `{}` 포매팅, 직접 구현
- **Clone / Copy** — 복제 / 비트 복사
- **Default** — `Type::default()` 생성
- **PartialEq / Eq** — `==` 비교
- **Iterator** — for 루프와 .map/.filter 등의 체이닝 가능하게 함
핵심 예제
trait 정의 + 여러 타입에 구현:
trait Area {
fn area(&self) -> f64;
fn report(&self) {
println!("area = {:.2}", self.area());
}
}
struct Circle { r: f64 }
struct Square { s: f64 }
impl Area for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
}
impl Area for Square {
fn area(&self) -> f64 { self.s * self.s }
}
fn main() {
Circle { r: 2.0 }.report(); // area = 12.57
Square { s: 3.0 }.report(); // area = 9.00
}dyn Trait 로 다형성 컬렉션:
fn main() {
let shapes: Vec<Box<dyn Area>> = vec![
Box::new(Circle { r: 1.0 }),
Box::new(Square { s: 2.0 }),
];
for s in &shapes { s.report(); }
}derive 매크로로 표준 트레잇 자동 구현:
#[derive(Debug, Clone, PartialEq, Default)]
struct Point { x: f64, y: f64 }
fn main() {
let p1 = Point::default(); // (0.0, 0.0)
let p2 = p1.clone();
println!("{:?} == {:?} ? {}", p1, p2, p1 == p2);
}자주 하는 실수
Q. impl Trait 와 dyn Trait 중 뭘 써야 하나요?
A. **다양한 구체 타입을 한 컬렉션에 담아야 하면 dyn**, 그 외엔 정적 디스패치(impl Trait 또는 제네릭) 가 빠르고 인라인이 됩니다. 라이브러리 API 의 반환 타입을 깔끔하게 숨길 때도 impl Trait 가 유용.
Q. orphan rule 이 뭔가요?
A. **타입 또는 trait 중 하나는 자신의 크레잇에서 정의된 것이어야** 외부 타입에 외부 trait 을 impl 할 수 있다는 규칙. "Vec<i32> 에 Display 를 직접 impl" 같은 건 금지 — 새 wrapper 타입을 만들어야 합니다.
Q. Display 를 derive 할 수 있나요?
A. 표준은 못 합니다 (포맷이 너무 자유로워서). `impl fmt::Display for X` 를 직접 작성하거나 외부 크레잇(예: derive_more) 사용.
정리
- trait 은 동작 약속 + 기본 구현까지 포함
- 트레잇 바운드로 제네릭의 동작을 컴파일 타임에 제한
- impl Trait = 정적 디스패치, dyn Trait = 동적 디스패치
- Debug / Clone / Default / PartialEq 등 자주 쓰는 트레잇은 derive 로 자동 생성
과제
- trait Summary { fn summarize(&self) -> String } 를 정의하고 Article / Tweet 두 타입에 구현
- 기본 메서드를 가진 trait Animal { fn name(); fn greet() { ... } } 작성 후 사용
- Box<dyn Animal> Vec 로 여러 종류 동물을 담아 .greet() 호출하기