Rust 기초: Copy와 Clone 트레이트 이해하기

Rust에서 변수를 다른 변수에 할당하면 값이 복사될 때도 있고 소유권이 이동할 때도 있습니다. 정수는 let y = x; 해도 x를 계속 쓸 수 있는데, String은 같은 걸 하면 원래 변수를 못 쓰게 되죠.

이 차이를 결정하는 게 바로 CopyClone 트레이트입니다. 둘 다 “값을 복사한다”는 점은 같지만, 동작 방식과 쓰임새가 꽤 다릅니다.

이 글에서는 CopyClone이 각각 무엇이고, 어떤 관계이며, 실제로 어떻게 쓰는지 알아보겠습니다. 소유권과 빌림에서 다룬 이동(move)과 복사(copy) 개념을 알고 있으면 더 수월합니다.

Copy란?

Copy는 값을 할당하거나 함수에 넘길 때 자동으로 비트 단위 복사가 일어나게 하는 트레이트입니다. Copy가 구현된 타입은 소유권이 이동하지 않고 값이 그대로 복사되기 때문에 원래 변수도 계속 사용할 수 있습니다.

fn main() {
    let x = 42;
    let y = x; // 복사됨

    println!("x = {x}"); // x 여전히 사용 가능
    println!("y = {y}");
}
결과
x = 42
y = 42

정수 i32Copy를 구현하고 있어서 let y = x;가 이동이 아니라 복사입니다. 함수에 넘길 때도 마찬가지예요.

fn print_number(n: i32) {
    println!("숫자: {n}");
}

fn main() {
    let x = 42;
    print_number(x);
    println!("x는 아직 쓸 수 있음: {x}"); // OK
}

Rust의 기본 타입 대부분이 Copy입니다. i32, f64, bool, char 같은 원시 타입은 물론이고, 이들로만 구성된 튜플이나 고정 크기 배열도 Copy입니다.

fn main() {
    let point = (3.0, 4.0); // (f64, f64)는 Copy
    let arr = [1, 2, 3];    // [i32; 3]도 Copy

    let point2 = point;
    let arr2 = arr;

    println!("{point:?}, {arr:?}"); // 둘 다 여전히 사용 가능
}

Clone이란?

Cloneclone() 메서드를 호출해서 값의 독립적인 복사본을 만드는 트레이트입니다. Copy와 달리 자동으로 일어나지 않고, 반드시 명시적으로 호출해야 합니다.

fn main() {
    let s1 = String::from("안녕하세요");
    let s2 = s1.clone(); // 명시적 복사

    println!("s1 = {s1}"); // s1도 사용 가능
    println!("s2 = {s2}");
}
결과
s1 = 안녕하세요
s2 = 안녕하세요

String은 힙에 데이터를 저장하기 때문에 Copy가 아닙니다. let s2 = s1;을 하면 소유권이 이동해서 s1을 못 쓰게 되는데요. clone()을 호출하면 힙 데이터까지 통째로 새로 만들어서 독립적인 복사본을 줍니다.

Vec도 마찬가지입니다.

fn main() {
    let v1 = vec![1, 2, 3];
    let v2 = v1.clone();

    println!("v1 = {v1:?}");
    println!("v2 = {v2:?}");
}

clone()은 깊은 복사(deep copy)를 수행하기 때문에 데이터가 클수록 비용이 커집니다. 그래서 Rust는 힙 데이터를 가진 타입에 대해 자동 복사를 허용하지 않고, 개발자가 의도적으로 clone()을 호출하도록 한 것이죠.

Copy와 Clone의 관계

여기서 헷갈리기 쉬운 부분이 있습니다. CopyClone의 하위 트레이트(subtrait)입니다. 즉, Copy를 구현하려면 반드시 Clone도 함께 구현해야 합니다.

// Copy의 정의를 보면 Clone을 요구합니다
pub trait Copy: Clone { }

