본문 바로가기

트레이트와 공통 동작

트레이트의 공통 동작과 기본 구현을 정의하고 trait bound·where·impl Trait로 허용할 타입의 능력을 제한합니다.

트레이트(trait)는 특정한 타입이 가지고 있으면서 다른 타입과 공유할 수 있는 기능을 정의합니다.

트레이트를 사용하면 공통된 기능을 추상적으로 정의할 수 있습니다.

트레이트 바운드(trait bound)를 이용하면 어떤 제네릭 타입 자리에 특정한 동작을 갖춘 타입이 올 수 있음을 명시할 수 있습니다.

이 절은 같은 Summary 정의를 점진적으로 바꾸므로 예제의 모든 정의를 동시에 붙이면 중복 이름이나 중복 구현이 생깁니다. aggregator의 src/lib.rs에는 선택한 Summary·구조체·impl 묶음을 두고, 다른 크레이트에서 쓸 때는 그 라이브러리 의존성을 먼저 연결합니다. 같은 패키지의 라이브러리와 바이너리로 따라 할 경우 패키지 이름이 aggregator인지 확인하고 src/main.rs에서 use로 가져옵니다. 아래 별도 완전 프로그램은 이 외부 의존성 준비 없이 동작 관계를 확인하는 대안입니다.

Note: 약간의 차이는 있으나, 트레이트는 다른 언어에서 흔히 인터페이스(interface)라고 부르는 기능과 유사합니다.


트레이트 정의하기

타입의 동작은 해당 타입에서 호출할 수 있는 메서드로 구성됩니다.

만약 다양한 타입에서 동일한 메서드를 호출할 수 있다면, 이 타입들은 동일한 동작을 공유한다고 표현할 수 있을 겁니다.

트레이트 정의는 메서드 시그니처를 그룹화하여 특정 목적을 달성하는 데 필요한 일련의 동작을 정의하는 것입니다.

예를 들어 다양한 종류 및 분량의 텍스트를 갖는 여러 가지 구조체가 있다고 칩시다.

NewsArticle 구조체는 특정 지역에서 등록된 뉴스 기사를 저장하고, Tweet 구조체는 최대 280자의 콘텐츠와 해당 트윗이 새 트윗인지, 리트윗인지, 다른 트윗의 대답인지를 나타내는 메타데이터를 저장합니다.

NewsArticle이나 Tweet 인스턴스에 저장된 데이터를 종합해 보여주는 종합 미디어 라이브러리 크레이트 aggregator를 만든다고 가정합시다.

이를 위해서는 각 타입의 요약 정보를 얻어와야 하는데, 인스턴스에서 summarize 메서드를 호출하여 이 요약 정보를 가져오려고 합니다.

예제 9-12는 이 동작을 공개 Summary 트레이트 정의로 표현합니다.

예제 9-12: summarize 메서드가 제공하는 동작으로 구성된 Summary 트레이트
src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}

trait 키워드 다음에 트레이트의 이름 Summary를 작성해 트레이트를 선언했습니다.

또한 몇몇 예제에서 보게 될 것처럼 트레이트를 pub으로 선언하여 이 크레이트에 의존하는 다른 크레이트가 이 트레이트를 사용할 수 있도록 하였습니다.

중괄호 안에는 이 트레이트를 구현할 타입의 동작을 묘사하는 메서드 시그니처를 선언했는데, 위의 경우는 fn summarize(&self) -> String입니다.

메서드 시그니처 뒤에는 중괄호로 시작하여 메서드를 구현하는 대신 세미콜론을 집어넣었습니다.

이 트레이트를 구현하는 각 타입이 메서드에 맞는 동작을 직접 제공해야 합니다.

컴파일러는 Summary 트레이트를 구현하는 모든 타입이 정확히 이와 같은 시그니처의 summarize 메서드를 갖도록 강제할 것입니다.

트레이트는 본문에 여러 메서드를 가질 수 있습니다.

메서드 시그니처는 한 줄에 하나씩 나열되며, 각 줄은 세미콜론으로 끝납니다.


특정 타입에 트레이트 구현하기

Summary 트레이트의 메서드 시그니처를 원하는 대로 정의했으니, 종합 미디어 크레이트의 각 타입에 Summary 트레이트를 구현해봅시다.

예제 9-13은 NewsArticle 구조체에 헤드라인, 저자, 지역 정보를 사용하여 summarize의 반환 값을 만드는 Summary 트레이트를 구현한 모습입니다.

Tweet 구조체에는 트윗 내용이 이미 280자로 제한되어 있음을 가정하고, 사용자명과 해당 트윗의 전체 텍스트를 가져오도록 summarize를 정의했습니다.

예제 9-13: NewsArticle과 Tweet 타입에 Summary 트레이트 구현하기
src/lib.rs
pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

어떤 타입에 대한 트레이트를 구현하는 것은 평범한 메서드를 구현하는 것과 비슷합니다.

다른 점은 impl 뒤에 구현하고자 하는 트레이트 이름을 적고, 그다음 for 키워드와 트레이트를 구현할 타입명을 명시한다는 점입니다.

