Rust 트레이트(Trait) 완전 정복 - 초보자용

1. 개념

1.1 트레이트가 뭐냐?

트레이트(Trait) = "어떤 타입이 할 수 있는 일을 정의하는 계약서"

쉽게 말하면:

  • "이 기능을 쓰고 싶으면, 이 메서드들을 반드시 구현해라!"
  • Java의 **인터페이스(Interface)**와 비슷한 개념
┌─────────────────────────────────────────────────────────────┐
│                    트레이트 = 계약서                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  📄 "요약 가능" 트레이트                                     │
│  ┌────────────────────────────────────────┐                │
│  │  계약 조건:                            │                │
│  │  • summarize() 메서드를 반드시 구현할 것 │                │
│  └────────────────────────────────────────┘                │
│                                                             │
│  이 계약서에 서명(impl)하면 → summarize()를 호출할 수 있음   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 왜 트레이트가 필요한가?

문제 상황:

struct Article { title: String, content: String }
struct Tweet { username: String, content: String }
struct Video { title: String, duration: u32 }

// 각각 요약하고 싶은데... 함수를 따로 만들어야 하나?
fn summarize_article(article: &Article) -> String { ... }
fn summarize_tweet(tweet: &Tweet) -> String { ... }
fn summarize_video(video: &Video) -> String { ... }
// 😱 너무 비효율적!

트레이트로 해결:

// "요약 가능하다"는 계약서를 만들고
trait Summary {
    fn summarize(&self) -> String;
}

// 각 타입이 계약서에 서명
impl Summary for Article { ... }
impl Summary for Tweet { ... }
impl Summary for Video { ... }

// 이제 하나의 함수로 모두 처리 가능!
fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

2. 트레이트 문법 완전 해부

2.1 트레이트 정의하기 (계약서 작성)

trait Summary {
// ↑
// └── 키워드: "트레이트를 정의하겠다"

//    ↓ 트레이트 이름 (자유롭게 지정)
    fn summarize(&self) -> String;
//  ↑↑ ↑       ↑        ↑
//  ││ │       │        └── 반환 타입: 이 함수가 String을 돌려줌
//  ││ │       │
//  ││ │       └── 매개변수: &self = "이 타입의 인스턴스 참조"
//  ││ │           (파이썬의 self, 자바의 this와 비슷)
//  ││ │
//  ││ └── 함수 이름
//  ││
//  │└── fn: 함수 정의 키워드
//  │
//  └── 세미콜론(;)으로 끝남 = 구현 없이 선언만 함 (반드시 구현해야 함)
}

한 줄씩 뜯어보기:

부분의미설명
trait트레이트 정의 키워드"계약서를 만들겠다"
Summary트레이트 이름"요약 가능"이라는 기능 이름
fn함수 정의"이 함수를 구현해야 함"
summarize메서드 이름함수 이름 (자유롭게 지정)
&self자기 자신 참조이 타입의 인스턴스를 가리킴
-> String반환 타입String을 돌려줌
;선언만 함중괄호 { } 없이 세미콜론으로 끝남 = "구현은 나중에 함"

2.2 &self가 뭔데?

fn summarize(&self) -> String;
//          ↑
//          이게 뭔데?

&self = "이 struct의 인스턴스를 빌려쓰겠다"

struct Article {
    title: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
//              ↑
//              self = "지금 이 Article 인스턴스"

        // self로 Article의 필드에 접근 가능
        format!("{}: {}", self.title, self.content)
        //                  ↑         ↑
        //                  self를 통해 title과 content에 접근
    }
}

fn main() {
    let article = Article {
        title: "제목".to_string(),
        content: "내용".to_string(),
    };

    article.summarize();
    //   ↑
    //   여기서 article이 self가 됨
    //   summarize() 안에서 self.title = "제목", self.content = "내용"
}

&self&는 뭔데?

표현의미설명
self소유권 이동인스턴스를 통째로 가져옴 (사용 후 사라짐)
&self불변 참조인스턴스를 빌려만 씀 (읽기만 가능)
&mut self가변 참조인스턴스를 빌려서 수정까지 가능

비유:

self      = 집을 통째로 사서 가져옴 (원래 집은 사라짐)
&self     = 집을 구경만 함 (열쇠만 빌림, 건드리면 안 됨)
&mut self = 집을 빌려서 인테리어까지 가능 (수정 OK)

2.3 트레이트 구현하기 (계약서에 서명)

