Project/개발 업무

Rust의 Copy와 Clone 차이점 정리

lgvv 2026. 7. 4. 20:04

Rust의 Copy와 Clone 차이점 정리

Rust의 소유권과 관련하여 Copy와 Clone의 차이점 정리

 

// 컴파일 O
let x = 5;
let y = x;

println!("{x}, {y}");

// 컴파일 X

let s1 = String::from("hello");
let s2 = s1;

println!("{s1}");

 

둘 다 값을 다른 변수에 대입했을 뿐인데, 왜 하나는 되고 하나는 안 되는지 이 차이를 설명하는 개념이 Copy와 Clone

 

목차

  • 요약
  • i32는 왜 대입 후에도 사용할 수 있을까?
  • String은 왜 move될까?
  • Copy란?
  • Clone이란?
  • 헷갈리는 부분 정리
  • 구조체에서 Copy와 Clone 사용하기
  • Copy를 붙일지 판단하는 기준
  • 정리

 

요약

구분 Copy Clone
호출 방식 암묵적 명시적
사용 형태 let y = x; let y = x.clone();
복제 비용 일반적으로 매우 작음 타입에 따라 다름
동작 방식 단순한 bit-wise copy 타입이 정의한 clone() 실행
Drop 타입 구현 불가 구현 가능
대표 타입 i32, bool, char, &T String, Vec<T>, Rc<T>, Arc<T>

 

핵심은 명시성으로 Rust에서 비용이 크거나 의미가 타입마다 달라질 수 있는 복사를 자동으로 숨기지 않음.

그래서 String, Vec<T> 같은 타입은 대입할 때 자동 복사되지 않고 move됨. 따라서 완전한 복제가 필요하다면 clone()을 명시적으로 직접 호출해야 함.

i32는 왜 대입 후에도 사용할 수 있을까?

먼저 i32 예제를 보면 이해가 쉬움.

let x = 5;
let y = x;

println!("{x}, {y}");

 

x를 y에 대입했는데도 x를 계속 사용할 수 있음. (이유는 i32가 Copy 타입이기 때문)

Copy 타입은 대입하거나 함수에 넘길 때 소유권이 이동(move)하지 않고 값이 복사됨.

정수처럼 크기가 고정되어 있고, 별도의 Heap 리소스를 관리하지 않는 값은 복사 비용이 작음.

이런 값은 Rust가 조용히 복사해도 큰 문제가 없음.


Rust에서 기본타입은 대표적인 Copy 타입은 아래와 같음.

  • 정수 타입
  • 부동소수점 타입
  • bool
  • char
  • 함수 포인터
  • 공유 참조 &T
  • 모든 요소가 Copy인 tuple

예를 들어 (i32, i32)는 Copy 가능.

let a = (1, 2);
let b = a;

println!("{a:?}, {b:?}");


하지만 (i32, String)은 Copy가 아님.

let a = (1, String::from("hello"));
let b = a;

println!("{a:?}");

 

tuple 안에 String이 들어있기 때문.

Copy는 타입의 모든 구성 요소가 Copy일 때만 가능함.

 

String은 왜 move될까?

아래 코드에서는 소유권이 이동되어서 s1을 더이상 사용할 수 없는 예제

let s1 = String::from("hello");
let s2 = s1; // move 

println!("{s2}"); // ok
println!("{s1}"); // error

 

위 동작은 String의 메모리 구조를 같이 봐야 하는데, String은 Stack에 포인터, 길이, 용량을 저장하고 실제 문자열 데이터는 Heap에 저장.



만약 s1의 소유권이 이동하지 않은 경우에는 같은 Heap을 바라보기 때문에 s1, s2 중 누군가 메모리를 정리할 경우 문제가 발생할 수 있음.

겉으로는 대입처럼 보이지만, Rust 입장에서는 소유권이 이동한 것(move)

 

Copy란?

공식 문서에서 Copy는 아래처럼 정의.

pub trait Copy: Clone { }

 

