러스트(Rust)의 Copy와 Clone, 뭐가 다르고 언제 쓸까요? 붕어빵 형제 파헤치기!

러스트(Rust)의 Copy와 Clone, 뭐가 다르고 언제 쓸까요? 붕어빵 형제 파헤치기!

러스트(Rust)에서 CopyClone 트레잇(trait)은 타입의 복사 동작을 제어하는 아주 중요한 역할을 한답니다.

이 두 가지를 통해 타입의 값이 어떻게 복사되고, 어떤 상황에서 복사가 허용되는지 정의할 수 있는데요.

이번 글에서는 이 두 트레잇(trait)의 목적과 사용법을 자세히 알아보고, 실제 코드 예제를 통해 어떻게 활용되는지 보여드릴게요.

Copy 트레잇(Trait) 살펴보기

Copy 트레잇(trait)은 타입이 비트 단위로 간단하게 복사될 수 있음을 나타냅니다.

어떤 타입이 Copy 트레잇(trait)을 구현하면, 그 타입의 값은 할당되거나, 함수 인자로 전달되거나, 함수에서 반환될 때 자동으로 복제된답니다.

마치 복사기를 사용하는 것처럼요!

Copy 트레잇(Trait)이란 무엇일까요?

Copy 트레잇(trait)은 특별한 종류의 트레잇(trait)인 마커 트레잇(marker trait)인데요.

마커 트레잇(marker trait)이라는 건 어떤 메서드도 정의하지 않고, 단지 해당 타입이 특정 속성(여기서는 비트 단위 복사가 가능하다는 속성)을 가짐을 표시하는 역할을 한답니다.

// Point 구조체에 Copy 트레잇(trait)을 적용합니다.
#[derive(Copy)] 
struct Point {
    x: i32, // 정수형 필드 x
    y: i32, // 정수형 필드 y
}



Copy 트레잇(Trait)은 어떻게 구현할까요?

Copy 트레잇(trait)을 구현하려면, 타입 정의 위에 #[derive(Copy)] 속성을 추가해주면 됩니다.

그런데 여기서 중요한 점이 하나 있는데요!

Copy 트레잇(trait)을 구현하는 타입은 반드시 Clone 트레잇(trait)도 함께 구현해야 한답니다.

왜냐하면 Copy를 구현하는 모든 타입은 Clone도 구현해야 한다는 규칙이 있기 때문이죠.

// Point 구조체에 Copy와 Clone 트레잇(trait)을 함께 적용합니다.
#[derive(Copy, Clone)] 
struct Point {
    x: i32,
    y: i32,
}



만약 Clone 트레잇(trait)을 구현하지 않고 Copy만 구현하려고 하면, 컴파일러가 오류를 발생시킬 거예요.

// Clone 없이 Copy만 적용하려고 시도하는 경우
#[derive(Copy)] 
struct Point {
    x: i32,
    y: i32,
}

// 컴파일 오류 발생!
// error[E0277]: the trait bound `Point: std::clone::Clone` is not satisfied
// Point 타입이 Clone 트레잇(trait)을 만족하지 않는다는 의미입니다.




오류 메시지를 보면 Point 타입이 Clone 트레잇(trait)을 구현하지 않아서 Copy를 구현할 수 없다고 알려주죠.

이런 요구사항이 있는 이유는 모든 Copy 타입은 Clone도 구현해야 하기 때문인데요.

우리가 명시적으로 clone 메서드를 호출할 때, 러스트(Rust)는 우리가 복사본을 만들 의도가 있다고 가정하고 복사 동작이 잘 정의되어 있는지 확인하고 싶어 한답니다.

따라서 Copy를 구현하고 싶다면, 반드시 Clone도 함께 구현해야 합니다.

어떤 타입이 Copy를 구현할 수 있을까요?

모든 타입이 Copy를 구현할 수 있는 건 아닌데요.

다음 기준을 만족하는 타입만 Copy를 구현할 수 있답니다.

  • 타입 자체가 단순한 데이터 덩어리(Plain Old Data, POD)여야 합니다.

    즉, 포인터나 참조 같은 복잡한 내부 구조를 포함하지 않아야 합니다.

  • 해당 타입의 모든 필드(멤버 변수)들도 반드시 Copy 트레잇(trait)을 구현해야 합니다.

예를 들어, 다음 타입은 참조 필드를 포함하고 있기 때문에 Copy를 구현할 수 없답니다.

struct Foo<'a> { // 'a는 라이프타임(lifetime) 매개변수입니다.
    x: &'a i32, // i32 타입에 대한 참조를 가집니다.
}

// 이 타입에 대해 Copy 트레잇(trait)을 구현하려고 하면 오류가 발생합니다.
// error[E0204]: the trait `Copy` may not be implemented for this type
// impl Copy for Foo<'_> {} // 수동으로 구현하려고 해도 안 됩니다.



왜 Copy 트레잇(Trait)이 필요할까요?

Copy 트레잇(trait)을 사용하면 타입의 복사 동작을 제어할 수 있습니다.

어떤 타입이 Copy를 구현하면, 그 값은 할당, 함수 인자 전달, 반환 시에 자동으로 복제되는데요.

덕분에 값을 복사하기 위해 매번 명시적으로 clone() 메서드를 호출할 필요가 없어집니다.

게다가, Copy 타입은 항상 비트 단위 복사를 하기 때문에 성능 오버헤드가 매우 작답니다.

이는 러스트(Rust) 프로그램의 성능을 최적화하는 데 특히 유용합니다.

