[Rust의 다형성] 동적 디스패치(dyn) vs 정적 디스패치(제네릭)

손호준·2024년 8월 13일

예시 코드 먼저 보자

// 동적 디스패치와 정적 디스패치(제네릭) 비교 예시

trait Trait {
    fn do_something(&self);
}

struct A;
struct B;

impl Trait for A {
    fn do_something(&self) {
        println!("A is doing something");
    }
}

impl Trait for B {
    fn do_something(&self) {
        println!("B is doing something");
    }
}

// 동적 디스패치
fn execute_trait_dyn(obj: &dyn Trait) {
    obj.do_something();
}

// 정적 디스패치
fn execute_trait_static<T: Trait>(obj: &T) {
    obj.do_something();
}

fn main() {
    let a = A;
    let b = B;
    
    // 동적 디스패치
    execute_trait_dyn(&a);
    execute_trait_dyn(&b);

    // 정적 디스패치
    execute_trait_static(&a);
    execute_trait_static(&b);
}

예시 코드에서 fn execute_trait(obj: &dyn Trait) {}fn execute_trait<T: Trait>(obj: &T) {}는 비슷해 보이지만 중요한 차이가 있다.

1. 동적 디스패치(dyn 사용)

fn execute_trait<T: Trait>(obj: &T) {}

  • 동작 방식: 이 함수는 &dyn Trait 타입의 참조를 인자로 받는다. dyn Trait은 동적 디스패치 trait 객체를 나타내며, 이로 인해 런타임에 메서드 호출이 실제 타입에 따라 결정된다. 이는 정적 디스패치에 비해 추가적인 비용이 발생하고, 컴파일러가 최적화를 수행하기 어렵다.
  • 장점: 다양한 타입의 객체를 동일한 함수에서 처리할 수 있는 유연성 제공하여 코드가 간결해지고 재사용 가능.
  • 단점: 동적 디스패치로 런타임에 추가적인 오버헤드 발생, 메서드 호출을 위한 메타데이터와 메서드 테이블 필요. 컴파일러 최적화 어려움

2. 정적 디스패치(제네릭 사용)

`fn execute_trait(obj: &dyn Trait) {}

  • 동작 방식: 이 함수는 제네릭 타입 T를 인자로 받는다. TTrait을 구현해야 하며, 컴파일 타임에 T의 실제 타입이 결정된다. 따라서 메서드 호출이 정적으로 결정된다.
  • 장점: 컴파일 타임에 타입이 결정되므로 런타임 오버헤드가 없고, 컴파일러가 최적화를 수행할 수 있다.
  • 단점: 제네릭 타입을 사용할 경우, 함수 호출 시 각 타입에 대해 개별적으로 코드가 생성되므로, 코드 크기가 커질 수 있다. 여러 타입의 객체를 동일한 함수에서 처리하려면, 각 타입별로 함수를 호출해야 한다.

비교

  • 동적 디스패치(dyn): 런타임에 메서드 호출이 결정됨. 유연성을 제공하지만, 런타임 오버헤드 발생.
  • 정적 디스패치(제네릭): 컴파일 타임에 타입과 메서드 호출이 결정됨. 성능이 향상되지만, 코드가 각 타입별로 생성될 수 있음.

결론

성능이 중요하면 정적 디스패치(제네릭) 쓰고, 유연성이 중요하면 동적 디스패치 쓰자.

profile
Rustacean🦀

0개의 댓글