왜 이런 관계일까요? Copy가 할 수 있는 일(비트 복사)은 Clone이 할 수 있는 일(값 복사)의 특수한 경우이기 때문입니다. Copy인 타입에서도 clone()을 호출할 수 있어야 하니까요.

fn main() {
    let x: i32 = 42;

    let y = x;         // Copy: 자동 복사
    let z = x.clone(); // Clone: 명시적 복사 (같은 결과)

    println!("{x}, {y}, {z}");
}
결과
42, 42, 42

Copy는 암묵적입니다. 할당이나 함수 호출 시 컴파일러가 알아서 비트를 복사해주죠. 스택에 저장되는 가벼운 타입에 적합합니다.

반면 Clone은 명시적이에요. clone() 메서드를 직접 호출해야 하고, 구현에 따라 깊은 복사를 수행합니다. String처럼 힙 데이터를 가진 타입에서 독립적인 복사본이 필요할 때 쓰는 거죠.

한 가지 주의할 점은, Copy인 타입은 반드시 Clone이기도 하지만 그 반대는 아니라는 겁니다. StringClone은 되지만 Copy는 안 되는 대표적인 예입니다.

커스텀 타입에 Copy 구현하기

직접 만든 구조체에도 Copy를 구현할 수 있습니다. #[derive(Copy, Clone)]을 붙이면 되는데요.

#[derive(Debug, Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = p1; // 복사됨, 이동이 아님

    println!("p1 = {p1:?}");
    println!("p2 = {p2:?}");
}
결과
p1 = Point { x: 1.0, y: 2.0 }
p2 = Point { x: 1.0, y: 2.0 }

Point의 필드가 모두 f64이고 f64Copy이기 때문에 Point에도 Copy를 구현할 수 있는 겁니다.

하지만 필드 중 하나라도 Copy가 아닌 타입이 있으면 안 됩니다.

#[derive(Copy, Clone)] // 컴파일 오류!
struct User {
    name: String, // String은 Copy가 아님
    age: u32,
}
컴파일 오류
error[E0204]: the trait `Copy` cannot be implemented for this type
 --> src/main.rs:1:10
  |
1 | #[derive(Copy, Clone)]
  |          ^^^^
2 | struct User {
3 |     name: String,
  |     ------------ this field does not implement `Copy`

이런 경우에는 Clone만 구현할 수 있습니다.

#[derive(Debug, Clone)]
struct User {
    name: String,
    age: u32,
}

fn main() {
    let user1 = User {
        name: String::from("철수"),
        age: 25,
    };
    let user2 = user1.clone();

    println!("user1 = {user1:?}");
    println!("user2 = {user2:?}");
}
결과
user1 = User { name: "철수", age: 25 }
user2 = User { name: "철수", age: 25 }

열거형에도 적용하기

열거형에도 같은 규칙이 적용됩니다. 모든 변형(variant)의 데이터가 Copy면 열거형에도 Copy를 구현할 수 있습니다.

#[derive(Debug, Copy, Clone)]
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

fn print_direction(d: Direction) {
    println!("방향: {d:?}");
}

fn main() {
    let d = Direction::Up;
    print_direction(d);
    print_direction(d); // Copy 덕분에 두 번 넘겨도 OK
}
결과
방향: Up
방향: Up

데이터를 가진 변형이 있어도 그 데이터가 Copy면 괜찮습니다.

#[derive(Debug, Copy, Clone)]
enum Shape {
    Circle(f64),         // f64는 Copy
    Rectangle(f64, f64), // f64, f64 모두 Copy
}

fn area(shape: Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
    }
}

fn main() {
    let s = Shape::Circle(5.0);
    println!("넓이: {:.2}", area(s));
    println!("다시 사용: {s:?}"); // Copy라서 가능
}
결과
넓이: 78.54
다시 사용: Circle(5.0)

하지만 변형 중 하나라도 String 같은 힙 타입을 가지면 Copy는 불가능합니다.

