Rust 기초: Copy와 Clone 트레이트 이해하기
Rust에서 변수를 다른 변수에 할당하면 값이 복사될 때도 있고 소유권이 이동할 때도 있습니다.
정수는 let y = x; 해도 x를 계속 쓸 수 있는데, String은 같은 걸 하면 원래 변수를 못 쓰게 되죠.
이 차이를 결정하는 게 바로 Copy와 Clone 트레이트입니다.
둘 다 “값을 복사한다”는 점은 같지만, 동작 방식과 쓰임새가 꽤 다릅니다.
이 글에서는 Copy와 Clone이 각각 무엇이고, 어떤 관계이며, 실제로 어떻게 쓰는지 알아보겠습니다.
소유권과 빌림에서 다룬 이동(move)과 복사(copy) 개념을 알고 있으면 더 수월합니다.
Copy란?
Copy는 값을 할당하거나 함수에 넘길 때 자동으로 비트 단위 복사가 일어나게 하는 트레이트입니다.
Copy가 구현된 타입은 소유권이 이동하지 않고 값이 그대로 복사되기 때문에 원래 변수도 계속 사용할 수 있습니다.
fn main() {
let x = 42;
let y = x; // 복사됨
println!("x = {x}"); // x 여전히 사용 가능
println!("y = {y}");
}
x = 42
y = 42
정수 i32는 Copy를 구현하고 있어서 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이란?
Clone은 clone() 메서드를 호출해서 값의 독립적인 복사본을 만드는 트레이트입니다.
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의 관계
여기서 헷갈리기 쉬운 부분이 있습니다.
Copy는 Clone의 하위 트레이트(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이기도 하지만 그 반대는 아니라는 겁니다.
String은 Clone은 되지만 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이고 f64는 Copy이기 때문에 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를 쓰면 모든 필드를 그대로 복사하지만, 직접 구현하면 특정 필드를 초기화하거나 변환하는 등 원하는 동작을 넣을 수 있습니다.
한눈에 보는 정리
어떤 타입에 어떤 트레이트를 쓸 수 있는지 정리하면 이렇습니다.
| 타입 | Copy | Clone | 예시 |
|---|---|---|---|
| 정수, 부동소수점 | O | O | i32, f64 |
| 불리언, 문자 | O | O | bool, char |
| Copy 타입의 튜플/배열 | O | O | (i32, f64), [u8; 4] |
참조 (&T) | O | O | &String, &Vec<i32> |
String | X | O | 힙에 데이터 저장 |
Vec<T> | X | O | 힙에 데이터 저장 |
Box<T> | X | O | 힙 포인터 |
마치며
Copy는 자동 비트 복사, Clone은 명시적 clone() 호출.
이 둘의 차이만 잡아두면 Rust의 값 복사가 한결 명확해집니다.
구현할 수 있다고 무조건 Copy를 붙이기보다는, 타입이 앞으로 어떻게 바뀔지 한번 생각해보고 결정하는 게 좋습니다.
소유권과 빌림이 아직 익숙하지 않다면 먼저 읽어보시는 걸 추천합니다. 구조체나 열거형이 궁금하다면 구조체와 열거형 글도 참고해보세요.
This work is licensed under
CC BY 4.0