콘텐츠로 이동

Rust 트레이트

트레이트(trait)는 타입이 구현할 수 있는 동작의 묶음을 정의해요. 다른 언어의 인터페이스와 조금 비슷해요. Rust는 트레이트로 “이 타입은 X를 할 수 있다”를 표현하고, 제네릭 페이지에서 본 트레이트 바운드도 바로 트레이트로 동작해요.

trait Speak {
fn speak(&self) -> String;
}

이 코드는 “Speak를 구현하는 타입은 String을 돌려주는 speak 메서드를 꼭 제공해야 한다”는 뜻이에요. 트레이트 자체는 약속일 뿐이에요. 어떤 타입이 구현하기 전까지는 아무 일도 하지 않아요.

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String {
String::from("멍멍!")
}
}
impl Speak for Cat {
fn speak(&self) -> String {
String::from("야옹!")
}
}
fn main() {
let dog = Dog;
let cat = Cat;
println!("{}", dog.speak());
println!("{}", cat.speak());
}

실행 결과:

멍멍!
야옹!
▶ 직접 실행해 보기

Dog와 Cat은 서로 관계없는 타입이지만, 둘 다 Speak를 구현했으니 둘 다 .speak()를 호출할 수 있어요. 구조체 페이지에서 본 impl 블록과 같은 패턴이에요. impl Speak for Dog는 “Dog가 Speak 트레이트를 이렇게 지킨다”고 말하는 것뿐이에요.

트레이트를 함수 매개변수로 쓰기

섹션 제목: “트레이트를 함수 매개변수로 쓰기”

진짜 좋은 점은 여기 있어요. 트레이트를 구현한 타입이라면 무엇이든 받는 함수를 만들 수 있고, 정확히 어떤 타입인지는 신경 쓰지 않아도 돼요.

trait Speak {
fn speak(&self) -> String;
}
struct Dog;
impl Speak for Dog {
fn speak(&self) -> String { String::from("멍멍!") }
}
fn announce(animal: &impl Speak) {
println!("동물이 말해요: {}", animal.speak());
}
fn main() {
let dog = Dog;
announce(&dog);
}
▶ 직접 실행해 보기

animal: &impl Speak는 “Speak를 구현한 타입이면 무엇이든 괜찮고, 그게 어떤 타입인지는 알 필요가 없다”는 뜻이에요. Speak를 구현하는 Bird 타입을 새로 추가해도, announce는 아무것도 고치지 않고 그대로 쓸 수 있어요.

구조체 페이지의 #[derive(Debug)]를 기억하나요? Debug 자체가 트레이트예요. derive는 여러분이 손으로 구현을 쓰는 대신 자동으로 만들어 줄 뿐이에요. 자주 만나게 될 다른 트레이트로는 Clone(.clone()용), PartialEq(==용), PartialOrd(<, >용. 제네릭 페이지에서 트레이트 바운드로 쓰였죠)가 있어요.

트레이트는 메서드의 기본 본문을 제공할 수 있어요. 구현하는 타입은 그것을 그대로 쓰거나 덮어쓸(override) 수 있어요.

trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("안녕하세요, {}님!", self.name())
}
}

Greet를 구현하는 타입은 name()만 정의하면 돼요. greet()는 직접 덮어쓰지 않는 한 공짜로 따라와요.

트레이트에는 이 페이지에서 다룬 것보다 더 많은 내용이 있어요. 자주 쓰는 트레이트 페이지에서는 매일 쓰게 될 표준 트레이트를 하나씩 살펴보고, 고급 트레이트 페이지에서는 연관 타입, 슈퍼트레이트처럼 큰 코드베이스에서 보게 될 도구를 다뤄요.

메서드 fn area(&self) -> f64 하나를 가진 트레이트 Area를 정의해 보세요. 그리고 Rectangle(width와 height를 가짐)과 Square(side를 가짐)에 구현하세요. 3×4 직사각형과 한 변이 5인 정사각형의 넓이를 출력해 보세요. 힌트: impl Area for ... 블록마다 메서드는 하나만 있으면 되고, 본문은 곱셈 한 번이에요.

