동작하는 코드
모듈 12에서 구조체를 만들 때 #[derive(Debug)]를 쓴 적 있죠? 그때 "이걸 붙이면 {:?}로 출력할 수 있다"고만 설명했어요. 이제 그 정체를 밝혀볼게요 — 그것은 바로 트레이트(trait) 예요!
struct Cat {
name: String,
age: u8,
}
struct Dog {
name: String,
age: u8,
}
trait Greet {
fn hello(&self) -> String;
}
impl Greet for Cat {
fn hello(&self) -> String {
format!("{}(이)가 야옹! 🐱", self.name)
}
}
impl Greet for Dog {
fn hello(&self) -> String {
format!("{}(이)가 멍멍! 🐶", self.name)
}
}
fn main() {
let cat = Cat { name: "나비".into(), age: 3 };
let dog = Dog { name: "초코".into(), age: 5 };
println!("{}", cat.hello());
println!("{}", dog.hello());
}
Greet는 트레이트예요. "이 메서드를 구현하면 인사할 수 있는 자격이 생긴다"는 뜻이에요. Cat과 Dog은 각각 다르게 인사하지만, 둘 다 Greet 자격증이 있어요!
직접 해보기
Bird구조체를 만들고Greet트레이트를 구현해보세요.hello()메서드를 호출해서 결과를 확인하세요.Greet트레이트에goodbye(&self) -> String메서드를 추가하면 어떻게 되나요? (힌트: 모든 구현체에서 에러가 나요!)
"왜?" — 자격증 비유
트레이트를 자격증으로 생각하면 명확해져요.
Display자격증 →println!("{}", x)사용 가능Debug자격증 →println!("{:?}", x)사용 가능Clone자격증 →.clone()사용 가능PartialEq자격증 →==로 비교 가능
자격증이 없으면? 컴파일러가 "이 타입에는 X 자격증이 없어요"라고 알려줘요.
Display — 사용자에게 보여주기
{}로 출력하려면 Display 트레이트가 필요해요. 자동으로 만들 수 없어서 직접 구현해야 해요.
use std::fmt;
struct Point {
x: f64,
y: f64,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("좌표: {p}"); // 좌표: (3, 4)
// println!("{:?}", p); // 에러! Debug가 없으니까
}
Debug — 개발자가 디버깅하기
{:?}로 출력하려면 Debug 트레이트가 필요해요. derive로 자동 생성할 수 있어요.
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("{:?}", p); // Point { x: 3.0, y: 4.0 }
println!("{:#?}", p); // 예쁘게 출력
}
Before / After — Display 구현 전과 후
Before — Display 없이
struct Temperature {
celsius: f64,
}
fn main() {
let t = Temperature { celsius: 36.5 };
// println!("{t}"); // 에러! `Temperature` doesn't implement `Display`
println!("온도: {}°C", t.celsius); // 필드를 직접 접근해야 해요
}
After — Display 구현
use std::fmt;
struct Temperature {
celsius: f64,
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
fn main() {
let t = Temperature { celsius: 36.5 };
println!("현재 온도: {t}"); // 현재 온도: 36.5°C
}
derive 매크로 — 자동 자격증
직접 구현하지 않아도 #[derive(...)]로 자동 생성할 수 있는 트레이트가 있어요. 모듈 12에서 이미 써봤죠!
| 트레이트 | 기능 | 예시 |
| ----------- | ------------------- | --------------------- |
| Debug | {:?}로 출력 | println!("{:?}", x) |
| Clone | .clone()으로 복제 | let y = x.clone() |
| PartialEq | ==로 비교 | if a == b |
| Default | 기본값 생성 | Point::default() |
#[derive(Debug, Clone, PartialEq)]
struct Color {
r: u8,
g: u8,
b: u8,
}
fn main() {
let red = Color { r: 255, g: 0, b: 0 };
let red2 = red.clone(); // Clone 덕분에 가능
println!("{:?}", red); // Debug 덕분에 가능
println!("{}", red == red2); // PartialEq 덕분에 가능 → true
}
Phase 1에서 .clone()을 쓸 수 있었던 이유? 그것도 Clone 트레이트 덕분이었어요!
트레이트 바운드 — "이 자격증 있는 타입만 받겠어"
함수가 특정 트레이트를 구현한 타입만 받도록 제한할 수 있어요.
use std::fmt::Display;
fn print_twice<T: Display>(item: T) {
println!("1회: {item}");
println!("2회: {item}");
}
fn main() {
print_twice("안녕하세요");
print_twice(42);
// print_twice(vec![1, 2, 3]); // 에러! Vec은 Display가 없어요
}
T: Display는 "T는 Display 자격증이 있어야 해"라는 뜻이에요. 제네릭과 함께 다음 모듈에서 더 자세히 다룰 거예요.
기본 구현 — 기본 행동을 정해두기
트레이트에 기본 구현을 넣을 수도 있어요. 필요한 타입만 재정의(override)하면 돼요.
trait Describable {
fn name(&self) -> &str;
// 기본 구현 — 재정의하지 않으면 이게 사용돼요
fn describe(&self) -> String {
format!("이름: {}", self.name())
}
}
struct Product {
name: String,
price: u32,
}
impl Describable for Product {
fn name(&self) -> &str {
&self.name
}
// describe()는 재정의하지 않아서 기본 구현이 사용돼요
}
struct PremiumProduct {
name: String,
price: u32,
}
impl Describable for PremiumProduct {
fn name(&self) -> &str {
&self.name
}
// 기본 구현을 재정의!
fn describe(&self) -> String {
format!("⭐ {} ({}원)", self.name(), self.price)
}
}
fn main() {
let apple = Product { name: "사과".into(), price: 1000 };
let gold_apple = PremiumProduct { name: "금사과".into(), price: 5000 };
println!("{}", apple.describe()); // 이름: 사과
println!("{}", gold_apple.describe()); // ⭐ 금사과 (5000원)
}
연습 1 (쉬움). 아래 Rectangle 구조체에 Display 트레이트를 구현해서 "3 x 5 직사각형"처럼 출력되게 하세요.
use std::fmt;
struct Rectangle {
width: u32,
height: u32,
}
// 여기에 Display를 구현하세요
fn main() {
let rect = Rectangle { width: 3, height: 5 };
println!("{rect}"); // "3 x 5 직사각형"
}
연습 2 (보통). Summary 트레이트를 정의하고, Article과 Tweet 구조체에 각각 구현하세요.
// Summary 트레이트를 정의하세요
// - summarize(&self) -> String 메서드가 필요합니다
struct Article {
title: String,
content: String,
}
struct Tweet {
username: String,
text: String,
}
// 각 구조체에 Summary를 구현하세요
// Article: "{title} — {content의 앞 20자}..."
// Tweet: "@{username}: {text}"
fn main() {
let article = Article {
title: "Rust 2024".into(),
content: "Rust가 올해도 가장 사랑받는 언어로 선정되었습니다.".into(),
};
let tweet = Tweet {
username: "rustlang".into(),
text: "Rust 1.80 출시!".into(),
};
println!("{}", article.summarize());
println!("{}", tweet.summarize());
}
연습 3 (도전). 트레이트 바운드를 사용해서 Display와 PartialOrd를 모두 구현한 타입만 받는 print_max 함수를 만드세요.
use std::fmt::Display;
fn print_max<T: Display + PartialOrd>(a: T, b: T) {
// a와 b 중 큰 값을 출력하세요
// 힌트: if a >= b { ... } else { ... }
}
fn main() {
print_max(10, 20); // 최대값: 20
print_max("apple", "banana"); // 최대값: banana
}
Q1. 트레이트(trait)란 무엇인가요?
- A) 구조체의 필드를 정의하는 방법
- B) 타입이 구현해야 할 공통 인터페이스를 정의하는 방법
- C) 함수의 반환 타입을 제한하는 방법
- D) 변수의 수명을 지정하는 방법
Q2. #[derive(Debug, Clone)]이 하는 일은?
- A) 구조체의 필드를 자동으로 추가한다
- B) Debug와 Clone 트레이트를 자동으로 구현한다
- C) 구조체를 삭제한다
- D) 구조체를 제네릭으로 만든다
Q3. 아래 코드에서 에러가 나는 이유는?
struct Point { x: f64, y: f64 }
fn main() {
let p = Point { x: 1.0, y: 2.0 };
println!("{p}");
}
- A)
Point에Debug트레이트가 없다 - B)
Point에Display트레이트가 없다 - C)
println!에 구조체를 넣을 수 없다 - D)
f64는 출력할 수 없다