Copy를 구현하면 안 되는 경우

Copy를 구현할 수 있다고 해서 항상 구현해야 하는 건 아닙니다.

파일 핸들, 네트워크 소켓, 뮤텍스 잠금 같은 리소스를 관리하는 타입은 복사되면 안 됩니다. 같은 파일을 두 곳에서 동시에 닫으려 하거나, 같은 잠금을 두 번 해제하려 하면 문제가 생기죠.

// 나쁜 예: 리소스 핸들에 Copy를 구현
#[derive(Copy, Clone)]
struct FileHandle {
    fd: i32, // 파일 디스크립터는 정수지만...
}
// fd가 복사되면 두 변수가 같은 파일을 소유하게 됨!

나중에 필드를 추가할 가능성이 있는 구조체도 조심해야 합니다. 처음에는 모든 필드가 Copy라서 #[derive(Copy, Clone)]을 붙였는데, 나중에 String 필드를 추가하면 Copy를 제거해야 합니다. 그러면 이 타입을 사용하던 코드가 전부 깨질 수 있어요.

// 처음에는 괜찮았지만...
#[derive(Copy, Clone)]
struct Config {
    max_retries: u32,
    timeout_secs: u64,
}

// 나중에 필드를 추가하면 Copy를 제거해야 함
// #[derive(Clone)]  // Copy 제거 → 기존 코드 컴파일 오류
// struct Config {
//     max_retries: u32,
//     timeout_secs: u64,
//     name: String,  // Copy 불가능한 필드 추가
// }

좌표, 색상, ID처럼 작고 가벼운 데이터 타입에는 Copy가 잘 맞습니다. 리소스를 관리하거나 앞으로 필드가 늘어날 수 있는 타입이라면 Clone만 두는 게 안전하고요.

Clone을 직접 구현하기

대부분의 경우 #[derive(Clone)]이면 충분하지만, 복사 과정을 커스터마이즈해야 할 때는 직접 구현할 수도 있습니다.

#[derive(Debug)]
struct Counter {
    name: String,
    count: u32,
}

impl Clone for Counter {
    fn clone(&self) -> Self {
        Counter {
            name: self.name.clone(),
            count: 0, // 복사할 때 카운터를 초기화
        }
    }
}

fn main() {
    let c1 = Counter {
        name: String::from("요청 수"),
        count: 42,
    };
    let c2 = c1.clone();

    println!("c1: {} = {}", c1.name, c1.count);
    println!("c2: {} = {}", c2.name, c2.count);
}
결과
c1: 요청 = 42
c2: 요청 = 0

derive를 쓰면 모든 필드를 그대로 복사하지만, 직접 구현하면 특정 필드를 초기화하거나 변환하는 등 원하는 동작을 넣을 수 있습니다.

한눈에 보는 정리

어떤 타입에 어떤 트레이트를 쓸 수 있는지 정리하면 이렇습니다.

타입CopyClone예시
정수, 부동소수점OOi32, f64
불리언, 문자OObool, char
Copy 타입의 튜플/배열OO(i32, f64), [u8; 4]
참조 (&T)OO&String, &Vec<i32>
StringXO힙에 데이터 저장
Vec<T>XO힙에 데이터 저장
Box<T>XO힙 포인터

마치며

Copy는 자동 비트 복사, Clone은 명시적 clone() 호출. 이 둘의 차이만 잡아두면 Rust의 값 복사가 한결 명확해집니다.

구현할 수 있다고 무조건 Copy를 붙이기보다는, 타입이 앞으로 어떻게 바뀔지 한번 생각해보고 결정하는 게 좋습니다.

소유권과 빌림이 아직 익숙하지 않다면 먼저 읽어보시는 걸 추천합니다. 구조체나 열거형이 궁금하다면 구조체열거형 글도 참고해보세요.

This work is licensed under CC BY 4.0 CC BY

Discord