impl 블록 안에는 트레이트 정의에서 정의된 메서드 시그니처를 집어넣되, 세미콜론 대신 중괄호를 사용하여 메서드 본문에 원하는 특정한 동작을 채워 넣습니다.

라이브러리가 NewsArticle과 Tweet에 대한 Summary 트레이트를 구현했으니, 크레이트 사용자는 NewsArticle과 Tweet 인스턴스에 대하여 보통의 메서드를 호출하는 것과 같은 방식으로 트레이트 메서드를 호출할 수 있습니다.

유일한 차이점은 크레이트 사용자가 타입뿐만 아니라 트레이트도 스코프로 가져와야 한다는 점입니다.

바이너리 크레이트가 aggregator 라이브러리 크레이트를 사용하는 방법에 대한 예제가 아래에 있습니다.

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

이 코드는 1 new tweet: horse_ebooks: of course, as you probably already know, people를 출력합니다.

aggregator 크레이트에 의존적인 다른 크레이트들 또한 Summary 트레이트를 스코프로 가져와서 자신들의 타입에 대해 Summary를 구현할 수 있습니다.

다음 예제처럼 제네릭 매개변수의 포함 관계가 복잡하지 않은 경우에는, 트레이트나 구현 대상 타입 중 적어도 하나가 현재 크레이트에 정의되어 있어야 한다고 이해할 수 있습니다. 실제 고아 규칙은 제네릭 타입 매개변수가 외부 타입에 어떻게 포함되는지도 검사하므로, 이 문장은 아래 사례를 읽기 위한 기본 직관입니다.

예를 들어, 우리가 만든 aggregator 크레이트의 일부 기능으로 Tweet 타입에 표준 라이브러리 트레이트인 Display 등을 구현할 수 있습니다.

Tweet 타입이 우리가 만든 aggregator 크레이트의 타입이기 때문입니다.

또한 aggregator 크레이트에서 Vec<T> 타입에 Summary 트레이트를 구현할 수도 있습니다.

마찬가지로 Summary 트레이트가 우리가 만든 aggregator 크레이트의 트레이트이기 때문입니다.

하지만 외부 타입에 외부 트레이트를 구현할 수는 없습니다.

예를 들어, 우리가 만든 aggregator 크레이트에서는 Vec<T>에 대한 Display 트레이트를 구현할 수 없습니다.

Vec<T>, Display 둘 다 우리가 만든 크레이트가 아닌 표준 라이브러리에 정의되어 있기 때문입니다.

이 제약은 프로그램의 특성 중 하나인 일관성(coherence), 보다 자세히는 고아 규칙(orphan rule)에서 나옵니다.

이 규칙으로 인해 다른 사람의 코드가 여러분의 코드를 망가뜨릴 수 없으며 반대의 경우도 마찬가지입니다.

이 규칙이 없다면 두 크레이트가 동일한 타입에 동일한 트레이트를 구현할 수 있게 되고, 러스트는 어떤 구현체를 이용해야 할지 알 수 없게 됩니다.

고아 규칙은 트레이트와 타입이 어느 크레이트에 정의되어 있는지를 함께 보아야 이해하기 쉽습니다.


기본 구현

타입에 트레이트를 구현할 때마다 모든 메서드를 구현할 필요는 없도록 트레이트의 메서드에 기본 동작을 제공할 수도 있습니다.

이러면 특정한 타입에 트레이트를 구현할 때 기본 동작을 유지할지 혹은 오버라이드(override)할지 선택할 수 있습니다.

예제 9-14는 예제 9-12에서 Summary 트레이트에 메서드 시그니처만 정의했던 것과는 달리 summarize 메서드에 기본 문자열을 명시하였습니다.

예제 9-14: summarize 메서드의 기본 구현이 있는 Summary 트레이트 정의하기
src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

NewsArticle 인스턴스에 기본 구현을 사용하려면 impl Summary for NewsArticle {}처럼 비어있는 impl 블록을 명시합니다.

NewsArticle에 summarize 메서드를 직접적으로 정의하지는 않았지만, NewsArticle은 Summary 트레이트를 구현하도록 지정되어 있으며, Summary 트레이트는 summarize 메서드의 기본 구현을 제공합니다.

결과적으로 아래처럼 NewsArticle 인스턴스에서 summarize 메서드를 여전히 호출할 수 있습니다.

    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());

이 코드는 New article available! (Read more...)를 출력합니다.

기본 구현을 생성한다고 해서 예제 9-13 코드의 Tweet의 Summary 구현을 변경할 필요는 없습니다.

기본 구현을 오버라이딩하는 문법과 기본 구현이 없는 트레이트 메서드를 구현하는 문법은 동일하기 때문입니다.

기본 구현 안쪽에서 트레이트의 다른 메서드를 호출할 수도 있습니다.

호출할 다른 메서드가 기본 구현을 제공하지 않는 메서드여도 상관없습니다.

이런 방식으로 트레이트는 구현자에게 작은 부분만 구현을 요구하면서 유용한 기능을 많이 제공할 수 있습니다.

