소개
Traits: 공유 동작 정의에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 타입에서 공유 동작을 정의하고 제네릭 타입에 대한 트레이트 바운드 (trait bounds) 를 지정하는 방법으로서 트레이트를 탐구합니다.
Traits: 공유 동작 정의
*트레이트 (trait)*는 특정 타입이 가지고 있으며 다른 타입과 공유할 수 있는 기능을 정의합니다. 트레이트를 사용하여 추상적인 방식으로 공유 동작을 정의할 수 있습니다. *트레이트 바운드 (trait bounds)*를 사용하여 제네릭 타입이 특정 동작을 가진 모든 타입이 될 수 있도록 지정할 수 있습니다.
참고: 트레이트는 다른 언어에서 종종 *인터페이스 (interfaces)*라고 불리는 기능과 유사하지만, 몇 가지 차이점이 있습니다.
트레이트 정의하기
타입의 동작은 해당 타입에서 호출할 수 있는 메서드로 구성됩니다. 서로 다른 타입이 동일한 동작을 공유하는 경우, 해당 타입 모두에서 동일한 메서드를 호출할 수 있습니다. 트레이트 정의는 메서드 시그니처 (method signatures) 를 함께 그룹화하여 어떤 목적을 달성하는 데 필요한 일련의 동작을 정의하는 방법입니다.
예를 들어, 다양한 종류와 양의 텍스트를 담는 여러 구조체 (struct) 가 있다고 가정해 보겠습니다. 특정 위치에 저장된 뉴스 기사를 담는 NewsArticle 구조체와, 최대 280 자까지의 문자를 가질 수 있으며, 새로운 트윗인지, 리트윗인지, 아니면 다른 트윗에 대한 답글인지 나타내는 메타데이터를 포함하는 Tweet 구조체가 있습니다.
NewsArticle 또는 Tweet 인스턴스에 저장된 데이터의 요약을 표시할 수 있는 aggregator라는 미디어 집계 라이브러리 크레이트 (crate) 를 만들고 싶습니다. 이를 위해 각 타입에서 요약이 필요하며, 인스턴스에서 summarize 메서드를 호출하여 해당 요약을 요청할 것입니다. Listing 10-12 는 이러한 동작을 표현하는 공개 Summary 트레이트의 정의를 보여줍니다.
파일 이름: src/lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
Listing 10-12: summarize 메서드에서 제공되는 동작으로 구성된 Summary 트레이트
여기서 trait 키워드와 트레이트의 이름 (이 경우 Summary) 을 사용하여 트레이트를 선언합니다. 또한 이 크레이트에 의존하는 크레이트도 이 트레이트를 사용할 수 있도록 트레이트를 pub로 선언합니다. 몇 가지 예제에서 보게 될 것입니다. 중괄호 안에서, 이 트레이트를 구현하는 타입의 동작을 설명하는 메서드 시그니처를 선언합니다. 이 경우 fn summarize(&self) -> String입니다.
메서드 시그니처 뒤에는 중괄호 안에 구현을 제공하는 대신 세미콜론을 사용합니다. 이 트레이트를 구현하는 각 타입은 메서드의 본문에 대한 자체 사용자 지정 동작을 제공해야 합니다. 컴파일러는 Summary 트레이트를 가진 모든 타입이 이 시그니처로 정확하게 정의된 summarize 메서드를 갖도록 강제합니다.
트레이트는 본문에 여러 메서드를 가질 수 있습니다. 메서드 시그니처는 한 줄에 하나씩 나열되며, 각 줄은 세미콜론으로 끝납니다.
타입에 트레이트 구현하기
이제 Summary 트레이트의 메서드에 대한 원하는 시그니처를 정의했으므로, 미디어 집계기에 있는 타입에 이를 구현할 수 있습니다. Listing 10-13 은 헤드라인, 작성자 및 위치를 사용하여 summarize의 반환 값을 생성하는 NewsArticle 구조체에 대한 Summary 트레이트의 구현을 보여줍니다. Tweet 구조체의 경우, 트윗 내용이 이미 280 자로 제한되어 있다고 가정하여 summarize를 사용자 이름과 트윗의 전체 텍스트로 정의합니다.
파일 이름: src/lib.rs
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!(
"{}, by {} ({})",
self.headline,
self.author,
self.location
)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Listing 10-13: NewsArticle 및 Tweet 타입에 Summary 트레이트 구현하기
타입에 트레이트를 구현하는 것은 일반 메서드를 구현하는 것과 유사합니다. 차이점은 impl 뒤에 구현하려는 트레이트 이름을 넣고, for 키워드를 사용한 다음, 트레이트를 구현하려는 타입의 이름을 지정한다는 것입니다. impl 블록 내에서 트레이트 정의가 정의한 메서드 시그니처를 넣습니다. 각 시그니처 뒤에 세미콜론을 추가하는 대신, 중괄호를 사용하고 트레이트의 메서드가 특정 타입에 대해 갖기를 원하는 특정 동작으로 메서드 본문을 채웁니다.
이제 라이브러리가 NewsArticle 및 Tweet에 Summary 트레이트를 구현했으므로, 크레이트 사용자는 일반 메서드를 호출하는 것과 동일한 방식으로 NewsArticle 및 Tweet의 인스턴스에서 트레이트 메서드를 호출할 수 있습니다. 유일한 차이점은 사용자가 트레이트와 타입을 모두 범위 내로 가져와야 한다는 것입니다. 다음은 바이너리 크레이트가 aggregator 라이브러리 크레이트를 사용하는 방법의 예입니다.
use aggregator::{Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
이 코드는 1 new tweet: horse_ebooks: of course, as you probably already know, people을 출력합니다.
aggregator 크레이트에 의존하는 다른 크레이트도 Summary 트레이트를 범위 내로 가져와 자체 타입에 Summary를 구현할 수 있습니다. 주목해야 할 한 가지 제한 사항은 트레이트 또는 타입, 또는 둘 다가 우리 크레이트에 로컬인 경우에만 타입에 트레이트를 구현할 수 있다는 것입니다. 예를 들어, Tweet 타입이 aggregator 크레이트에 로컬이기 때문에 aggregator 크레이트 기능의 일부로 Tweet과 같은 사용자 지정 타입에 Display와 같은 표준 라이브러리 트레이트를 구현할 수 있습니다. 또한 Summary 트레이트가 aggregator 크레이트에 로컬이기 때문에 aggregator 크레이트에서 Vec<T>에 Summary를 구현할 수 있습니다.
그러나 외부 타입에 외부 트레이트를 구현할 수는 없습니다. 예를 들어, Display와 Vec<T>가 모두 표준 라이브러리에 정의되어 있고 aggregator 크레이트에 로컬이 아니기 때문에 aggregator 크레이트 내에서 Vec<T>에 Display 트레이트를 구현할 수 없습니다. 이 제한 사항은 coherence(일관성) 라고 하는 속성의 일부이며, 더 구체적으로는 orphan rule(고아 규칙) 이라고 합니다. 이는 부모 타입이 존재하지 않기 때문에 그렇게 명명되었습니다. 이 규칙은 다른 사람의 코드가 여러분의 코드를 망가뜨릴 수 없고 그 반대의 경우도 마찬가지임을 보장합니다. 이 규칙이 없으면 두 크레이트가 동일한 타입에 대해 동일한 트레이트를 구현할 수 있으며, Rust 는 어떤 구현을 사용해야 할지 알 수 없습니다.
기본 구현
때로는 모든 타입의 모든 메서드에 대한 구현을 요구하는 대신, 트레이트의 일부 또는 모든 메서드에 대한 기본 동작을 갖는 것이 유용합니다. 그런 다음, 특정 타입에 트레이트를 구현할 때 각 메서드의 기본 동작을 유지하거나 재정의할 수 있습니다.
Listing 10-14 에서, Listing 10-12 에서 했던 것처럼 메서드 시그니처만 정의하는 대신, Summary 트레이트의 summarize 메서드에 대한 기본 문자열을 지정합니다.
파일 이름: src/lib.rs
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
Listing 10-14: summarize 메서드의 기본 구현이 있는 Summary 트레이트 정의
NewsArticle의 인스턴스를 요약하기 위해 기본 구현을 사용하려면, impl Summary for NewsArticle {}로 빈 impl 블록을 지정합니다.
더 이상 NewsArticle에서 summarize 메서드를 직접 정의하지 않더라도, 기본 구현을 제공하고 NewsArticle이 Summary 트레이트를 구현하도록 지정했습니다. 결과적으로, 다음과 같이 NewsArticle의 인스턴스에서 summarize 메서드를 계속 호출할 수 있습니다.
let article = NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!"
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
이 코드는 New article available! (Read more...)를 출력합니다.
기본 구현을 생성하는 것은 Listing 10-13 에서 Tweet에 대한 Summary의 구현에 대해 아무것도 변경할 필요가 없습니다. 그 이유는 기본 구현을 재정의하는 구문이 기본 구현이 없는 트레이트 메서드를 구현하는 구문과 동일하기 때문입니다.
기본 구현은 동일한 트레이트의 다른 메서드를 호출할 수 있으며, 해당 다른 메서드가 기본 구현을 갖지 않더라도 마찬가지입니다. 이러한 방식으로, 트레이트는 많은 유용한 기능을 제공하고 구현자가 그 중 작은 부분만 지정하도록 요구할 수 있습니다. 예를 들어, 구현이 필요한 summarize_author 메서드를 갖도록 Summary 트레이트를 정의한 다음, summarize_author 메서드를 호출하는 기본 구현이 있는 summarize 메서드를 정의할 수 있습니다.
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!(
"(Read more from {}...)",
self.summarize_author()
)
}
}
이 버전의 Summary를 사용하려면, 타입에 트레이트를 구현할 때 summarize_author만 정의하면 됩니다.
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
summarize_author를 정의한 후, Tweet 구조체의 인스턴스에서 summarize를 호출할 수 있으며, summarize의 기본 구현은 우리가 제공한 summarize_author의 정의를 호출합니다. summarize_author를 구현했으므로, Summary 트레이트는 더 이상 코드를 작성할 필요 없이 summarize 메서드의 동작을 제공했습니다. 다음은 그 모습입니다.
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
이 코드는 1 new tweet: (Read more from @horse_ebooks...)를 출력합니다.
해당 메서드의 재정의 구현에서 기본 구현을 호출하는 것은 불가능하다는 점에 유의하십시오.
매개변수로서의 트레이트
이제 트레이트를 정의하고 구현하는 방법을 알았으므로, 다양한 타입을 허용하는 함수를 정의하기 위해 트레이트를 사용하는 방법을 살펴볼 수 있습니다. Listing 10-13 에서 NewsArticle 및 Tweet 타입에 구현한 Summary 트레이트를 사용하여, Summary 트레이트를 구현하는 타입인 item 매개변수에서 summarize 메서드를 호출하는 notify 함수를 정의합니다. 이를 위해 다음과 같이 impl Trait 구문을 사용합니다.
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
item 매개변수에 대한 구체적인 타입 대신, impl 키워드와 트레이트 이름을 지정합니다. 이 매개변수는 지정된 트레이트를 구현하는 모든 타입을 허용합니다. notify의 본문에서, summarize와 같이 Summary 트레이트에서 제공되는 item에 대한 모든 메서드를 호출할 수 있습니다. notify를 호출하고 NewsArticle 또는 Tweet의 모든 인스턴스를 전달할 수 있습니다. String 또는 i32와 같은 다른 타입으로 함수를 호출하는 코드는 해당 타입이 Summary를 구현하지 않으므로 컴파일되지 않습니다.
트레이트 바운드 구문
impl Trait 구문은 간단한 경우에 작동하지만, 실제로 트레이트 바운드라고 하는 더 긴 형식의 구문 설탕입니다. 다음과 같습니다.
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
이 더 긴 형식은 이전 섹션의 예제와 동일하지만 더 장황합니다. 제네릭 타입 매개변수의 선언과 함께 콜론 뒤와 꺾쇠 괄호 안에 트레이트 바운드를 배치합니다.
impl Trait 구문은 편리하며 간단한 경우에 더 간결한 코드를 만들 수 있으며, 더 완전한 트레이트 바운드 구문은 다른 경우에 더 많은 복잡성을 표현할 수 있습니다. 예를 들어, Summary를 구현하는 두 개의 매개변수를 가질 수 있습니다. impl Trait 구문을 사용하여 그렇게 하면 다음과 같습니다.
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
impl Trait를 사용하는 것은 이 함수가 item1과 item2가 서로 다른 타입을 갖도록 허용하려는 경우에 적합합니다 (두 타입 모두 Summary를 구현하는 한). 그러나 두 매개변수가 동일한 타입을 갖도록 강제하려면 다음과 같이 트레이트 바운드를 사용해야 합니다.
pub fn notify<T: Summary>(item1: &T, item2: &T) {
item1 및 item2 매개변수의 타입으로 지정된 제네릭 타입 T는 item1 및 item2에 대한 인수로 전달된 값의 구체적인 타입이 동일해야 하도록 함수를 제한합니다.
+ 구문을 사용하여 여러 트레이트 바운드 지정하기
또한 둘 이상의 트레이트 바운드를 지정할 수 있습니다. notify가 item에 대해 summarize뿐만 아니라 표시 형식 (display formatting) 도 사용하도록 하려는 경우를 가정해 보겠습니다. notify 정의에서 item이 Display와 Summary를 모두 구현해야 한다고 지정합니다. + 구문을 사용하여 그렇게 할 수 있습니다.
pub fn notify(item: &(impl Summary + Display)) {
+ 구문은 제네릭 타입에 대한 트레이트 바운드와 함께 사용할 수도 있습니다.
pub fn notify<T: Summary + Display>(item: &T) {
두 개의 트레이트 바운드가 지정되면, notify의 본문은 summarize를 호출하고 {}를 사용하여 item을 형식화할 수 있습니다.
where 절을 사용한 더 명확한 트레이트 바운드
너무 많은 트레이트 바운드를 사용하는 것은 단점이 있습니다. 각 제네릭은 자체 트레이트 바운드를 가지므로, 여러 제네릭 타입 매개변수를 가진 함수는 함수의 이름과 매개변수 목록 사이에 많은 트레이트 바운드 정보를 포함할 수 있어 함수 시그니처를 읽기 어렵게 만듭니다. 이러한 이유로 Rust 는 함수 시그니처 뒤의 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,
{
이 함수의 시그니처는 덜 복잡합니다. 함수 이름, 매개변수 목록 및 반환 타입이 서로 가깝게 위치하여, 많은 트레이트 바운드가 없는 함수와 유사합니다.
트레이트를 구현하는 타입 반환하기
또한 반환 위치에서 impl Trait 구문을 사용하여 트레이트를 구현하는 특정 타입의 값을 반환할 수 있습니다. 다음은 그 예입니다.
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
반환 타입으로 impl Summary를 사용함으로써, returns_summarizable 함수가 구체적인 타입을 명시하지 않고 Summary 트레이트를 구현하는 어떤 타입을 반환한다고 지정합니다. 이 경우, returns_summarizable는 Tweet을 반환하지만, 이 함수를 호출하는 코드는 그것을 알 필요가 없습니다.
트레이트만으로 반환 타입을 지정하는 기능은 13 장에서 다루는 클로저 (closures) 와 이터레이터 (iterators) 의 맥락에서 특히 유용합니다. 클로저와 이터레이터는 컴파일러만 알고 있거나, 지정하기에 매우 긴 타입을 생성합니다. impl Trait 구문을 사용하면 매우 긴 타입을 작성할 필요 없이 함수가 Iterator 트레이트를 구현하는 어떤 타입을 반환한다고 간결하게 지정할 수 있습니다.
그러나, 단일 타입만 반환하는 경우에만 impl Trait를 사용할 수 있습니다. 예를 들어, NewsArticle 또는 Tweet 중 하나를 반환하고 반환 타입을 impl Summary로 지정하는 다음 코드는 작동하지 않습니다.
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}
NewsArticle 또는 Tweet 중 하나를 반환하는 것은 impl Trait 구문이 컴파일러에서 구현되는 방식과 관련된 제한 사항 때문에 허용되지 않습니다. "다양한 타입의 값을 허용하는 트레이트 객체 사용하기"에서 이 동작을 가진 함수를 작성하는 방법을 다룰 것입니다.
트레이트 바운드를 사용하여 조건부로 메서드 구현하기
제네릭 타입 매개변수를 사용하는 impl 블록과 함께 트레이트 바운드를 사용하면, 지정된 트레이트를 구현하는 타입에 대해 조건부로 메서드를 구현할 수 있습니다. 예를 들어, Listing 10-15 의 Pair<T> 타입은 항상 new 함수를 구현하여 Pair<T>의 새 인스턴스를 반환합니다 ("메서드 정의하기"에서 Self가 impl 블록의 타입에 대한 타입 별칭임을 기억하세요. 이 경우 Pair<T>입니다). 그러나 다음 impl 블록에서 Pair<T>는 내부 타입 T가 비교를 가능하게 하는 PartialOrd 트레이트 및 출력을 가능하게 하는 Display 트레이트를 구현하는 경우에만 cmp_display 메서드를 구현합니다.
파일 이름: src/lib.rs
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
Listing 10-15: 트레이트 바운드에 따라 제네릭 타입에 메서드를 조건부로 구현하기
또한 다른 트레이트를 구현하는 모든 타입에 대해 트레이트를 조건부로 구현할 수 있습니다. 트레이트 바운드를 만족하는 모든 타입에 대한 트레이트의 구현을 *블랭킷 구현 (blanket implementations)*이라고 하며, Rust 표준 라이브러리에서 광범위하게 사용됩니다. 예를 들어, 표준 라이브러리는 Display 트레이트를 구현하는 모든 타입에 대해 ToString 트레이트를 구현합니다. 표준 라이브러리의 impl 블록은 이 코드와 유사합니다.
impl<T: Display> ToString for T {
--snip--
}
표준 라이브러리에 이 블랭킷 구현이 있기 때문에, Display 트레이트를 구현하는 모든 타입에 대해 ToString 트레이트에 의해 정의된 to_string 메서드를 호출할 수 있습니다. 예를 들어, 정수가 Display를 구현하므로 정수를 해당 String 값으로 변환할 수 있습니다.
let s = 3.to_string();
블랭킷 구현은 "구현자 (Implementors)" 섹션의 트레이트 문서에 나타납니다.
트레이트와 트레이트 바운드를 사용하면 제네릭 타입 매개변수를 사용하여 중복을 줄이는 코드를 작성할 수 있을 뿐만 아니라, 제네릭 타입이 특정 동작을 갖도록 컴파일러에 지정할 수 있습니다. 그러면 컴파일러는 트레이트 바운드 정보를 사용하여 코드와 함께 사용되는 모든 구체적인 타입이 올바른 동작을 제공하는지 확인할 수 있습니다. 동적으로 타입이 지정된 언어에서는 메서드를 정의하지 않은 타입에 대해 메서드를 호출하면 런타임에 오류가 발생합니다. 그러나 Rust 는 이러한 오류를 컴파일 시간에 이동하므로 코드를 실행하기 전에 문제를 해결해야 합니다. 또한 컴파일 시간에 이미 확인했기 때문에 런타임에 동작을 확인하는 코드를 작성할 필요가 없습니다. 이렇게 하면 제네릭의 유연성을 포기하지 않고 성능을 향상시킬 수 있습니다.
요약
축하합니다! 트레이트: 공유 동작 정의 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.