💻 프로그래밍

Trait

트레이트

Rust 공유 동작 정의. 인터페이스 유사. 제네릭과 결합.

📖 상세 설명

Trait(트레이트)는 타입이 가져야 할 공유 동작(behavior)을 정의하는 추상화 메커니즘입니다. Interface와 유사하게 메서드 시그니처를 선언하지만, 핵심적인 차이점은 기본 구현(default implementation)을 포함할 수 있다는 점입니다. 즉, Trait는 "무엇을 할 수 있는가"를 정의하면서도 "어떻게 하는가"의 기본 방법까지 제공할 수 있어서, Interface의 계약 역할과 추상 클래스의 코드 재사용을 동시에 달성합니다.

Rust에서 Trait는 언어의 핵심 기능입니다. impl Trait for Type 구문으로 타입에 Trait를 구현하고, 제네릭과 결합해 강력한 다형성을 제공합니다. Rust의 유명한 Trait들로는 Clone(복제), Debug(디버그 출력), Iterator(반복), Send/Sync(스레드 안전성)가 있습니다. 특히 Trait Bound(trait + 제약조건)를 통해 컴파일 타임에 타입 안전성을 보장하면서도 제로 코스트 추상화를 달성하는 것이 Rust Trait의 강점입니다.

PHP와 Scala에서의 Trait는 약간 다른 의미를 가집니다. PHP의 Trait는 클래스 간에 메서드를 공유하는 코드 재사용 메커니즘으로, use 키워드로 클래스에 포함시킵니다. 단일 상속의 한계를 극복하면서 다중 상속의 다이아몬드 문제를 피하는 실용적인 해결책입니다. Scala의 Trait도 비슷하게 믹스인(mixin) 방식으로 여러 Trait를 조합하여 클래스에 기능을 추가할 수 있습니다.

Trait는 객체지향 프로그래밍에서 다중 상속의 대안으로 널리 사용됩니다. 전통적인 다중 상속은 다이아몬드 문제(같은 메서드를 가진 두 부모 클래스)와 복잡한 상속 계층 문제를 야기하는데, Trait는 수평적인 코드 공유를 통해 이를 해결합니다. 현대 언어들(Rust, Scala, PHP, Kotlin의 interface default method)은 Trait 개념을 채택하여 유연하고 안전한 코드 재사용을 지원합니다.

💻 코드 예제

// Rust - Trait 정의와 구현 예제

// Trait 정의: 직렬화 가능한 타입의 동작 정의
trait Serializable {
    // 필수 구현 메서드 (시그니처만)
    fn serialize(&self) -> String;

    // 기본 구현이 있는 메서드
    fn serialize_pretty(&self) -> String {
        format!("[\n  {}\n]", self.serialize())
    }
}

// Trait 정의: 요약 기능
trait Summary {
    fn summarize(&self) -> String;

    // 기본 구현: summarize()를 호출
    fn summarize_author(&self) -> String {
        String::from("(작성자 미상)")
    }
}

// 구조체 정의
struct Article {
    title: String,
    author: String,
    content: String,
}

struct Tweet {
    username: String,
    content: String,
    retweets: u32,
}

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

    fn summarize_author(&self) -> String {
        format!("@{}", self.author)
    }
}

impl Serializable for Article {
    fn serialize(&self) -> String {
        format!(
            r#"{{"title":"{}","author":"{}","content":"{}"}}"#,
            self.title, self.author, self.content
        )
    }
}

// Tweet에 Trait 구현
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {} (RT: {})", self.username, self.content, self.retweets)
    }
    // summarize_author는 기본 구현 사용
}

// Trait Bound를 사용한 제네릭 함수
fn notify(item: &T) {
    println!("속보! {}", item.summarize());
}

// 여러 Trait를 요구하는 함수 (+ 문법)
fn process(item: &T) {
    println!("요약: {}", item.summarize());
    println!("JSON: {}", item.serialize());
}

// where 절을 사용한 더 깔끔한 Trait Bound
fn complex_process(t: &T, u: &U) -> String
where
    T: Summary + Serializable,
    U: Summary,
{
    format!("{} & {}", t.summarize(), u.summarize())
}