예시로 알아봅시다.

Summary 트레이트에 summarize_author 메서드를 추가하고, summarize 메서드의 기본 구현 내에서 summarize_author 메서드를 호출하도록 만들어 보았습니다.

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

이 Summary를 어떤 타입에 구현할 때는 summarize_author만 정의하면 됩니다.

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

summarize_author를 정의하고 나면 Tweet 인스턴스에서 summarize를 호출할 수 있습니다.

이러면 summarize 기본 구현이 직접 정의한 summarize_author 메서드를 호출할 겁니다.

summarize_author만 구현하고 추가적인 코드를 전혀 작성하지 않았지만, Summary 트레이트는 summarize 메서드의 기능도 제공해 주는 것을 알 수 있습니다.

    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());

이 코드는 1 new tweet: (Read more from @horse_ebooks...)를 출력합니다.

어떤 메서드를 오버라이딩하는 구현을 하면 해당 메서드의 기본 구현을 호출할 수는 없다는 점을 주의하세요.

예시

다음은 별도 src/main.rs용 프로그램입니다. Post는 필수 메서드만 구현하여 기본 summarize를 쓰고, Alert는 summarize를 재정의합니다. notify_any의 두 입력은 각각 Summary만 충족하면 되지만, notify_same의 두 입력은 같은 T여야 합니다.

기본 구현과 두 입력의 타입 관계
trait Summary {
    fn summarize_author(&self) -> String;
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

struct Post { author: String }
struct Alert { author: String }

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

impl Summary for Alert {
    fn summarize_author(&self) -> String {
        format!("@{}", self.author)
    }
    fn summarize(&self) -> String {
        format!("ALERT {}", self.author)
    }
}

fn notify_any(a: &impl Summary, b: &impl Summary) -> String {
    format!("{} / {}", a.summarize(), b.summarize())
}

fn notify_same<T: Summary>(a: &T, b: &T) -> String {
    format!("{} / {}", a.summarize(), b.summarize())
}

fn main() {
    let post = Post { author: String::from("mina") };
    let other_post = Post { author: String::from("jun") };
    let alert = Alert { author: String::from("ops") };
    println!("{}", post.summarize());
    println!("{}", alert.summarize());
    println!("{}", notify_any(&post, &alert));
    println!("{}", notify_same(&post, &other_post));
}

출력의 네 줄은 다음과 같습니다.

(Read more from @mina...)
ALERT ops
(Read more from @mina...) / ALERT ops
(Read more from @mina...) / (Read more from @jun...)

Post의 첫 호출은 기본 본문에서 자신의 summarize_author를 부르고, Alert의 호출은 재정의 본문을 직접 사용합니다. 바운드를 충족한 뒤 어떤 내용을 반환할지는 각 구현의 계약이 정합니다. 두 함수는 입력을 빌려 읽으며 서로 다른 타입을 자동으로 하나의 타입으로 변환하지 않습니다.

독립 확인: 마지막 호출을 notify_same(&post, &alert)로 바꾸면 어떤 관계가 깨지나요? Post의 impl을 비워 impl Summary for Post {}로만 두면 기본 summarize가 있어도 통과할까요? Alert의 summarize 재정의만 지우면 그 출력은 무엇으로 바뀌나요?

필수 동작과 타입 관계 확인

Post와 Alert는 모두 Summary를 구현하지만 서로 다른 구체 타입이므로 같은 T를 요구하는 notify_same 호출은 거절됩니다. 비어 있는 Post impl은 필수 summarize_author를 제공하지 않아 거절됩니다. 기본 summarize가 이 메서드를 부른다는 사실이 필수 구현을 대신하지는 않습니다. Alert의 재정의만 지우면 기본 본문이 @ops를 받아 (Read more from @ops...)를 반환합니다.

Rust 트레이트 구현과 coherence 결정 흐름

Post.summarize가 기본 Summary.summarize를 사용하고 그 본문이 Post.summarize_author를 호출한 뒤 요약을 반환하는 실제 호출 순서다. Alert 재정의는 별도 호출 비교로 유지하며 Post 다음에 Alert로 전환하는 화살표를 넣지 않는다.

Rust 트레이트가 필수 메서드와 기본 구현으로 공통 동작을 정의하고, 현재 크레이트가 트레이트나 대상 타입을 정의한 기본 예제에서는 구현할 수 있지만 외부 트레이트와 외부 타입의 조합은 coherence와 고아 규칙 때문에 거부되는 결정 흐름

TRAIT CONTRACT · IMPL · DEFAULT · COHERENCE

트레이트 구현의 동작·위치 규칙

트레이트는 공유 동작 계약을 정의하고, impl Trait for Type은 구현을 선언합니다. 컴파일러는 시그니처와 구현 위치도 검사합니다.

기본 동작과 재정의의 호출 관계Post와 Alert는 서로 다른 두 호출 사례다. Post.summarize는 기본 Summary.summarize를 사용하고 그 본문이 Post.summarize_author를 호출한다. Alert.summarize는 재정의한 본문을 직접 사용한다. 실선은 동작 선택과 호출이며 Post에서 Alert로 이어지는 실행 화살표는 없다.기본 사용호출재정의Post.summarize()재정의 없음Summary.summarize기본 본문Post.summarize_author구현한 필수 동작Alert.summarize()재정의 있음Alert.summarize재정의 본문
기본 동작과 재정의의 호출 관계Post와 Alert는 서로 다른 두 호출 사례다. Post.summarize는 기본 Summary.summarize를 사용하고 그 본문이 Post.summarize_author를 호출한다. Alert.summarize는 재정의한 본문을 직접 사용한다. 실선은 동작 선택과 호출이며 Post에서 Alert로 이어지는 실행 화살표는 없다. 모바일의 위 세 노드는 Post 사례이고 아래 두 노드는 Alert 사례다. 서로 연결하지 않는다.기본 사용호출재정의Post · 기본 동작 사용Alert · 재정의 사용Post.summarize()재정의 없음Summary.summarize기본 본문Post의 필수 구현summarize_authorAlert.summarize()재정의 있음Alert.summarize재정의 본문
  1. 트레이트가 필수·기본 동작을 선언한다