Clone 트레잇(Trait) 살펴보기

Copy와 달리, Clone 트레잇(trait)은 타입의 값을 명시적으로 복사할 수 있게 해준답니다.

어떤 타입이 Clone을 구현하면, clone() 메서드를 호출해서 새로운 인스턴스(복사본)를 만들 수 있습니다.

Clone 트레잇(Trait)이란 무엇일까요?

Copy와 다르게, Clone은 일반적인 트레잇(trait)이며 clone()이라는 메서드를 포함하고 있는데요.

clone() 메서드가 값의 새로운 복사본을 만드는 책임을 진답니다.

// Point 구조체에 Clone 트레잇(trait)을 적용합니다.
#[derive(Clone)] 
struct Point {
    x: i32,
    y: i32,
}



Clone 트레잇(Trait)은 어떻게 구현할까요?

Clone 트레잇(trait)을 구현하려면 #[derive(Clone)] 속성을 추가하거나, clone() 메서드를 직접 수동으로 구현하면 됩니다.

// #[derive(Clone)] 속성을 사용하여 Clone 트레잇(trait)을 자동으로 구현합니다.
#[derive(Clone)] 
struct Point {
    x: i32,
    y: i32,
}

// clone() 메서드를 수동으로 구현하는 방법
// 위에서 #[derive(Clone)]을 사용했으므로, 아래 코드는 주석 처리하거나 다른 이름의 구조체로 만들어야 합니다.
/*
struct ManualPoint {
    x: i32,
    y: i32,
}

impl Clone for ManualPoint {
    fn clone(&self) -> Self { // self는 현재 인스턴스에 대한 참조입니다.
        Self { x: self.x, y: self.y } // 현재 인스턴스의 필드 값으로 새 인스턴스를 만듭니다.
    }
}
*/



어떤 타입이 Clone을 구현할 수 있을까요?

거의 모든 타입이 Clone을 구현할 수 있답니다.

값의 새로운 복사본을 만드는 방법을 정의할 수만 있다면, Clone을 구현할 수 있는 거죠.

왜 Clone 트레잇(Trait)이 필요할까요?

Clone 트레잇(trait)을 사용하면 값을 명시적으로 복제할 수 있는데요.

이는 포인터나 참조를 포함하는 타입처럼 비트 단위 복사가 불가능한 타입에 특히 유용합니다.

또한, Clone은 복사 과정을 사용자가 직접 정의할 수 있게 해준답니다.

clone() 메서드 내부에 필요한 로직을 추가해서 복사 중에 특정 작업을 수행하도록 할 수 있는 거죠.

Copy와 Clone의 차이점 및 관계

CopyClone은 모두 타입이 복사되는 방식을 제어하지만, 몇 가지 중요한 차이점이 있습니다.

  • Copy는 마커 트레잇(marker trait)으로, 타입이 비트 단위 복사를 지원함을 나타냅니다.

    타입이 Copy를 구현하면, 값은 할당, 함수 인자 전달, 반환 시에 자동으로 복제됩니다.

  • Cloneclone()이라는 메서드를 포함하는 일반적인 트레잇(trait)입니다.

    타입이 Clone을 구현하면, 명시적으로 clone()을 호출하여 새로운 복사본을 만들 수 있습니다.

추가적으로, 모든 Copy 타입은 반드시 Clone도 구현해야 한답니다.

이는 명시적으로 clone()을 호출할 때 러스트(Rust)가 여러분이 무엇을 하는지 알고 있다고 가정하고 비트 단위 복사를 허용하도록 하기 위함입니다.

Copy는 "암묵적 복사"의 가능성을, Clone은 "명시적 복사"의 기능을 제공한다고 이해할 수 있습니다.

예제 분석

아래는 CopyClone의 사용법을 보여주는 예제입니다.

// Point 구조체에 Copy와 Clone 트레잇(trait)을 모두 적용합니다.
#[derive(Copy, Clone, Debug)] // Debug 트레잇(trait)은 값을 출력하기 위해 추가했습니다.
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 }; // Point 인스턴스 p1 생성

    // p1을 p2에 할당합니다. Point가 Copy를 구현했으므로, p1의 값이 p2로 자동으로 복사됩니다.
    // p1은 여전히 유효하며 사용할 수 있습니다 (소유권이 이동하지 않음).
    let p2 = p1; 

    // p1의 clone() 메서드를 명시적으로 호출하여 p3를 만듭니다.
    // Point가 Clone을 구현했으므로, 새로운 복사본이 생성됩니다.
    let p3 = p1.clone(); 

    println!("p1: {:?}, p2: {:?}, p3: {:?}", p1, p2, p3); 
    // p1, p2, p3는 모두 동일한 값을 가지지만, 서로 다른 메모리 공간을 차지하는 독립적인 인스턴스입니다.
}




이 예제에서는 Point 타입을 정의하고 CopyClone 트레잇(trait)을 모두 구현했습니다.

main 함수에서는 Point 값을 만들고 다른 변수에 할당하는데요.

PointCopy를 구현했기 때문에 할당 연산은 자동으로 값을 복제합니다.

또한, 명시적으로 clone()을 호출하여 값의 또 다른 복사본을 만들기도 했습니다.

Copy는 "복사해도 괜찮아, 간단하니까 그냥 해"라는 느낌이고, Clone은 "복사하고 싶으면 명시적으로 말해줘, 내가 어떻게 하는지 알려줄게"라는 느낌이랍니다.