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 철수
// ↑ ↑
// 어느 동아리? 누가 가입?
→ "철수가 축구 동아리에 가입하겠다"
→ "이제 철수도 축구할 수 있다"
핵심 한 줄
트레이트는 "네가 무엇인지"가 아니라 "너는 무엇을 할 수 있는지"를 정의하는 거예요!