Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

값 복제하기, 2부: Copy

앞서 보았던 것과 비슷한 예제를 다시 살펴봅시다. 이번에는 String 대신 u32 타입을 사용해 보겠습니다.

fn consumer(s: u32) { /* 값을 소비하는 함수 */ }

fn example() {
     let s: u32 = 5;
     consumer(s); // s의 소유권이 넘어가나요?
     let t = s + 1; // 여기서 s를 다시 써도 오류가 나지 않습니다!
}

놀랍게도 이 코드는 아무런 오류 없이 컴파일됩니다. String을 쓸 때는 .clone()을 직접 호출해야만 했는데, u32는 왜 그냥 되는 걸까요? 그 비밀은 바로 Copy 트레이트에 있습니다.

Copy 트레이트

Copy는 Rust 표준 라이브러리에 정의된 또 다른 중요한 트레이트입니다.

pub trait Copy: Clone { }

앞서 배운 Sized와 마찬가지로, Copy 역시 메서드가 없는 마커 트레이트입니다.

어떤 타입이 Copy를 구현하고 있다면, 명시적으로 .clone()을 호출하지 않아도 Rust가 암시적으로 값을 복제해 줍니다. u32가 바로 그 예입니다. consumer(s)가 호출될 때, Rust는 s의 데이터를 **비트 단위로 복사(bitwise copy)**하여 새로운 u32 인스턴스를 만들고 이를 함수에 전달합니다. 이 모든 과정이 자동으로 일어나기 때문에 개발자는 소유권 이동을 걱정할 필요가 없습니다.

어떤 타입이 Copy가 될 수 있을까요?

모든 타입을 Copy로 만들 수 있는 것은 아닙니다. Copy를 구현하려면 몇 가지 엄격한 조건을 만족해야 합니다.

우선 CopyClone의 서브트레이트입니다. 즉, 암시적으로 복제될 수 있는 타입이라면 당연히 .clone()을 통한 명시적인 복제도 가능해야 한다는 뜻입니다.

하지만 그보다 더 중요한 조건들이 있습니다.

  1. 타입이 스택 메모리에 저장된 데이터(std::mem::size_of로 계산되는 크기) 외에 추가적인 리소스(힙 메모리, 파일 핸들 등)를 관리하지 않아야 합니다.
  2. 타입이 가변 참조(&mut T)가 아니어야 합니다.

이 조건들이 충족되면, Rust는 데이터를 단순히 복사(메모리의 memcpy 연산과 유사)하는 것만으로도 안전하게 새로운 인스턴스를 만들 수 있다고 판단합니다.

사례 1: String은 왜 Copy가 아닐까요?

StringCopy를 구현하지 않습니다. 왜냐하면 문자열 데이터를 저장하기 위해 힙(Heap) 메모리라는 추가 리소스를 관리하기 때문입니다.

만약 StringCopy라면 어떤 일이 벌어질지 상상해 봅시다. String 변수를 복사하면 비트 단위 복사가 일어나고, 원본과 복사본 모두 동일한 힙 메모리 주소를 가리키게 됩니다.

              s                                 복사된 s
+---------+--------+----------+      +---------+--------+----------+
| 포인터  | 길이   | 용량     |      | 포인터  | 길이   | 용량     |
|  |      |   5    |    5     |      |  |      |   5    |    5     |
+--|------+--------+----------+      +--|------+--------+----------+
   |                                    |
   |                                    |
   v                                    |
 +---+---+---+---+---+                  |
 | H | e | l | l | o |                  |
 +---+---+---+---+---+                  |
   ^                                    |
   |                                    |
   +------------------------------------+

이런 상태에서 두 변수가 범위를 벗어나 소멸될 때, 똑같은 메모리 공간을 두 번 해제하려고 시도하게 되어 이중 해제(double-free) 오류가 발생합니다. 또한 같은 데이터를 가리키는 두 개의 가변 참조가 생길 수 있어 Rust의 빌림 규칙에도 어긋나게 됩니다.

사례 2: u32와 정수 타입

u32를 포함한 모든 정수 타입은 Copy를 구현합니다. 정수는 그 자체로 메모리에 저장된 숫자일 뿐, 별도로 관리하는 외부 메모리나 리소스가 없기 때문입니다. 그저 바이트를 복사하는 것만으로 완벽하게 독립적인 새로운 정수 값을 얻을 수 있으므로 아무런 위험이 없습니다.

사례 3: &mut u32는 왜 Copy가 아닐까요?

우리는 가변 빌림에 대해 배울 때 “특정 시점에 가변 참조는 오직 하나만 존재해야 한다“는 규칙을 배웠습니다. 만약 &mut u32Copy라면, 복사를 통해 동일한 데이터를 가리키는 여러 개의 가변 참조를 마구 만들어낼 수 있게 됩니다. 이는 Rust의 안전성 원칙을 근본적으로 뒤흔드는 일입니다. 따라서 T가 무엇이든 &mut T는 결코 Copy가 될 수 없습니다.

Copy 구현하기

여러분이 만든 구조체가 위의 조건들을 만족한다면, 다음과 같이 derive 매크로를 사용해 간단히 Copy를 구현할 수 있습니다.

#[derive(Copy, Clone)]
struct MyStruct {
    field: u32,
}

Exercise

The exercise for this section is located in 04_traits/12_copy