← 목록  ·  스마트 포인터  ·  동시성  ·  이터레이터  ·  고급 트레이트

Rust 디자인 패턴

Rust의 타입 시스템과 소유권 모델을 활용해 다른 언어에선 불가능하거나 불편한 패턴들을 구현하는 방법. 기초 문법은 알고 있다고 가정한다.


1. Newtype Pattern 타입 안전성

type Meters = f64는 별칭일 뿐이다 — 컴파일러 눈엔 그냥 f64. Newtype은 새 타입을 진짜로 만든다.

❌ 타입 별칭 — 구분 불가

type Meters = f64;
type Kilograms = f64;

fn launch(dist: Meters, mass: Kilograms) {}

let d: Meters = 100.0;
let m: Kilograms = 75.0;
launch(m, d); // 컴파일 OK — 버그!

✅ Newtype — 컴파일 에러

struct Meters(f64);
struct Kilograms(f64);

fn launch(dist: Meters, mass: Kilograms) {}

let d = Meters(100.0);
let m = Kilograms(75.0);
launch(m, d); // 컴파일 에러!

Orphan Rule 우회 — 외부 타입에 외부 트레이트를 직접 impl 할 수 없다. Newtype으로 감싸면 된다.

use std::fmt;

// Vec에 Display를 impl 하고 싶다 → orphan rule 위반
// 해결: newtype

struct Wrapper(Vec);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

let w = Wrapper(vec!["hello".to_string(), "world".to_string()]);
println!("{}", w); // [hello, world]
Deref 구현하면 편함impl Deref for Meters를 추가하면 .0 없이 내부 값처럼 쓸 수 있다. 단, 그러면 타입 안전성이 일부 희생되니 트레이드오프를 고려할 것.

2. Builder Pattern 복잡한 초기화

선택적 필드가 많거나, 생성 시점에 validation이 필요할 때. 생성자를 new(required)로 단순하게 유지하면서 선택 옵션은 체이닝으로.

#[derive(Debug)]
struct HttpRequest {
    method: String,
    url: String,
    timeout_ms: u64,
    headers: Vec<(String, String)>,
    body: Option>,
}

#[derive(Default)]
struct HttpRequestBuilder {
    method: Option,
    url: Option,
    timeout_ms: u64,
    headers: Vec<(String, String)>,
    body: Option>,
}

impl HttpRequestBuilder {
    pub fn new() -> Self { Self { timeout_ms: 5000, ..Default::default() } }

    pub fn method(mut self, m: impl Into) -> Self {
        self.method = Some(m.into()); self
    }
    pub fn url(mut self, u: impl Into) -> Self {
        self.url = Some(u.into()); self
    }
    pub fn timeout_ms(mut self, t: u64) -> Self {
        self.timeout_ms = t; self
    }
    pub fn header(mut self, k: impl Into, v: impl Into) -> Self {
        self.headers.push((k.into(), v.into())); self
    }
    pub fn body(mut self, b: Vec) -> Self {
        self.body = Some(b); self
    }

    pub fn build(self) -> Result {
        Ok(HttpRequest {
            method:     self.method.ok_or("method is required")?,
            url:        self.url.ok_or("url is required")?,
            timeout_ms: self.timeout_ms,
            headers:    self.headers,
            body:       self.body,
        })
    }
}

impl HttpRequest {
    pub fn builder() -> HttpRequestBuilder { HttpRequestBuilder::new() }
}

// 사용
let req = HttpRequest::builder()
    .method("POST")
    .url("https://api.example.com/data")
    .timeout_ms(3000)
    .header("Content-Type", "application/json")
    .body(b"{\"key\":\"value\"}".to_vec())
    .build()
    .unwrap();
derive_builder 크레이트를 쓰면 이 보일러플레이트를 #[derive(Builder)] 한 줄로 줄일 수 있다. 실 프로젝트에서 자주 씀.

3. Typestate Pattern Rust만의 패턴