impl Summary for Article {
// ↑          ↑     ↑
// │          │     └── 타입: "무엇에 대해?"
// │          │         (구조체, 열거형 등)
// │          │
// │          └── for: "~를 위해" (연결어)
// │
// └── impl: "구현하겠다" (implement의 약어)

    fn summarize(&self) -> String {
        format!("{}: {}", self.title, self.content)
    }
}

자연어로 번역:

impl Summary for Article

→ "Article 타입에 대해 Summary 트레이트를 구현하겠다"
→ "Article이 '요약 가능' 기능을 갖도록 만들겠다"
→ "이제 Article도 요약할 수 있다!"

for 뒤에 뭐가 오나요?

// 내가 만든 구조체
struct MyStruct { x: i32 }
impl Summary for MyStruct { }   // ✅

// 내가 만든 열거형
enum MyEnum { A, B }
impl Summary for MyEnum { }     // ✅

// 표준 라이브러리 타입도 가능!
impl Summary for i32 { }        // ✅ (Summary가 내 트레이트면)
impl Summary for String { }     // ✅
impl Summary for Vec<i32> { }   // ✅

2.4 전체 예제: 처음부터 끝까지

// ═══════════════════════════════════════════════════════════
// 1단계: 트레이트 정의 (계약서 작성)
// ═══════════════════════════════════════════════════════════
trait Summary {
    // 메서드 선언만 함 (구현은 나중에)
    // &self: 이 타입의 인스턴스를 읽기 전용으로 참조
    // -> String: String 타입을 반환
    fn summarize(&self) -> String;
}

// ═══════════════════════════════════════════════════════════
// 2단계: 구조체 정의 (데이터 구조 만들기)
// ═══════════════════════════════════════════════════════════
struct Article {
    // struct: 여러 데이터를 묶는 컨테이너
    title: String,    // 필드: title은 String 타입
    content: String,  // 필드: content는 String 타입
}

// ═══════════════════════════════════════════════════════════
// 3단계: 트레이트 구현 (계약서에 서명)
// ═══════════════════════════════════════════════════════════
impl Summary for Article {
// ↑             ↑
// impl: 구현    for: ~에 대해
// Summary를     Article 타입에 적용

    fn summarize(&self) -> String {
        //   ↑
        //   self = 지금 이 Article 인스턴스
        //   & = 빌려쓰기 (읽기만 함)

        // format!: 매크로로 문자열 포맷팅
        // {} {} 자리에 순서대로 값이 들어감
        format!("제목: {}, 내용: {}", self.title, self.content)
        //                           ↑          ↑
        //                           self로 필드 접근
    }
}

// ═══════════════════════════════════════════════════════════
// 4단계: 사용하기
// ═══════════════════════════════════════════════════════════
fn main() {
    // Article 인스턴스 생성
    let article = Article {
        title: String::from("Rust 트레이트 배우기"),
        content: String::from("트레이트는 계약서와 같다..."),
    };

    // summarize() 호출 가능!
    // 이게 가능한 이유: Article이 Summary 트레이트를 구현했으니까
    println!("{}", article.summarize());
    // 출력: 제목: Rust 트레이트 배우기, 내용: 트레이트는 계약서와 같다...
}

3. 트레이트 안에 뭐가 들어갈 수 있나요?

3.1 전체 구조

trait MyTrait {
    // ══════════════════════════════════════════════════════
    // 1. 메서드 선언 (반드시 구현해야 함)
    // ══════════════════════════════════════════════════════
    fn required_method(&self) -> i32;
    // ↑
    // 세미콜론(;)으로 끝남 = 선언만 함
    // 이 트레이트를 구현하는 타입은 이 메서드를 반드시 구현해야 함

    // ══════════════════════════════════════════════════════
    // 2. 메서드 기본 구현 (구현해도 되고 안 해도 됨)
    // ══════════════════════════════════════════════════════
    fn default_method(&self) -> String {
        // 중괄호 { }가 있음 = 구현이 있음
        // 이 트레이트를 구현하는 타입은 그냥 써도 되고,
        // 오버라이드(덮어쓰기)해도 됨

        String::from("기본 동작입니다")
    }

    // ══════════════════════════════════════════════════════
    // 3. 연관 타입 (Associated Type)
    // ══════════════════════════════════════════════════════
    type Output;
    // ↑
    // type: 타입 별명을 정의하는 키워드
    // Output: 타입 이름 (자유롭게 지정)
    //
    // 뜻: "이 트레이트를 구현할 때, Output이라는 타입을 정해라"
    // 구현할 때 구체적인 타입으로 지정해야 함

