이 글은 Claude Opus 4.5 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.

트레이트(Trait)는 Rust의 다형성 메커니즘입니다. Go의 인터페이스와 비슷한 역할을 하지만, 구현 방식과 기능에서 큰 차이가 있습니다. 이 섹션에서는 트레이트와 제네릭을 Go와 비교하며 깊이 있게 학습합니다.


5.1 트레이트 기초#

트레이트란?#

트레이트는 타입이 구현해야 하는 메서드 집합을 정의합니다:

// 트레이트 정의
trait Summary {
    fn summarize(&self) -> String;
}

// 타입 정의
struct Article {
    title: String,
    author: String,
    content: String,
}

// 트레이트 구현
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}

Go interface vs Rust trait#

가장 큰 차이: 암묵적 vs 명시적

// Go: 암묵적 구현 (덕 타이핑)
type Summary interface {
    Summarize() string
}

type Article struct {
    Title   string
    Author  string
    Content string
}

// interface를 언급하지 않아도 자동으로 구현됨
func (a Article) Summarize() string {
    return a.Title + " by " + a.Author
}

// Article은 자동으로 Summary를 구현
var s Summary = Article{}  // OK
// Rust: 명시적 구현
trait Summary {
    fn summarize(&self) -> String;
}

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

// 반드시 impl Trait for Type으로 구현 명시
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}

// Summary 없이는 사용 불가
fn print_summary(item: &impl Summary) {
    println!("{}", item.summarize());
}

비교:

측면Go interfaceRust trait
구현 방식암묵적명시적
발견 시점런타임컴파일타임
확장성어디서든 구현고아 규칙 적용
디스패치항상 동적정적/동적 선택
메서드 기본 구현불가가능

기본 구현 (Default Implementation)#

Go에는 없는 기능:

trait Summary {
    fn summarize_author(&self) -> String;
    
    // 기본 구현 제공
    fn summarize(&self) -> String {
        format!("Read more from {}...", self.summarize_author())
    }
}

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

impl Summary for Article {
    // summarize_author만 구현하면 됨
    fn summarize_author(&self) -> String {
        self.author.clone()
    }
    // summarize()는 기본 구현 사용
}

let article = Article {
    author: String::from("Alice"),
    content: String::from("..."),
};
println!("{}", article.summarize());  // "Read more from Alice..."

고아 규칙 (Orphan Rule)#

외부 타입에 외부 트레이트를 구현할 수 없습니다:

// 우리 크레이트에서...

// OK: 우리 트레이트를 외부 타입에 구현
trait MyTrait {}
impl MyTrait for Vec<i32> {}

// OK: 외부 트레이트를 우리 타입에 구현
struct MyType;
impl std::fmt::Display for MyType {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "MyType")
    }
}

// 에러: 외부 트레이트를 외부 타입에 구현
// impl std::fmt::Display for Vec<i32> {}  // 불가!

해결책: newtype 패턴

// wrapper 타입 생성
struct Wrapper(Vec<String>);

impl std::fmt::Display for Wrapper {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

5.2 표준 라이브러리의 주요 트레이트#

Debug와 Display#

use std::fmt;

struct Point {
    x: i32,
    y: i32,
}

// Debug: {:?} 포맷, 개발자용
impl fmt::Debug for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
    }
}

// Display: {} 포맷, 사용자용
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: 1, y: 2 };
    println!("{:?}", p);  // Point { x: 1, y: 2 }
    println!("{}", p);    // (1, 2)
}

// derive로 자동 구현 (Debug만)
#[derive(Debug)]
struct Point2 { x: i32, y: i32 }

Go 비교:

// Go: String() 메서드가 fmt.Stringer 구현
type Point struct { X, Y int }

func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

Clone과 Copy#

// Clone: 명시적 깊은 복사
#[derive(Clone)]
struct Article {
    title: String,
    content: String,
}

let a1 = Article { title: String::from("Hi"), content: String::from("...") };
let a2 = a1.clone();  // 깊은 복사

// Copy: 암묵적 복사 (스택 데이터만)
#[derive(Copy, Clone)]  // Copy는 Clone 필요
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1;  // 복사 (p1 여전히 유효)

Copy가 될 수 있는 조건:

  • 모든 필드가 Copy 타입
  • Drop 구현이 없음

PartialEq, Eq, PartialOrd, Ord#

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Person {
    age: u32,
    name: String,
}