가장 Rust스러운 패턴. 잘못된 상태에서 메서드를 호출하면 컴파일 에러가 난다. 런타임 패닉 없음.

핵심 아이디어: 상태를 제네릭 타입 파라미터로 인코딩한다. 특정 상태에서만 가능한 메서드는 그 상태 타입에만 impl한다.
use std::marker::PhantomData;

// 상태 마커 타입 (데이터 없음, 제로 사이즈)
struct Disconnected;
struct Connected;
struct Authenticated;

struct Connection {
    host: String,
    _state: PhantomData,
}

// Disconnected 상태에서만 connect() 가능
impl Connection {
    pub fn new(host: impl Into) -> Self {
        Connection { host: host.into(), _state: PhantomData }
    }

    pub fn connect(self) -> Result, String> {
        println!("{}에 연결 중...", self.host);
        // 실제로는 TCP 연결 시도
        Ok(Connection { host: self.host, _state: PhantomData })
    }
}

// Connected 상태에서만 login() 가능
impl Connection {
    pub fn login(self, user: &str, pass: &str) -> Result, String> {
        if pass == "secret" {
            println!("{} 로그인 성공", user);
            Ok(Connection { host: self.host, _state: PhantomData })
        } else {
            Err("인증 실패".into())
        }
    }

    pub fn disconnect(self) -> Connection {
        println!("연결 해제");
        Connection { host: self.host, _state: PhantomData }
    }
}

// Authenticated 상태에서만 query() 가능
impl Connection {
    pub fn query(&self, sql: &str) -> Vec {
        println!("실행: {}", sql);
        vec!["row1".into(), "row2".into()]
    }
}

// 사용 예시
fn main() {
    let conn = Connection::::new("db.example.com");
    let conn = conn.connect().unwrap();
    let conn = conn.login("admin", "secret").unwrap();
    let rows = conn.query("SELECT * FROM users");

    // conn.connect()  → 컴파일 에러! Connected 상태가 아님
    // 미연결 상태에서 query() → 컴파일 에러!
}
Connection <Disconnected> Connection <Connected> Connection <Authenticated> connect() login() connect()만 가능 login(), disconnect() query() 가능
트레이드오프: 상태가 많아질수록 타입이 복잡해진다. 상태가 10개 이상이면 상태 머신 크레이트(sm, statig)를 고려할 것.

4. RAII & Drop Pattern 자원 관리

C++에서 온 개념이지만 Rust에서 더 강제된다. 소유권 + Drop = 자원 누수 불가.

use std::sync::{Mutex, MutexGuard};

// 커스텀 파일 핸들 — Drop으로 자동 닫기
struct FileHandle {
    path: String,
    fd: i32, // 실제로는 OS 파일 디스크립터
}

impl FileHandle {
    fn open(path: &str) -> Result {
        println!("열기: {}", path);
        Ok(FileHandle { path: path.to_string(), fd: 42 })
    }

    fn write(&self, data: &[u8]) { /* ... */ }
}

impl Drop for FileHandle {
    fn drop(&mut self) {
        println!("닫기: {} (fd={})", self.path, self.fd);
        // 실제로 close(self.fd) 호출
    }
}

// Guard 패턴 — 락을 스코프에 묶기
struct DbTransaction<'a> {
    conn: &'a mut Connection,
    committed: bool,
}

impl<'a> DbTransaction<'a> {
    fn new(conn: &'a mut Connection) -> Self {
        conn.execute("BEGIN");
        DbTransaction { conn, committed: false }
    }

    fn execute(&mut self, sql: &str) { self.conn.execute(sql); }

    fn commit(mut self) {
        self.conn.execute("COMMIT");
        self.committed = true;
    }
}

impl<'a> Drop for DbTransaction<'a> {
    fn drop(&mut self) {
        if !self.committed {
            self.conn.execute("ROLLBACK"); // 커밋 없이 드롭되면 자동 롤백
        }
    }
}
// ManuallyDrop — 드롭을 직접 제어해야 할 때 (FFI, 저수준 코드)
use std::mem::ManuallyDrop;

