트레이트 고급 활용

기본적인 트레이트 사용법을 넘어서, 실전에서 마주치는 고급 패턴들을 정리한다.

1. 연관 타입 (Associated Type)

제네릭과 비슷하지만, 트레이트 구현 시 타입이 하나로 고정된다.

trait Iterator {
    type Item;  // 연관 타입
    fn next(&mut self) -> Option<Self::Item>;
}
 
struct Counter {
    count: u32,
}
 
impl Iterator for Counter {
    type Item = u32;  // Counter의 Item은 u32로 고정
    
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        Some(self.count)
    }
}

제네릭과의 차이

// 제네릭: 하나의 타입에 여러 구현 가능
trait From<T> {
    fn from(value: T) -> Self;
}
 
impl From<i32> for String { /* ... */ }
impl From<bool> for String { /* ... */ }  // 둘 다 가능
 
// 연관 타입: 하나의 타입에 하나의 구현만
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
// Counter는 Item이 u32로 고정됨. i32로 또 구현 불가능.

연관 타입은 “이 타입의 Iterator는 뭘 뱉는지 유일하게 정해져 있다”를 표현할 때 쓴다.

2. 제네릭 트레이트 vs 연관 타입 선택 기준

상황선택
한 타입에 여러 구현이 필요하다제네릭 (trait Foo<T>)
한 타입에 하나의 구현만 있어야 한다연관 타입 (type Item)
호출 시 타입 명시가 필요하다제네릭
타입 추론이 자연스럽다연관 타입

3. 기본 타입 파라미터

제네릭에 기본값을 줄 수 있다.

trait Add<Rhs = Self> {  // 기본값: 자기 자신
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}
 
struct Point { x: i32, y: i32 }
 
// Rhs를 생략하면 Self(Point)가 됨
impl Add for Point {
    type Output = Point;
    fn add(self, other: Point) -> Point {
        Point { x: self.x + other.x, y: self.y + other.y }
    }
}

대부분의 경우 같은 타입끼리 더하니까 기본값으로 Self를 줘서 편하게 쓸 수 있다.

4. 완전 정규화 문법 (Fully Qualified Syntax)

같은 이름의 메서드가 여러 트레이트에 있을 때, 어떤 걸 호출할지 명시한다.

trait Pilot {
    fn fly(&self);
}
 
trait Wizard {
    fn fly(&self);
}
 
struct Human;
 
impl Pilot for Human {
    fn fly(&self) { println!("기장입니다"); }
}
 
impl Wizard for Human {
    fn fly(&self) { println!("레비오사~"); }
}
 
impl Human {
    fn fly(&self) { println!("팔을 흔듦"); }
}
 
fn main() {
    let person = Human;
    
    person.fly();              // "팔을 흔듦" (고유 메서드)
    Pilot::fly(&person);       // "기장입니다"
    Wizard::fly(&person);      // "레비오사~"
    
    // 완전 정규화 문법
    <Human as Pilot>::fly(&person);
    <Human as Wizard>::fly(&person);
}

연관 함수(self 없는 함수)에서는 완전 정규화가 필수다.

trait Animal {
    fn name() -> String;
}
 
struct Dog;
 
impl Animal for Dog {
    fn name() -> String { String::from("멍멍이") }
}
 
// Dog::name()은 모호할 수 있음
// 명확하게:
let name = <Dog as Animal>::name();

5. Blanket Implementation

“특정 트레이트를 구현한 모든 타입에 대해 자동으로 다른 트레이트도 구현해준다”

// 표준 라이브러리의 실제 예시
impl<T: Display> ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

Display를 구현한 모든 타입은 자동으로 ToString도 갖게 된다. 그래서 .to_string() 메서드를 바로 쓸 수 있는 것이다.

직접 만들어보면:

trait Describable {
    fn describe(&self) -> String;
}
 
// Debug를 구현한 모든 타입에 Describable 자동 구현
impl<T: Debug> Describable for T {
    fn describe(&self) -> String {
        format!("디버그 출력: {:?}", self)
    }
}

6. 트레이트 바운드에서의 라이프타임

트레이트 바운드와 라이프타임을 함께 쓸 때:

// 참조를 포함하지 않는 타입만 받음
fn spawn_thread<T: Send + 'static>(data: T) {
    std::thread::spawn(move || {
        // data 사용
    });
}
 
// 특정 라이프타임 동안 유효한 참조를 가진 타입
fn process<'a, T: Debug + 'a>(item: &'a T) {
    println!("{:?}", item);
}

'static은 “이 타입은 임시 참조를 포함하지 않는다”를 의미한다. 스레드로 데이터를 보낼 때 필수다.

7. 객체 안전성 (Object Safety)

모든 트레이트가 dyn Trait로 쓸 수 있는 건 아니다.

객체 안전하지 않은 경우

trait NotObjectSafe {
    fn returns_self() -> Self;           // Self 반환 불가
    fn generic_method<T>(&self, x: T);   // 제네릭 메서드 불가
}

dyn NotObjectSafe는 컴파일 에러난다. 런타임에 구체적인 타입 크기를 알 수 없기 때문이다.

객체 안전한 트레이트

trait ObjectSafe {
    fn do_something(&self);
    fn do_another(&self, x: i32) -> String;
}
 
// 이건 됨
let obj: Box<dyn ObjectSafe> = Box::new(MyType);

규칙: Self를 반환하거나, 제네릭 메서드가 있으면 객체 안전하지 않다.

정리

개념용도
연관 타입타입당 하나의 구현만 허용
기본 타입 파라미터제네릭 기본값 제공
완전 정규화 문법메서드 충돌 해결
Blanket Implementation조건부 자동 구현
객체 안전성dyn Trait 사용 가능 여부

이 정도 알면 러스트 트레이트의 대부분을 이해한 것이다. 다음 글부터는 Send, Sync 같은 마커 트레이트를 다룰 예정이다.