제네릭(Generic)과 트레잇(Trait)

제너릭과 트레잇

Rust는 코드의 재사용성과 타입 안전성을 동시에 보장하기 위해 제네릭(Generic)과 트레잇(Trait)을 사용합니다. 이 개념은 Rust의 함수, 구조체, 열거형 등 다양한 곳에 적용되는데, 제네릭은 일반화된 타입을 의미하고, Trait은 공통된 행위 또는 특성을 의미합니다.


1. 제네릭(Generic)

가. 형식별 별도 함수

제네릭은 “특정 타입에 얽매이지 않고 다양한 타입에 대해 작동할 수 있도록 하는 일반화된 타입”을 의미합니다. 따라서, 타입에 의존하지 않는 유연한 코드를 작성할 수 있도록 도와줍니다.

아래 코드는 정수 타입 i32에 대해서는 largest_i32 함수를 사용하고, char에 대해서는 largest_char 함수를 따로 적용하고 있습니다.

fn largest_i32(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> char {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&numbers);
    println!("The largest number is {}", result);

    let chars = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&chars);
    println!("The largest char is {}", result);
}

위 코드를 실행하면 아래와 같이 “가장 큰 수는 100, 가장 큰 문자는 y(번역)”라고 화면에 표시됩니다.

i32 정수와 char에 따라 다른 함수 적용

나. 제네릭 타입의 하나의 함수

위와 같이 i32와 char라는 데이터 타입에 따라 다른 함수를 하나의 함수로 합치기 위해 T라는 제네릭을 이용했습니다.

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    let result = largest(&numbers);
    println!("The largest number is {}", result);
    let chars = vec!['y', 'm', 'a', 'q'];

    let result = largest(&chars);
    println!("The largest char is {}", result);
}

위 코드를 실행하면
The largest number is 100
The largest char is y
가 화면에 출력되는데,

Generic T를 정의하는 구문에서 : PartialOrd + Copy를 제거하고 실행하면

제너릭 사용시 PartialOrd 트레잇을 사용하지 않아 에러 발생

error[E0369]: binary operation > cannot be applied to type T
(에러 E0369: 이진 동작인 > 는 T 타입에 적용될 수 없습니다)
란 에러 메시지가 표시되고,

그 아래 T 다음에 “: std:cmp::PartialOrd를 추가하라고 합니다.

PartialOrd는 두 값을 비교하여 크거나 작거나 같은지 부분적으로만 결정할 수 있는 경우를 위해 설계되었습니다.
예를 들어 부동 소수점 숫자(floating-point numbers)의 경우 NaN (Not a Number) 값이 존재하기 때문에 완전한 비교가 불가능합니다.
Ord는 두 값을 항상 비교할 수 있는 경우를 위해 설계되었습니다.

그래서 T 다음에 PartialOrd만 추가하고 Copy는 빼고 실행하면

제너릭 사용시 Copy 트레잇을 사용하지 않아 에러 발생

let mut largest = list[0];의 list[0]에서
error[E0508]: cannot move out of type [T], a non-copy slice
(복사할 수 없는 슬라이스인 `[T]` 유형에서 이동할 수 없습니다)
란 에러가 발생하고,

let mut largest = &list[0];에서는
type ‘T’가 Copy trait를 구현하지 않았기 때문에 move가 일어났는데, 여기서 data가 move될 수 없다고 합니다.

정수나 문자 타입에 공통적으로 적용하기 위해 제네릭 타입을 적용하는 것은 좋은데, 적용하기 위해 추가해야 할 요소들이 많습니다. 그리고, 두 개의 Trait을 연결하기 위해서는 +를 사용헸습니다.


다. 구조체에 한 가지 제네릭 적용

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

fn main() {
    let int_point = Point { x: 1, y: 2 };
    let float_point = Point { x: 1.0, y: 2.0 };

    println!("({:.1}, {:.1})", int_point.x, int_point.y);
    println!("({:.1}, {:.1})", float_point.x, float_point.y);
}
  • 위 코드의 경우 타입이 T 하나뿐이 없기 때문에, 타입이 한 가지인 경우만 적용가능합니다. 따라서 Point 구조체를 정의할 때 i32 또는 f64 타입 한 가지로만 지정했습니다.
  • 그리고, {}라고 하면 실수인 경우에도 1로 출력이 돼서 {:.1}로 출력 형식을 변경했습니다.

라. 구조체에 두 가지 제네릭 적용

  • 아래와 같이 하나의 구조체에 여러 타입, T와 U를 지정할 수 있습니다. 이 경우 T나 U는 특별한 의미가 있는 것이 아니고, 타입이 다르다는 것을 의미합니다.
struct Mixed<T, U> {
    x: T,
    y: U,
}

위에서 두 가지 타입을 지정했으므로 아래와 같이 i32와 f64를 같이하거나 달리해서 지정할 수 있습니다.

제너릭으로 두 가지 타입 사용

위 코드를 실행하면
(1, 2)
(1.0, 2)
식으로 화면에 표시됩니다.


2. 트레잇(Trait)

트레잇은 공통된 동작을 정의하는 인터페이스입니다. 특정 타입이 트레잇을 구현하면, 해당 트레잇의 메서드를 사용할 수 있습니다.

가. 예제 코드

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

struct Article {
    title: String,
    author: String,
}

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