    // ══════════════════════════════════════════════════════
    // 4. 연관 상수 (Associated Constant)
    // ══════════════════════════════════════════════════════
    const MAX_SIZE: usize;
    // ↑    ↑         ↑
    // │    │         └── 타입: usize (부호 없는 정수)
    // │    │
    // │    └── 상수 이름
    // │
    // const: 상수 정의 키워드
    //
    // 뜻: "이 트레이트를 구현할 때, MAX_SIZE라는 상수를 정해라"
}

3.2 연관 타입(type)이 뭔데?

type = "구현할 때 구체적인 타입을 정해라"

// ═══════════════════════════════════════════════════════════
// 트레이트 정의: "Item 타입을 가지고 있다"
// ═══════════════════════════════════════════════════════════
trait Container {
    type Item;
    // ↑
    // "Item이라는 타입이 있을 건데,
    //  구현할 때 구체적으로 어떤 타입인지 정해라"

    fn get(&self) -> Option<&Self::Item>;
    //                      ↑
    //                      Self::Item = "내가 정의한 Item 타입"
}

// ═══════════════════════════════════════════════════════════
// 구현 1: 숫자를 담는 컨테이너
// ═══════════════════════════════════════════════════════════
struct NumberBox {
    value: i32,
}

impl Container for NumberBox {
    type Item = i32;
    //     ↑
    //     여기서 Item이 i32라고 정의

    fn get(&self) -> Option<&Self::Item> {
        //                  ↑
        //                  Self::Item = i32
        Some(&self.value)
    }
}

// ═══════════════════════════════════════════════════════════
// 구현 2: 문자열을 담는 컨테이너
// ═══════════════════════════════════════════════════════════
struct StringBox {
    value: String,
}

impl Container for StringBox {
    type Item = String;
    //     ↑
    //     여기서 Item이 String이라고 정의

    fn get(&self) -> Option<&Self::Item> {
        //                  ↑
        //                  Self::Item = String
        Some(&self.value)
    }
}

// ═══════════════════════════════════════════════════════════
// 사용
// ═══════════════════════════════════════════════════════════
fn main() {
    let num_box = NumberBox { value: 42 };
    let str_box = StringBox { value: String::from("hello") };

    // NumberBox::Item = i32
    let num: Option<&i32> = num_box.get();
    //                      ↑ i32를 반환

    // StringBox::Item = String
    let txt: Option<&String> = str_box.get();
    //                        ↑ String을 반환
}

type이 필요한가요?

// 제네릭 없이:
trait Container {
    type Item;  // 구현할 때 타입 결정
    fn get(&self) -> &Self::Item;
}

// 제네릭으로도 할 수 있지만:
trait ContainerGeneric<T> {
    fn get(&self) -> &T;
}

// 차이점:
// - type: 타입마다 딱 하나의 Item만 가능 (명확함)
// - 제네릭: 타입마다 여러 T 가능 (유연하지만 복잡)

impl Container for MyStruct {
    type Item = i32;  // 딱 하나만
}

impl ContainerGeneric<i32> for MyStruct { }  // 가능
impl ContainerGeneric<String> for MyStruct { }  // 가능 (여러 개)

3.3 기본 구현 예제

// ═══════════════════════════════════════════════════════════
// 트레이트 정의: 필수 메서드 + 기본 메서드
// ═══════════════════════════════════════════════════════════
trait Greet {
    // 필수 메서드: 반드시 구현해야 함
    fn name(&self) -> &str;
    //          ↑
    //          세미콜론으로 끝남 = 구현 없음

    // 기본 메서드: 이미 구현되어 있음
    fn greet(&self) {
        //          ↑
        //          중괄호가 있음 = 구현되어 있음

        println!("안녕하세요, {}님!", self.name());
        //                      ↑
        //                      필수 메서드를 호출
    }
}

// ═══════════════════════════════════════════════════════════
// 구현 1: 기본 greet() 사용
// ═══════════════════════════════════════════════════════════
struct Person {
    name: String,
}

impl Greet for Person {
    // name()만 구현하면 됨 (필수)
    fn name(&self) -> &str {
        &self.name
    }
    // greet()은 구현 안 해도 됨 (기본 구현 사용)
}

