[2026] Rust 트레이트 | Trait, 제네릭, 트레이트 바운드
이 글의 핵심
트레이트(trait)는 “이 타입은 이런 메서드를 구현한다”는 공통 행동 묶음입니다. 제네릭과 함께 쓰면 여러 타입에 같은 틀을 씌우되, 소유권·참조 규칙은 그대로 유지할 수 있습니다.
들어가며
트레이트(trait)는 “이 타입은 이런 메서드를 구현한다”는 공통 행동 묶음입니다. 제네릭과 함께 쓰면 여러 타입에 같은 틀을 씌우되, 소유권·참조 규칙은 그대로 유지할 수 있습니다.
1. 트레이트 정의와 구현
기본 트레이트
다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
trait Drawable {
fn draw(&self);
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("원 그리기: 반지름 {}", self.radius);
}
}
impl Drawable for Rectangle {
fn draw(&self) {
println!("사각형 그리기: {}x{}", self.width, self.height);
}
}
fn main() {
let circle = Circle { radius: 5.0 };
let rect = Rectangle { width: 10.0, height: 20.0 };
circle.draw();
rect.draw();
}
기본 구현
다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
trait Summary {
fn summarize(&self) -> String {
String::from("(더 읽기...)")
}
}
struct Article {
title: String,
content: String,
}
impl Summary for Article {} // 기본 구현 사용
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} - {}", self.title, &self.content[..50])
}
}
2. 제네릭 (Generics)
제네릭 함수
다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![10, 50, 25, 100, 75];
let result = largest(&numbers);
println!("최댓값: {}", result);
let chars = vec!['a', 'z', 'm', 'b'];
let result = largest(&chars);
println!("최댓값: {}", result);
}
제네릭 구조체
다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
// 특정 타입에만 메서드 추가
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let int_point = Point::new(5, 10);
let float_point = Point::new(1.0, 4.0);
println!("거리: {}", float_point.distance_from_origin());
}
여러 타입 매개변수
다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct Pair<T, U> {
first: T,
second: U,
}
impl<T, U> Pair<T, U> {
fn new(first: T, second: U) -> Self {
Pair { first, second }
}
}
fn main() {
let pair = Pair::new(1, "hello");
println!("{}, {}", pair.first, pair.second);
}
3. 트레이트 바운드
기본 트레이트 바운드
아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
use std::fmt::{Display, Debug};
fn print_info<T: Display + Debug>(value: T) {
println!("Display: {}", value);
println!("Debug: {:?}", value);
}
fn main() {
print_info(42);
print_info("hello");
}
where 절
아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
fn complex_function<T, U>(t: T, u: U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
println!("{}", t);
println!("{:?}", u);
}
impl Trait
아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
fn returns_summarizable() -> impl Summary {
Article {
title: String::from("제목"),
content: String::from("내용"),
}
}
4. 표준 트레이트
Clone과 Copy
아래 코드는 rust를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#[derive(Clone)]
struct User {
name: String,
age: u32,
}
let user1 = User {
name: String::from("홍길동"),
age: 25,
};
let user2 = user1.clone(); // 명시적 복사
Debug와 Display
다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
use std::fmt;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
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: 10, y: 20 };
println!("{:?}", p); // Debug
println!("{}", p); // Display
}
5. 실전 예제
예제: 도형 시스템
다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
trait Shape {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn perimeter(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
}
fn print_shape_info<T: Shape>(shape: &T) {
println!("넓이: {:.2}", shape.area());
println!("둘레: {:.2}", shape.perimeter());
}
fn main() {
let circle = Circle { radius: 5.0 };
let rect = Rectangle { width: 10.0, height: 20.0 };
print_shape_info(&circle);
print_shape_info(&rect);
}
6. 트레이트 바운드와 제네릭 (실전)
제네릭 T에 어떤 트레이트를 구현해야 하는지 적으면 그게 트레이트 바운드입니다. 컴파일러는 호출 시점에 구체 타입을 단일화(monomorphization)해 코드를 생성하므로, 정적 디스패치에 가깝고 런타임 비용이 작습니다.
아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 단일 바운드
fn show<T: std::fmt::Display>(x: T) {
println!("{x}");
}
// 여러 바운드: + 로 나열하거나 where로 정리
fn clone_and_debug<T>(x: &T) -> String
where
T: Clone + std::fmt::Debug,
{
format!("{:?}", x.clone())
}
- T: Trait:
T는 해당 트레이트를 구현해야 함. - T: ?Sized: 크기가 정해지지 않은 타입(예:
str, 트레이트 객체)까지 받을 때T: ?Sized와 함께&T패턴을 씁니다. for<'a>(HRTB): 고급 주제로, “모든 수명'a에 대해” 같은 제약을 적을 때 사용합니다.
7. 연관 타입(Associated Types) vs 제네릭 매개변수
| 구분 | 연관 타입 (type Item) | 제네릭 (Iterator<Item = T> 스타일) |
|---|---|---|
| 의미 | 구현체가 하나의 연관 타입을 고정 | 호출부가 타입 매개변수를 넘김 |
| 예 | Iterator::Item | 여러 Item을 동시에 쓰기 어려움 |
| 언제 | “이 트레이트당 출력 타입은 하나”일 때 | 동일 트레이트를 다른 타입 인자로 여러 번 구현해야 할 때 |
| 아래 코드는 rust를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다. |
// 연관 타입: 구현마다 Output이 하나로 정해짐
trait Processor {
type Output;
fn process(&self) -> Self::Output;
}
// 제네릭 트레이트: 동일 타입에 대해 다른 T로 여러 impl 가능 (일부 제약 하에서)
trait Convert<T> {
fn convert(&self) -> T;
}
실무에서는 Iterator처럼 “결과 타입이 구현에 고정”이면 연관 타입, AsRef<T>처럼 타입 인자를 바꿔가며 쓰면 제네릭 트레이트 쪽이 자연스럽습니다.
8. 트레이트 객체 (dyn Trait)
동적 디스패치: 같은 슬롯에 서로 다른 구체 타입을 담을 때 dyn Trait + 포인터(&dyn Trait, Box<dyn Trait>)를 씁니다. vtable을 통해 메서드가 호출됩니다.
다음은 rust를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
trait Event {
fn name(&self) -> &str;
}
struct Click;
impl Event for Click {
fn name(&self) -> &str {
"click"
}
}
fn print_events(events: &[Box<dyn Event + Send>]) {
for e in events {
println!("{}", e.name());
}
}
- 객체 안전(object-safe): 트레이트 객체로 쓰려면 객체 안전 규칙을 만족해야 합니다(예:
Self: Sized가 아닌 연관 함수만 허용 등). - dyn Trait + Send + Sync: 스레드 간 공유할 때 자주 붙입니다.
9. Derive 매크로
#[derive(...)]는 컴파일러가 제공하는 도출 매크로로, 반복 구현을 줄여 줍니다.
자주 쓰는 것:
| 매크로 | 효과 |
|---|---|
Clone, Copy | 복사 의미 |
Debug, PartialEq, Eq | 디버그·동등 비교 |
Default | Default::default() |
Serialize/Deserialize | serde 사용 시 |
| 아래 코드는 rust를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요. |
#[derive(Debug, Clone, PartialEq, Eq, Default)]
// 타입 정의
struct Config {
port: u16,
host: String,
}
커스텀 Derive는 프로시저 매크로(derive 크레이트)로 만들며, 이 글 범위를 넘어서므로 The Rust Reference - Procedural Macros를 참고하면 됩니다.
10. 실전 패턴: Iterator, From/Into, Display/Debug
Iterator
표준 라이브러리의 반복자 체인은 트레이트 조합의 대표 예입니다. map, filter, collect는 모두 Iterator 트레이트에 의존합니다.
let sum: i32 = [1, 2, 3].iter().copied().filter(|x| x % 2 == 1).sum();
From / Into
From<T>를 구현하면 Into<U>가 자동으로 따라옵니다(역은 성립하지 않을 수 있음). 타입 변환 API를 하나로 통일할 때 씁니다.
아래 코드는 rust를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
struct UserId(u64);
impl From<u64> for UserId {
fn from(id: u64) -> Self {
UserId(id)
}
}
let id: UserId = 42.into();
Display vs Debug
Debug({:?}): 개발자용, 구조체 덤프.derive(Debug)로 충분한 경우가 많습니다.Display({}): 사용자-facing 문자열. 로그·UI에 맞게 직접fmt구현합니다. 아래 코드는 rust를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
use std::fmt;
struct Point(i32, i32);
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.0, self.1)
}
}
정리
핵심 요약
- Trait: 공통 동작 정의 (인터페이스)
- 제네릭: 타입 매개변수로 재사용성 향상
- 트레이트 바운드: 제네릭 타입 제약
- 표준 트레이트: Clone, Copy, Debug, Display
- impl Trait: 반환 타입 간소화
- 연관 타입 vs 제네릭: “구현당 하나의 연관 타입” vs “타입 인자를 바꿔가며 impl”
- dyn Trait: 동적 디스패치·객체 안전 규칙
- Derive: 반복 impl 축소; serde 등과 조합
- 실전:
Iterator체인,From/Into,Display/Debug역할 분리