    세미콜론 선언은 구현자가 채웁니다. 본문은 기본 동작이며 그대로 쓰거나 같은 시그니처로 재정의합니다.

  2. 트레이트와 타입의 정의 주체를 확인한다

    아래 예제에서는 트레이트나 타입 중 하나를 현재 crate가 정의하면 구현할 수 있습니다. 둘 다 외부 정의면 충돌을 막기 위해 거부됩니다.

  3. 허용된 impl이 계약을 완성한다

    필수 동작이 남으면 빈 impl은 불완전합니다. 각 동작은 기본 구현이 있거나 직접 제공되어야 계약이 성립합니다.

required and default methods

기본 동작은 필수 동작을 부를 수 있다

pub trait Summary {
    fn summarize_author(
        &self,
    ) -> String;

    fn summarize(&self) -> String {
        format!(
            "(Read more from {}...)",
            self.summarize_author(),
        )
    }
}

impl Summary for Tweet {
    fn summarize_author(
        &self,
    ) -> String {
        format!("@{}", self.username)
    }
}

Tweet은 summarize_author를 구현해 기본 summarize를 씁니다. 재정의하면 기본 본문을 부를 수 없습니다.

현재 문서의 세 예제로 읽는 구현 가능성
트레이트 정의 위치대상 타입 정의 위치예결과와 이유
현재 crate외부 crateSummary + Vec<T>allowed 현재 crate 소유 트레이트.
외부 crate현재 crateDisplay + Tweetallowed 현재 crate 소유 타입.
외부 crate외부 crateDisplay + Vec<T>rejected 외부 조합의 충돌 가능성.
local trait

Summary + Vec<T>

외부 타입이어도 이 crate의 트레이트면 허용됩니다.

local type

Display + Tweet

외부 트레이트여도 이 crate의 타입이면 허용됩니다.

both external

Display + Vec<T>

트레이트와 타입이 모두 외부라 이 crate에서는 거부됩니다.

“트레이트나 타입 중 하나는 내 것”은 직접 예제 요약입니다. 제네릭·fundamental type까지 반영한 컴파일러 coherence 검사가 최종 허용 여부를 판정합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
Post 호출

재정의가 없는 Post.summarize를 부른다.

  • 선택: Summary 기본 본문
  • 구체 타입: Post
  • Alert의 재정의와 이어 실행하는 과정이 아니다.
필수 동작 호출

기본 본문이 Post.summarize_author를 부른다.

  • 호출: Post.summarize_author
  • 필수 구현: Post가 제공
  • 기본 본문이 필수 메서드 구현을 대신해 주지 않는다.
요약 반환

author 결과로 기본 본문의 요약을 만든다.