fn main() {
    let article = Article {
        title: String::from("Rust 배우기"),
        author: String::from("홍길동"),
    };

    println!("요약: {}", article.summarize());
}

(1) trait 트레잇 이름 { 메소드 선언 }

  • trait 키워드로 트레잇, 여기서는 Summary 트레잇을 정의하는데, 함수는 함수명과 반환 타입만 지정했고, 구체적인 동작은 정의되지 않았습니다.

(2) 구조체 정의

  • struct Article { }은 Article이란 구조체를 title과 author 모두 String으로 정의했습니다.

(3) impl 트레잇명 for type명

  • Trait을 구현하는 방식은 impl Trait for Type으로서
    impl Summary for Article이라고 적은 다음
    중괄호 안에서 fn summarize로 메소드를 구현하는데,
    인수는 &self로 자기 자신, 여기서는 Article을 받고,
    반환 형식은 위에서 지정했듯이 String 타입이며,
    구체적으로 실행하는 것은
    format!(“{} by {}”, self.title, self.author)으로
    self.title by self.author란 문자열을 생성합니다.

(4) main 함수에서 구조체 생성 및 트레잇 메소드 실행

  • main 함수에서는 구조체를 생성한 다음 aricle.summarize로 title과 author를 이용한 문자열을 만든(format) 후 println!로 화면에 출력합니다.
  • 따라서, 위 코드를 실행하면 아래와 같이 “요약 : Rust 배우기 by 홍길동”이 출력됩니다.
트레잇 정의(trait 트레잇명)와 구현(impl trait for type)을 실행한 결과

나. 트레잇 바운드(Trait Bound)

함수 인자에서 트레잇을 사용하는 방법은 다음과 같이 세 가지가 있습니다.

(1) 케이스 1

item을 impl Summary로 지정하고, main 함수에서 notify 함수의 인수로 aritcle을 입력하는 것입니다.

fn notify(item: impl Summary) {
    println!("알림: {}", item.summarize());
}

예제는 아래와 같습니다.

기존 코드에 위 fn notify를 추가하고,
main함수에서 println!(“요약: {}”, article.summarize());을 주석 처리하고, notify(article);을 추가하면 됩니다.

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

struct Article {
    title: String,
    author: String,
}

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

fn notify(item: impl Summary) {
    println!("New notification: {}", item.summarize());
}

fn main() {
    let article = Article {
        title: String::from("Rust 배우기"),
        author: String::from("홍길동"),
    };

    notify(article);
    // println!("요약: {}", article.summarize());
}

(2) 케이스 2

아래는 “notify 함수를 정의하는데, 제네릭 타입 T는 Summary 트레잇을 구현한 타입으로 제한하고, 함수의 매개변수 item은 T 타입이다”라는 의미입니다. 가장 많이 사용하는 형식입니다.

fn notify<T: Summary>(item: T) {
    println!("알림: {}", item.summarize());
}

위 코드를 활용한 코드는 아래와 같이 Generic에서 설명한 코드와 비슷합니다. 케이스 1과 다른 점은 article을 참조 형식(&article)으로 전달하는 것입니다.

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

struct Article {
    title: String,
    author: String,
}

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

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

fn main() {
    let article = Article {
        title: String::from("Rust 배우기"),
        author: String::from("홍길동"),
    };

    notify(&article);
    // println!("요약: {}", article.summarize());
}

(3) 케이스 3

여러 트레잇을 조합할 수도 있습니다.

fn process<T: Summary + Clone>(item: T) {
    println!("처리: {}", item.summarize());
}

구조체가 Clone이 가능하도록 Struct 구조체 선언문 위에
#[derive(Clone)] 문이 있어야 하고,
main함수에서 notify의 인수를 aricle로 지정하면 됩니다.

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

#[derive(Clone)]
struct Article {
    title: String,
    author: String,
}

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

fn notify<T: Summary + Clone>(item: T) {
    println!("New notification: {}", item.summarize());
}

fn main() {
    let article = Article {
        title: String::from("Rust 배우기"),
        author: String::from("홍길동"),
    };

    notify(article);
    // println!("요약: {}", article.summarize());
}

다. 트레잇 객체 (Trait Object)

정확한 타입을 모르더라도 트레잇이 구현된 어떤 타입이든 받아들이고 싶을 때는 다음과 같이 dyn 키워드를 사용합니다.

fn notify(item: &dyn Summary) {
    println!("알림: {}", item.summarize());
}
  • 런타임 시 어떤 타입인지 결정됩니다 (동적 디스패치).
  • 반드시 참조(&) 형태로만 사용 가능합니다.
trait Summary {
    fn summarize(&self) -> String;
}

// #[derive(Clone)]
struct Article {
    title: String,
    author: String,
}

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

fn notify(item: &dyn Summary) {
    println!("New notification: {}", item.summarize());
}

fn main() {
    let article = Article {
        title: String::from("Rust 배우기"),
        author: String::from("홍길동"),
    };

    notify(&article);
    // println!("요약: {}", article.summarize());
}

🧠 요약

개념설명
Generic여러 타입에 대해 재사용 가능한 코드
Trait공통 기능 정의, 타입에 메서드 제공
Trait Bound함수나 구조체에 트레잇 구현 타입만 허용
impl Trait간결하게 바운드 지정 가능
dyn Trait트레잇 객체, 런타임에 타입이 결정됨

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다