OOP vs 트레이트 개념

객체지향 프로그래밍(OOP)은 캡슐화, 추상화, 상속, 다형성과 같은 핵심 개념을 통해 재사용성과 확장성을 높이는 데 중점을 둔다. 예를 들어 클래스 내부에 데이터를 저장하고 메서드를 통해 접근하는 방식으로 캡슐화를 구현하거나, 클래스를 상속받아 기능을 확장하면서 다형성을 지원하는 방식을 많이 볼 수 있다. 이러한 OOP 패러다임을 여러 언어에서 흔히 사용하는 이유는 추상적인 개념을 실체화하기 쉽고, 인간이 이해하기 쉬운 구조로 프로그램을 구성할 수 있기 때문이다. 그러나 Rust는 전통적인 의미의 클래스나 상속 개념 대신 트레이트와 구조체, 그리고 다양한 기법을 조합하여 OOP적 문제를 해결한다.

Rust에서 OOP적 설계를 가능하게 하는 기초가 되는 요소는 트레이트다. 트레이트는 특정 동작, 즉 “행동(behavior)”을 정의하는 일종의 인터페이스 개념을 담당한다. 이 트레이트는 구조체나 열거형 같은 구체 타입에 대해 구현할 수 있으며, 이때 각 타입이 해당 트레이트의 메서드를 어떻게 구현하는지를 선택할 수 있다. 만약 트레이트를 통해 동일한 메서드 시그니처를 가진 여러 타입을 추상화한다면, Rust는 이 트레이트를 만족하는 모든 타입을 동일한 인터페이스로 다룰 수 있게 해준다. 즉, OOP에서 추상 클래스나 인터페이스를 두고 그 구현체들을 다양한 자식 클래스로 구분하듯이, Rust에서는 트레이트가 이러한 역할을 수행한다.

Rust의 트레이트는 여러 가지 측면에서 OOP의 인터페이스나 추상 클래스와 유사하지만, 근본적으로 다른 동작 원리를 가지고 있다. 전통적인 OOP 언어에서는 클래스 상속 계층을 통해 공유할 수 있는 속성과 메서드가 물리적으로 결합되지만, Rust에서는 데이터(구조체 등)와 동작(트레이트)을 완전히 분리함으로써 결합도를 낮추고 자유도를 높인다. 또한 Rust에는 클래스 상속이 존재하지 않으므로, 기존 클래스를 확장하는 대신 트레이트 구현을 통해 동일한 기능을 여러 구조체가 선택적으로 공유할 수 있다. 이러한 구조는 크게 두 가지 이점을 제공한다. 첫째, 다중 상속으로 인한 복잡성을 우회한다. 둘째, 특정 구조체에 필요한 동작만을 유연하게 조합할 수 있어 설계가 단순해진다. 이처럼 Rust의 트레이트는 상속 중심의 OOP 구조를 직접 제공하기보다는, 각 타입이 필요한 만큼의 기능을 선택적으로 “믹스인(mixin)” 하는 느낌에 가깝다.

Rust에서 다형성을 구현할 때에는 제너릭과 트레이트 바운드를 활용하는 방식과 트레이트 객체(dyn Trait)를 사용하는 방식이 존재한다. 제너릭 함수나 구조체 정의에서 트레이트 바운드를 적용하면, 컴파일 시점에 해당 트레이트를 구현하는 모든 타입에 대해 최적화된 코드를 생성할 수 있다. 이를 정적 디스패치라고 부르며, 구체 타입이 결정되어 있는 만큼 성능 이점이 있다. 반면, 런타임 중에 서로 다른 타입을 하나의 트레이트로 추상화하여 다루고 싶다면, &dyn Trait 같은 트레이트 객체를 이용한다. 이렇게 하면 어떠한 타입이든지 해당 트레이트를 구현하기만 하면 공통의 인터페이스로 접근할 수 있고, 호출 시점에는 가상 함수 테이블(가상 메서드 테이블)을 통해 동적으로 어떤 메서드를 부를지 결정한다. 이것을 동적 디스패치라고 한다. 전통적인 OOP 언어의 “부모 클래스 참조가 자식 클래스 인스턴스를 가리키는” 방식과 유사하지만, Rust는 트레이트 객체를 통해 이 구현을 제공한다.

Rust가 추구하는 안전성도 트레이트 개념의 설계에 고스란히 녹아 있다. 전통적인 OOP 언어에서는 클래스 상속을 잘못 사용했을 때, 부모 클래스와 자식 클래스 간의 상호작용에서 런타임 오류가 발생하거나 복잡한 의존성이 생길 위험이 높다. Rust는 컴파일러가 타입과 트레이트 구현 간의 부합성을 엄격하게 검사한다. 트레이트는 구현해야 할 메서드 시그니처를 정확히 정의하므로, 빌드 단계에서 요구 사항이 충족되지 않으면 컴파일이 실패한다. 그 결과 런타임 오류가 줄고, 더욱 안전한 코드를 작성할 수 있게 된다.