정답 보기
trait Area {
fn area(&self) -> f64;
}
struct Rectangle {
width: f64,
height: f64,
}
struct Square {
side: f64,
}
impl Area for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Area for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {
let rect = Rectangle { width: 3.0, height: 4.0 };
let square = Square { side: 5.0 };
println!("직사각형: {}", rect.area());
println!("정사각형: {}", square.area());
}

실행 결과:

직사각형: 12
정사각형: 25
▶ 직접 실행해 보기

fn describe(&self) -> String을 가진 트레이트 Describe를 정의해 보세요. 이것을 Book(title을 가짐)과 Movie(title과 minutes를 가짐)에 구현하세요. 그다음 설명을 출력하는 함수 fn show(item: &impl Describe) 하나를 만들고, 두 타입으로 모두 호출해 보세요. 힌트: 설명 문자열은 format!으로 만들면 돼요.

정답 보기
trait Describe {
fn describe(&self) -> String;
}
struct Book {
title: String,
}
struct Movie {
title: String,
minutes: u32,
}
impl Describe for Book {
fn describe(&self) -> String {
format!("책: {}", self.title)
}
}
impl Describe for Movie {
fn describe(&self) -> String {
format!("영화: {} ({}분)", self.title, self.minutes)
}
}
fn show(item: &impl Describe) {
println!("{}", item.describe());
}
fn main() {
let book = Book { title: String::from("호빗") };
let movie = Movie { title: String::from("업"), minutes: 96 };
show(&book);
show(&movie);
}

실행 결과:

책: 호빗
영화: 업 (96분)
▶ 직접 실행해 보기

3. 기본 구현을 쓰거나 덮어쓰기

섹션 제목: “3. 기본 구현을 쓰거나 덮어쓰기”

이 페이지의 Greet 트레이트를 기본 greet() 메서드와 함께 가져오세요. 기본 구현을 그대로 쓰는 Person과, greet()를 덮어써서 어이, ...!라고 인사하는 Pirate에 구현해 보세요. 힌트: 두 타입 모두 name()은 정의해야 해요. greet()까지 직접 쓰는 건 Pirate뿐이에요.

정답 보기
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("안녕하세요, {}님!", self.name())
}
}
struct Person {
name: String,
}
struct Pirate {
name: String,
}
impl Greet for Person {
fn name(&self) -> String {
self.name.clone()
}
}
impl Greet for Pirate {
fn name(&self) -> String {
self.name.clone()
}
fn greet(&self) -> String {
format!("어이, {}!", self.name())
}
}
fn main() {
let ana = Person { name: String::from("Ana") };
let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet());
println!("{}", jack.greet());
}

실행 결과:

안녕하세요, Ana님!
어이, Jack!
▶ 직접 실행해 보기
  • 트레이트는 타입이 구현할 수 있는 동작을 정의해요. 인터페이스와 비슷해요.
  • impl 트레이트이름 for 타입으로 구현을 제공해요.
  • 매개변수 타입으로 &impl 트레이트이름을 쓰면, 그 트레이트를 구현한 타입은 무엇이든 받을 수 있어요.
  • 트레이트는 메서드의 기본 본문을 제공할 수 있고, 구현하는 쪽은 그것을 그대로 쓰거나 덮어쓸 수 있어요.
  • Debug, Clone, PartialEq는 표준 라이브러리에서 자주 쓰는 트레이트예요. #[derive(...)]가 자동으로 구현해 줘요.

이해도 점검

1. 트레이트는 무엇을 정의할까요?

2. 매개변수 타입 animal: &impl Speak는 무슨 뜻일까요?

3. #[derive(Debug)]는 실제로 무슨 일을 할까요?

4. 트레이트의 기본 구현이란 무엇일까요?

5. 이 페이지에 따르면, 다음 중 구조체나 열거형이 아니라 트레이트인 것은 무엇일까요?

점수: 0 / 5