  • 결과: 기본 본문의 요약 String
  • Alert: 독립 재정의 비교
  • 현재 Post 호출의 결과만 반환하며, 다른 타입으로 상태가 바뀌지 않는다.

Rust 트레이트 구현과 coherence 결정 흐름

Post.summarize가 기본 Summary.summarize를 사용하고 그 본문이 Post.summarize_author를 호출한 뒤 요약을 반환하는 실제 호출 순서다. Alert 재정의는 별도 호출 비교로 유지하며 Post 다음에 Alert로 전환하는 화살표를 넣지 않는다.

Rust 트레이트가 필수 메서드와 기본 구현으로 공통 동작을 정의하고, 현재 크레이트가 트레이트나 대상 타입을 정의한 기본 예제에서는 구현할 수 있지만 외부 트레이트와 외부 타입의 조합은 coherence와 고아 규칙 때문에 거부되는 결정 흐름

TRAIT CONTRACT · IMPL · DEFAULT · COHERENCE

트레이트 구현의 동작·위치 규칙

트레이트는 공유 동작 계약을 정의하고, impl Trait for Type은 구현을 선언합니다. 컴파일러는 시그니처와 구현 위치도 검사합니다.

기본 동작과 재정의의 호출 관계Post와 Alert는 서로 다른 두 호출 사례다. Post.summarize는 기본 Summary.summarize를 사용하고 그 본문이 Post.summarize_author를 호출한다. Alert.summarize는 재정의한 본문을 직접 사용한다. 실선은 동작 선택과 호출이며 Post에서 Alert로 이어지는 실행 화살표는 없다.기본 사용호출재정의Post.summarize()재정의 없음Summary.summarize기본 본문Post.summarize_author구현한 필수 동작Alert.summarize()재정의 있음Alert.summarize재정의 본문
기본 동작과 재정의의 호출 관계Post와 Alert는 서로 다른 두 호출 사례다. Post.summarize는 기본 Summary.summarize를 사용하고 그 본문이 Post.summarize_author를 호출한다. Alert.summarize는 재정의한 본문을 직접 사용한다. 실선은 동작 선택과 호출이며 Post에서 Alert로 이어지는 실행 화살표는 없다. 모바일의 위 세 노드는 Post 사례이고 아래 두 노드는 Alert 사례다. 서로 연결하지 않는다.기본 사용호출재정의Post · 기본 동작 사용Alert · 재정의 사용Post.summarize()재정의 없음Summary.summarize기본 본문Post의 필수 구현summarize_authorAlert.summarize()재정의 있음Alert.summarize재정의 본문
  1. 트레이트가 필수·기본 동작을 선언한다

    세미콜론 선언은 구현자가 채웁니다. 본문은 기본 동작이며 그대로 쓰거나 같은 시그니처로 재정의합니다.

  2. 트레이트와 타입의 정의 주체를 확인한다

    아래 예제에서는 트레이트나 타입 중 하나를 현재 crate가 정의하면 구현할 수 있습니다. 둘 다 외부 정의면 충돌을 막기 위해 거부됩니다.

  3. 허용된 impl이 계약을 완성한다

    필수 동작이 남으면 빈 impl은 불완전합니다. 각 동작은 기본 구현이 있거나 직접 제공되어야 계약이 성립합니다.

required and default methods

기본 동작은 필수 동작을 부를 수 있다

pub trait Summary {
    fn summarize_author(
        &self,
    ) -> String;

    fn summarize(&self) -> String {
        format!(
            "(Read more from {}...)",
            self.summarize_author(),
        )
    }
}

impl Summary for Tweet {
    fn summarize_author(
        &self,
    ) -> String {
        format!("@{}", self.username)
    }
}

Tweet은 summarize_author를 구현해 기본 summarize를 씁니다. 재정의하면 기본 본문을 부를 수 없습니다.

현재 문서의 세 예제로 읽는 구현 가능성
트레이트 정의 위치대상 타입 정의 위치예결과와 이유
현재 crate외부 crateSummary + Vec<T>allowed 현재 crate 소유 트레이트.
외부 crate현재 crateDisplay + Tweetallowed 현재 crate 소유 타입.
외부 crate외부 crateDisplay + Vec<T>rejected 외부 조합의 충돌 가능성.
local trait

Summary + Vec<T>

외부 타입이어도 이 crate의 트레이트면 허용됩니다.

local type

Display + Tweet

외부 트레이트여도 이 crate의 타입이면 허용됩니다.

both external

Display + Vec<T>

트레이트와 타입이 모두 외부라 이 crate에서는 거부됩니다.

“트레이트나 타입 중 하나는 내 것”은 직접 예제 요약입니다. 제네릭·fundamental type까지 반영한 컴파일러 coherence 검사가 최종 허용 여부를 판정합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
Post 호출

재정의가 없는 Post.summarize를 부른다.

  • 선택: Summary 기본 본문
  • 구체 타입: Post
  • Alert의 재정의와 이어 실행하는 과정이 아니다.
필수 동작 호출

기본 본문이 Post.summarize_author를 부른다.

  • 호출: Post.summarize_author
  • 필수 구현: Post가 제공
  • 기본 본문이 필수 메서드 구현을 대신해 주지 않는다.
요약 반환

author 결과로 기본 본문의 요약을 만든다.