OOP에서는 클래스가 상태와 동작을 모두 내장하여 상속 계층을 통해 확장되는 형태가 일반적이다. Rust에서는 데이터는 구조체가, 동작은 트레이트가 담당하며, 구조체와 트레이트의 관계는 상속이 아닌 구현(implement)으로만 이루어진다. 또한 여러 개의 트레이트를 동시에 구현함으로써 믹스인처럼 재사용 가능한 동작을 혼합하기 용이하다. 이는 Rust에서 “객체 지향”적인 문제를 처리할 때 매우 강력한 방법이 되며, 상속이 없는 대신 트레이트와 구조체의 조합이 더욱 세밀하고 안전한 추상화를 가능하게 한다.

코드 예시를 통해 비교해 보면, 전통적인 OOP 언어에서는 부모 클래스나 인터페이스를 두고 그 인터페이스를 구현하는 클래스가 존재한다. Rust에서는 트레이트로 인터페이스를 정의하고, 구조체가 이를 구현한다. 예를 들어 다음과 같은 구조체와 트레이트가 있을 수 있다.

trait Drawable {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("반지름 {}인 원을 그린다.", self.radius);
    }
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Drawable for Rectangle {
    fn draw(&self) {
        println!("가로 {} x 세로 {} 사각형을 그린다.", self.width, self.height);
    }
}

여기서 Drawable이라는 트레이트는 “그릴 수 있다”는 인터페이스를 대표한다. Circle과 Rectangle은 각각 “draw” 메서드를 구현하여 이 인터페이스를 만족시킨다. 전통적인 OOP 방식으로 본다면 Drawable은 추상 클래스 혹은 인터페이스, Circle과 Rectangle은 그 구현체 클래스에 해당한다. 이 트레이트를 사용하는 방식은 크게 두 가지가 있다. 첫 번째는 정적 디스패치를 사용하는 제너릭 함수다.

이 경우 draw_generic 함수는 컴파일 시점에 구체화되어 Circle이 올 때에는 Circle용 코드가, Rectangle이 올 때에는 Rectangle용 코드가 생성된다. 반면, 실행 중에 서로 다른 Drawable 타입을 한꺼번에 처리하고 싶다면 &dyn Drawable 같은 트레이트 객체를 사용할 수 있다.

이런 방식으로 &dyn Drawable을 인자로 받으면, 호출될 때마다 어떤 구체 타입이 들어가든지 draw 메서드를 호출 가능하다. 런타임에는 vtable(가상 메서드 테이블)을 사용해 실제 어떤 타입의 draw 메서드를 부를지 결정한다. 이는 전통적인 OOP에서 부모 클래스의 포인터가 자식 클래스를 참조하고, 자식의 오버라이드된 메서드를 호출하는 것과 유사한 원리다.

Rust는 이러한 트레이트 기반 추상화를 통해 OOP에서 제공하는 주요 기능인 다형성을 강력하게 지원하지만, 상속 계층을 두지 않는 설계 철학을 취한다. 그 결과로 얻는 장점은 코드 중복의 감소, 추상화 수준의 명확성, 그리고 컴파일 타임 검사를 통한 안전성 향상 등이다. 반대로 전통적인 클래스를 통한 객체지향 스타일에 이미 익숙하다면, 상속과 같은 특정 기능이 Rust에는 없다는 사실에 처음엔 적잖이 혼동을 겪을 수 있다. 하지만 트레이트와 구조체의 조합, 제너릭, 트레이트 객체 등의 메커니즘을 이해하면 Rust에서 어떻게 OOP적 문제를 확장 가능하고 안전하게 해결할 수 있는지 명확히 알게 된다.

정리하자면 Rust는 전통적인 OOP 문법을 제공하지 않지만, 트레이트를 통해 추상화 개념을 제공하고, 정적 디스패치와 동적 디스패치를 지원함으로써 다양한 다형적 시나리오를 처리할 수 있다. 필요하다면 구조체와 트레이트를 적절히 조합해 객체지향적인 설계를 구현하면서도, Rust가 제공하는 제너릭과 엄격한 빌드 시 검사 덕분에 안정성과 성능을 모두 얻을 수 있다. 이때 트레이트와 구조체는 상속 대신 구현을 통한 결합을 사용하므로, 상속 트리로 인한 복잡성이나 의존성 문제를 피하고 더 유연한 설계를 시도할 수 있다. 이러한 점들이 Rust가 추구하는 “안전성, 성능, 그리고 병행성”이라는 세 마리 토끼를 잡는 데 중요한 역할을 한다.

Last updated