Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

트레이트로 공통 동작 정의하기

트레이트(trait) 는 특정 타입이 가지는 기능을 정의하며, 그 기능을 다른 타입과 공유할 수 있게 해 줍니다. 트레이트를 사용하면 공통 동작을 추상적인 방식으로 정의할 수 있습니다. 또한 트레이트 바운드(trait bounds) 를 사용하면, 제네릭 타입이 “아무 타입이나” 가 아니라 어떤 특정 동작을 가진 타입만 받아들이도록 제한할 수 있습니다.

Note: 트레이트는 다른 언어에서 흔히 인터페이스(interface) 라고 부르는 기능과 비슷하지만, 몇 가지 차이가 있습니다.

트레이트 정의하기

타입의 동작은 그 타입에 대해 호출할 수 있는 메서드들로 이루어집니다. 서로 다른 여러 타입에서 같은 메서드를 호출할 수 있다면, 그 타입들은 같은 동작을 공유한다고 할 수 있습니다. 트레이트 정의는 메서드 시그니처들을 한데 모아, 어떤 목적을 달성하기 위해 필요한 동작 집합을 정의하는 방법입니다.

예를 들어 서로 다른 종류와 길이의 텍스트를 담는 여러 구조체가 있다고 합시다. 하나는 특정 장소에서 작성된 뉴스 기사를 담는 NewsArticle 구조체이고, 다른 하나는 최대 280자까지의 글과 함께 새 글인지, 재게시인지, 답글인지 같은 메타데이터를 담는 SocialPost 구조체입니다.

우리는 이런 데이터들을 요약해서 보여 줄 수 있는 aggregator 라는 미디어 집계 라이브러리 크레이트를 만들고 싶습니다. 그렇게 하려면 각 타입에서 요약을 얻을 수 있어야 하고, 인스턴스에서 summarize 메서드를 호출해 그 요약을 받아오고 싶습니다. 목록 10-12는 이런 동작을 표현하는 public Summary 트레이트 정의를 보여 줍니다.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}
Listing 10-12: summarize 메서드가 제공하는 동작으로 이루어진 Summary 트레이트

여기서는 trait 키워드와 트레이트 이름 Summary 를 사용해 트레이트를 선언합니다. 이 트레이트를 사용하는 다른 크레이트도 접근할 수 있도록 pub 으로 선언했다는 점도 주의하세요. 중괄호 안에는 이 트레이트를 구현하는 타입들이 가져야 할 동작을 설명하는 메서드 시그니처를 적습니다. 이 경우에는 fn summarize(&self) -> String 하나입니다.

메서드 시그니처 뒤에는 중괄호로 본문을 쓰는 대신 세미콜론만 붙입니다. 이 트레이트를 구현하는 각 타입은 이 메서드 본문에 대해 자기만의 구체적인 동작을 제공해야 합니다. 컴파일러는 Summary 트레이트를 구현한 어떤 타입이든 정확히 이 시그니처를 가진 summarize 메서드를 정의했는지 확인합니다.

하나의 트레이트는 여러 메서드를 가질 수도 있습니다. 시그니처를 한 줄에 하나씩 적고, 각 줄은 세미콜론으로 끝내면 됩니다.

타입 위에 트레이트 구현하기

이제 Summary 트레이트 메서드의 원하는 시그니처를 정의했으니, 이를 미디어 집계기 안의 각 타입에 구현할 수 있습니다. 목록 10-13은 NewsArticle 구조체에 대해 Summary 트레이트를 구현한 예를 보여 줍니다. 여기서는 제목, 작성자, 장소를 이용해 summarize 의 반환값을 만듭니다. SocialPost 구조체의 경우에는, 게시물 본문이 이미 280자로 제한된다고 가정하고 사용자 이름과 본문 전체를 이어 붙여 요약을 만듭니다.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}

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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Listing 10-13: NewsArticleSocialPost 타입에 Summary 트레이트 구현하기

타입 위에 트레이트를 구현하는 방식은 일반 메서드 구현과 비슷합니다. 차이점은 impl 뒤에 구현하려는 트레이트 이름을 쓰고, 그 뒤에 for 키워드, 그리고 트레이트를 구현할 타입 이름을 쓴다는 점입니다. impl 블록 안에는 트레이트 정의가 요구한 메서드 시그니처들을 적습니다. 단, 시그니처마다 세미콜론을 붙이는 대신 중괄호와 실제 메서드 본문을 넣어 그 타입에 맞는 구체 동작을 정의합니다.