  • 결과: 기본 본문의 요약 String
  • Alert: 독립 재정의 비교
  • 현재 Post 호출의 결과만 반환하며, 다른 타입으로 상태가 바뀌지 않는다.

매개변수로서의 트레이트

트레이트를 정의하고 구현하는 방법을 알아보았으니, 트레이트를 이용하여 어떤 함수가 다양한 타입으로 작동하게 만드는 법을 알아봅시다.

예제 9-13에서 NewsArticle, Tweet 타입에 구현한 Summary 트레이트를 사용하여, Summary 트레이트를 구현하는 어떤 타입의 item 매개변수에서 summarize 메서드를 호출하는 notify 함수를 정의하겠습니다.

이렇게 하려면 아래와 같이 impl Trait 문법을 사용합니다.

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

item 매개변수의 구체적 타입을 명시하는 대신 impl 키워드와 트레이트 이름을 명시했습니다.

이 매개변수에는 지정된 트레이트를 구현하는 타입이라면 어떤 타입이든 전달받을 수 있습니다.

notify 본문 내에서는 item에서 summarize와 같은 Summary 트레이트의 모든 메서드를 호출할 수 있습니다.

notify는 NewsArticle 인스턴스로도, Tweet 인스턴스로도 호출할 수 있습니다.

만약 Summary 트레이트를 구현하지 않는 String, i32 등의 타입으로 notify 함수를 호출하는 코드를 작성한다면 컴파일 에러가 발생합니다.

트레이트 바운드 문법

매개변수 위치의 impl Trait 문법은 간단하지만, 이 경우에는 이름 없는 제네릭 타입 매개변수에 트레이트 바운드(trait bound)를 붙인 형태로 이해할 수 있습니다. 이름을 붙인 형식은 다음과 같습니다.

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

앞서 본 예시와 동일한 코드지만, 더 장황합니다.

트레이트 바운드는 꺾쇠괄호 안의 제네릭 타입 매개변수 선언에 붙은 콜론(:) 뒤에 위치합니다.

impl Trait 문법이 단순한 상황에서는 편리하고 코드를 더 간결하게 만들어 주는 반면, 트레이트 바운드 문법은 더 복잡한 상황을 표현할 수 있습니다.

예를 들어, Summary를 구현하는 두 매개변수를 전달받는 함수를 구현할 때, impl Trait 문법으로 표현하면 다음과 같은 모양이 됩니다.

pub fn notify(item1: &impl Summary, item2: &impl Summary) {
    println!("{} / {}", item1.summarize(), item2.summarize());
}

item1과 item2가 (둘 다 Summary를 구현하는 타입이되) 서로 다른 타입이어도 상관없다면 impl Trait 문법 사용도 적절합니다.

하지만 만약 두 매개변수가 같은 타입으로 강제되어야 한다면, 이는 아래와 같이 트레이트 바운드를 사용해야 합니다.

pub fn notify<T: Summary>(item1: &T, item2: &T) {
    println!("{} / {}", item1.summarize(), item2.summarize());
}

item1 및 item2 매개변수의 타입으로 지정된 제네릭 타입 T는 함수를 호출할 때 item1, item2 인수 값의 구체적인 타입이 반드시 동일하도록 제한합니다.

+ 구문으로 트레이트 바운드를 여럿 지정하기

트레이트 바운드는 여러 개 지정될 수도 있습니다.

notify에서 item의 summarize 메서드뿐만 아니라 출력 포맷팅까지 사용하고 싶다고 가정해봅시다.

즉 notify의 정의를 할 때 item이 Display, Summary를 모두 구현해야 하도록 지정해야 합니다.

+ 문법을 사용하면 트레이트를 여러 개 지정할 수 있습니다.

use std::fmt::Display;

pub fn notify(item: &(impl Summary + Display)) {

+ 구문은 제네릭 타입의 트레이트 바운드에도 사용할 수 있습니다.

use std::fmt::Display;

pub fn notify<T: Summary + Display>(item: &T) {

두 개의 트레이트 바운드가 지정됐으니, notify 본문에서는 item의 summarize 메서드를 호출할 수도 있고 item을 {}로 포맷팅할 수도 있습니다.

where 조항으로 트레이트 바운드 정리하기

트레이트 바운드가 너무 많아지면 문제가 생깁니다.

제네릭마다 트레이트 바운드를 갖게 되면, 여러 제네릭 타입 매개변수를 사용하는 함수는 함수명과 매개변수 사이에 너무 많은 트레이트 바운드 정보를 담게 될 가능성이 있습니다.

이는 가독성을 해치기 때문에, 러스트는 트레이트 바운드를 함수 시그니처 뒤의 where 조항에 명시하는 대안을 제공합니다.

즉, 다음과 같이 작성하는 대신.

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

다음과 같이 where 조항을 사용할 수 있습니다.

use std::fmt::{Debug, Display};

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // 함수 본문
    0
}

트레이트 바운드로 도배되지 않고, 평범한 함수처럼 함수명과 매개변수 목록, 반환 타입이 붙어 있으니, 함수 시그니처를 읽기 쉬워집니다.

이 절 끝의 트레이트 바운드 다이어그램은 impl Trait, 제네릭 바운드, where 조항을 선택하는 기준을 함수 시그니처 관점에서 정리한 것입니다.


트레이트를 구현하는 타입을 반환하기

아래처럼 impl Trait 문법을 반환 타입 위치에 써서 어떤 트레이트를 구현한 타입의 값을 반환시키는 데에도 사용할 수 있습니다.

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

반환 타입에 구체적인 타입명이 아닌 impl Summary를 작성하여 returns_summarizable 함수는 Summary 트레이트를 구현하는 하나의 구체 타입을 숨겨 반환한다고 명시했습니다.

위의 경우 returns_summarizable 함수는 Tweet을 반환하지만, 이 함수를 호출하는 쪽의 코드에서는 구체적인 타입을 알 필요가 없습니다.

구현되는 트레이트로 반환 타입을 명시하는 기능은 12장에서 다룰 클로저 및 반복자의 컨텍스트에서 굉장히 유용합니다.

클로저와 반복자는 컴파일러만 아는 타입이나, 직접 명시하기에는 굉장히 긴 타입을 생성합니다.

impl Trait 문법을 사용하면 굉장히 긴 타입을 직접 작성할 필요 없이 Iterator 트레이트를 구현하는 어떤 타입이라고 간결하게 지정할 수 있습니다.

하지만 반환 위치의 impl Trait는 호출할 때마다 서로 다른 임의의 타입을 고르는 트레이트 객체가 아닙니다. 같은 제네릭 인스턴스에서는 모든 반환 경로가 동일한 구체 타입을 반환해야 합니다. 지금의 returns_summarizable에는 제네릭 매개변수가 없으므로 모든 호출에서 같은 숨은 타입을 반환합니다. 제네릭 함수라면 그 숨은 타입이 타입 매개변수에 의존할 수 있지만, 실행 중의 if 조건만으로 서로 다른 반환 타입을 고를 수는 없습니다.

다음은 반환형을 impl Summary로 지정하고 NewsArticle, Tweet 중 하나를 반환하는 코드 예시입니다.

이 코드는 컴파일할 수 없습니다.

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
    }
}