let alice = Person { age: 30, name: String::from("Alice") };
let bob = Person { age: 25, name: String::from("Bob") };

// PartialEq: == 연산자
println!("{}", alice == bob);  // false

// Ord: 비교 연산자와 정렬
println!("{}", alice > bob);   // true (age로 먼저 비교)

let mut people = vec![alice, bob];
people.sort();  // Ord 필요

Partial vs Full:

  • PartialEq: 모든 값이 비교 가능하지 않을 수 있음 (예: f64::NAN)
  • Eq: 모든 값이 자기 자신과 같음
  • PartialOrd: 일부 값은 비교 불가 (예: f64::NAN)
  • Ord: 모든 값이 비교 가능 (전순서)

Default#

#[derive(Default)]
struct Config {
    debug: bool,        // false
    timeout: u32,       // 0
    name: String,       // ""
}

let config = Config::default();

// 커스텀 기본값
struct ServerConfig {
    port: u16,
    max_connections: u32,
}

impl Default for ServerConfig {
    fn default() -> Self {
        ServerConfig {
            port: 8080,
            max_connections: 100,
        }
    }
}

// 일부만 지정하고 나머지는 기본값
let config = ServerConfig {
    port: 3000,
    ..Default::default()
};

From과 Into#

// From 구현
struct Wrapper(String);

impl From<String> for Wrapper {
    fn from(s: String) -> Self {
        Wrapper(s)
    }
}

impl From<&str> for Wrapper {
    fn from(s: &str) -> Self {
        Wrapper(s.to_string())
    }
}

// From을 구현하면 Into는 자동
let w1: Wrapper = String::from("hello").into();
let w2: Wrapper = "hello".into();
let w3 = Wrapper::from("hello");

Iterator#

struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self {
        Counter { count: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;  // 연관 타입
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let counter = Counter::new(5);
    for n in counter {
        println!("{}", n);  // 1, 2, 3, 4, 5
    }
}

Drop#

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping: {}", self.data);
    }
}

fn main() {
    let a = CustomSmartPointer { data: String::from("A") };
    let b = CustomSmartPointer { data: String::from("B") };
    println!("Created pointers");
}
// 출력:
// Created pointers
// Dropping: B
// Dropping: A

5.3 제네릭#

제네릭 함수#

// 제네릭 함수
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);

let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);

제네릭 구조체#

struct Point<T> {
    x: T,
    y: T,
}

// 다른 타입의 x, y
struct Point2<T, U> {
    x: T,
    y: U,
}

let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
let mixed_point = Point2 { x: 5, y: 4.0 };

제네릭 메서드#

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// 특정 타입에만 구현
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

let p = Point { x: 3.0, y: 4.0 };
println!("Distance: {}", p.distance_from_origin());  // 5.0

let p2 = Point { x: 3, y: 4 };
// p2.distance_from_origin();  // 에러! Point<i32>에는 없음

Go 1.18+ 제네릭과의 비교#

// Go 제네릭 (1.18+)
func Largest[T constraints.Ordered](list []T) T {
    largest := list[0]
    for _, item := range list {
        if item > largest {
            largest = item
        }
    }
    return largest
}

type Point[T any] struct {
    X, Y T
}

func (p Point[T]) GetX() T {
    return p.X
}
// Rust 제네릭
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

차이점:

측면GoRust
문법[T constraint]<T: Bound>
제약constraints 패키지트레이트
복잡한 제약제한적where 절로 풍부
연관 타입없음있음
구현항상 단형화단형화 또는 동적

단형화 (Monomorphization)#

Rust는 제네릭 코드를 컴파일할 때 단형화합니다:

// 소스 코드
fn double<T: std::ops::Mul<Output = T> + Copy>(x: T) -> T {
    x * x
}

let a = double(5i32);
let b = double(3.14f64);

컴파일 후:

// 컴파일러가 생성한 코드 (개념적)
fn double_i32(x: i32) -> i32 {
    x * x
}

fn double_f64(x: f64) -> f64 {
    x * x
}

let a = double_i32(5);
let b = double_f64(3.14);

장점: 런타임 비용 없음 (정적 디스패치) 단점: 바이너리 크기 증가 가능


5.4 트레이트 바운드#

기본 문법#

// 방법 1: 함수 시그니처에 직접
fn print_info<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