이제 라이브러리는 NewsArticleSocialPostSummary 를 구현했으므로, 크레이트 사용자는 이 타입 인스턴스에서 일반 메서드처럼 트레이트 메서드를 호출할 수 있습니다. 차이가 있다면 사용자 쪽 코드도 타입과 함께 그 트레이트를 스코프로 가져와야 한다는 점입니다. 다음은 바이너리 크레이트가 우리 aggregator 라이브러리 크레이트를 사용하는 예입니다.

use aggregator::{SocialPost, Summary};

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

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

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

aggregator 크레이트에 의존하는 다른 크레이트도 Summary 트레이트를 스코프로 가져와 자신만의 타입에 Summary 를 구현할 수 있습니다. 한 가지 제한이 있는데, 어떤 타입에 트레이트를 구현하려면 그 트레이트나 타입, 혹은 둘 다가 현재 크레이트 로컬이어야 합니다. 예를 들어 SocialPost 타입은 우리 aggregator 크레이트 안에 정의되어 있으므로, 표준 라이브러리 트레이트인 DisplaySocialPost 에 구현할 수 있습니다. 반대로 Summary 트레이트는 우리 크레이트 안에 정의되었으므로, Vec<T> 같은 표준 라이브러리 타입에 대해서도 Summary 를 구현할 수 있습니다.

하지만 외부 트레이트를 외부 타입에 구현할 수는 없습니다. 예를 들어 DisplayVec<T> 는 둘 다 표준 라이브러리 안에 정의되어 있으므로, 우리 aggregator 크레이트 안에서 Vec<T>Display 를 구현하는 것은 불가능합니다. 이 제약은 일관성(coherence) 이라고 하는 속성의 일부이며, 더 구체적으로는 고아 규칙(orphan rule) 이라고 부릅니다. 부모 타입이 현재 크레이트에 없기 때문에 붙은 이름입니다. 이 규칙 덕분에 다른 사람의 코드가 여러분의 코드를 망가뜨리거나, 그 반대가 되는 일을 막을 수 있습니다. 이 규칙이 없다면 두 크레이트가 같은 타입에 같은 트레이트를 구현할 수 있고, 그때 러스트는 어떤 구현을 사용해야 할지 알 수 없게 됩니다.

기본 구현 사용하기

때로는 트레이트의 모든 메서드에 대해 각 타입이 반드시 구현을 제공하게 하는 대신, 일부 또는 전부에 기본 동작을 제공하는 편이 유용합니다. 그러면 특정 타입에 트레이트를 구현할 때, 각 메서드의 기본 동작을 그대로 쓰거나 필요에 따라 덮어쓸 수 있습니다.

목록 10-14에서는, 목록 10-12처럼 summarize 메서드 시그니처만 두는 대신, Summary 트레이트의 summarize 메서드에 기본 문자열 구현을 제공합니다.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Listing 10-14: summarize 메서드의 기본 구현을 가진 Summary 트레이트 정의

NewsArticle 인스턴스에 대해 이 기본 구현을 사용하려면, impl Summary for NewsArticle {} 처럼 빈 impl 블록만 적으면 됩니다.

NewsArticle 위에 summarize 메서드를 직접 정의하지는 않았지만, 기본 구현을 제공했고 NewsArticleSummary 트레이트를 구현한다고 명시했으므로, 우리는 여전히 NewsArticle 인스턴스에 대해 summarize 를 호출할 수 있습니다.

use aggregator::{self, NewsArticle, Summary};

fn main() {
    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...) 를 출력합니다.

기본 구현을 만들더라도, 목록 10-13에서 SocialPost 에 대해 했던 구현을 바꿀 필요는 없습니다. 기본 구현을 덮어쓰는 문법은, 애초에 기본 구현이 없는 트레이트 메서드를 구현하는 문법과 동일하기 때문입니다.

기본 구현은 같은 트레이트 안의 다른 메서드를 호출할 수도 있습니다. 그 다른 메서드가 기본 구현을 갖지 않아도 됩니다. 이런 방식으로 트레이트는 꽤 많은 유용한 기능을 제공하면서, 실제 구현자에게는 극히 일부만 정의하게 만들 수 있습니다. 예를 들어 Summary 트레이트에 구현이 필수인 summarize_author 메서드를 두고, 그 메서드를 호출하는 기본 구현을 가진 summarize 메서드를 다음처럼 정의할 수 있습니다.

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

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

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

이 버전의 Summary 를 사용하려면, 타입 위에 트레이트를 구현할 때 summarize_author 만 정의하면 됩니다.

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

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

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