Rust의 대부분의 원시타입은 Copy trait을 구현하고 있어서 `=` 연산자를 사용하면 Copy가 자동으로 됨.

 

 

fn print_number(value: i32) {
    println!("{value}");
}

fn main() {
    let x = 10;

    print_number(x);
    println!("{x}");
}

 

i32는 Copy이므로 print_number(x) 이후에도 x를 계속 사용할 수 있음.

여기서 중요한 점은 Copy의 동작을 타입이 마음대로 바꿀 수 없음.

 

공식 문서 기준으로 Copy는 단순한 bit-wise copy임.

그래서 Copy를 구현하려면 조건이 있음.

  • 타입의 모든 필드가 Copy여야 함
  • Drop을 구현한 타입은 Copy가 될 수 없음
  • 리소스를 직접 관리하는 타입은 보통 Copy가 될 수 없음

Drop 타입이 Copy가 될 수 없는 이유는 명확함.

값이 암묵적으로 복사되면 어떤 값이 리소스를 정리해야 하는지 애매해짐.

Rust는 리소스 정리 책임이 흐려지는 상황을 막음.

 

Clone이란?

Clone은 값을 명시적으로 복제하기 위한 trait.

공식 문서에서 Clone은 아래처럼 정의됨.

pub trait Clone: Sized {
    fn clone(&self) -> Self;
}

 

Clone에는 clone() 메서드가 있음.

그래서 복제가 필요할 때 직접 호출해야 함.

 

let s1 = String::from("hello");
let s2 = s1.clone();

println!("{s1}, {s2}");

 

String의 clone()은 Heap에 있는 문자열 데이터까지 새로 복사함.

먼저 s1이 가진 메모리 구조를 단순화하면 아래처럼 볼 수 있음.

 

 

s1은 스택에 있고, 실제 문자열 hello는 힙에 존재.

clone()을 호출하면 이 구조와 같은 새 String이 s2에도 만들어져서 s1과 s2는 서로 다른 Heap 데이터를 가리킴
이 경우 각자 자기 데이터를 정리하면 되므로 double free 문제가 생기지 않음.

Rust에서는 메모리 문제가 발생할 수 있는 영역에 대해서 개발자가 clone()을 직접 호출하게 만들어서 복제가 일어난다는 것을 명확하게 표현.

 

Copy와 Clone의 헷갈리는 부분 정리

Rust의 Copy와 Clone에서 헷갈리는 부분 정리

 

  • 1) Copy는 Stack 타입만 가능할까? -> (X)

대체로 단순한 Stack 값이 Copy인 경우가 많음.

 

하지만 "Stack이면 Copy, Heap이면 non-Copy"는 아님

예를 들어 공유 참조 &T는 Copy임.

 

let s = String::from("hello");

let r1 = &s;
let r2 = r1;

println!("{r1}");
println!("{r2}");

 

참조 자체는 포인터처럼 복사될 수 있으며 이 경우에는 소유권이 이동하는 것은 아님.

반대로 String은 Heap 데이터를 소유하고 정리할 책임이 있으므로 Copy가 아님.

  • 2) Clone은 항상 깊은 복사일까? -> (X)

String의 clone()은 Heap 문자열 데이터를 새로 복사하므로 깊은 복사처럼 보임.

하지만 Clone 자체가 항상 깊은 복사를 의미하지는 않음.

예를 들어 Rc<T>의 clone()은 내부 데이터를 새로 복사하지 않으며, 참조 카운트를 증가시키고 같은 값을 공유.

use std::rc::Rc;

let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);

println!("{a}");
println!("{b}");

 

이 코드에서 a와 b는 같은 내부 데이터를 공유하기 때문에 `Clone = 깊은 복사`가 아닐수 있음.

즉, `Clone = 명시적 복제`가 더 가까움

 

  • 3) Copy면 Clone도 되는 걸까? -> (O)