// 방법 2: impl Trait (간편 문법)
fn print_info(item: &impl Summary) {
    println!("{}", item.summarize());
}

// 방법 3: where 절
fn print_info<T>(item: &T)
where
    T: Summary,
{
    println!("{}", item.summarize());
}

다중 바운드#

// + 로 여러 트레이트 요구
fn print_and_clone<T: Summary + Clone>(item: &T) {
    println!("{}", item.summarize());
    let cloned = item.clone();
}

// where 절이 더 읽기 쉬움
fn complex_function<T, U>(t: &T, u: &U) -> i32
where
    T: Summary + Clone,
    U: Debug + PartialEq,
{
    // ...
}

Go의 타입 제약과 비교#

// Go: 인터페이스로 제약
type Number interface {
    int | int64 | float64
}

func Sum[T Number](numbers []T) T {
    var sum T
    for _, n := range numbers {
        sum += n
    }
    return sum
}
// Rust: 트레이트로 제약
use std::ops::Add;
use std::iter::Sum;

fn sum<T>(numbers: &[T]) -> T
where
    T: Add<Output = T> + Sum + Copy,
{
    numbers.iter().copied().sum()
}

조건부 구현#

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

// 모든 T에 대해 구현
impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

// Display + PartialOrd를 구현하는 T에 대해서만
impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("Largest: {}", self.x);
        } else {
            println!("Largest: {}", self.y);
        }
    }
}

블랭킷 구현:

// 표준 라이브러리의 예
// Display를 구현하는 모든 타입에 ToString 자동 구현
impl<T: Display> ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

5.5 고급 트레이트 기능#

연관 타입 (Associated Types)#

제네릭 파라미터 대신 트레이트 내부에 타입 정의:

// 연관 타입 사용
trait Iterator {
    type Item;  // 연관 타입
    
    fn next(&mut self) -> Option<Self::Item>;
}

impl Iterator for Counter {
    type Item = u32;  // 구현 시 타입 지정
    
    fn next(&mut self) -> Option<Self::Item> {
        // ...
    }
}

// 제네릭으로 했다면...
trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

// 같은 타입에 여러 구현 가능 (혼란)
impl Iterator<u32> for Counter { /* ... */ }
impl Iterator<i32> for Counter { /* ... */ }

연관 타입 vs 제네릭:

  • 연관 타입: 타입당 하나의 구현만 (명확)
  • 제네릭: 타입당 여러 구현 가능 (유연하지만 복잡)

트레이트 객체: dyn Trait#

동적 디스패치:

trait Animal {
    fn speak(&self);
}

struct Dog;
impl Animal for Dog {
    fn speak(&self) { println!("Woof!"); }
}

struct Cat;
impl Animal for Cat {
    fn speak(&self) { println!("Meow!"); }
}

fn main() {
    // 정적 디스패치 (컴파일 타임에 결정)
    fn make_speak<T: Animal>(animal: &T) {
        animal.speak();
    }
    
    // 동적 디스패치 (런타임에 결정)
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
    ];
    
    for animal in &animals {
        animal.speak();  // vtable을 통한 호출
    }
}

Go interface와의 유사성:

// Go: 항상 동적 디스패치
type Animal interface {
    Speak()
}

type Dog struct{}
func (Dog) Speak() { fmt.Println("Woof!") }

type Cat struct{}
func (Cat) Speak() { fmt.Println("Meow!") }

func main() {
    animals := []Animal{Dog{}, Cat{}}
    for _, a := range animals {
        a.Speak()  // 동적 디스패치
    }
}

객체 안전성 (Object Safety)#

모든 트레이트가 dyn Trait로 사용될 수 있는 것은 아닙니다:

// 객체 안전 (dyn 사용 가능)
trait Animal {
    fn speak(&self);
}

// 객체 불안전 (dyn 사용 불가)
trait Clone {
    fn clone(&self) -> Self;  // Self 반환 = 객체 불안전
}

trait Sized {}  // Sized 바운드 = 객체 불안전

// 이유: 동적 디스패치 시 반환 타입 크기를 알 수 없음

객체 안전 조건:

  • 반환 타입이 Self가 아님
  • 제네릭 타입 파라미터가 없음
  • Self: Sized 바운드가 없음

impl Trait#

반환 타입에서:

// 구체적 타입을 숨김
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

