Rust에서 Trait는 코드의 재사용성과 모듈성을 향상시킵니다. 다른 언어에서 인터페이스(interface)나 추상 클래스와 비슷한 역할을 하지만, Rust 고유의 설계 원칙과 소유권 개념에 맞춰 독특하게 동작합니다. 이번 글에서는 Rust의 Trait에 대해 자세히 알아보고, 주요 개념과 사용 사례를 소개합니다.
Trait란 무엇인가?
Trait는 객체의 행동(behavior)을 정의하는 메커니즘입니다. 특정 타입(예> 구조체, 열거형)이 반드시 구현해야 하는 공통 적인 동작을 선언합니다.
기본문법
trait TraitName { fn method_name(&self); }
예시
trait Greet { fn greet(&self); }
위의 예제에서 Greet라는 이름의 Trait를 정의했습니다. 이를 구현하는 모든 타입은 반드시 greet 메서드를 제공해야 합니다.
Trait의 주요 특징
인터페이스 역할
Trait는 타입 간의 공통 동작을 정의하는 데 사용합니다. 아래 예제를 보겠습니다.
trait Drawable { fn draw(&self); //공통 행동입니다. } struct Circle; struct Square; impl Drawable for Circle { //원에 대한 draw를 구현했습니다. fn draw(&self) { println!("drawing a circle"); } } impl Drawable for Square { fn draw(&self) { println!("drawing a square"); } fn main() { let circle = Circle; let square = Square; circle.draw(); square.draw(); }
디폴트 메서드
기본 메서드를 사용하거나, 새로 구현할 수 있습니다. 아래 예제를 보겠습니다.
trait Greet { fn greet(&self) { println!("default implementation"); } } struct Person; struct Robot; impl Greet for Person {} impl Greet for Robot { fn greet(&self) { println!("Hello from a Robot!"); } } fn main() { let person = Person; let robot = Robot; person.greet(); robot.greet(); }
제너릭 타입과 Trait 바운드
Trait는 제너릭 타입과 결합하여 동작을 제한하거나 특정 행동을 요구할 수 있습니다. 아래 예제를 보면, Summable trait를 구현해야 된다는 것을 강제합니다.
trait Summable { fn sum(&self) -> i32; } struct Numbers(Vec<i32>); impl Summable for Numbers { fn sum(&self) -> i32 { self.0.iter().sum() } } fn print_sum<T: Summable>(item: T) { println!("The sum is: {}", item.sum()); } fn main() { let nums = Numbers(vec![1, 2, 3, 4, 5]); print_sum(nums); }
dyn Trait와 동적 디스패치
런타임 메서드를 호출시, 동적으로 받을 수 있습니다. dyn = dynamic, 아래 예제 보시면 금방 이해가 될 겁니다. 위쪽의 인터페이스 역할에 나온 예제와 비교해보세요.
trait Drawable { fn draw(&self); } struct Circle; struct Square; impl Drawable for Circle { fn draw(&self) { println!("Drawing Circle"); } impl Drawable for Square { fn draw(&self) { println!("Drawing Square"); } } fn draw_shape(shape: &dyn Drawable) { shape.draw(); } fn main() { let circle = Circle; let square = Square; draw_shape(&circle); draw_shape(&square); }
Trait 고급 기능
연관 타입
제너릭 가독성을 위해 Associated Types 사용
trait Iterator { type Item; // 연관 타입 fn next(&mut self) -> Option<Self::Item>; } struct Counter { count: i32, } impl Iterator for Counter { type Item = i32; // 연관 타입 정의 fn next(&mut self) -> Option<Self::Item> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } } fn main() { let mut counter = Counter { count: 0 }; while let Some(value) = counter.next() { println!("{}", value); } }
아래는 연관 타입을 사용하지 않은 예제입니다.
trait Iterator<T> { fn next(&mut self) -> Option<T>; } struct Counter { count: i32, } impl Iterator<i32> for Counter { fn next(&mut self) -> Option<i32> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } } fn main() { let mut counter = Counter { count: 0 }; while let Some(value) = counter.next() { println!("{}", value); } }
Trait 상속
Trait간에 상속이 가능합니다. 이를 통해 계층 구조를 만들 수도 있습니다. 아래 예제를 보면, Mammal이 Animal을 상속받았습니다.
trait Animal { fn speak(&self); } trait Mammal: Animal { fn walk(&self); } struct Dog; impl Animal for Dog { fn speak(&self) { println!("Woof!"); } } impl Mammal for Dog { fn walk(&self) { println!("The dog is walking."); } } fn main() { let dog = Dog; dog.speak(); // Woof! dog.walk(); // The dog is walking. }
Trait 합성
하나의 매서드에서 여러 Trait를 사용할 수 있습니다.
trait Fly { fn fly(&self); } trait Swim { fn swim(&self); } struct Duck; impl Fly for Duck { fn fly(&self) { println!("Duck is flying"); } } impl Swim for Duck { fn swim(&self) { println!("Duck is swimming"); } } fn perform_actions(creature: &(impl Fly + Swim)) { creature.fly(); creature.swim(); } fn main() { let duck = Duck; perform_actions(&duck); }
자동 파생 Trait
특정 Trait에 대해 잦동 구현을 제공합니다. 수동으로 코드를 작성하지 않고, 편리하게 구조체나 열거형에 Trait를 부여할 수 있습니다. #[derive(…)] 키워드를 사용합니다.
주요 자동 파생 Trait는 아래와 같습니다.
- Debug: 디버깅 목적으로 출력할 때 사용
- Clone: 복사(shallow copy)를 수행할 때 사용
- PartialEq : 객체 간의 값 비교시 사용
- PartialOrd : 객체간 크기 비교시 사용
아래 예제로 확인해 보겠습니다. 소스코드 상단의 매크로가 정의되어야 하고, 그안에 사용할 Trait를 명시하면 됩니다.
#[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32, } fn main() { let p1 = Point { x: 10, y: 20 }; let p2 = p1.clone(); //Clone trait println!("{:?}", p1); //Debug trait if p1 == p2 { //PartialEq trait println!("p1 and p2 are equal"); } else { println!("p1 and p2 are not equal"); } println!("{:?}", p2); }
결론
Trait에 대해서 조금 감이 오셨나요? 인터페이스 정의를 넘어서 다양한 기능을 제공합니다. 아직 애매하다면, 위에 부터 차근 차근 다시 읽어 보세요. 다른 언어들을 사용해 보셨다면 그렇게 어렵지 않을 겁니다.