이제 summarize_author 를 정의해 두었으므로, SocialPost 인스턴스에 대해 summarize 를 호출할 수 있습니다. Summary 트레이트의 기본 구현이 우리가 제공한 summarize_author 를 호출하기 때문입니다. 즉, summarize_author 만 구현하면 Summary 트레이트가 summarize 동작까지 제공해 주는 셈이고, 추가 코드를 더 쓸 필요가 없습니다. 실제로 쓰면 이런 모습입니다.

use aggregator::{self, SocialPost, Summary};

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

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

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

한 가지 주의할 점은, 같은 메서드를 덮어쓴 구현 안에서 그 메서드의 기본 구현을 직접 호출하는 것은 불가능하다는 것입니다.

트레이트를 매개변수로 사용하기

이제 트레이트를 정의하고 구현하는 법을 알게 되었으니, 트레이트를 사용해 여러 타입을 받는 함수를 어떻게 정의하는지 살펴봅시다. 목록 10-13에서 NewsArticleSocialPost 위에 구현한 Summary 트레이트를 이용해, item 매개변수에 대해 summarize 메서드를 호출하는 notify 함수를 정의해 보겠습니다. 이를 위해 impl Trait 문법을 사용합니다.

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

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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

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

item 매개변수에 구체 타입을 적는 대신, impl 키워드와 트레이트 이름을 지정했습니다. 이 매개변수는 해당 트레이트를 구현하는 어떤 타입이든 받을 수 있습니다. notify 본문 안에서는 Summary 트레이트에서 온 메서드들, 예를 들어 summarizeitem 에 대해 호출할 수 있습니다. 따라서 notifyNewsArticle 이나 SocialPost 인스턴스를 넘길 수 있습니다. 하지만 String 이나 i32 같은 타입은 Summary 를 구현하지 않았으므로, 그런 타입으로 호출하려 하면 컴파일되지 않습니다.

트레이트 바운드 문법

impl Trait 문법은 단순한 경우에는 아주 편리하지만, 사실은 트레이트 바운드 라는 더 긴 형태의 문법에 대한 설탕 문법입니다. 그 모습은 다음과 같습니다.

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) {

이 방식은 item1item2 가 서로 다른 타입이어도 둘 다 Summary 만 구현하고 있으면 허용하고 싶을 때 적절합니다. 하지만 두 매개변수가 같은 타입이길 강제하고 싶다면, 다음처럼 트레이트 바운드를 사용해야 합니다.

pub fn notify<T: Summary>(item1: &T, item2: &T) {

item1item2 의 타입으로 지정된 제네릭 타입 T 가 하나뿐이라는 사실이, 두 인수에 들어오는 실제 타입이 반드시 같아야 한다는 제약을 만들어 냅니다.

+ 문법으로 여러 트레이트 바운드 지정하기

하나 이상의 트레이트 바운드도 지정할 수 있습니다. 예를 들어 notifyitem 에 대해 summarize 를 호출하는 동시에 출력 형식화도 하고 싶다고 해 봅시다. 그러면 itemDisplaySummary 를 둘 다 구현해야 한다고 notify 정의에 적어 주면 됩니다. 이때 + 문법을 사용합니다.

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

+ 문법은 제네릭 타입에 대한 트레이트 바운드와 함께 쓸 때도 유효합니다.

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

이 두 바운드가 지정되면, notify 본문 안에서는 summarize 를 호출할 수 있고, {} 형식화를 사용해 item 을 출력할 수도 있습니다.

where 절로 트레이트 바운드 더 명확하게 쓰기

트레이트 바운드가 많아지면 단점도 생깁니다. 각 제네릭마다 자기 바운드가 있고, 여러 제네릭 타입 매개변수를 가진 함수는 함수 이름과 매개변수 목록 사이에 바운드 정보가 잔뜩 들어가서 시그니처를 읽기 어려워질 수 있습니다. 이를 위해 러스트는 함수 시그니처 뒤의 where 절 안에 트레이트 바운드를 적는 대체 문법을 제공합니다. 즉, 다음과 같이 쓰는 대신

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

다음처럼 where 절을 쓸 수 있습니다.

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

이 함수 시그니처는 훨씬 덜 복잡해 보입니다. 함수 이름, 매개변수 목록, 반환 타입이 가깝게 모여 있어서, 트레이트 바운드가 많은 함수가 아닌 일반 함수처럼 읽히기 때문입니다.

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

반환 위치에서도 impl Trait 문법을 사용하여, 어떤 트레이트를 구현하는 타입 값을 돌려줄 수 있습니다.

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

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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

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

반환 타입으로 impl Summary 를 사용함으로써, returns_summarizable 함수가 Summary 트레이트를 구현하는 어떤 타입을 반환한다는 사실을 구체 타입 이름 없이 표현할 수 있습니다. 이 경우 returns_summarizableSocialPost 를 반환하지만, 이 함수를 호출하는 코드가 그 사실을 알 필요는 없습니다.

반환 타입을 “어떤 트레이트를 구현하는 타입”으로만 지정할 수 있다는 점은 특히 클로저와 반복자 맥락에서 유용합니다. 이것들은 13장에서 다루는데, 클로저와 반복자는 컴파일러만 알 수 있거나 이름이 너무 길어서 일일이 적기 어려운 타입을 만들어 내기 때문입니다. impl Trait 문법은 함수가 Iterator 같은 트레이트를 구현하는 타입을 반환한다고 짧고 간결하게 적게 해 줍니다.

다만 impl Trait 는 오직 하나의 구체 타입만 반환할 때만 사용할 수 있습니다. 예를 들어 반환 타입을 impl Summary 로 지정해 놓고, 상황에 따라 NewsArticle 이나 SocialPost 를 반환하는 코드는 동작하지 않습니다.

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

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 SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

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

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 {
        SocialPost {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            repost: false,
        }
    }
}

NewsArticle 또는 SocialPost 둘 중 하나를 반환하는 것은, 컴파일러 안에서 impl Trait 문법이 구현되는 방식의 제약 때문에 허용되지 않습니다. 이런 동작을 하는 함수는 18장의 “공통 동작을 추상화하기 위해 트레이트 객체 사용하기” 절에서 작성하는 법을 다룹니다.

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

제네릭 타입 매개변수를 사용하는 impl 블록에 트레이트 바운드를 걸면, 특정 트레이트를 구현하는 타입에 대해서만 메서드를 구현할 수 있습니다. 예를 들어 목록 10-15의 Pair<T> 타입은 항상 새 Pair<T> 인스턴스를 반환하는 new 함수를 구현합니다 (5장의 “메서드 문법” 절에서 보았듯, Selfimpl 블록의 타입, 여기서는 Pair<T> 의 별칭입니다). 하지만 다음 impl 블록에서 Pair<T> 는 내부 타입 T 가 비교를 가능하게 하는 PartialOrd 와 출력을 가능하게 하는 Display 를 모두 구현한 경우에만 cmp_display 메서드를 구현합니다.

Filename: 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);
        }
    }
}
Listing 10-15: 트레이트 바운드에 따라 제네릭 타입에 조건부로 메서드 구현하기

