Menu
Coddy logo textTech

동적 디스패치

Coddy Rust 여정의 Object Oriented Programming 섹션에 포함된 레슨 — 61개 중 45번째.

<T: Summary>와 같이 트레이트 바운드(trait bounds)가 있는 제네릭을 사용하면, Rust는 컴파일 타임에 정확한 타입을 결정합니다. 이것을 정적 디스패치(static dispatch)라고 하며, 컴파일러는 사용자가 사용하는 각 구체적인 타입에 대해 특수화된 코드를 생성합니다. 이는 빠르지만, 변수가 하나의 특정 타입만 가질 수 있다는 제한이 있습니다.

런타임에 서로 다른 타입을 가질 수 있는 단일 변수가 필요하다면 어떻게 해야 할까요? 이럴 때 트레이트 객체(trait objects)가 사용됩니다. Box<dyn Trait>를 사용하면 해당 트레이트를 구현하는 모든 타입을 저장할 수 있습니다:

trait Speak {
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) -> String { String::from("Woof!") }
}

impl Speak for Cat {
    fn speak(&self) -> String { String::from("Meow!") }
}

fn main() {
    let animal: Box<dyn Speak> = Box::new(Dog);
    println!("{}", animal.speak());  // "Woof!"
    
    let animal: Box<dyn Speak> = Box::new(Cat);
    println!("{}", animal.speak());  // "Meow!"
}

dyn 키워드는 동적 디스패치(dynamic dispatch)를 나타냅니다. 즉, Rust는 컴파일 타임이 아닌 런타임에 어떤 메서드를 호출할지 결정합니다. 트레이트 객체는 알려진 크기를 가지지 않기 때문에 Box가 필요합니다. Box는 고정된 크기를 가진 포인터를 제공합니다.

이렇게 생각해 보세요. 제네릭은 "나는 타입 T와 함께 작동한다"라고 말하는 반면, 트레이트 객체는 "나는 이것을 할 수 있는 어떤 것과도 함께 작동한다"라고 말합니다. 그 대가는 동일한 변수를 통해 서로 다른 타입을 처리할 수 있는 유연성을 얻는 대신 약간의 런타임 비용이 발생한다는 점입니다.

challenge icon

챌린지

쉬움

동적 디스패치(dynamic dispatch)의 강력함을 보여주는 차량 대여 시스템을 구축해 봅시다! 대여 가능한 모든 차량이 수행할 수 있는 기능을 정의하는 트레이트를 만들고, Box<dyn Trait>를 사용하여 동일한 변수에 서로 다른 차량 유형을 저장해 볼 것입니다.

코드는 두 개의 파일로 구성됩니다:

  • vehicle.rs: &self를 매개변수로 받고 String을 반환하는 rental_info 메서드를 가진 공개(public) Rentable 트레이트를 정의합니다. 그런 다음 두 개의 공개 구조체를 만듭니다:
    • Car — 공개 model 필드(String)를 가집니다. rental_info"Car: {model}"을 반환해야 합니다.
    • Bike — 공개 brand 필드(String)를 가집니다. rental_info"Bike: {brand}"를 반환해야 합니다.
  • main.rs: vehicle 모듈을 가져와서 입력을 사용해 동적 디스패치를 시연합니다. 먼저 Car를 담는 Box<dyn Rentable> 변수를 생성하여 렌탈 정보를 출력한 다음, 동일한 변수에 Bike를 다시 할당하고 렌탈 정보를 다시 출력합니다.

여기서 핵심은 Box<dyn Rentable> 타입의 단일 변수가 Car 또는 Bike를 모두 보유할 수 있다는 점입니다. Rust는 런타임에 어떤 rental_info 메서드를 호출할지 결정합니다. 이것이 바로 동적 디스패치의 실제 작동 방식입니다!

출력 결과는 두 차량의 정보를 모두 보여주어야 합니다:

Car: {model}
Bike: {brand}

예를 들어, 입력값이 "Tesla Model 3""Trek"인 경우:

Car: Tesla Model 3
Bike: Trek

자동차 모델과 자전거 브랜드라는 두 개의 입력을 받게 됩니다.

치트 시트

Rust는 제네릭을 사용하여 **정적 디스패치(static dispatch)**를 수행합니다. 컴파일러는 컴파일 타임에 각 타입에 대해 특수화된 코드를 생성합니다. 이는 빠르지만, 변수가 하나의 특정 타입만 가질 수 있음을 의미합니다.

**트레이트 객체(Trait objects)**는 **동적 디스패치(dynamic dispatch)**를 가능하게 하여, 단일 변수가 런타임에 서로 다른 타입들을 가질 수 있도록 합니다. 트레이트를 구현하는 모든 타입을 저장하려면 Box<dyn Trait>를 사용하세요.

trait Speak {
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) -> String { String::from("Woof!") }
}

impl Speak for Cat {
    fn speak(&self) -> String { String::from("Meow!") }
}

fn main() {
    let animal: Box<dyn Speak> = Box::new(Dog);
    println!("{}", animal.speak());  // "Woof!"
    
    let animal: Box<dyn Speak> = Box::new(Cat);
    println!("{}", animal.speak());  // "Meow!"
}

dyn 키워드는 동적 디스패치를 나타내며, 메서드 호출은 런타임에 결정됩니다. 트레이트 객체는 컴파일 타임에 크기를 알 수 없기 때문에 Box가 필요합니다.

**주요 차이점:** 제네릭은 "나는 타입 T(하나의 특정 타입)와 함께 작동한다"고 말하는 반면, 트레이트 객체는 "나는 이 트레이트를 구현하는 무엇과도(동일한 변수를 통해 여러 타입) 작동한다"고 말합니다. 유연성을 얻는 대신 약간의 런타임 비용이 발생하는 트레이드오프가 있습니다.

직접 해보기

mod vehicle;

use vehicle::{Rentable, Car, Bike};

fn main() {
    // 입력 읽기
    let mut car_model = String::new();
    std::io::stdin().read_line(&mut car_model).expect("Failed to read line");
    let car_model = car_model.trim().to_string();
    
    let mut bike_brand = String::new();
    std::io::stdin().read_line(&mut bike_brand).expect("Failed to read line");
    let bike_brand = bike_brand.trim().to_string();
    
    // TODO: Car를 담는 Box<dyn Rentable> 변수 생성
    // rental_info() 메서드를 사용하여 대여 정보 출력
    
    // TODO: 동일한 변수에 Bike를 담도록 재할당
    // 대여 정보를 다시 출력
    
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

Object Oriented Programming의 모든 레슨