OOP의 정의는 매우 다양하다. 어떤 정의에 따르면 러스트는 객체지향이지만, 또 다른 정의에 따르면 그렇지 않다. 러스트는 OOP를 비롯한 다양한 프로그래밍 패러다임에 영향을 받았으며, 일부 기능은 함수형 프로그래밍 패러다임에서 도출되었다.

객체지향 언어의 특징

  • 객체지향 프로그램은 객체로 이루어진다. 객체는 데이터와 그 데이터를 운영하는 절차(procedure)를 모아둔 것이다. 여기서 절차는 주로 메서드(methods) 혹은 운영(operations)이라고 한다. - GOF에서의 OOP 정의
  • 위 정의에 따르면 러스트는 객체지향이다. 구조체와 열거자는 데이터를 가지며 impl블록을 이용해 메서드를 제공할 수 있기 때문이다.
  • OOP의 개념중 하나인 캡슐화(encapsulation)는 객체를 사용하는 코드가 객체의 상세 구현에 접근하지 못하도록 하는 방법이다.
  • 러스트에서는 모듈, 타입, 함수 그리고 메서드pub키워드를 적용해서 공개할 API를 정의할 수 있다(기본적으로 모든것은 비공개다)
  • 상속(inheritance)는 객체가 다른 객체의 정의를 물려받는 메커니즘이다. 만일 객체지향 언어가 상속을 반드시 지원해야 한다면 러스트는 객체지향 언어가 아니다.
  • 러스트에서는 부모 구조체의 필드와 메서드 구현을 이어받는 구조체를 정의할 방법이 없다.
  • 대신 Trait의 기본구현을 이용하면 상속 및 오버라이딩과 같은 개념을 유사하게 사용할 수 있다.
  • 상속은 최근 들어 여러 프로그래밍 언어에서 디자인 해법으로서의 가치를 잃어가고 있다. 그 이유는 필요 이상으로 많은 코드를 공유하게 되는 경우가 빈번하기 때문이다.
  • 이러한 이유로 러스트는 다른 방법, 즉 상속 대신 Trait 객체를 이용하는 방법을 채택했다.

다른 타입의 값을 허용하는 Trait 객체

  • Trait 객체는 &참조나 Box<T> 스마트 포인터같은 포인터를 이용해 생성해야 하며, 그 뒤에 dyn 키워드와 함께 관련된 트레이트를 명시해야 한다.
  • 러스트에서 다른 언어의 객체와 구분하고자 구조체와 열거자를 ‘객체’ 라고 부르지 않는다. 구조체나 열거자는 필드에 저장된 데이터와 impl 블록에 정의하는 행위가 별개지만, 다른 언어에서는 데이터와 행위가 객체라고 부르는 하나의 개념으로 합쳐진다.
  • 하지만 Trait 객체는 데이터와 행위가 결합하므로 다른 언어에서 말하는 객체와 유사하다. 다만, 객체에 데이터를 추가할 수 없다는 점에서 Trait 객체는 다른 언어의 객체만큼 범용적이지 않다. Trait 객체의 목적은 공통된 행위에 대한 추상화를 제공하는것이다.

    // Draw trait 객체의 정의
    pub trait Draw {
        fn draw(&self);
    }
    
    // components 필드를 같는 Screen구조체. components필드에는
    // Draw trait를 구현하는 트레이트 객체의 벡터를 저장한다.
    pub struct Screen {
        pub components: Vec<Box<dyn Draw>>,
    }
    
    // 제네릭을 사용하면 한가지 타입에 대해서만 draw()를 실행하지만
    // Box<dyn Draw>타입을 사용하면 여러 타입에 대응이 가능하다.
    impl Screen {
        pub fn run(&self) {
            for component in self.components.iter() {
                component.draw();
            }
        }
    }
    
    // trait 구현하기 예
    pub struct Button {
        pub width: u32,
        pub height: u32,
        pub label: String,
    }
    
    impl Draw for Button {
        fn draw(&self) {
            // code to actually draw a button
        }
    }
    
    struct SelectBox {
        width: u32,
        height: u32,
        options: Vec<String>,
    }
    
    impl Draw for SelectBox {
        fn draw(&self) {
            // code to actually draw a select box
        }
    }
    
    // trait객체를 이용해 같은 trait를 구현하는 다른 값을 저장하기
    fn main() {
        let screen = Screen {
            components: vec![
                Box::new(SelectBox {
                    width: 75,
                    height: 10,
                    options: vec![
                        String::from("Yes"),
                        String::from("Maybe"),
                        String::from("No")
                    ],
                }),
                Box::new(Button {
                    width: 50,
                    height: 10,
                    label: String::from("OK"),
                }),
            ],
        };
    
        screen.run();
    }
    
  • Trait 객체는 제네릭이 단형화를 통해 컴파일 타임에 어떤 메서드를 호출하는지 알고있는것과 달리 런타임에 호출할 메서드를 찾아내기 위한 코드를 추가하는 동적 호출이다.
  • 따라서 코드의 유연성과 런타임 비용을 등을 잘 고려해 사용해야 한다.
  • Trait 객체는 객체 안정성을 가진 trait만 사용할 수 있는데 아래 속성에 부합하면 객체 안정성을 확보했다고 볼 수 있다.
    • 메서드의 리턴 타입이 Self가 아니다
    • 메서드에 제네릭 타입 매개변수가 없다
  • 예를들어 객체 안정성을 가지지 않은 Clone trait는 trait 객체로 사용할 수 없다.

    // Error!
    pub struct Screen {
        pub components: Vec<Box<dyn Clone>>,
    }