Traits and Trait Bounds in Rust: 종합설
Olivia Novak
Dev Intern · Leapcell

Rust에서 트레이트는 다른 프로그래밍 언어에서 흔히 "인터페이스"라고 불리는 것과 유사하지만 몇 가지 차이점이 있습니다. 트레이트는 특정 타입이 다른 타입과 공유할 수 있는 기능을 가지고 있음을 Rust 컴파일러에게 알려줍니다. 트레이트를 통해 추상적인 방식으로 공유 동작을 정의할 수 있습니다. 트레이트 바운드를 사용하여 제네릭 타입이 특정 동작을 구현해야 함을 지정할 수 있습니다.
간단히 말해서, 트레이트는 Rust에서 인터페이스와 같으며, 타입을 구현할 때 제공해야 하는 동작을 정의합니다. 트레이트는 여러 타입 간에 공유되는 동작을 제한할 수 있으며, 제네릭 프로그래밍에서 사용할 때 제네릭을 트레이트에 지정된 동작을 준수하는 타입으로 제한할 수 있습니다.
트레이트 정의
서로 다른 타입이 동일한 동작을 나타내는 경우 트레이트를 정의한 다음 해당 타입에 대해 구현할 수 있습니다. 트레이트를 정의한다는 것은 특정 목적을 달성하는 데 필요한 동작과 요구 사항을 설명하기 위해 메서드 집합을 함께 그룹화하는 것을 의미합니다.
트레이트는 일련의 메서드를 정의하는 인터페이스입니다.
pub trait Summary { // 트레이트 내부의 메서드는 선언만 필요합니다. fn summarize_author(&self) -> String; // 이 메서드에는 기본 구현이 있습니다. 다른 타입은 직접 구현할 필요가 없습니다. fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } }
- 이는
summarize_author
와summarize
라는 두 개의 메서드를 제공하는Summary
라는 트레이트를 정의합니다. - 트레이트의 메서드는 선언만 필요하며, 구현은 특정 타입에 맡겨집니다. 그러나 메서드는 기본 구현을 가질 수도 있습니다. 이 경우
summarize
메서드에는 기본 구현이 있으며, 내부적으로 기본 구현이 없는summarize_author
를 호출합니다. Summary
트레이트의 두 메서드는 구조체의 메서드와 마찬가지로self
를 매개변수로 사용합니다. 여기서self
는 트레이트 메서드의 첫 번째 인수입니다.
참고: 실제로
self
는self: Self
의 줄임말이고,&self
는self: &Self
의 줄임말이고,&mut self
는self: &mut Self
의 줄임말입니다.Self
는 트레이트를 구현하는 타입을 나타냅니다. 예를 들어,Foo
타입이Summary
트레이트를 구현하는 경우 구현 내에서Self
는Foo
를 나타냅니다.
pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("@{0} posted a tweet...", self.summarize_author()) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } pub struct Post { pub title: String, pub author: String, pub content: String, } impl Summary for Post { fn summarize_author(&self) -> String { format!("{} posted an article", self.author) } fn summarize(&self) -> String { format!("{} posted: {}", self.author, self.content) } } impl Summary for Tweet { fn summarize_author(&self) -> String { format!("{} tweeted", self.username) } fn summarize(&self) -> String { format!("@{0} tweeted: {}", self.username, self.content) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; println!("{}", tweet.summarize()) }
트레이트와 해당 구현이 정의될 수 있는 위치에 관한 중요한 원칙이 있는데, 이를 고아 규칙(orphan rule)이라고 합니다. 타입 A
에 대해 트레이트 T
를 구현하려면 T
또는 A
가 현재 크레이트에서 정의되어야 합니다.
이 규칙은 다른 사람이 작성한 코드가 사용자의 코드를 손상시키지 않도록 하고, 마찬가지로 사용자의 코드가 의도치 않게 다른 사람의 코드를 손상시키지 않도록 합니다.
함수 매개변수로 트레이트 사용
트레이트는 함수 매개변수로 사용할 수 있습니다. 다음은 트레이트를 매개변수로 사용하는 함수를 정의하는 예입니다.
pub fn notify(item: &impl Summary) { // trait parameter println!("Breaking news! {}", item.summarize()); }
item
매개변수는 "Summary
트레이트를 구현하는 값"을 의미합니다. Summary
트레이트를 구현하는 모든 타입을 이 함수의 인수로 사용할 수 있습니다. 함수 본문 내에서 트레이트에 정의된 메서드를 매개변수에서 호출할 수도 있습니다.
트레이트 바운드
위에서 impl Trait
를 사용하는 것은 실제로 구문 설탕입니다. 완전한 구문은 T: Summary
와 같습니다. 이를 _트레이트 바운드_라고 합니다.
pub fn notify<T: Summary>(item: &T) { // trait bound println!("Breaking news! {}", item.summarize()); }
더 복잡한 사용 사례의 경우 트레이트 바운드는 더 큰 유연성과 표현력을 제공합니다. 예를 들어, 두 개의 매개변수를 사용하는 함수는 모두 Summary
트레이트를 구현합니다.
pub fn notify(item1: &impl Summary, item2: &impl Summary) {} pub fn notify<T: Summary>(item1: &T, item2: &T) {}
+
를 사용하여 여러 트레이트 바운드 지정
단일 제약 조건 외에도 매개변수가 여러 트레이트를 구현하도록 요구하는 것과 같은 여러 제약 조건을 지정할 수 있습니다.
pub fn notify(item: &(impl Summary + Display)) {} pub fn notify<T: Summary + Display>(item: &T) {}
where
를 사용하여 트레이트 바운드 단순화
트레이트 제약 조건이 많은 경우 함수 서명을 읽기 어려울 수 있습니다. 이러한 경우 where
절을 사용하여 구문을 정리할 수 있습니다.
// 여러 제네릭 타입에 트레이트 바운드가 많은 경우 서명을 읽기 어려울 수 있습니다. fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ... } // `where`를 사용하여 단순화하면 함수 이름, 매개변수 및 반환 유형이 더 가까워집니다. fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug { ... }
트레이트 바운드를 사용하여 메서드 또는 트레이트 조건부 구현
트레이트 바운드를 매개변수로 사용하면 특정 타입과 트레이트를 기반으로 메서드를 조건부로 구현할 수 있으며, 함수가 다양한 타입의 인수를 허용할 수 있습니다. 예:
fn notify(summary: impl Summary) { println!("notify: {}", summary.summarize()) } fn notify_all(summaries: Vec<impl Summary>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Weibo { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets = vec![tweet]; notify_all(tweets); }
함수의 summary
매개변수는 구체적인 타입 대신 impl Summary
를 사용합니다. 즉, 함수는 Summary
트레이트를 구현하는 모든 타입을 허용할 수 있습니다.
값을 소유하고 특정 트레이트를 구현하는지(구체적인 타입에 대해서는 중요하지 않음)만 신경 쓰고 싶은 경우 Box
와 같은 스마트 포인터를 dyn
키워드와 결합하는 트레이트 객체 형식을 사용할 수 있습니다.
fn notify(summary: Box<dyn Summary>) { println!("notify: {}", summary.summarize()) } fn notify_all(summaries: Vec<Box<dyn Summary>>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets: Vec<Box<dyn Summary>> = vec![Box::new(tweet)]; notify_all(tweets); }
제네릭에서 트레이트 사용
제네릭 프로그래밍에서 제네릭 타입을 제한하는 데 트레이트가 어떻게 사용되는지 살펴보겠습니다.
이전 예에서 notify
함수를 fn notify(summary: impl Summary)
로 정의했을 때 구체적인 타입을 지정하는 대신 summary
매개변수의 타입이 Summary
트레이트를 구현해야 한다고 지정했습니다. 실제로 impl Summary
는 제네릭 프로그래밍의 트레이트 바운드에 대한 구문 설탕입니다. impl Trait
를 사용하는 코드는 다음과 같이 다시 작성할 수 있습니다.
fn notify<T: Summary>(summary: T) { println!("notify: {}", summary.summarize()) } fn notify_all<T: Summary>(summaries: Vec<T>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets = vec![tweet]; notify_all(tweets); }
함수에서 impl Trait
반환
impl Trait
를 사용하여 함수가 특정 트레이트를 구현하는 타입을 반환하도록 지정할 수 있습니다.
fn returns_summarizable() -> impl Summary { Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, } }
impl Trait
가 있는 이러한 종류의 반환 타입은 단일 구체적인 타입으로 해결되어야 합니다. 함수가 동일한 트레이트를 구현하는 다른 타입을 반환할 수 있는 경우 컴파일 오류가 발생합니다. 예를 들어:
fn returns_summarizable(switch: bool) -> impl Summary { if switch { Tweet { ... } // 여기서는 두 개의 다른 타입을 반환할 수 없습니다. } else { Post { ... } // 여기서는 두 개의 다른 타입을 반환할 수 없습니다. } }
위의 코드는 Tweet
와 Post
라는 두 개의 다른 타입을 반환하기 때문에 오류를 발생시킵니다. 두 타입 모두 동일한 트레이트를 구현하더라도 말입니다. 다른 타입을 반환하려면 트레이트 객체를 사용해야 합니다.
fn returns_summarizable(switch: bool) -> Box<dyn Summary> { if switch { Box::new(Tweet { ... }) // 트레이트 객체 } else { Box::new(Post { ... }) // 트레이트 객체 } }
요약
Rust의 핵심 설계 목표 중 하나는 제로 비용 추상화입니다. 런타임 성능을 희생하지 않고도 고급 언어 기능을 사용할 수 있습니다. 이 제로 비용 추상화의 기초는 제네릭과 트레이트입니다. 이를 통해 고급 구문을 컴파일 중에 효율적인 저수준 코드로 컴파일하여 런타임 효율성을 높일 수 있습니다.
트레이트는 추상적인 방식으로 공유 동작을 정의하는 반면, 트레이트 바운드는 함수 매개변수 또는 반환 타입(예: impl SuperTrait
또는 T: SuperTrait
)에 대한 제약 조건을 정의합니다. 트레이트와 트레이트 바운드를 통해 제네릭 타입 매개변수를 사용하여 반복을 줄이면서도 컴파일러에 이러한 제네릭 타입이 구현해야 하는 동작에 대한 명확한 지침을 제공할 수 있습니다. 컴파일러에 트레이트 바운드 정보를 제공하므로 코드에 사용된 фактические 타입을 제공하는지 확인할 수 있습니다.
요약하자면, Rust의 트레이트는 두 가지 주요 목적을 수행합니다.
- 동작 추상화: 인터페이스와 유사하게 공유 기능을 정의하여 타입의 공통 동작을 추상화합니다.
- 타입 제약 조건: 타입 동작을 제약하여 타입이 구현하는 트레이트를 기반으로 타입의 범위를 좁힙니다.
Rust 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무료로 무제한 프로젝트 배포
- 사용량에 따라 지불하세요. 요청도 없고, 요금도 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하세요.
- 예: $25로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI입니다.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합입니다.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅입니다.
간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없으므로 구축에만 집중하세요.
설명서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