// ═══════════════════════════════════════════════════════════
// 구현 2: greet() 오버라이드 (덮어쓰기)
// ═══════════════════════════════════════════════════════════
struct Robot {
    name: String,
}

impl Greet for Robot {
    fn name(&self) -> &str {
        &self.name
    }

    // 기본 구현 대신 직접 구현
    fn greet(&self) {
        println!("띠리리리! 저는 {}입니다.", self.name());
    }
}

// ═══════════════════════════════════════════════════════════
// 사용
// ═══════════════════════════════════════════════════════════
fn main() {
    let person = Person { name: String::from("철수") };
    let robot = Robot { name: String::from("R2D2") };

    person.greet();  // "안녕하세요, 철수님!" (기본 구현)
    robot.greet();   // "띠리리리! 저는 R2D2입니다." (오버라이드)
}

4. 트레이트 바운드 (제약 조건)

4.1 기본 개념

트레이트 바운드 = "이 타입은 반드시 이 트레이트를 구현해야 함"

// ═══════════════════════════════════════════════════════════
// T: Summary = "T는 반드시 Summary 트레이트를 구현해야 함"
// ═══════════════════════════════════════════════════════════
fn print_summary<T: Summary>(item: &T) {
//            ↑     ↑
//            │     └── 제약 조건: T는 Summary여야 함
//            │
//            └── 제네릭 타입 T: 어떤 타입이든 될 수 있음

    println!("{}", item.summarize());
    //              ↑
    //              Summary 트레이트를 구현했으니
    //              summarize()를 호출할 수 있음이 보장됨
}

fn main() {
    let article = Article { ... };
    let tweet = Tweet { ... };

    print_summary(&article);  // ✅ Article은 Summary 구현함
    print_summary(&tweet);    // ✅ Tweet도 Summary 구현함
    print_summary(&123);      // ❌ 컴파일 에러! i32는 Summary 구현 안 함
}

4.2 여러 트레이트 바운드

// ═══════════════════════════════════════════════════════════
// 방법 1: +로 연결
// ═══════════════════════════════════════════════════════════
fn process<T: Summary + Clone + Debug>(item: &T) {
    //      ↑                       ↑       ↑
    //      │                       │       └── 디버그 출력 가능
    //      │                       └── 복사 가능
    //      └── 요약 가능

    println!("{:?}", item);  // Debug 필요
    let copy = item.clone(); // Clone 필요
    item.summarize();        // Summary 필요
}

// ═══════════════════════════════════════════════════════════
// 방법 2: where 절 (가독성 좋음)
// ═══════════════════════════════════════════════════════════
fn process<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Clone,   // T는 Summary와 Clone을 구현해야 함
    U: Display + Debug,   // U는 Display와 Debug를 구현해야 함
{
    // ...
}

5. 정적 디스패치 vs 동적 디스패치

5.1 정적 디스패치 (<T: Trait>)

// 컴파일 타임에 어떤 타입인지 결정됨
fn make_sound<T: Speak>(animal: &T) {
    animal.speak();
}

fn main() {
    let dog = Dog;
    let cat = Cat;

    make_sound(&dog);  // 컴파일러가 make_sound_Dog() 함수 생성
    make_sound(&cat);  // 컴파일러가 make_sound_Cat() 함수 생성
}

// 장점: 빠름 (인라인 최적화 가능)
// 단점: 바이너리 크기가 커짐 (각 타입별 함수 생성)

5.2 동적 디스패치 (dyn Trait)

// 런타임에 어떤 타입인지 결정됨
fn make_sound(animal: &dyn Speak) {
    //            ↑
    //            dyn = dynamic의 약어
    animal.speak();
}

// 서로 다른 타입을 한 컬렉션에 저장 가능!
fn main() {
    let animals: Vec<&dyn Speak> = vec![&Dog, &Cat];
    //                ↑
    //                서로 다른 타입이지만 &dyn Speak로 통일

    for animal in animals {
        animal.speak();  // 런타임에 어떤 타입인지 확인 후 호출
    }
}

// 장점: 유연함, 바이너리 크기 작음
// 단점: 약간의 런타임 오버헤드

6. 실전 예제

6.1 도형 면적 계산

// ═══════════════════════════════════════════════════════════
// 1. 트레이트 정의
// ═══════════════════════════════════════════════════════════
trait Area {
    fn area(&self) -> f64;
    //         ↑
    //         &self: 읽기 전용 참조
    //         -> f64: 64비트 부동소수점 반환
}