또한 어떤 다른 트레이트를 구현하는 모든 타입에 대해, 특정 트레이트를 조건부로 구현할 수도 있습니다. 트레이트 바운드를 만족하는 모든 타입에 대한 이런 구현을 블랭킷 구현(blanket implementation) 이라고 하며, 러스트 표준 라이브러리에서 광범위하게 사용됩니다. 예를 들어 표준 라이브러리는 Display 를 구현하는 모든 타입에 대해 ToString 트레이트를 구현합니다. 표준 라이브러리 안의 impl 블록은 대략 다음과 비슷합니다.

impl<T: Display> ToString for T {
    // --snip--
}

표준 라이브러리에 이런 블랭킷 구현이 있기 때문에, Display 를 구현하는 모든 타입은 ToString 트레이트에 정의된 to_string 메서드를 사용할 수 있습니다. 예를 들어 정수는 Display 를 구현하므로, 다음처럼 정수를 대응하는 String 값으로 바꿀 수 있습니다.

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

블랭킷 구현은 해당 트레이트 문서의 “Implementors” 섹션에서 확인할 수 있습니다.

트레이트와 트레이트 바운드를 사용하면, 제네릭 타입 매개변수를 활용해 중복을 줄이면서도 그 제네릭 타입이 특정 동작을 가져야 한다고 컴파일러에게 명시할 수 있습니다. 그러면 컴파일러는 이 트레이트 바운드 정보를 사용해, 실제로 우리 코드와 함께 쓰이는 모든 구체 타입이 올바른 동작을 제공하는지 검사할 수 있습니다. 동적 타입 언어에서는, 어떤 메서드를 구현하지 않은 타입에 대해 그 메서드를 호출하면 런타임에서야 오류를 보게 됩니다. 반면 러스트는 그런 오류를 컴파일 시점으로 끌어올려, 코드가 실행되기도 전에 반드시 고치게 만듭니다. 게다가 런타임에 동작 여부를 검사하는 코드를 우리가 직접 작성할 필요도 없습니다. 컴파일 시점에 이미 검사가 끝났기 때문입니다. 이런 방식은 제네릭의 유연성을 유지하면서도 성능을 떨어뜨리지 않습니다.