fn main() {
    let article = Article {
        title: String::from("Rust Trait 완전 정복"),
        author: String::from("김러스트"),
        content: String::from("Trait는 Rust의 핵심 기능입니다..."),
    };

    let tweet = Tweet {
        username: String::from("rustacean"),
        content: String::from("Rust 최고!"),
        retweets: 42,
    };

    // Trait 메서드 호출
    println!("{}", article.summarize());        // "Rust Trait 완전 정복, by 김러스트"
    println!("{}", article.summarize_author()); // "@김러스트"
    println!("{}", tweet.summarize_author());   // "(작성자 미상)" - 기본 구현

    // 제네릭 함수 호출
    notify(&article);   // "속보! Rust Trait 완전 정복, by 김러스트"
    notify(&tweet);     // "속보! rustacean: Rust 최고! (RT: 42)"

    // 여러 Trait 요구하는 함수
    process(&article);  // Article은 Summary + Serializable 둘 다 구현
    // process(&tweet); // 컴파일 에러! Tweet은 Serializable 미구현
}

🗣️ 실무에서 이렇게 말하세요

💬 기술 면접에서 (Trait vs Interface)
"Trait와 Interface의 핵심 차이는 '기본 구현 포함 여부'입니다. Interface는 순수 계약으로 '무엇을 해야 하는가'만 정의하지만, Trait는 '어떻게 하는가'까지 기본 구현을 제공할 수 있습니다. Rust에서는 Iterator trait처럼 next() 하나만 구현하면 map, filter, collect 등 수십 개의 기본 메서드를 공짜로 얻는 강력한 코드 재사용이 가능합니다."
💬 설계 회의에서 (코드 재사용)
"이 프로젝트 모든 모델에 감사 로깅이랑 소프트 딜리트 기능이 필요한데, 상속으로 해결하면 계층이 복잡해집니다. Auditable, SoftDeletable Trait로 분리해서 필요한 모델에만 use로 추가하면 훨씬 유연해요. PHP에서 이 패턴 많이 쓰는데, Laravel Eloquent의 HasFactory, SoftDeletes가 대표적인 예시입니다."
💬 코드 리뷰에서
"이 Rust 함수에서 impl Summary 대신 dyn Summary를 쓰셨는데, 동적 디스패치가 필요한 상황인가요? 단일 타입만 반환한다면 impl Trait가 성능상 유리합니다. 여러 타입을 Box로 감싸서 반환해야 할 때만 dyn을 쓰는 게 좋습니다. Trait Object는 vtable 오버헤드가 있거든요."

⚠️ 흔한 실수 & 주의사항

Rust Orphan Rule 위반 시도

외부 크레이트의 타입에 외부 크레이트의 Trait를 구현할 수 없습니다. 예: Vec에 Display 구현 불가. 해결책은 newtype 패턴(struct MyVec(Vec<T>))으로 래핑하거나, 자체 Trait를 정의하는 것입니다.

PHP Trait 메서드 충돌 무시

같은 이름의 메서드를 가진 두 Trait를 use하면 Fatal Error가 발생합니다. 반드시 insteadof로 우선순위를 지정하거나, as로 별칭을 만들어야 합니다. IDE 경고를 무시하지 마세요.

Trait에 상태(필드) 과도하게 의존

PHP Trait에서 프로퍼티를 정의할 수 있지만, 이를 남용하면 클래스 내부 상태가 불투명해집니다. Trait는 행동(메서드) 공유에 집중하고, 상태는 최소화하세요. 상태가 많이 필요하면 컴포지션 패턴을 고려하세요.

작고 단일 책임을 가진 Trait 설계

하나의 Trait가 너무 많은 메서드를 가지면 구현 부담이 커집니다. Rust 표준 라이브러리처럼 Clone, Copy, Debug를 분리하듯이, 관련 기능끼리 작은 Trait로 나누면 필요한 것만 선택적으로 구현할 수 있습니다.

🔗 관련 용어

📚 더 배우기