Rust에서 Data vs Behavior
Source: Dev.to
Rust의 구조체: 데이터 모델링
Rust에서 struct는 순수히 데이터를 나타내는 데 사용됩니다.
struct Rect {
width: f32,
height: f32,
}
구조체는 자체적으로 동작을 정의하지 않습니다. 이는 데이터 표현을 명시적이고 단순하게 유지합니다.
impl으로 동작 추가하기
Rust는 impl 블록을 사용해 데이터에 동작을 붙입니다.
impl Rect {
fn area(&self) -> f32 {
self.width * self.height
}
}
이 방식은 동작이 단일 구체 타입에 속할 때 이상적입니다. 메서드는 &self를 통해 구조체를 빌려오므로 소유권을 이전하지 않고 안전하게 접근할 수 있습니다.
Rust의 트레이트: 동작 계약 정의
트레이트는 타입들 간에 공유되는 동작을 정의합니다.
trait Shape {
fn area(&self) -> f32;
}
트레이트는 무엇을 할 수 있는지를 지정하고, 어떻게 하는지는 지정하지 않습니다.
impl Shape for Rect {
fn area(&self) -> f32 {
self.width * self.height
}
}
이를 통해 여러 타입이 동일한 동작을 구현하면서도 내부 로직은 서로 독립적으로 유지됩니다.
트레이트 바운드와 확장 가능한 설계
트레이트는 제네릭 함수와 함께 사용할 때 특히 강력해집니다.
fn get_area(shape: impl Shape) -> f32 {
shape.area()
}
이 함수는 Shape 트레이트를 구현한 어떤 타입과도 동작합니다. 새로운 도형을 추가해도 기존 코드를 수정할 필요가 없으므로, 트레이트는 Rust에서 확장성을 위한 핵심 도구로 여겨집니다.
Clone vs Copy: 소유권을 인식한 복제
Rust는 값이 복제되는 방식을 제어하기 위해 트레이트를 사용합니다.
#[derive(Clone)]
struct Person {
name: String,
age: u32,
}
String은 힙 메모리를 소유하므로 복제는 명시적으로 이루어져야 합니다:
let p2 = p1.clone();
스택 전용 데이터에 대해서는 Rust가 암시적인 복사를 허용합니다:
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
이러한 명시적 구분은 실수로 인한 메모리 버그를 방지합니다.
Debug vs Display: Rust에서 출력 포맷 구분
Rust는 개발자용 출력과 사용자용 출력을 구분합니다.
Debug 포맷
#[derive(Debug)]
struct Rect {
width: u32,
height: u32,
}
println!("{:?}", rect);
Display 포맷
use std::fmt;
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.width, self.height)
}
}
println!("{}", rect);
이 구분은 깔끔한 로깅과 더 나은 사용자 경험을 장려합니다.
왜 이런 설계가 효과적인가
**데이터(struct)**와 **동작(trait)**을 분리함으로써 Rust는 상속과 관련된 문제를 피하고 컴포지션을 촉진합니다. 결과적으로 코드는 확장·리팩터링·이해가 더 쉬워집니다.