Copy의 trait 정의 자체가 Copy: Clone이기 때문에, Copy 타입은 Clone도 구현해야 함.

 

let x = 10;
let y = x.clone();

println!("{x}, {y}");

 

i32는 Copy이면서 Clone도 가능한데, Copy 타입에서는 보통 clone()을 직접 호출할 필요가 없음. (그냥 대입하면 끝)

 

구조체에서 Copy와 Clone 사용하기

구조체도 모든 필드가 Copy라면 Copy를 구현할 수 있음. (보통은 derive를 사용)

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

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1;

    println!("{p1:?}");
    println!("{p2:?}");
}

 

Point의 필드는 모두 i32. (i32는 Copy.)

모든 Point의 모든 필드가 Copy가 될 수 있음.

반대로 구조체 안에 String이 있으면 Copy를 구현할 수 없음.

 

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

 

이 코드는 컴파일되지 않는데, String이 Copy가 아니기 때문에 이 경우에는 Clone만 사용할 수 있음.

 

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

fn main() {
    let user1 = User {
        name: String::from("lgvv"),
    };

    let user2 = user1.clone();

    println!("{user1:?}");
    println!("{user2:?}");
}

 

이렇게 하면 user1과 user2를 둘 다 사용할 수 있음.

다만 String 필드도 함께 clone되므로 비용이 발생할 수 있음.

 

Copy를 붙일지 판단하는 기준

어떤 타입이 Copy가 될 수 있다고 해서 항상 붙이는 게 좋은 것은 아님.

공식 문서에서도 Copy 구현은 public API의 일부라고 나와 있는데, 한번 Copy를 제공하면 그 타입을 대입해도 원래 값이 계속 유효하다고 개발자기 기대함.

 

한 번 Copy를 제공하면 사용자는 그 타입을 대입해도 원래 값이 계속 유효하다고 기대하게 됨.

나중에 구조체에 String, Vec<T>, 파일 핸들 같은 non-Copy 필드가 추가되면 Copy를 유지할 수 없음.

그때 Copy를 제거하면 기존 코드가 깨질 수 있음.

 

따라서 Copy는 현재 컴파일을 위해 붙이기 보다는 해당 타입이 앞으로 값 타입처럼 쓰일지 생각하고 붙이는 편이 좋음.

 

Copy를 쓰는게 자연스러운 경우

  • 좌표, 크기, 색상, 플래그처럼 작은 값
  • 숫자 기반 ID처럼 복사 의미가 단순한 값
  • 리소스 정리 책임이 없는 값
  • 대입 후에도 기존 값을 계속 쓰는 것이 자연스러운 값
#[derive(Copy, Clone, Debug)]
struct UserId(u64);


Clone만 두는 편이 자연스러운 경우.

  • String, Vec<T>처럼 Heap 데이터를 소유하는 값
  • 파일, 소켓, 락처럼 외부 리소스와 연결된 값
  • 복제 비용이 클 수 있는 값
  • 복제 의미를 코드에서 명시적으로 보여주는 편이 좋은 값
#[derive(Clone, Debug)]
struct Profile {
    name: String,
    tags: Vec<String>,
}

 

Profile은 name과 tags가 Heap 데이터를 소유함.

따라서 대입할 때 자동으로 복사되면 비용도 숨겨지고 의미도 애매해짐.

이런 경우에는 clone()을 명시적으로 호출하는 편이 Rust답다.

 

정리

Copy Clone
Copy의 경우에는 암묵적으로 일어남 (`=` 연산자) `clone`을 통해 명시적으로 발생
Copy의 경우에는 스택에 있는 데이터 복사, 힙 데이터를 복사하는 경우는 발생하지 않음. clone()을 통해 힙 영역까지 명시적 복사
따라서 String에 대해서 스택 정보만 복사되어 소유권 이동만 발생함.
즉, 스택 영역만 새롭게 생성되고, Heap 데이터가 연결됨. (move)
스택과 힙에 대한 정보가 모두 복사 

 

참고