Rust 트레이트에 깊이 알아보기: 상속, 구성 및 다형성
Takashi Yamamoto
Infrastructure Engineer · Leapcell

트레이트란 무엇인가?
러스트에서 트레이트(trait)는 공유된 동작을 정의하는 방법입니다. 이를 통해 특정 타입이 반드시 구현해야 하는 메서드를 지정하여 다형성 및 인터페이스 추상화를 가능하게 합니다.
다음은 Printable
이라는 트레이트를 정의하는 간단한 예제입니다. 이 트레이트에는 print
라는 메서드가 포함되어 있습니다.
trait Printable { fn print(&self); }
트레이트 정의 및 구현
트레이트를 정의하려면 trait
키워드를 사용하고 그 뒤에 트레이트 이름과 중괄호 쌍을 붙입니다. 중괄호 안에는 트레이트에 포함될 메서드를 정의합니다.
트레이트를 구현하려면 impl
키워드를 사용하고 그 뒤에 트레이트 이름, for
키워드, 그리고 트레이트를 구현할 타입을 지정합니다. 중괄호 안에는 트레이트에 정의된 모든 메서드에 대한 구현을 제공해야 합니다.
아래는 이전에 정의한 Printable
트레이트를 i32
타입에 대해 구현하는 방법을 보여주는 예제입니다.
impl Printable for i32 { fn print(&self) { println!("{}", self); } }
이 예제에서는 Printable
트레이트를 i32
타입에 대해 구현하고 print
메서드의 간단한 구현을 제공했습니다.
트레이트 상속 및 합성
러스트에서는 상속과 합성을 통해 기존 트레이트를 확장할 수 있습니다. 상속을 통해 부모 트레이트에 정의된 메서드를 새로운 트레이트 내에서 재사용할 수 있으며, 합성을 통해 여러 다양한 트레이트를 새로운 트레이트에서 사용할 수 있습니다.
다음은 상속을 사용하여 Printable
트레이트를 확장하는 방법을 보여주는 예제입니다.
trait PrintableWithLabel: Printable { fn print_with_label(&self, label: &str) { print!("{}: ", label); self.print(); } }
이 예제에서는 PrintableWithLabel
이라는 새로운 트레이트를 정의하고, 이는 Printable
트레이트에서 상속받습니다. 이는 PrintableWithLabel
을 구현하는 모든 타입이 Printable
도 구현해야 함을 의미합니다. 또한 값을 출력하기 전에 레이블을 출력하는 새로운 메서드인 print_with_label
을 제공합니다.
다음은 합성을 사용하여 새로운 트레이트를 정의하는 방법을 보여주는 또 다른 예제입니다.
trait DisplayAndDebug: Display + Debug {}
이 예제에서는 표준 라이브러리의 두 가지 트레이트인 Display
와 Debug
로 구성된 새로운 트레이트 DisplayAndDebug
를 정의합니다. 이는 DisplayAndDebug
를 구현하는 모든 타입이 Display
와 Debug
를 모두 구현해야 함을 의미합니다.
파라미터 및 반환 값으로서의 트레이트
러스트에서는 함수 시그니처에서 트레이트를 파라미터 및 반환 값으로 사용하여 코드를 더욱 일반적이고 유연하게 만들 수 있습니다.
다음은 PrintableWithLabel
트레이트를 함수 파라미터로 사용하는 방법을 보여주는 예제입니다.
fn print_twice<T: PrintableWithLabel>(value: T) { value.print_with_label("First"); value.print_with_label("Second"); }
이 예제에서는 제네릭 파라미터 T
를 받는 print_twice
라는 함수를 정의합니다. 파라미터는 PrintableWithLabel
트레이트를 구현해야 합니다. 함수 본문 안에서는 파라미터에 대해 print_with_label
메서드를 호출합니다.
다음은 트레이트를 함수 반환 값으로 사용하는 방법을 보여주는 예제입니다.
fn get_printable() -> impl Printable { 42 }
그러나 fn get_printable() -> impl Printable { 42 }
는 42
가 정수이고 Printable
트레이트를 구현하지 않기 때문에 올바르지 않습니다.
올바른 접근 방식은 Printable
트레이트를 구현하는 타입을 반환하는 것입니다. 예를 들어, i32
타입에 대해 Printable
을 구현하면 다음과 같이 작성할 수 있습니다.
impl Printable for i32 { fn print(&self) { println!("{}", self); } } fn get_printable() -> impl Printable { 42 }
이 예제에서는 Printable
트레이트를 i32
타입에 대해 구현하고 print
메서드의 간단한 구현을 제공합니다. 그런 다음 get_printable
함수에서 i32
값 42
를 반환합니다. i32
타입이 Printable
트레이트를 구현하므로 이 코드는 올바릅니다.
트레이트 객체와 정적 디스패치
러스트에서는 정적 디스패치와 동적 디스패치라는 두 가지 방법으로 다형성을 달성할 수 있습니다.
- 정적 디스패치는 제네릭을 사용하여 달성됩니다. 제네릭 파라미터를 사용하면 컴파일러는 가능한 각 타입에 대해 별도의 코드를 생성합니다. 이를 통해 함수 호출을 컴파일 시간에 결정할 수 있습니다.
- 동적 디스패치는 트레이트 객체를 사용하여 달성됩니다. 트레이트 객체를 사용하면 컴파일러는 트레이트를 구현하는 모든 타입을 처리할 수 있는 범용 코드를 생성합니다. 이를 통해 함수 호출을 런타임에 결정할 수 있습니다.
다음은 정적 디스패치와 동적 디스패치를 모두 사용하는 방법을 보여주는 예제입니다.
fn print_static<T: Printable>(value: T) { value.print(); } fn print_dynamic(value: &dyn Printable) { value.print(); }
이 예제에서:
print_static
은Printable
트레이트를 구현해야 하는 **제네릭 파라미터T
**를 사용합니다. 이 함수가 호출되면 컴파일러는 전달된 각 타입에 대해 별도의 코드를 생성합니다 (정적 디스패치).print_dynamic
은 파라미터로 **트레이트 객체 (&dyn Printable
)**를 사용합니다. 이를 통해 동적 디스패치가 가능해져 함수가Printable
트레이트를 구현하는 모든 타입을 처리할 수 있습니다.
연관 타입과 제네릭 제약 조건
러스트에서는 연관 타입과 제네릭 제약 조건을 사용하여 더욱 복잡한 트레이트를 정의할 수 있습니다.
연관 타입
연관 타입을 사용하면 특정 트레이트와 연관된 타입을 정의할 수 있습니다. 이는 연관 타입에 의존하는 메서드를 정의하는 데 유용합니다.
다음은 연관 타입을 사용하여 Add
라는 트레이트를 정의하는 예제입니다.
trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; }
이 예제에서:
Add
라는 트레이트를 정의합니다.add
메서드의 반환 타입을 나타내는 연관 타입Output
을 포함합니다.RHS
제네릭 파라미터는 덧셈 연산의 오른쪽 피연산자를 지정하며, 기본값은Self
입니다.
제네릭 제약 조건
제네릭 제약 조건을 사용하면 제네릭 파라미터가 특정 조건(예: 특정 트레이트 구현)을 만족해야 함을 지정할 수 있습니다.
다음은 SummableIterator
라는 트레이트에서 제네릭 제약 조건을 사용하는 방법을 보여주는 예제입니다.
use std::iter::Sum; trait SummableIterator: Iterator where Self::Item: Sum, { fn sum(self) -> Self::Item { self.fold(Self::Item::zero(), |acc, x| acc + x) } }
이 예제에서:
- 표준
Iterator
트레이트를 확장하는SummableIterator
트레이트를 정의합니다. - 제네릭 제약 조건 (
where Self::Item: Sum
)을 사용하여 반복자의Item
타입이Sum
트레이트를 구현해야 함을 지정합니다. sum
메서드는 반복자의 모든 요소의 총 합을 계산합니다.
예제: 트레이트를 사용하여 다형성 구현
다음은 PrintableWithLabel
트레이트를 사용하여 다형성을 구현하는 방법을 보여주는 예제입니다.
struct Circle { radius: f64, } impl Printable for Circle { fn print(&self) { println!("Circle with radius {}", self.radius); } } impl PrintableWithLabel for Circle {} struct Square { side: f64, } impl Printable for Square { fn print(&self) { println!("Square with side {}", self.side); } } impl PrintableWithLabel for Square {} fn main() { let shapes: Vec<Box<dyn PrintableWithLabel>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Square { side: 2.0 }), ]; for shape in shapes { shape.print_with_label("Shape"); } }
이 예제에서:
- 두 개의 구조체
Circle
과Square
를 정의합니다. - 두 구조체 모두
Printable
및PrintableWithLabel
트레이트를 구현합니다. main
함수에서 트레이트 객체 (Box<dyn PrintableWithLabel>
)를 저장하는 벡터shapes
를 생성합니다.shapes
벡터를 반복하고 각 도형에 대해print_with_label
을 호출합니다.
Circle
과 Square
모두 PrintableWithLabel
을 구현하므로 벡터에 트레이트 객체로 저장할 수 있습니다. print_with_label
을 호출하면 컴파일러는 객체의 실제 타입에 따라 호출할 메서드를 동적으로 결정합니다.
이것이 트레이트가 러스트에서 다형성을 가능하게 하는 방법입니다. 이 기사가 트레이트를 더 잘 이해하는 데 도움이 되기를 바랍니다.
Rust 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무제한 프로젝트를 무료로 배포
- 사용량에 따라 지불하세요. 요청이나 요금이 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예: $25로 평균 응답 시간이 60ms인 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없으므로 구축에만 집중하세요.
설명서에서 자세히 알아보세요!
X에서 저희를 팔로우하세요: @LeapcellHQ