let v = ManuallyDrop::new(vec![1, 2, 3]);
// v는 스코프를 벗어나도 drop()이 호출되지 않는다
// 직접 호출하려면:
// unsafe { ManuallyDrop::drop(&mut v); }

5. Strategy Pattern 런타임 다형성

알고리즘을 교체 가능하게. Rust에선 세 가지 방법이 있고 트레이드오프가 다르다.

trait Sorter {
    fn sort(&self, data: &mut Vec);
    fn name(&self) -> &str;
}

struct QuickSort;
struct MergeSort;
struct InsertionSort;

impl Sorter for QuickSort {
    fn sort(&self, data: &mut Vec) { data.sort_unstable(); }
    fn name(&self) -> &str { "QuickSort" }
}
impl Sorter for MergeSort {
    fn sort(&self, data: &mut Vec) { data.sort(); }
    fn name(&self) -> &str { "MergeSort" }
}
impl Sorter for InsertionSort {
    fn sort(&self, data: &mut Vec) {
        for i in 1..data.len() {
            let mut j = i;
            while j > 0 && data[j-1] > data[j] { data.swap(j-1, j); j -= 1; }
        }
    }
    fn name(&self) -> &str { "InsertionSort" }
}

방법 1: 제네릭 — 컴파일 타임 결정

// 단형화(monomorphization) → 최적화 최대
// 단, 런타임에 전략 교체 불가
fn sort_with(sorter: &S, data: &mut Vec) {
    println!("{}로 정렬", sorter.name());
    sorter.sort(data);
}

sort_with(&QuickSort, &mut data);

방법 2: dyn Trait — 런타임 결정

// vtable 오버헤드 있음
// 런타임에 전략 교체 가능
struct Processor {
    sorter: Box,
}
impl Processor {
    fn set_sorter(&mut self, s: Box) {
        self.sorter = s;
    }
    fn run(&self, data: &mut Vec) {
        self.sorter.sort(data);
    }
}
방식속도런타임 교체바이너리 크기
impl Trait최고증가
제네릭 <T: Trait>최고증가
Box<dyn Trait>vtable 호출작음

6. Extension Trait Pattern 외부 타입 확장

외부 타입에 메서드를 추가하고 싶을 때. 특히 표준 라이브러리 타입을 확장하거나 서드파티 타입에 편의 메서드를 붙일 때 쓴다.

// &str / String에 편의 메서드 추가
pub trait StringExt {
    fn to_snake_case(&self) -> String;
    fn truncate_at(&self, max: usize, suffix: &str) -> String;
    fn is_valid_email(&self) -> bool;
}

impl StringExt for str {
    fn to_snake_case(&self) -> String {
        let mut result = String::new();
        for (i, ch) in self.chars().enumerate() {
            if ch.is_uppercase() && i > 0 { result.push('_'); }
            result.push(ch.to_lowercase().next().unwrap());
        }
        result
    }

    fn truncate_at(&self, max: usize, suffix: &str) -> String {
        if self.chars().count() <= max {
            return self.to_string();
        }
        let truncated: String = self.chars().take(max).collect();
        format!("{}{}", truncated, suffix)
    }

    fn is_valid_email(&self) -> bool {
        self.contains('@') && self.contains('.')
    }
}

// 이제 모든 &str, String에서 사용 가능
// (use crate::StringExt; 가 필요)
let s = "CamelCaseString";
println!("{}", s.to_snake_case()); // camel_case_string

let long = "이것은 아주 긴 문장입니다.";
println!("{}", long.truncate_at(7, "...")); // 이것은 아주 긴...
관용구: 확장 트레이트는 보통 *Ext 접미사를 붙인다 (IteratorExt, FutureExt 등). futures 크레이트의 FutureExt가 대표적인 예.