Send: 스레드 간의 안전한 소유권 이전
Send 트레이트는 러스트의 “두려움 없는 동시성(Fearless Concurrency)“을 뒷받침하는 핵심 마커 트레이트(Marker Trait)입니다. 이 트레이트에는 어떠한 메서드도 없으며, 오직 **‘이 타입은 스레드 간에 소유권을 안전하게 이전할 수 있다’**는 사실을 컴파일러에게 알려주는 표식 역할을 합니다.
Send는 왜 필요한가?
멀티스레드 환경에서는 하나의 데이터를 여러 스레드에서 접근할 때 데이터 경쟁(Data Race)과 같은 심각한 문제가 발생할 수 있습니다. Send 트레이트는 어떤 타입의 값이 다른 스레드로 이동될 때, 해당 타입이 스레드 간에 안전하게 전달될 수 있는지 컴파일 타임에 검사하여 이러한 문제를 원천적으로 방지합니다.
컴파일러는 thread::spawn과 같이 스레드 경계를 넘는 코드에서 전달되는 모든 값이 Send 트레이트를 구현했는지 확인합니다. 만약 구현하지 않았다면, 컴파일 에러를 발생시켜 위험한 상황을 미리 막아줍니다.
대부분의 타입은 Send 이다
러스트의 기본적인 타입들(예: i32, f64, bool, String, Vec<T>)은 대부분 Send를 구현합니다. 또한, 어떤 구조체나 열거형의 모든 필드가 Send를 구현한다면, 그 타입 자체도 자동으로 Send를 구현하게 됩니다. 이런 특성 때문에 Send를 ‘자동 트레이트(Auto Trait)‘라고 부르기도 합니다.
Send가 아닌 타입의 예: Rc<T>
Send 트레이트가 구현되지 않은 대표적인 예는 std::rc::Rc<T> (Reference Counted)입니다. Rc<T>는 단일 스레드 환경에서 참조 카운팅을 통해 데이터를 공유하기 위해 설계되었습니다. 이 참조 카운트는 원자적(Atomic) 연산이 아니므로, 여러 스레드에서 동시에 접근하여 카운트를 수정하려고 하면 데이터 경쟁이 발생하여 카운트가 깨질 수 있습니다.
따라서 러스트는 Rc<T>가 스레드 경계를 넘지 못하도록 컴파일 단계에서 막습니다.
use std::rc::Rc;
use std::thread;
fn main() {
// Rc<T>는 Send 트레이트를 구현하지 않습니다.
let non_send_data = Rc::new("이 데이터는 스레드간 이동 불가!");
// 다른 스레드로 non_send_data의 소유권을 이동시키려고 하면...
thread::spawn(move || {
// 컴파일 에러 발생!
// `std::rc::Rc<&str>` cannot be sent safely between threads
println!("{}", non_send_data);
});
}위 코드는 컴파일되지 않으며, Rc<T>가 스레드 안전하지 않다는 명확한 에러 메시지를 보여줍니다. 멀티스레드 환경에서 데이터를 공유하고 싶다면 Send와 Sync를 모두 구현한 Arc<T> (Atomic Reference Counted)를 사용해야 합니다.
결론
Send 트레이트는 눈에 잘 띄지 않지만, 러스트의 동시성 모델에서 극히 중요한 역할을 합니다. 컴파일러가 스레드 간의 데이터 전달을 정적으로 분석하여 안전성을 보장하게 함으로써, 개발자가 데이터 경쟁의 공포 없이 안심하고 동시성 코드를 작성할 수 있도록 돕습니다.