// 복잡한 이터레이터 타입 숨김
fn even_numbers() -> impl Iterator<Item = i32> {
    (0..).filter(|n| n % 2 == 0)
}

매개변수에서 (impl Trait vs 제네릭):

// 이 둘은 비슷하지만...
fn process(item: &impl Summary) { /* ... */ }
fn process<T: Summary>(item: &T) { /* ... */ }

// 제네릭은 터보피시 가능
process::<Article>(&article);

// impl Trait은 불가
// process::<Article>(&article);  // 에러

정적 vs 동적 디스패치 비교#

// 정적 디스패치 (impl Trait / 제네릭)
fn speak_static(animal: &impl Animal) {
    animal.speak();
}
// 장점: 인라인 가능, 최적화, 성능
// 단점: 바이너리 크기 증가 (단형화)

// 동적 디스패치 (dyn Trait)
fn speak_dynamic(animal: &dyn Animal) {
    animal.speak();
}
// 장점: 바이너리 크기 작음, 유연성
// 단점: vtable 오버헤드, 인라인 불가

5.6 Derive 매크로#

#[derive(…)] 속성#

// 여러 트레이트 자동 구현
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct User {
    name: String,
    age: u32,
}

// 이것은 이것과 동등:
impl Debug for User { /* ... */ }
impl Clone for User { /* ... */ }
impl PartialEq for User { /* ... */ }
// ...

자동 구현 가능한 트레이트들#

트레이트설명요구사항
Debug{:?} 포맷모든 필드가 Debug
Clone.clone()모든 필드가 Clone
Copy암묵적 복사모든 필드가 Copy, Clone 필요
PartialEq== 비교모든 필드가 PartialEq
Eq반사적 동등성PartialEq 필요
PartialOrd<, > 비교PartialEq 필요
Ord전순서PartialOrd + Eq 필요
Hash해시 가능모든 필드가 Hash
Default기본값모든 필드가 Default

serde와 직렬화/역직렬화#

# Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    age: u32,
    #[serde(rename = "emailAddress")]
    email: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    nickname: Option<String>,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        age: 30,
        email: String::from("[email protected]"),
        nickname: None,
    };
    
    // 직렬화
    let json = serde_json::to_string(&user).unwrap();
    println!("{}", json);
    // {"name":"Alice","age":30,"emailAddress":"[email protected]"}
    
    // 역직렬화
    let json = r#"{"name":"Bob","age":25,"emailAddress":"[email protected]"}"#;
    let user: User = serde_json::from_str(json).unwrap();
    println!("{:?}", user);
}

Go의 encoding/json과 비교:

// Go
type User struct {
    Name     string `json:"name"`
    Age      int    `json:"age"`
    Email    string `json:"emailAddress"`
    Nickname string `json:"nickname,omitempty"`
}

user := User{Name: "Alice", Age: 30, Email: "[email protected]"}
json, _ := json.Marshal(user)

5.7 요약#

Go interface vs Rust trait#

측면Go interfaceRust trait
구현암묵적명시적 (impl Trait for Type)
디스패치항상 동적정적 또는 동적 선택
기본 구현불가가능
연관 타입불가가능
제네릭별도 문법트레이트 바운드로 통합
조건부 구현불가가능

언제 무엇을 사용할까#

상황선택
성능이 중요제네릭 + 트레이트 바운드 (정적)
다양한 타입을 컬렉션에 저장dyn Trait (동적)
반환 타입 숨기기impl Trait
외부 타입에 기능 추가newtype + 트레이트 구현

연습 문제#

  1. 트레이트 정의: Drawable 트레이트를 정의하고 Circle, Rectangle에 구현하세요.

  2. 제네릭 함수: 두 값 중 큰 값을 반환하는 제네릭 함수 max를 작성하세요.

  3. 연관 타입: 키-값 저장소를 나타내는 Storage 트레이트를 연관 타입으로 정의하세요.

  4. 트레이트 객체: Vec<Box<dyn Drawable>>를 만들고 모든 요소를 그리는 코드를 작성하세요.

  5. serde 활용: JSON 파일을 읽어 구조체로 역직렬화하고, 수정 후 다시 저장하는 프로그램을 작성하세요.

  6. 조건부 구현: Display를 구현하는 타입에 대해서만 println! 메서드를 제공하는 wrapper 타입을 만드세요.