Post

6. Rust copy와 clone 정리(깊은 복사, 얕은 복사 포함)

clone과 copy

Rust에서 CopyClone은 두 가지 서로 다른 트레이트(trait)입니다. 이 두 트레이트는 값의 복사와 복제를 다르게 처리합니다.

Copy 트레이트

  • Copy 트레이트는 값의 복사를 나타내며, 스택에 저장된 값의 복사본이 생성됩니다.
  • Copy 트레이트는 스택에 저장된 고정 크기의 값들에 대해서만 적용됩니다.
  • 변수를 다른 변수에 할당할 때나 함수로 전달할 때 복사가 발생합니다.
  • Copy 트레이트를 구현하는 타입은 일반적으로 Copy 성격을 가진 기본 타입(primitive type)들이며, Clone 트레이트를 구현하지 않습니다.

Clone 트레이트

  • Clone 트레이트는 값의 복제를 나타내며, 힙에 저장된 값의 복사본이 생성됩니다.
  • Clone 트레이트는 값의 복제가 필요한 경우에 사용됩니다.
  • Clone 트레이트를 구현하는 타입은 *Copy와는 달리 복제를 지원하는 사용자 정의 타입에 적용됩니다.

예를 들어, 다음과 같은 코드에서는 Copy 트레이트가 사용됩니다.

1
2
3
let x = 5;
let y = x; // 복사 발생

반면에, Clone 트레이트는 사용자 정의 타입에서 직접 구현해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
#[derive(Debug, Copy, Clone)]
struct MyStruct {
    x: i32,
    y: i32,
}

fn main() {
    let original = MyStruct { x: 10, y: 20 };
    let copied = original; // !!! 복사 발생 !!!
    println!("{:?}", original); // 복사 이후에도 원본 변수를 사용할 수 있습니다.
}

만약 MyStruct가 가변 크기 데이터를 갖는 경우 Copy trait을 구현할 수 없기 떄문에 #[derive(Copy)]는 에러를 유발한다.

1
2
3
4
5
6
struct MyStruct {
    x: i32,
    y: i32,
    name: String // 빌드 에러 유발 - Copy trait 불가
}

만약 사용자정의 struct에 Copy trait을 구현하지 않는다면, =을 통해서 copy가 이뤄지지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#[derive(Debug, Clone)]
struct MyStruct {
    x: i32,
    y: i32,
    // name: String
}

fn main() {
    let original = MyStruct { x: 10, y: 20 };
    let copied = original; // 이동 발생
    println!("{:?}", original); // 컴파일 에러. 복사 이후에도 원본 변수를 사용할 수 없음
}

깊은 복사_와 _얕은 복사

얕은 복사는 데이터 구조의 복사본이 원본과 동일한 메모리를 공유하며, 내부 객체는 참조로 복사되어 변경 시 동일한 객체가 변경될 수 있습니다. 반면에 깊은 복사는 복사본이 원본과는 다른 메모리를 사용하며, 내부 객체가 복제되어 새로운 객체가 생성되어 원본과 독립적으로 존재합니다. C,C++,Java와 같은 언어에서 Vector(or List)와 같은 자료구조에 대해 대입 혹은 clone method를 호출 할 경우 기본적으로 레퍼런스만 복제되는 얕은 복사가 이뤄집니다. 이 경우 복제된 변수의 수정은 원본 data의 수정을 유발합니다. 반면 Rust에서는 대입연산(=)은 소유권 이동을 의미하기 때문에 이러한 얕은 복사가 발생할 여지가 없고, clone trait 역시 다른 언어와 다르게 깊은 복사를 유발합니다. C++에서 깊은 복사와 얕은복사로 인해 소멸자 호출등으로 비정상적인 메모리 접근이 발생하거나 이를 방지하기 위해 다양한 테크닉을 써야하는데, rust는 언어적으로 훨씬 간편하게 이를 해결했다고 볼 수 있습니다.

깊은 복사 예제

#[derive(Clone)] 메크로를 통해 구현된 코드는 해당 타입의 필드가 구현된 Clone 트레이트를 사용하여 복제할 수 있습니다. Vec<String>과 같이 여러 개의 String을 포함하는 컬렉션을 필드로 갖는 경우는 #[derive(Clone)]선언 만으로 깊은 복사가 이뤄 집니다. Clone 트레이트의 기본 동작은 깊은 복사(deep copy)를 수행합니다. 즉, Vec가 복제될 때는 해당 Vec의 요소들에 대해 clone이 호출되며 새로운 복사본이 생성됩니다.

따라서 Vec<String>을 포함하는 사용자 정의 타입에 대한 Clone 구현은 깊은 복사를 수행하는 것을 알 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[derive(Debug, Clone)]
struct MyStruct {
    age: i32,
    data: Vec<String>,
}

fn main() {
    let original = MyStruct {
        age: 10,
        data: vec![String::from("hello"), String::from("world")],
    };

    // 깊은 복사 수행
    let copied = original.clone();

    // 원본과 복사본이 동일한 메모리를 가리키고 있음을 확인하기 위해 주소 출력
    println!("Original: {:p}", &original.data); // 0xb4ffb2f690
    println!("Copied: {:p}", &copied.data);     // 0xb4ffb2f708
    println!("===================================");
    println!("Original: {:p}", &original.age);  // 0xb4ffb2f6a8
    println!("Copied: {:p}", &copied.age);      // 0xb4ffb2f720

    println!("Original: {:?}, Copied: {:?}", original, copied); // Original: MyStruct { age: 10, data: ["hello", "world"] }, Copied: MyStruct { age: 10, data: ["hello", "world"] }

    original.data.push(String::from("123"));
    copied.data.push(String::from("456"));
    println!("Original: {:?}, Copied: {:?}", original, copied); //Original: MyStruct { age: 10, data: ["hello", "world", "123"] }, Copied: MyStruct { age: 10, data: ["hello", "world", "456"] }
}

소유권과 Clone trait를 통해 rust에서 C++에서 실수하기 쉬운 객체 이동/복사 부분을 간단히 해결한 것을 알 수 있습니다. 참고로 C++에서는 복사 생성자, 복사 대입 연산자, rlavue, rvalue 참조, move semantics등 개념을 통해 이런 문제를 해결하는데, Rust는 언어적으로 이를 극복한 것 같습니다.

This post is licensed under CC BY 4.0 by the author.