Rust 스마트 포인터 완벽 정리: Box, Rc, Arc와 RefCell (2026)
Rust 스마트 포인터 Box, Rc, Arc, RefCell을 컴파일 가능한 2026 예제, 결정 표, 자주 나오는 면접 질문과 함께 설명합니다.

Rust의 스마트 포인터는 빌림 검사기만으로는 표현할 수 없는 소유권 패턴, 즉 힙 할당, 공유 소유권, 그리고 공유 참조를 통한 변경을 가능하게 하는 도구입니다. 단순한 참조(&T)가 값을 빌리기만 하는 것과 달리, 스마트 포인터는 자신의 데이터를 소유하고 그 위에 추가 동작을 얹습니다. 이 가이드는 Rust 개발자가 프로덕션에서도 면접에서도 반드시 마주치는 네 가지 타입인 Box, Rc, Arc, RefCell을 Rust 2024 에디션을 대상으로 한 컴파일 가능한 예제와 함께 설명합니다.
단일 소유자의 힙 할당에는 Box<T>를, 단일 스레드에서의 공유 소유권에는 Rc<T>를, 스레드를 넘나드는 공유 소유권에는 Arc<T>를, 공유 참조를 통해 값을 변경하려면 RefCell<T>를 사용합니다. Rc<RefCell<T>>와 Arc<Mutex<T>> 조합이 공유 가변 상태를 다룹니다.
Rust에서 스마트 포인터란 무엇인가
스마트 포인터는 포인터처럼 동작하면서도 추가 메타데이터나 기능을 지닌 구조체입니다. 대부분 Deref 트레이트를 구현하므로 *pointer와 메서드 호출이 마치 일반 참조인 것처럼 동작하며, Drop 트레이트도 구현하므로 값이 스코프를 벗어나면 정리 작업이 자동으로 실행됩니다. 표준 라이브러리는 여기서 다루는 네 가지를 제공하며, 이들을 이해하려면 소유권과 빌림을 탄탄히 파악하는 것이 관건입니다. 소유권과 빌림이 각 할당을 언제 누가 해제하는지를 결정하기 때문입니다.
참조와의 결정적 차이는 소유권입니다. &T는 가리키는 데이터를 결코 소유하지 않으므로 그 값보다 오래 살아남을 수 없습니다. 스마트 포인터는 데이터를 소유하고 그 수명을 제어하며 결정적으로 해제합니다. 스마트 포인터에 관한 Rust Book 장이 이들을 하나의 범주로 다루는 것은 바로 이 '소유권에 더해 동작을 지닌다'는 형태를 공유하기 때문입니다.
Box<T>: 재귀 타입과 크기 확정을 위한 힙 할당
Box<T>는 가장 단순한 스마트 포인터입니다. 값을 힙에 저장하고 스택에는 그 포인터만 담으며, 소유자는 하나뿐이고 할당 자체를 제외하면 런타임 오버헤드가 없습니다. 가장 흔한 역할은 재귀 타입에 알려진 크기를 부여하는 것입니다. 자기 자신을 직접 담는 타입은 무한히 커지지만, Box는 그저 포인터이므로 가리키는 대상과 무관하게 크기가 고정됩니다.
아래 예제는 이진 트리를 정의합니다. 각 Node는 두 자식을 담는데, 간접 참조가 없으면 컴파일러는 Tree의 크기를 계산할 수 없습니다. 각 자식을 Box로 감싸면 타입 수준에서 재귀를 끊습니다.
#[derive(Debug)]
enum Tree {
Leaf(i32),
// Box<Tree> puts each child on the heap, so Node has a fixed size (two pointers).
Node(Box<Tree>, Box<Tree>),
}
fn sum(tree: &Tree) -> i32 {
match tree {
Tree::Leaf(value) => *value, // base case: return the leaf
Tree::Node(left, right) => sum(left) + sum(right), // recurse into both children
}
}
fn main() {
// Build (1) + ((2) + (3)) = 6
let tree = Tree::Node(
Box::new(Tree::Leaf(1)),
Box::new(Tree::Node(
Box::new(Tree::Leaf(2)),
Box::new(Tree::Leaf(3)),
)),
);
println!("sum = {}", sum(&tree)); // sum = 6
}Box는 큰 값을 이동하는 비용이 클 때나 Box<dyn Error> 같은 트레이트 객체를 반환할 때도 중요합니다. 어느 경우든 규칙은 동일합니다. 소유자는 하나이며, Box가 드롭되면 자동으로 해제됩니다.
Rc<T>: 단일 스레드 코드에서의 공유 소유권
때로는 여러 소유자를 가지면서도 그중 어느 것이 마지막으로 사용될지 분명하지 않은 값이 필요합니다. Rc<T>(참조 카운트)는 몇 개의 소유자가 존재하는지를 나타내는 강한 참조 카운트를 유지하여 이를 해결합니다. Rc::clone은 바탕 데이터를 복사하지 않고 그 카운트를 증가시키며, 각 드롭이 이를 감소시킵니다. 카운트가 0에 도달하면 값이 해제됩니다.
여러 워커가 읽는 설정 객체를 생각해 봅니다. 각 워커는 필요한 동안 설정을 보유해야 하며, 할당은 마지막 워커가 사라질 때 비로소 없어져야 합니다.
use std::rc::Rc;
#[derive(Debug)]
struct Config {
endpoint: String,
timeout_ms: u32,
}
fn main() {
// Rc::new moves Config onto the heap with a strong count of 1.
let config = Rc::new(Config {
endpoint: "https://api.example.com".to_string(),
timeout_ms: 5000,
});
// Rc::clone only bumps the reference count; it does not deep-copy Config.
let worker_a = Rc::clone(&config);
let worker_b = Rc::clone(&config);
// All three handles point to the same allocation.
println!("endpoint: {}", worker_a.endpoint);
println!("timeout: {}", worker_b.timeout_ms);
// strong_count reports how many owners currently hold the value.
println!("owners: {}", Rc::strong_count(&config)); // owners: 3
}config.clone()이 아니라 Rc::clone을 명시적으로 호출하는 것이 관용적입니다. 이 연산이 저렴하며 카운터만 건드린다는 점을 읽는 이에게 알려 주기 때문입니다. 함정은 Rc<T>가 공유된 불변 참조만 내준다는 것입니다. 공유하는 값을 변경할 수 없고, 스레드 간에 보내는 것도 안전하지 않습니다. Rc 문서가 두 제약을 모두 명시합니다.
RefCell<T>와 Rust의 내부 가변성
Rust는 보통 값이 여러 불변 참조를 통해 공유되거나 정확히 하나의 가변 참조를 통해 변경되도록 강제하며, 이를 컴파일 타임에 검사합니다. RefCell<T>는 내부 가변성을 제공합니다. 같은 검사를 런타임으로 옮겨, 공유 참조를 통해 값을 변경할 수 있게 합니다. borrow()는 공유 읽기 가드를 반환하고, borrow_mut()는 배타적 쓰기 가드를 반환합니다. 규칙은 동일하지만, 위반은 컴파일에 실패하는 대신 패닉을 일으킵니다.
Rc와 결합하면 단일 스레드의 공유 가변 정석 패턴인 Rc<RefCell<T>>가 만들어집니다. 같은 값을 모두 갱신할 수 있는 여러 소유자입니다.
use std::rc::Rc;
use std::cell::RefCell;
// Rc gives shared ownership; RefCell allows mutation through a shared reference.
type SharedCounter = Rc<RefCell<u32>>;
fn increment(counter: &SharedCounter) {
// borrow_mut() hands out an exclusive reference, checked at runtime.
*counter.borrow_mut() += 1;
}
fn main() {
let counter: SharedCounter = Rc::new(RefCell::new(0));
let handle_a = Rc::clone(&counter);
let handle_b = Rc::clone(&counter);
increment(&handle_a);
increment(&handle_b);
increment(&counter);
// borrow() gives a shared read guard; the value is now 3.
println!("count = {}", counter.borrow()); // count = 3
}다른 borrow() 또는 borrow_mut() 가드가 아직 살아 있는 동안 borrow_mut()을 호출하면 already borrowed: BorrowMutError로 패닉합니다. 안전성 보장은 유지되지만, 컴파일러가 잡아냈을 논리 버그가 런타임 크래시로 바뀝니다. 가드는 짧게 유지하고, 같은 RefCell에 다시 진입할 수 있는 함수 호출을 가로질러 빌림을 유지하는 일은 피하는 것이 좋습니다.
내부 가변성은 참조 순환이 숨는 곳이기도 합니다. 서로를 가리키는 두 Rc<RefCell<T>> 값은 서로의 강한 참조 카운트를 영원히 0보다 높게 유지하므로, 어느 쪽도 결코 해제되지 않습니다. 해결책은 강한 참조 카운트에 영향을 주지 않는 비소유 핸들인 Weak<T>입니다. 고전적인 설명은 Learn Rust With Entirely Too Many Linked Lists에 있습니다.
Arc<T>: 동시성을 위한 스레드 안전 참조 카운트
Arc<T>(원자적 참조 카운트)는 Rc<T>의 멀티스레드 형제입니다. 공개 API는 거의 동일하지만 카운터가 원자적 연산을 사용하므로, 여러 스레드에서 동시에 Arc를 클론하거나 드롭해도 올바르게 동작합니다. 이 원자성이 바로 공유가 스레드 경계를 넘을 때만 Arc를 선택하는 이유입니다. 원자적 증가는 Rc가 사용하는 일반 증가보다 측정 가능할 만큼 느립니다.
Arc<T>도 여전히 불변 참조만 내주므로, 스레드를 넘나드는 변경에는 동기화 기본 요소가 필요합니다. 보통의 짝은 Mutex<T>이며, 동시성 코드에서 Rc<RefCell<T>>에 대응하는 Arc<Mutex<T>> 패턴을 제공합니다. 아래 예제는 공유된 합계를 각각 1000번씩 증가시키는 네 개의 스레드를 생성합니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc is safe to share across threads; Mutex serializes access to the inner value.
let total = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for _ in 0..4 {
// Each thread gets its own Arc handle (an atomic count bump).
let total = Arc::clone(&total);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock() blocks until the mutex is free, then returns a guard.
let mut value = total.lock().unwrap();
*value += 1;
}
}));
}
for handle in handles {
handle.join().unwrap(); // wait for every thread before reading
}
println!("total = {}", *total.lock().unwrap()); // total = 4000
}컴파일러가 그 경계를 대신 강제합니다. Rc<T>는 Send가 아니므로 thread::spawn으로 이동하려 하면 컴파일에 실패하여 Arc로 향하게 됩니다. 단일 스레드 안에서 Arc에 손을 뻗으면 결코 쓰지 않을 원자적 연산의 대가를 치를 뿐입니다. Arc 문서가 메모리 순서 보장을 자세히 다루며, Arc<Mutex<T>>는 Tokio를 사용한 비동기 Rust의 공유 상태 상당 부분을 떠받칩니다.
Rust 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
Box vs Rc vs Arc vs RefCell: 각각을 언제 사용하는가
네 가지 타입은 두 축을 따라 조합됩니다. 값이 몇 명의 소유자를 갖는지, 그리고 공유 핸들을 통해 변경할 수 있는지입니다. 다음 표가 트레이드오프를 요약합니다.
| 타입 | 소유권 | 공유 핸들을 통한 변경 | 스레드 안전 | 오버헤드 |
|------|-----------|----------------------------|-------------|----------|
| Box<T> | 단일 | 불가 | T: Send이면 가능 | 힙 할당만 |
| Rc<T> | 공유 | 불가 | 불가 | 비원자적 카운트 |
| Arc<T> | 공유 | 불가 | 가능 | 원자적 카운트 |
| RefCell<T> | 단일 | 가능(런타임 검사) | 불가 | 런타임 빌림 플래그 |
실용적인 결정 트리는 짧습니다. 힙에 소유자가 하나 필요하면 Box. 한 스레드에서 여러 소유자가 필요하면 Rc. 스레드를 넘나들며 여러 소유자가 필요하면 Arc. 공유된 값을 변경해야 하면 안쪽 타입을 RefCell(단일 스레드)이나 Mutex(멀티스레드)로 감쌉니다. 참조와 포인터 타입에 관한 Effective Rust 지침도 같은 계층 구조에 도달합니다. 가장 권한이 작은 선택지에서 시작하고, 컴파일러가 강제할 때만 기능을 더합니다.
Rust 스마트 포인터 면접 질문
스마트 포인터는 믿을 만한 면접 주제입니다. 지원자가 문법이 아니라 소유권을 이해하는지 시험하기 때문입니다. 아래 질문은 자주 등장하며, 더 많은 질문은 Rust 스마트 포인터 면접 모듈에 모여 있습니다.
Rc와 Arc의 차이는 무엇인가요? 둘 다 참조 카운트를 통해 공유 소유권을 제공합니다. Rc는 비원자적 카운터를 사용하며 단일 스레드에 국한됩니다. Arc는 원자적 연산을 사용하며 약간의 런타임 비용을 치르고 스레드 간에 공유할 수 있습니다. 컴파일러가 이 구분을 강제합니다. Rc는 Send도 Sync도 아니므로 스레드 경계를 넘을 수 없습니다.
왜 Rc를 RefCell과 결합하나요? Rc는 공유 소유권을 주지만 접근은 불변에 국한됩니다. RefCell은 내부 가변성을 더해, 런타임 빌림 검사와 함께 소유자가 공유 참조를 통해 값을 변경할 수 있게 합니다. 둘을 합친 Rc<RefCell<T>>가 단일 스레드의 공유 가변 관용 구성 요소입니다.
Rust에서 참조 카운트가 어떻게 메모리를 누수시킬 수 있나요? 서로를 참조하는 두 Rc(또는 Arc) 값은 순환을 형성하며, 그 강한 참조 카운트가 결코 0에 도달하지 않으므로 할당이 해제되지 않습니다. 비소유 참조를 지니는 Weak<T>로 순환을 끊으면 올바른 정리가 회복됩니다.
언제 Box가 Rc보다 나은가요? 단일 소유자로 충분할 때는 언제나 그렇습니다. Box는 참조 카운트 오버헤드가 없으므로 힙 할당, 재귀 타입, 트레이트 객체의 기본값입니다. 진정한 공유 소유권이 필요할 때만 Rc에 손을 뻗습니다.
결론
Rust의 스마트 포인터는 소유권을 제약에서 조합 가능한 구성 요소의 집합으로 바꿉니다. 그중 무엇을 선택할지는 문제의 형태에서 곧바로 따라 나옵니다.
- 값이 힙에 단일 소유자를 필요로 하거나, 재귀 타입이 고정된 크기를 필요로 하거나, 함수가 트레이트 객체를 반환할 때는
Box<T>를 택합니다. - 한 스레드에서 여러 소유자에는
Rc<T>를 사용하고, 소유권이 스레드 경계를 넘는 순간Arc<T>로 전환합니다. - 단일 스레드 코드의 내부 가변성에는
RefCell<T>를, 그 동시성 대응물에는Mutex<T>를 더하며, 빌림 가드와 잠금 가드는 짧게 유지합니다. - 의도적으로 조합합니다. 한 스레드의 공유 가변 상태에는
Rc<RefCell<T>>, 스레드를 넘나들 때는Arc<Mutex<T>>입니다. - 카운트 포인터 사이의 참조 순환에 주의하고, 누수를 피하기 위해
Weak<T>로 끊습니다. - 가장 권한이 작은 타입을 기본으로 삼고, 더 많은 기능이 필요할 때 컴파일러가 알려 주도록 합니다.
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

Rust 2026 트레이트와 제네릭 완벽 가이드: 업캐스팅, AsyncFn, 고급 면접 대비
Rust 2024 Edition의 트레이트 오브젝트 업캐스팅, AsyncFn 트레이트, RPITIT 등 신기능을 컴파일 가능한 코드와 함께 설명합니다. 2026년 기술 면접에서 자주 출제되는 고급 질문도 다룹니다.

Rust 소유권과 빌림: 메모리 안전성의 핵심 원리 완벽 해설
Rust의 소유권(Ownership), 빌림(Borrowing), 라이프타임(Lifetime) 개념을 체계적으로 분석합니다. 빌림 검사기의 동작 원리와 실무 패턴, 기술 면접 대비 핵심 포인트를 다룹니다.

Rust 면접 질문: 2026년 완벽 가이드
Rust 기술 면접에서 자주 출제되는 25개 질문을 완벽하게 다룹니다. 소유권, 빌림, 라이프타임, 트레이트, async/await, 동시성에 대한 상세한 답변과 코드 예제를 제공합니다.