Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

17-1. 최적화: 과도한 clone() 피하기 🟡

학습 목표:

  • Rust에서 .clone()이 왜 때때로 '코드 스멜(Code Smell)'로 간주되는지 이해하고, 불필요한 복사를 줄이는 설계를 배웁니다.
  • 성능과 안전성을 동시에 잡는 Cow (Clone-on-Write) 패턴을 익힙니다.
  • 순환 참조(Reference Cycle)를 끊는 Weak 포인터의 활용법을 배웁니다.
  • **Copy**와 **Clone**의 근본적인 차이를 마스터하여 최적의 트레이트를 선택합니다.

1. clone()은 언제 피해야 하는가?

C++ 개발자에게 .clone()은 익숙한 복사 생성자처럼 느껴질 수 있지만, Rust에서는 소유권 설계를 제대로 고민하지 않았을 때 선택하는 '손쉬운 회피책'인 경우가 많습니다.

  • 안 좋은 패턴: 읽기 전용 함수에 데이터를 넘기기 위해 String 전체를 복사함.
  • 권장 패턴: 소유권을 넘기지 말고 **빌림(&str, &T)**을 사용하여 힙 할당 비용을 0으로 만듭니다.
#![allow(unused)]
fn main() {
// [비효율적] 매번 새로운 힙 할당(Deep Copy) 발생
fn process_data(data: String) { /* 읽기만 수행 */ }
process_data(my_string.clone());

// [효율적] 참조(Borrow)만 전달 (제로 코스트)
fn process_data_ref(data: &str) { /* 읽기만 수행 */ }
process_data_ref(&my_string);
}

clone()이 정당한 경우

  1. 스레드 간 데이터 공유: Arc::clone()은 데이터 복사가 아닌 참조 카운트만 올리므로 매우 저렴합니다.
  2. 독립적 소유권: 두 객체가 동일한 데이터를 각자 독자적으로 소유하고 수정해야 할 때.
  3. 비동기/클로저: move 클로저로 데이터를 넘겨야 하는데 원본도 계속 유지해야 할 때.

2. Cow<'a, T>: 필요할 때만 복제하기 (Clone-on-Write)

"평소에는 빌려 쓰다가(Borrowed), 수정이 필요할 때만 내 것으로 만든다(Owned)"는 영리한 지연 복제 전략입니다.

#![allow(unused)]
fn main() {
use std::borrow::Cow;

fn normalize_name(name: &str) -> Cow<str> {
    if name.chars().any(char::is_uppercase) {
        // 대문자가 섞여 있으면 소문자로 변환하여 '새 소유권' 생성 (Alloc)
        Cow::Owned(name.to_lowercase())
    } else {
        // 이미 소문자라면 기존 데이터를 그대로 '참조'만 함 (Zero Alloc)
        Cow::Borrowed(name)
    }
}
}

3. Weak<T>: 순환 참조의 고리 끊기

두 객체가 RcArc로 서로를 강력하게(Strong) 붙잡고 있으면, 참조 카운트가 영원히 0이 되지 않아 메모리 누수가 발생합니다.

  • 해결책: 한쪽은 강한 참조(Rc), 반대쪽은 **약한 참조(Weak)**를 사용하세요.
  • C++ 비교: std::shared_ptrstd::weak_ptr의 관계와 정확히 일치합니다.
#![allow(unused)]
fn main() {
struct Node {
    parent: Weak<Node>, // 부모는 자식을 소유하지만, 자식은 부모를 '바라보기만' 함
    children: Vec<Rc<Node>>,
}
}

4. Copy vs Clone: 무엇을 구현할 것인가?

특징Copy (암시적 복사)Clone (명시적 복제)
방식비트 단위 memcpy (초고속).clone() 메서드 호출 (상대적으로 느림)
대상정수, 불리언, 단순 구조체String, Vec 등 힙 메모리 소유 타입
C++ 비유Trivially Copyable (POD)커스텀 복사 생성자 (Deep Copy)

💡 derive 결정 트리

  1. 힙 메모리(String, Vec 등)를 소유하는가?
    • YES → **Clone**만 가능
    • NO → Copy, Clone 둘 다 구현 (적극 권장)
  2. f32, f64 필드가 포함되어 있는가?
    • YES → **PartialEq**만 가능 (NaN 비교 문제 때문)
    • NO → Eq, PartialEq 둘 다 가능 (HashMap 키로 활용 가능)

💡 실무 팁: Arc::clone은 안심하세요

코드 리뷰 중 .clone()이 보이면 유심히 살펴보되, 그것이 **Arc**나 **Rc**의 메서드라면 안심하셔도 됩니다. 그것은 무거운 데이터를 복사하는 것이 아니라 단순히 '공유권'을 하나 더 늘리는 행위이며, 성능 영향은 무시해도 좋을 만큼 미미합니다.


📌 요약

  • **.clone()**은 힙 할당을 수반하므로 참조(&)로 대체 가능한지 먼저 확인하세요.
  • **Cow**는 읽기가 많고 가끔 수정되는 데이터 최적화에 탁월합니다.
  • Weak 포인터로 복잡한 객체 그래프에서의 메모리 누수를 방지하세요.
  • 데이터 타입 설계 시 Copy 트레이트를 허용할 수 있다면 적극적으로 활용하세요.