// ═══════════════════════════════════════════════════════════
// 2. 구조체 정의
// ═══════════════════════════════════════════════════════════
struct Circle {
    radius: f64,  // 반지름
}

struct Rectangle {
    width: f64,   // 너비
    height: f64,  // 높이
}

// ═══════════════════════════════════════════════════════════
// 3. 트레이트 구현
// ═══════════════════════════════════════════════════════════
impl Area for Circle {
    fn area(&self) -> f64 {
        // 원의 넓이 = π × r²
        3.14159 * self.radius * self.radius
        //               ↑
        //               self.radius로 필드 접근
    }
}

impl Area for Rectangle {
    fn area(&self) -> f64 {
        // 직사각형 넓이 = 가로 × 세로
        self.width * self.height
    }
}

// ═══════════════════════════════════════════════════════════
// 4. 사용
// ═══════════════════════════════════════════════════════════
fn main() {
    let circle = Circle { radius: 5.0 };
    let rect = Rectangle { width: 4.0, height: 3.0 };

    // 정적 디스패치
    println!("원 넓이: {}", circle.area());
    println!("사각형 넓이: {}", rect.area());

    // 동적 디스패치 (다양한 도형을 한 번에 처리)
    let shapes: Vec<&dyn Area> = vec![&circle, &rect];

    let total: f64 = shapes.iter().map(|s| s.area()).sum();
    println!("총 넓이: {}", total);
}

7. 핵심 정리

7.1 암기해야 할 패턴

// 패턴 1: 트레이트 정의
trait 이름 {
    fn 메서드(&self) -> 반환타입;        // 필수
    fn 기본메서드(&self) { ... }         // 선택
    type 연관타입;                       // 선택
    const 상수: 타입;                    // 선택
}

// 패턴 2: 트레이트 구현
impl 트레이트 for 타입 {
    // 필수 메서드 구현
}

// 패턴 3: 제네릭 + 트레이트 바운드
fn 함수<T: 트레이트>(param: T) { }

// 패턴 4: 동적 디스패치
fn 함수(param: &dyn 트레이트) { }

7.2 핵심 키워드 요약

키워드의미예시
trait트레이트 정의trait Summary { }
impl구현impl Summary for Article { }
for~에 대해impl Summary for Article
&self인스턴스 참조fn method(&self)
type연관 타입type Item = i32;
T: Trait트레이트 바운드fn f<T: Clone>(t: T)
dyn Trait동적 디스패치&dyn Speak

8. 장단점

8.1 장점

장점설명
다형성다양한 타입에 동일 인터페이스 적용
안전성컴파일 타임에 구현 여부 검증
재사용성기본 구현으로 코드 중복 감소
유연성외부 타입에도 트레이트 구현 가능
성능정적 디스패치 시 제로 오버헤드

8.2 단점

단점설명
학습 곡선제네릭 + 트레이트 + 라이프타임 결합 시 복잡
에러 메시지복잡한 제약 조건 시 긴 에러 메시지
고아 규칙외부 트레이트를 외부 타입에 구현 불가

🧒 어린이를 위한 설명

트레이트 = 동아리 가입 조건

┌─────────────────────────────────────────────────────────┐
│            "축구 동아리" 트레이트                         │
├─────────────────────────────────────────────────────────┤
│  가입 조건:                                              │
│  • 공을 찰 수 있어야 함      → 필수 메서드               │
│  • 달릴 수 있어야 함         → 필수 메서드               │
│  • 유니폼은 빌려줌 (선택)    → 기본 구현                 │
└─────────────────────────────────────────────────────────┘

다양한 친구들이 가입:

철수 (사람)      → 축구 동아리 가입 ✅ (공 차기, 달리기 가능)
로봇친구         → 축구 동아리 가입 ✅ (바퀴로 굴러가기 가능)
컴퓨터          → 축구 동아리 가입 ❌ (공을 찰 수 없음)

impl for = 동아리 가입 신청서

impl 축구동아리 for 철수
//   ↑              ↑
//   어느 동아리?    누가 가입?

→ "철수가 축구 동아리에 가입하겠다"
→ "이제 철수도 축구할 수 있다"

핵심 한 줄

트레이트는 "네가 무엇인지"가 아니라 "너는 무엇을 할 수 있는지"를 정의하는 거예요!