NewsArticle, Tweet 중 하나를 반환하는 행위는 impl Trait 문법이 컴파일러 내에 구현된 방식으로 인한 제약 때문에 허용되지 않습니다.

함수가 이런 식으로 동작하도록 만드는 방법은 16장의 ‘트레이트 객체를 사용하여 다른 타입의 값 허용하기’ 절에서 알아볼 예정입니다.


트레이트 바운드를 사용해 조건부로 메서드 구현하기

제네릭 타입 매개변수를 사용하는 impl 블록에 트레이트 바운드를 이용하면, 지정된 트레이트를 구현하는 타입에 대해서만 메서드를 구현할 수도 있습니다.

예를 들어, 예제 9-15의 Pair<T> 타입은 모든 T에서 새로운 Pair<T> 인스턴스를 반환하는 new 함수를 구현합니다. (4장의 ‘메서드 정의하기’ 절에서 다룬 것처럼 Self는 impl 블록에 대한 타입의 별칭이고, 지금의 경우에는 Pair<T>라는 점을 상기합시다.)

하지만 그다음의 impl 블록에서는 어떤 T 타입이 비교를 가능하게 해주는 PartialOrd 트레이트와 출력을 가능하게 만드는 Display 트레이트를 모두 구현한 타입인 경우에 대해서만 cmp_display 메서드를 구현하고 있습니다.

예제 9-15: 트레이트 바운드를 이용해 제네릭 타입에 조건부로 메서드 구현하기
src/lib.rs
use std::fmt::Display;

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

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

예시

조건부 메서드를 선택할 때 타입 이름과 능력을 따로 확인합니다. Pair<i32>는 new와 cmp_display를 모두 사용할 수 있습니다. 비교·표시 트레이트를 따로 구현하지 않은 사용자 타입 Token을 넣은 Pair<Token>도 new로 만들 수 있지만 cmp_display는 없습니다. new의 impl에는 그 바운드가 없기 때문입니다.

독립 확인: Pair::new(5, 5).cmp_display()는 어느 필드를 출력하나요? 어느 사용자 타입에 Display만 구현했다면 cmp_display를 쓸 수 있나요? f64가 두 바운드를 만족한다는 사실만으로 NaN도 일관된 최댓값을 출력한다고 보장할 수 있나요?

메서드 존재와 비교 정책 확인

5≥5가 참이라 x=5 쪽 문구를 출력합니다. Display만 있고 PartialOrd가 없다면 두 능력을 모두 요구하는 cmp_display를 사용할 수 없습니다. f64는 PartialOrd를 구현하지만 NaN 비교는 불가능할 수 있으므로 이 본문의 >= 조건만으로 모든 부동소수점 값의 최댓값 정책이 완성되는 것은 아닙니다. 바운드 충족, 분기 조건, 업무상 비교 정책을 각각 확인합니다.

타입이 특정 트레이트를 구현하는 경우에만 해당 타입에 트레이트를 구현할 수도 있습니다.

트레이트 바운드를 만족하는 모든 타입에 대해 트레이트를 구현하는 것을 포괄 구현(blanket implementations)이라 하며, 이는 러스트 표준 라이브러리 내에서 광범위하게 사용됩니다.

예를 들어, 표준 라이브러리는 Display 트레이트를 구현하는 모든 타입에 ToString 트레이트도 구현합니다.

표준 라이브러리의 impl 블록을 핵심 관계만 남겨 단순화하면 다음과 비슷합니다. 실제 표준 라이브러리 구현에는 ?Sized 같은 추가 범위와 메서드 본문이 있으며, 외부 크레이트가 아래 외부 트레이트 구현을 다시 정의할 수 있다는 뜻은 아닙니다.

impl<T: Display + ?Sized> ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

Display 트레이트가 구현된 모든 타입에서(ToString 트레이트에 정의된) to_string() 메서드를 호출할 수 있는 건 표준 라이브러리의 이 포괄 구현 덕분입니다.

