본문 바로가기

Rust

[Rust] 제네릭(Generic), 트레잇(Trait), 동적 디스패처(dynamic dispatch)

안녕하세요.

오늘은 제네릭, 트레잇에 대해 알아보겠습니다.


📌 Preview

제네릭을 사용하면 타입을 일반화하여 재사용성을 높일 수 있습니다.

 

트레잇은 공통된 동작을 정의하여 여러 타입에서 사용할 수 있도록 합니다.

  • 다른 언어의 Interface와 유사한 개념입니다

📌 제네릭 (Generic)

구체적인 타입을 명시하지 않고 다양한 타입에 대해 동작할 수 있도록 하는 기능입니다.

  • 함수, 구조체, 열거형 등을 특정 타입에 의존하지 않고 재사용할 수 있도록 합니다.

제네릭 구조체

다른 언어와 유사한 방식으로 구조체를 사용할 수 있습니다.

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

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.2, y: 3.4 };

    println!("integer: ({}, {}), float: ({}, {})",
             integer_point.x, integer_point.y, float_point.x, float_point.y);
    // integer: (5, 10), float: (1.2, 3.4)
}

 

구조체 함수에서도 사용할 수 있습니다.

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

impl<T, U> Point<T, U> {
    fn mix_up<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 2.5 };
    let p2 = Point { x: 'k', y: "Kang" };

    let p3 = p1.mix_up(p2);
    println!("p3.x = {}, p3.y = {}", p3.x, p3.y); // p3.x = 1, p3.y = Kang
}

제네릭 열거형

열거형에서도 구조체와 똑같이 사용할 수 있습니다.

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

제네릭 함수

제네릭을 사용하면 다양한 타입을 받아 들이는 함수를 만들 수 있습니다.

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

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

fn main() {
    let numbers = vec![10, 20, 30, 5, 50];
    let chars = vec!['a', 'z', 'm', 'd'];

    println!("가장 큰 숫자: {}", largest(&numbers)); // 50
    println!("가장 큰 문자: {}", largest(&chars)); // z
}
  • T: PartialOrd + Copy → T는 PartialOrd, Copy를 구현하는 타입만 올 수 있음
    • 제네릭 제약 (→ 트레잇 바운드에서 자세히 살펴볼 예정입니다)
  • PartialOrd: 비교 가능
  • Copy: 복사 가능

📌 트레잇 (Trait)

다른 언어에서 인터페이스(Interface) 나 추상 클래스(Abstract Class) 와 유사한 개념입니다.

  • 구조체나 열거형이 공통적으로 가져야 할 동작을 정의합니다.
trait Speak {
    fn speak(&self);
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) {
        println!("멍멍!");
    }
}

impl Speak for Cat {
    fn speak(&self) {
        println!("야옹!");
    }
}

fn main() {
    let dog = Dog;
    let cat = Cat;

    dog.speak(); // 멍멍!
    cat.speak(); // 야옹!
}

Default 구현도 가능합니다.

trait Animal {
    fn make_sound(&self) {
        println!("(조용함...)");
    }
}
struct Cat;

impl Animal for Cat {} // 기본 구현을 사용

fn main() {
    let cat = Cat;

    cat.make_sound(); // (조용함...)
}

📌 트레잇 바운드

다른 언어의 제네릭 제약과 같은 기능입니다.

  • 제네릭 타입이 특정 트레잇을 구현해야 한다는 조건을 설정합니다
use std::fmt::Display;

fn print_info<T: Display>(item: T) {
    println!("출력값: {}", item);
}

fn main() {
    print_info(42);      // 정수(i32) 출력 가능
    print_info("Hello"); // 문자열 출력 가능
    print_info(vec![1, 2, 3]);  // ❌ 컴파일 에러 (Vec<T>는 Display를 구현하지 않음)
}
  • T는 반드시 Display 트레잇을 구현해야 합니다


