Rust - 제네릭(Generic) & 트레이트(Trait)

Chan Heo·2024년 12월 3일

Rust 공부

목록 보기
7/7
post-thumbnail

Rust에 존재하는 제네릭 (Generic), 트레이트 (Trait), 라이프타임 (Lifetime) 개념에 대해 공부하고, 정리해 보겠습니다.

제네릭: 중복되는 타입 효율적으로 처리하기 위한 도구

🦀 제네릭 (generic)
모든 프로그래밍 언어는 중복되는 개념을 효율적으로 처리하기 위한 도구를 가지고 있습니다. 러스트에서는 제네릭 (generic) 이 그 역할을 맡습니다.
제네릭은 구체(concrete) 타입 혹은 기타 속성에 대한 추상화된 대역입니다. 컴파일과 실행 시점에 제네릭들이 실제로 무슨 타입으로 채워지는지 알 필요 없이 제네릭의 동작이나 다른 제네릭과의 관계를 표현할 수 있습니다.

교재에 나와 있는 제네릭에 대한 설명입니다. 다시 요약하자면, 러스트에서는 타입을 추상화하는 도구로 제네릭을 사용합니다.

반복되는 코드 블록을 여러 번 반복해서 작성하는 대신 ‘함수’로 만들어서 재사용하는 개념을 떠올려보면 제네릭을 이해하는 데 도움이 될 것 같습니다.

제네릭을 사용하면 타입과 기능을 일반화하여 코드 중복을 줄이고 유지보수성을 향상시킬 수 있습니다.

제네릭은 타입 매개변수를 사용하여 구현되며, 이는 코드가 사용될 때 구체적인 타입으로 지정될 placeholder 역할을 합니다.

함수의 매개변수나 반환 타입, 구조체, 열거형, 메서드 등에서 경우에 따라 여러 타입을 사용해야 할 수도 있습니다. 이때, 제네릭이 빛을 발하게 됩니다.

제네릭 사용 예시 코드

// 제네릭 함수 정의
fn largest<T>(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,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

// 제네릭 열거형 정의 - Option<T>, Result<T, E>
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

// 제네릭 메서드 정의
struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

제네릭을 사용할 때는 <> 꺾쇠괄호 안에 타입 매개변수의 이름을 작성해 줘야 합니다. (관행적으로 Type의 첫 글자를 딴 T를 사용합니다.)

함수나 구조체, 열거형, impl 키워드 바로 뒤에 <T> 를 붙여 타입 매개변수 이름을 선언하고 각각의 정의 내에서 구체적인 타입 대신 타입 매개변수 이름을 활용하면 됩니다!

트레이트: 공통된 동작을 정의하기

🦀 트레이트 (trait)
트레이트 (trait) 는 특정한 타입이 가지고 있으면서 다른 타입과 공유할 수 있는 기능을 정의합니다. 트레이트를 사용하면 공통된 기능을 추상적으로 정의할 수 있습니다. 
트레이트 바운드 (trait bound) 를 이용하면 어떤 제네릭 타입 자리에 특정한 동작을 갖춘 타입이 올 수 있음을 명시할 수 있습니다.

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

여러 타입(type)의 공통된 행위/특성을 표시한 것을 Trait 라고 합니다.

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

우리는 러스트에서 트레이트를 활용해서, 다양한 타입들이 공통적으로 갖는 동작에 대해 추상화할 수 있습니다. 마치 객체지향 프로그래밍의 인터페이스처럼 말이죠!
트레이트를 통해 우리는 코드의 재사용성, 유연성, 안전성을 높일 수 있습니다.

위의 트레이트 정의 예시에서, 메서드 시그니처 뒤에는 중괄호로 시작하여 메서드를 구현하는 대신 세미콜론을 집어넣었습니다. 이 트레이트를 구현하는 각 타입이 메서드에 맞는 동작을 직접 제공해야 합니다.
컴파일러는 Summary 트레이트가 있는 모든 타입에 정확히 이와 같은 시그니처의 summarize 메서드를 가지고 있도록 강제할 것입니다.

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

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

이 예시에서는 NewsArticleTweet 타입에 각각 Summary 트레이트를 구현하였습니다.

특정 타입에 트레이트를 구현하는 문법은 위 예시에서 확인할 수 있듯이,

impl Trait for Type { }

형태로 명시한 다음, 트레이트 정의에 정의된 메서드 시그니처에 대한 메서드 본문을 작성해주면 됩니다.

트레이트 바운드 (trait bound): 제네릭과 트레이트를 활용한 Rust의 유연하면서도 강력한 기능

앞서 소개한 제네릭과 트레이트를 함께 사용하는 방법으로 트레이트 바운드가 있습니다.

트레이트 바운드는 제네릭에 제약을 걸어, 특정 동작을 보장하는 방법입니다. 즉, 제네릭 타입이 특정 트레이트를 구현한 타입이어야 한다는 조건을 명시해둔 것입니다.

이를 통해, 다양한 이점을 얻을 수 있는데, 아래와 같습니다:

  • 컴파일러에게 제네릭 타입이 어떤 기능을 가져야 하는지 알려주고, 컴파일 시점에 타입 검사를 실행하여 런타임 오류를 방지할 수 있습니다.
  • 함수나 구조체가 구현해야 하는 기능을 명확히 표현해둘 수 있습니다.
  • 트레이트 바운드를 통해 Rust는 강력한 타입 시스템을 유지하면서도 유연한 제네릭 프로그래밍을 가능하게 합니다.
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

트레이트는, 꺾쇠괄호 안의 제네릭 타입 매개변수 선언 뒤에 콜론(:)과 함께 위치하게 됩니다.

이제, notify 함수의 item 매개변수로 올 수 있는 타입은 Summary 트레이트를 구현한 타입으로 제한됩니다.

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

위와 같이 + 구문으로 여러 개의 트레이트를 모두 구현하도록 바운드를 설정할 수도 있습니다.

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

위 예시는 where 조항입니다.

트레이트 바운드가 복잡해지면 자칫 코드의 가독성을 해칠 수 있습니다. 이때, 트레이트 바운드의 내용은 where 조항으로 따로 빼 버리면 함수 시그니처를 읽기가 더욱 쉬워질 것입니다.

읽어주셔서 감사합니다 🙇‍♂️

References

https://doc.rust-kr.org/ch10-01-syntax.html

https://doc.rust-kr.org/ch10-02-traits.html

https://www.linkedin.com/pulse/generics-rust-amit-nadiger

profile
안녕하세요:)

0개의 댓글