예를 들어, 정수는 Display를 구현하므로 String 값으로 변환할 수 있습니다.

let s = 3.to_string();

포괄 구현은 트레이트 문서 페이지의 ‘구현자(Implementors)’ 절에 있습니다.

트레이트와 트레이트 바운드를 사용하면 제네릭 타입 매개변수로 코드 중복을 제거하면서 특정 동작을 하는 제네릭 타입이 필요하다는 사실을 컴파일러에게 전달할 수 있습니다.

컴파일러는 트레이트 바운드를 이용하여 코드에 사용된 구체적인 타입들이 올바른 동작을 제공하는지 검사합니다.

동적 타입 언어에서는 해당 타입이 정의하지 않은 메서드를 호출하면 런타임에 에러가 발생할 수 있습니다.

하지만 러스트는 컴파일 시점에 에러를 발생시켜 코드를 실행하기도 전에 문제를 해결하도록 강제합니다.

정적으로 디스패치되는 제네릭 코드에서는 따라서 호출 시점마다 해당 트레이트 구현 여부를 검사할 필요가 없습니다.

컴파일 시점에 이미 다 확인했기 때문이죠.

이는 메서드 본문 자체에 런타임 비용이 없다는 뜻도, dyn Trait의 동적 디스패치까지 사라진다는 뜻도 아닙니다. 트레이트 바운드는 우선 어떤 동작을 사용할 권한이 있는지를 컴파일 시점에 검사하는 계약입니다.

트레이트 바운드는 문법 모양보다 "어떤 타입이 어떤 약속을 증명해야 하는가"를 먼저 잡으면 읽기 쉬워집니다.

Rust 트레이트 바운드가 제네릭 본문이 사용할 능력을 컴파일 시점에 증명하고, 매개변수 impl Trait, 이름 있는 제네릭, 더하기 바운드, where 절, 반환 impl Trait, 조건부 impl과 포괄 impl에서 각각 다른 타입 관계와 API 범위를 만드는 선택표

CAPABILITY · TYPE RELATION · PLACEMENT · API

바운드는 장식이 아닌 동작 증명이다

제네릭 본문은 summarize, 비교, 출력에 필요한 동작만 요구합니다. 같은 능력 요구라도 타입 관계와 제약 위치에 따라 표기가 달라집니다.

의도부터 고르면 바운드 문법과 타입 관계가 함께 정해진다
의도대표 문법타입 관계와 사용 범위
입력별&impl Summary각 위치는 서로 다른 구체 타입을 받고, 그 인수에서 Summary 동작을 엽니다.
같은 타입<T: Summary>이름 T를 공유한 위치는 같은 구체 타입이며 함수 전체에서 그 관계를 재사용합니다.
복수 능력T: Summary + DisplayT가 두 트레이트를 모두 구현해야 요약과 출력 동작을 함께 쓸 수 있습니다.
긴 제약 정리where T: Display + Clone꺾쇠괄호 바운드와 같은 관계를 시그니처 뒤에 배치합니다.
반환 숨김-> impl Summary같은 제네릭 인스턴스의 반환 경로는 같은 숨은 구체 타입이며 호출자는 트레이트 동작만 봅니다.
조건부 APIPair<T> 조건부 implDisplay + PartialOrd여야 cmp_display가 생기고, new는 항상 남습니다.
포괄 구현Display → ToString표준 구현은 T: ?Sized까지 허용하며 coherence와 고아 규칙을 지킵니다.
independent input

&impl Summary

각 위치가 독립적인 구체 타입을 받을 수 있고 그 인수에서 Summary 동작을 엽니다.

same concrete type

<T: Summary>

같은 T 이름을 쓴 위치는 같은 구체 타입입니다.

multiple capabilities

Summary + Display

본문이 실제로 사용하는 여러 동작을 모두 요구합니다.

readable placement

where T: ...

의미는 그대로 두고 긴 제약을 where 절에 둡니다.

opaque return

-> impl Summary

같은 제네릭 인스턴스의 구체 타입은 숨겨도 모든 반환 경로의 타입은 같아야 합니다.

conditional impl

impl<T: ...> Pair<T>

cmp_display는 바운드를 충족해야 사용할 수 있습니다.

blanket impl

impl<T: ...> Trait for T

만족하는 모든 타입에 구현하므로 coherence와 충돌 규칙을 확인합니다.

conditional method set

Pair<T>의 기본 API와 조건부 API를 분리한다

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T> Pair<T>
where
    T: Display + PartialOrd,
{
    fn cmp_display(&self) {
        // bound가 비교와 출력을 허용
    }
}

new: 모든 T · cmp_display: 두 바운드 충족 T.

scope of the proof

바운드가 보장하지 않는 것까지 확대하지 않는다

바운드는 동작 사용만 허용합니다. 논리·수명·I/O 비용은 별도이며 trait object 디스패치와도 다릅니다.

impl Trait는 위치마다 다릅니다. 매개변수는 익명 타입 바운드, 반환은 한 구체 타입을 숨기는 opaque type입니다.