여러 개의 트레잇 바운드를 적용할 수도 있습니다.

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

fn show_info<T: Display + Debug>(item: T) {
    println!("출력값: {}", item);
    println!("디버그 정보: {:?}", item);
}

fn main() {
    show_info(42);  // ✅ 정수는 Display + Debug 모두 구현
    show_info("Hi"); // ✅ 문자열도 Display + Debug 모두 구현
}
  • '+'를 사용하면 됩니다

where

트레잇 바운드가 많아지면 where 문법을 사용하여 가독성을 높일 수 있습니다.

fn example<T, U>(a: T, b: U)
where
    T: Display + Clone,
    U: Debug + Default,
{
    println!("{}", a);
    println!("{:?}", b);
}

함수 반환값 타입에도 트레잇 바운드를 설정할 수 있습니다.

use std::fmt::Display;

fn get_message() -> impl Display {
    "Hello, Rust!"
}

fn main() {
    println!("{}", get_message());
}
  • get_message() 반환값은 Display를 구현한 타입만 가능합니다

📌 동적 디스패치 (Dynamic Dispatch)

지금까지 살펴본 디스패치는 정적 디스패치 입니다.

  • impl Trait은 정적 디스패치 → 컴파일 타임에 결정됩니다
  • dyn Trait은 동적 디스패치 → 런타임에 결정 됩니다

 

왜 이런 동적 디스패치가 필요할까요?

→ 모든 타입을 컴파일 타임에 결정할 수 없는 경우가 있습니다.


Case1)

아래와 같이 트레잇과 구조체를 선언해 줍니다.

trait Animal {
    fn make_sound(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn make_sound(&self) {
        println!("멍멍!");
    }
}

impl Animal for Cat {
    fn make_sound(&self) {
        println!("야옹!");
    }
}

 

Dog와 Cat을 담은 벡터를 사용하고 싶지만, 정적 디스패치를 사용하면 컴파일 에러가 납니다.

fn main() {
    let animals: Vec<impl Animal> = vec![Dog, Cat]; // ❌ 불가능
}

 

동적 디스패치를 사용하면 가능합니다.

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];

    for animal in animals {
        animal.make_sound();
    }
}

Case2)

정적 디스패치는 반환 타입이 하나로 고정되어야 합니다.

fn get_animal(is_dog: bool) -> impl Animal {
    if is_dog {
        Dog
    } else {
        Cat // ❌ 오류: `impl Trait`을 사용하면 반환 타입이 고정되어야 함!
    }
}

 

동적 디스패치를 사용하면 가능합니다.

fn get_animal(is_dog: bool) -> Box<dyn Animal> {
    if is_dog {
        Box::new(Dog)
    } else {
        Box::new(Cat)
    }
}


fn main() {
    let animal = get_animal(true);
    animal.make_sound(); // 멍멍!
}

dyn Trait vs impl Trait 비교 정리

특징 정적 디스패치 동적 디스패치
타입 결정 시점 컴파일 타임에 결정 런타임에 결정
성능 빠름 (최적화 가능) 느림 (vtable 사용)
사용 예시 반환 타입이 항상 같은 타입일 때 반환 타입이 여러 개일 때
메모리 스택에 저장 가능 힙 할당 필요 (Box 사용)
메서드 호출 직접 호출 (인라인 최적화 가능) vtable을 통해 호출

📌 정리

개념 설명 예제
제네릭 (Generics) 타입을 일반화하여 재사용 가능 fn largest<T: PartialOrd + Copy>(list: &[T]) -> T
제네릭 구조체 여러 타입을 다룰 수 있는 구조체 struct Point<T, U> { x: T, y: U }
트레잇 (Traits) 공통된 동작(메서드)을 정의 trait Speak { fn speak(&self); }
트레잇 바운드 특정 트레잇을 구현한 타입만 사용 가능 fn print_summary<T: Summary>(item: &T)