Copy 특성은 Rust의 초기 디자인에서 리소스 관리 문제 없이 단순 비트 복사를 통해 중복될 수 있는 유형에 대한 마커로 발생했습니다. Drop은 파일 디스크립터나 힙 메모리와 같은 외부 자원을 관리하는 유형에 대한 결정론적 리소스 정리를 처리하기 위해 도입되었습니다. 암묵적인 중복과 고유 소유권 간의 충돌은 비트 복사본이 공유할 수 없는 리소스 핸들을 공유하게 된다는 것을 디자이너들이 깨달으면서 분명해졌습니다. 결과적으로 컴파일러는 두 Trait를 동시에 구현하려는 유형을 거부하도록 설계되었습니다.
만약 Drop을 구현하는 유형(예: 파일 디스크립터를 관리하는 경우)이 Copy이기도 하다면, 값이 새 변수에 할당될 때 비트적으로 동일한 두 개의 복사본이 생성됩니다. 두 복사본이 모두 스코프를 벗어날 경우, 사용자 정의 Drop 구현이 동일한 기본 리소스에 대해 두 번 실행됩니다. 이는 double-free 취약점이나 리소스가 첫 번째 드롭에 의해 무효화되고 두 번째 복사본에 의해 접근될 경우 use-after-free를 초래하여 메모리 안전성을 저해합니다.
Rust 컴파일러는 trait 시스템에 일관성 검사를 포함하여 유형이 Copy와 Drop을 동시에 구현하는 것을 명시적으로 금지합니다. 이 제약은 개발자들이 사용자 정의 파기 유형에 대해 Clone(명시적 복제)를 사용하도록 강제하여 구현이 참조 카운트를 올바르게 증가시키거나 깊은 복사를 수행할 수 있도록 합니다. 각 논리적 엔티티가 해당 고유 드롭을 가지도록 보장함으로써, 유형 시스템은 안전성 보장을 훼손하지 않으면서 제로 비용 추상화를 유지합니다.
외부 C 라이브러리의 연결 객체에 대한 원시 포인터를 감싸는 DatabaseHandle 구조체를 고려해 보세요. 이 애플리케이션은 로그를 위해 여러 클로저에 값을 전달해야 하지만, 드롭될 때마다 각각의 핸들이 고유한 연결을 닫아야 합니다. 핸들이 Copy였다면, 암묵적인 중복이 동일한 기본 C 리소스에 소유권을 주장하는 여러 핸들을 생성하여 스코프에서 벗어날 때 이중 닫기나 use-after-free를 초래하게 됩니다.
한 가지 접근 방식은 Copy를 허용하고 내부 참조 카운팅을 사용하여 Drop을 구현하는 것이었습니다. 이는 모든 핸드에 대한 동기화 오버헤드를 추가하고 이진 크기와 모든 작업에서의 런타임 비용을 증가시킵니다. 또한, 원시 포인터를 Arc에서 원자적으로 추출해야 하는 FFI 경계를 복잡하게 만들어 동작 로직이 Rust 코드에 다시 호출될 경우 잠재적인 교착 상태를 유발합니다.
또 다른 접근 방식은 Copy를 사용하되 값이 드롭되기 전에 수동으로 close 메서드를 호출해야 한다고 문서화하는 것이었습니다. 이는 메모리 안전성에 대한 부담을 전적으로 프로그래머에게 두어 Rust의 컴파일 타임 오류 방지라는 핵심 원칙을 위배합니다. 이는 개발자가 close를 호출하는 것을 잊어버리거나 핸들을 실수로 복사한 후 두 복사본 모두를 닫으려 할 경우 이중 닫기를 초래하게 됩니다.
선택된 해결책은 Copy를 제거하고 Clone을 수동으로 구현하고 Drop을 구현하는 것이었습니다. Clone은 새로운 데이터베이스 연결을 열어 깊은 복사를 수행하고, 각 인스턴스가 고유한 리소스를 소유하도록 하여 기본 C 포인터의 별칭 생성을 방지합니다. Drop은 자신의 연결만 닫으며, 컴파일러는 우연한 비트 복사를 방지하여 안전성을 유지하고 런타임 오버헤드를 없음으로 합니다.
유형 시스템은 이제 컴파일 타임에 우연한 복사를 방지하여 개발자들이 명시적으로 clone을 호출해야 하며, 리소스 획득이 소스 코드에서 가시화되도록 합니다. 이 프로그램은 핸들이 스레드나 클로저에 전달되었을 때 이중 해제 오류를 피하고 결정론적 파기 보장이 유지되며, 원자적 작업이나 수동 메모리 관리가 필요하지 않습니다.
Vec을 포함하는 구조체에 대해 Copy를 어떻게 유도할 수 없는가?
Vec는 힙에 할당된 메모리를 소유하며, 벡터가 스코프를 벗어날 때 해당 메모리를 해제하기 위해 Drop을 구현합니다. 만약 Vec을 포함하는 구조체가 Copy라면, 비트적으로 중복이 두 개의 구조체를 생성하여 스택에서 동일한 힙 버퍼를 가리키게 되지만 둘 다 힙을 가리키는 동일한 포인터를 포함하게 됩니다. 첫 번째 구조체가 드롭될 때 메모리가 해제되고, 두 번째가 드롭될 때 동일한 메모리를 다시 해제하려고 시도하게 되어 정의되지 않은 동작을 초래하게 됩니다. Rust는 모든 Copy 유형의 필드가 또한 Copy가 되어야 한다고 요구하여 중첩된 Drop 구현이 존재하지 않도록 재귀적으로 보장합니다.
mem::forget이 Copy와 Drop 문제를 예방하는가?
std::mem::forget은 값을 소비하되 소멸자를 실행하지 않지만, 이는 특정한 소유 값에만 영향을 미치고 모든 복사본에는 영향을 미치지 않습니다. Copy와 Drop이 허용된다면 한 복사본을 잊어도 나머지 비트 복사본이 스코프를 벗어날 때 자신의 Drop 구현을 실행하게 되어 사용 후 해제나 이중 해제가 발생하게 됩니다.
ManuallyDrop을 사용하여 Copy를 안전하게 구현할 수 있는가?
필드를 ManuallyDrop으로 감싸면 자동 드롭 호출을 방지하여 외부 구조체가 Copy를 파생하도록 기술적으로 허용합니다. 그러나 이는 사용자가 생성된 모든 복사본에 대해 ManuallyDrop::drop을 호출할 책임을 지게 하여 사실상 수동 메모리 관리 시나리오를 만듭니다. 만약 사용자가 심지어 하나의 복사본을 드롭하는 것을 잊는다면, 리소스는 영구적으로 누수되며; Rust는 결정론적 자동 정리의 안전성 보증을 약화시기 때문에 리소스를 소유하는 유형에 대해 이 패턴을 금지합니다.