이 글은 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 interface Rust 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
    }
}

차이점:

측면 Go Rust
문법 [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("alice@example.com"),
        nickname: None,
    };
    
    // 직렬화
    let json = serde_json::to_string(&user).unwrap();
    println!("{}", json);
    // {"name":"Alice","age":30,"emailAddress":"alice@example.com"}
    
    // 역직렬화
    let json = r#"{"name":"Bob","age":25,"emailAddress":"bob@example.com"}"#;
    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: "alice@example.com"}
json, _ := json.Marshal(user)

5.7 요약#

Go interface vs Rust trait#

측면 Go interface Rust 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 타입을 만드세요.