프로그래밍백엔드 개발자

Rust에서 스마트 포인터(Box, Rc, Arc, RefCell)는 어떻게 작동하나요? 서로의 차이점은 무엇이며, 어떤 경우에 어떤 것을 선택해야 하나요?

Hintsage AI 어시스턴트로 면접 통과

답변

Rust에는 전통적인 가비지 컬렉터가 없기 때문에 복잡한 구조체에 대한 소유권 관리를 위해 스마트 포인터(smart pointers)를 사용합니다. 가장 자주 사용되는 포인터는 다음과 같습니다:

  • Box<T> — 객체를 힙에 할당하고 그에 대한 소유권을 전달합니다. 컴파일 시 데이터 크기가 불확실하거나 이동할 수 있지만 고유한 자원이 필요할 때 사용됩니다.

  • Rc<T> (Reference Counted) — 참조 카운트를 제공하여 여러 변수가 불변 데이터를 "공유"하는 소유권을 허용합니다 (단일 스레드 컨텍스트에서만).

  • Arc<T> (Atomic Reference Counted) — 또한 참조 카운트를 구현하지만 원자적입니다; 다중 스레드 프로그램에서 사용이 허용됩니다.

  • RefCell<T> — 실행 시 "내부 가변" 소유권을 제공하며, 불변 참조를 통해서도 콘텐츠를 변경할 수 있지만 단일 스레드에서만 가능 (스레드 안전하지 않습니다!).

예:

use std::rc::Rc; let a = Rc::new(vec![1,2,3]); let b = Rc::clone(&a); // 이제 a와 b는 같은 데이터의 소유자입니다

함정 질문

모든 스레드가 데이터만 읽는 경우 Rc<T>를 다중 스레드 코드에서 사용할 수 있나요? 설명하세요.

답변: 아니요, 안 됩니다! Rc<T>가 데이터에 대한 불변 접근만 허용하더라도, Rc<T> 컨테이너 자체는 스레드 안전하지 않기 때문에 내부 참조 카운트가 데이터 경합으로부터 보호되지 않습니다. 이를 위해 Arc<T>가 필요합니다 — 내부 카운터가 스레드 안전합니다.

예:

// 다음 코드는 컴파일되지 않습니다! use std::thread; use std::rc::Rc; let five = Rc::new(5); for _ in 0..10 { let five = Rc::clone(&five); thread::spawn(move || { println!("{}", five); }); }

시사점이 있는 실제 오류 사례


이야기

스레드 간 캐시를 공유하기 위해 Rc<T>를 사용하려 했으나, 웹 서비스 가속화 과정에서 이상한 충돌과 손상된 데이터를 경험했습니다. 조사 후 Rc는 스레드 안전하지 않아 참조 카운트가 손상된 사실을 알게 되었습니다. 해결책: Arc<T>로 교체.

이야기

데스크탑 응용 프로그램에서 큰 객체 트리를 Box<T>에 저장했으나, UI의 여러 부분이 데이터 소유권을 공유해야 한다는 점을 간과했습니다. 이는 컴파일 오류로 이어졌습니다. 해결책은 데이터 접근을 공유하기 위해 Rc<T>를 사용하는 것이었습니다.

이야기

비즈니스 로직 모듈에서는 RefCell<T>를 사용하여 Arc<T>를 통해 스레드 간에 전달되는 데이터에 대한 가변 접근을 조직했습니다. 그러나 RefCell<T>와 Arc<T>를 결합하려다 데이터 경합과 실행 중 패닉이 발생했습니다. 스레드 안전한 옵션으로는 RefCell<T> 대신 Mutex<T> 또는 RwLock<T>를 사용하는 것이 좋습니다.