트레이트와 제네릭: 인터페이스를 넘어선 다형성
학습 목표: Rust의 핵심 추상화 도구인 **트레이트(Traits)**를 C#의 인터페이스와 비교하며 배웁니다. 제네릭을 통한 정적 디스패치와 트레이트 객체를 통한 동적 디스패치의 차이를 이해하고, 상황에 맞는 설계를 할 수 있는 능력을 기릅니다.
1. 트레이트(Trait) vs 인터페이스(Interface)
트레이트는 C#의 인터페이스와 유사하지만, 기존 타입에 나중에 기능을 덧붙일 수 있다는 점에서 더 강력합니다.
| 비교 항목 | C# 인터페이스 | Rust 트레이트 (Trait) |
|---|---|---|
| 정의 시점 | 클래스 선언 시 함께 정의 | 외부 타입에도 구현 가능 (고아 규칙 준수 시) |
| 기본 구현 | C# 8.0부터 지원 | 처음부터 강력하게 지원 |
| 상속 | 인터페이스 간 상속 가능 | 트레이트 바운드(T: A + B)로 표현 |
| 데이터 포함 | 프로퍼티 정의 가능 | 메서드만 정의 가능 (데이터는 구조체에) |
#![allow(unused)] fn main() { // [트레이트 정의 및 구현 예시] trait Summary { fn summarize(&self) -> String; // 기본 구현 제공 fn read_more(&self) -> String { format!("(자세히 보기...)") } } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}: {}", self.author, self.headline) } } }
2. 정적 디스패치 vs 동적 디스패치
Rust는 성능을 위해 컴파일 타임에 모든 타입을 결정하는 방식을 선호하지만, 필요할 때는 런타임 다형성도 지원합니다.
- 정적 디스패치 (
impl Trait/ 제네릭): 컴파일러가 각 타입에 맞는 코드를 따로 생성합니다 (단형성화). 성능이 가장 빠릅니다. - 동적 디스패치 (
dyn Trait): 런타임에 vtable을 통해 메서드를 호출합니다. C#의 일반적인 인터페이스 호출 방식과 유사하며, 서로 다른 타입을 하나의 리스트에 담을 때 유용합니다.
#![allow(unused)] fn main() { // 정적: "Animal을 구현한 어떤 한 가지 타입 T" fn make_it_sound<T: Animal>(item: &T) { ... } // 동적: "Animal을 구현한 여러 타입이 섞인 무언가" fn make_all_sound(items: &[Box<dyn Animal>]) { ... } }
3. 주요 표준 트레이트
Debug/Display: 개발자용/사용자용 출력 포맷 제어Clone/Copy: 데이터 복제 방식 결정PartialEq/PartialOrd: 비교 및 정렬 기능Default: 기본값 생성 (C#의new()제약 조건과 유사)Iterator: 반복자 패턴 구현 (LINQ의 기반)
4. 고아 규칙 (Orphan Rule)
Rust의 엄격한 규칙 중 하나로, **"내가 만든 타입에 남의 트레이트를 구현하거나, 남의 타입에 내가 만든 트레이트를 구현할 수는 있지만, 남의 타입에 남의 트레이트를 구현할 수는 없다"**는 규칙입니다. 이는 라이브러리 간의 충돌을 방지합니다.
💡 실무 팁: derive 매크로 적극 활용하기
대부분의 공통 기능(Debug, Clone, Default 등)은 직접 구현할 필요 없이 #[derive(Debug, Clone)] 처럼 한 줄만 추가하면 컴파일러가 알아서 구현해 줍니다. 코드가 훨씬 깔끔해지고 실수를 줄일 수 있습니다.