Copy: 이동(Move) 대신 값 복사하기

러스트의 가장 독특한 특징은 ‘소유권(Ownership)’ 시스템이며, 기본적으로 값의 할당은 소유권의 ‘이동(Move)‘으로 이어집니다. 하지만 정수(i32)와 같이 간단한 타입의 경우, 값을 이동시키는 것보다 단순히 비트 단위로 복사하는 것이 훨씬 직관적이고 효율적입니다. Copy 트레이트는 바로 이러한 ‘복사 의미론(Copy Semantics)‘을 가능하게 하는 마커 트레이트입니다.

이동(Move) vs 복사(Copy)

먼저 러스트의 기본 동작인 ‘이동’을 살펴보겠습니다.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1의 소유권이 s2로 '이동(move)'됩니다.
 
    // 아래 코드는 컴파일 에러를 발생시킵니다.
    // s1은 더 이상 유효한 값을 가지고 있지 않기 때문입니다.
    println!("s1 is: {}", s1); // error[E0382]: borrow of moved value: `s1`
}

String 타입은 힙에 데이터를 저장하므로, 복사가 비쌀 수 있고 이중 해제와 같은 문제를 일으킬 수 있어 Copy 트레이트를 구현하지 않습니다.

반면, Copy 트레이트를 구현하는 타입은 할당 시 값이 복사됩니다.

fn main() {
    let x = 5; // i32는 Copy 트레이트를 구현합니다.
    let y = x; // x의 값이 y로 '복사(copy)'됩니다.
 
    // x와 y 모두 유효한 값을 가집니다.
    println!("x = {}, y = {}", x, y); // x = 5, y = 5
}

Copy 트레이트의 조건

어떤 타입이 Copy가 되려면 다음 두 가지 조건을 만족해야 합니다.

  1. Clone 트레이트를 반드시 구현해야 합니다. Copy는 암묵적인 복사를, Clone.clone()을 통한 명시적인 복사를 다룹니다. Copy가 가능하다는 것은 Clone도 가능하다는 의미의 상위 집합과 같습니다. Copy는 컴파일러에 의해 수행되는 memcpy와 같은 단순한 비트 단위 복사로 구현될 수 있는 타입에만 허용됩니다.

  2. 모든 필드가 Copy여야 합니다. 구조체나 열거형이 Copy가 되려면, 그 안에 포함된 모든 필드나 값들도 Copy 트레이트를 구현해야 합니다. 예를 들어, String 필드를 가진 구조체는 Copy가 될 수 없습니다.

사용자 정의 타입에 Copy 구현하기

#[derive] 속성을 사용하면 간단하게 CopyClone을 구현할 수 있습니다.

// Point 구조체의 모든 필드(i32, i32)가 Copy이므로, Point도 Copy가 될 수 있습니다.
#[derive(Debug, Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}
 
fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1; // 이동이 아닌 복사가 일어납니다.
 
    // p1은 여전히 유효합니다.
    println!("p1: {:?}, p2: {:?}", p1, p2);
}

Drop과의 관계

CopyDrop 트레이트는 함께 구현될 수 없습니다. Copy는 값이 여러 복사본을 가질 수 있음을 의미하는데, 만약 Drop이 구현되어 있다면 각 복사본이 스코프를 벗어날 때마다 drop 로직이 호출되어 이중 해제와 같은 문제가 발생할 수 있습니다. 러스트 컴파일러는 이러한 위험을 원천적으로 차단합니다.

결론

Copy 트레이트는 스택에 저장되며 비트 단위 복사가 안전하고 저렴한 타입에 대해, 러스트의 기본 이동 의미론 대신 더 직관적인 복사 의미론을 적용할 수 있게 해주는 유용한 도구입니다. #[derive(Copy, Clone)]을 통해 사용자 정의 타입도 쉽게 복사 가능하게 만들 수 있으며, 이를 통해 더 간결하고 읽기 쉬운 코드를 작성할 수 있습니다.