DaleSchool

트레이트: 타입의 자격증

중급20분

학습 목표

  • trait로 공통 인터페이스를 정의할 수 있다
  • 구조체에 trait을 구현할 수 있다
  • derive 매크로로 자동 구현되는 trait을 활용할 수 있다
  • trait 바운드의 개념을 이해한다

동작하는 코드

모듈 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는 트레이트예요. "이 메서드를 구현하면 인사할 수 있는 자격이 생긴다"는 뜻이에요. CatDog은 각각 다르게 인사하지만, 둘 다 Greet 자격증이 있어요!

직접 해보기

  1. Bird 구조체를 만들고 Greet 트레이트를 구현해보세요.
  2. hello() 메서드를 호출해서 결과를 확인하세요.
  3. 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 트레이트를 정의하고, ArticleTweet 구조체에 각각 구현하세요.

// 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 (도전). 트레이트 바운드를 사용해서 DisplayPartialOrd를 모두 구현한 타입만 받는 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) PointDebug 트레이트가 없다
  • B) PointDisplay 트레이트가 없다
  • C) println!에 구조체를 넣을 수 없다
  • D) f64는 출력할 수 없다