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()이 정당한 경우
- 스레드 간 데이터 공유:
Arc::clone()은 데이터 복사가 아닌 참조 카운트만 올리므로 매우 저렴합니다. - 독립적 소유권: 두 객체가 동일한 데이터를 각자 독자적으로 소유하고 수정해야 할 때.
- 비동기/클로저:
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>: 순환 참조의 고리 끊기
두 객체가 Rc나 Arc로 서로를 강력하게(Strong) 붙잡고 있으면, 참조 카운트가 영원히 0이 되지 않아 메모리 누수가 발생합니다.
- 해결책: 한쪽은 강한 참조(
Rc), 반대쪽은 **약한 참조(Weak)**를 사용하세요. - C++ 비교:
std::shared_ptr와std::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 결정 트리
- 힙 메모리(String, Vec 등)를 소유하는가?
- YES → **
Clone**만 가능 - NO →
Copy,Clone둘 다 구현 (적극 권장)
- YES → **
f32,f64필드가 포함되어 있는가?- YES → **
PartialEq**만 가능 (NaN 비교 문제 때문) - NO →
Eq,PartialEq둘 다 가능 (HashMap 키로 활용 가능)
- YES → **
💡 실무 팁: Arc::clone은 안심하세요
코드 리뷰 중 .clone()이 보이면 유심히 살펴보되, 그것이 **Arc**나 **Rc**의 메서드라면 안심하셔도 됩니다. 그것은 무거운 데이터를 복사하는 것이 아니라 단순히 '공유권'을 하나 더 늘리는 행위이며, 성능 영향은 무시해도 좋을 만큼 미미합니다.
📌 요약
- **
.clone()**은 힙 할당을 수반하므로 참조(&)로 대체 가능한지 먼저 확인하세요. - **
Cow**는 읽기가 많고 가끔 수정되는 데이터 최적화에 탁월합니다. Weak포인터로 복잡한 객체 그래프에서의 메모리 누수를 방지하세요.- 데이터 타입 설계 시
Copy트레이트를 허용할 수 있다면 적극적으로 활용하세요.