# Rust 스마트 포인터 완벽 정리: Box, Rc, Arc와 RefCell (2026) > Rust 스마트 포인터 Box, Rc, Arc, RefCell을 컴파일 가능한 2026 예제, 결정 표, 자주 나오는 면접 질문과 함께 설명합니다. - Published: 2026-07-05 - Updated: 2026-07-07 - Author: SharpSkill - Tags: rust, smart-pointers, memory-management, interview, rust-2024-edition - Reading time: 11 min --- Rust의 스마트 포인터는 빌림 검사기만으로는 표현할 수 없는 소유권 패턴, 즉 힙 할당, 공유 소유권, 그리고 공유 참조를 통한 변경을 가능하게 하는 도구입니다. 단순한 참조(`&T`)가 값을 빌리기만 하는 것과 달리, 스마트 포인터는 자신의 데이터를 소유하고 그 위에 추가 동작을 얹습니다. 이 가이드는 Rust 개발자가 프로덕션에서도 면접에서도 반드시 마주치는 네 가지 타입인 `Box`, `Rc`, `Arc`, `RefCell`을 Rust 2024 에디션을 대상으로 한 컴파일 가능한 예제와 함께 설명합니다. > **한 줄 요약** > > 단일 소유자의 힙 할당에는 `Box`를, 단일 스레드에서의 공유 소유권에는 `Rc`를, 스레드를 넘나드는 공유 소유권에는 `Arc`를, 공유 참조를 통해 값을 변경하려면 `RefCell`를 사용합니다. `Rc>`와 `Arc>` 조합이 공유 가변 상태를 다룹니다. ## Rust에서 스마트 포인터란 무엇인가 스마트 포인터는 포인터처럼 동작하면서도 추가 메타데이터나 기능을 지닌 구조체입니다. 대부분 `Deref` 트레이트를 구현하므로 `*pointer`와 메서드 호출이 마치 일반 참조인 것처럼 동작하며, `Drop` 트레이트도 구현하므로 값이 스코프를 벗어나면 정리 작업이 자동으로 실행됩니다. 표준 라이브러리는 여기서 다루는 네 가지를 제공하며, 이들을 이해하려면 [소유권과 빌림](/blog/rust/ownership-borrowing-rust-complete-guide)을 탄탄히 파악하는 것이 관건입니다. 소유권과 빌림이 각 할당을 언제 누가 해제하는지를 결정하기 때문입니다. 참조와의 결정적 차이는 소유권입니다. `&T`는 가리키는 데이터를 결코 소유하지 않으므로 그 값보다 오래 살아남을 수 없습니다. 스마트 포인터는 데이터를 소유하고 그 수명을 제어하며 결정적으로 해제합니다. [스마트 포인터에 관한 Rust Book 장](https://doc.rust-lang.org/book/ch15-00-smart-pointers.html)이 이들을 하나의 범주로 다루는 것은 바로 이 '소유권에 더해 동작을 지닌다'는 형태를 공유하기 때문입니다. ## `Box`: 재귀 타입과 크기 확정을 위한 힙 할당 `Box`는 가장 단순한 스마트 포인터입니다. 값을 힙에 저장하고 스택에는 그 포인터만 담으며, 소유자는 하나뿐이고 할당 자체를 제외하면 런타임 오버헤드가 없습니다. 가장 흔한 역할은 재귀 타입에 알려진 크기를 부여하는 것입니다. 자기 자신을 직접 담는 타입은 무한히 커지지만, `Box`는 그저 포인터이므로 가리키는 대상과 무관하게 크기가 고정됩니다. 아래 예제는 이진 트리를 정의합니다. 각 `Node`는 두 자식을 담는데, 간접 참조가 없으면 컴파일러는 `Tree`의 크기를 계산할 수 없습니다. 각 자식을 `Box`로 감싸면 타입 수준에서 재귀를 끊습니다. ```rust // tree.rs #[derive(Debug)] enum Tree { Leaf(i32), // Box puts each child on the heap, so Node has a fixed size (two pointers). Node(Box, Box), } 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` 같은 트레이트 객체를 반환할 때도 중요합니다. 어느 경우든 규칙은 동일합니다. 소유자는 하나이며, `Box`가 드롭되면 자동으로 해제됩니다. ## `Rc`: 단일 스레드 코드에서의 공유 소유권 때로는 여러 소유자를 가지면서도 그중 어느 것이 마지막으로 사용될지 분명하지 않은 값이 필요합니다. `Rc`(참조 카운트)는 몇 개의 소유자가 존재하는지를 나타내는 강한 참조 카운트를 유지하여 이를 해결합니다. `Rc::clone`은 바탕 데이터를 복사하지 않고 그 카운트를 증가시키며, 각 드롭이 이를 감소시킵니다. 카운트가 0에 도달하면 값이 해제됩니다. 여러 워커가 읽는 설정 객체를 생각해 봅니다. 각 워커는 필요한 동안 설정을 보유해야 하며, 할당은 마지막 워커가 사라질 때 비로소 없어져야 합니다. ```rust // shared_config.rs 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`가 공유된 불변 참조만 내준다는 것입니다. 공유하는 값을 변경할 수 없고, 스레드 간에 보내는 것도 안전하지 않습니다. [`Rc` 문서](https://doc.rust-lang.org/std/rc/struct.Rc.html)가 두 제약을 모두 명시합니다. ## `RefCell`와 Rust의 내부 가변성 Rust는 보통 값이 여러 불변 참조를 통해 공유되거나 정확히 하나의 가변 참조를 통해 변경되도록 강제하며, 이를 컴파일 타임에 검사합니다. `RefCell`는 내부 가변성을 제공합니다. 같은 검사를 런타임으로 옮겨, 공유 참조를 통해 값을 변경할 수 있게 합니다. `borrow()`는 공유 읽기 가드를 반환하고, `borrow_mut()`는 배타적 쓰기 가드를 반환합니다. 규칙은 동일하지만, 위반은 컴파일에 실패하는 대신 패닉을 일으킵니다. `Rc`와 결합하면 단일 스레드의 공유 가변 정석 패턴인 `Rc>`가 만들어집니다. 같은 값을 모두 갱신할 수 있는 여러 소유자입니다. ```rust // counter.rs use std::rc::Rc; use std::cell::RefCell; // Rc gives shared ownership; RefCell allows mutation through a shared reference. type SharedCounter = Rc>; 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 } ``` > **RefCell은 빌림 검사를 런타임으로 옮긴다** > > 다른 `borrow()` 또는 `borrow_mut()` 가드가 아직 살아 있는 동안 `borrow_mut()`을 호출하면 `already borrowed: BorrowMutError`로 패닉합니다. 안전성 보장은 유지되지만, 컴파일러가 잡아냈을 논리 버그가 런타임 크래시로 바뀝니다. 가드는 짧게 유지하고, 같은 `RefCell`에 다시 진입할 수 있는 함수 호출을 가로질러 빌림을 유지하는 일은 피하는 것이 좋습니다. 내부 가변성은 참조 순환이 숨는 곳이기도 합니다. 서로를 가리키는 두 `Rc>` 값은 서로의 강한 참조 카운트를 영원히 0보다 높게 유지하므로, 어느 쪽도 결코 해제되지 않습니다. 해결책은 강한 참조 카운트에 영향을 주지 않는 비소유 핸들인 `Weak`입니다. 고전적인 설명은 [Learn Rust With Entirely Too Many Linked Lists](https://rust-unofficial.github.io/too-many-lists/)에 있습니다. ## `Arc`: 동시성을 위한 스레드 안전 참조 카운트 `Arc`(원자적 참조 카운트)는 `Rc`의 멀티스레드 형제입니다. 공개 API는 거의 동일하지만 카운터가 원자적 연산을 사용하므로, 여러 스레드에서 동시에 `Arc`를 클론하거나 드롭해도 올바르게 동작합니다. 이 원자성이 바로 공유가 스레드 경계를 넘을 때만 `Arc`를 선택하는 이유입니다. 원자적 증가는 `Rc`가 사용하는 일반 증가보다 측정 가능할 만큼 느립니다. `Arc`도 여전히 불변 참조만 내주므로, 스레드를 넘나드는 변경에는 동기화 기본 요소가 필요합니다. 보통의 짝은 `Mutex`이며, 동시성 코드에서 `Rc>`에 대응하는 `Arc>` 패턴을 제공합니다. 아래 예제는 공유된 합계를 각각 1000번씩 증가시키는 네 개의 스레드를 생성합니다. ```rust // parallel_sum.rs 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`는 `Send`가 아니므로 `thread::spawn`으로 이동하려 하면 컴파일에 실패하여 `Arc`로 향하게 됩니다. 단일 스레드 안에서 `Arc`에 손을 뻗으면 결코 쓰지 않을 원자적 연산의 대가를 치를 뿐입니다. [`Arc` 문서](https://doc.rust-lang.org/std/sync/struct.Arc.html)가 메모리 순서 보장을 자세히 다루며, `Arc>`는 [Tokio를 사용한 비동기 Rust](/blog/rust/rust-async-await-tokio-futures-concurrency)의 공유 상태 상당 부분을 떠받칩니다. ## Box vs Rc vs Arc vs RefCell: 각각을 언제 사용하는가 네 가지 타입은 두 축을 따라 조합됩니다. 값이 몇 명의 소유자를 갖는지, 그리고 공유 핸들을 통해 변경할 수 있는지입니다. 다음 표가 트레이드오프를 요약합니다. | 타입 | 소유권 | 공유 핸들을 통한 변경 | 스레드 안전 | 오버헤드 | |------|-----------|----------------------------|-------------|----------| | `Box` | 단일 | 불가 | `T: Send`이면 가능 | 힙 할당만 | | `Rc` | 공유 | 불가 | 불가 | 비원자적 카운트 | | `Arc` | 공유 | 불가 | 가능 | 원자적 카운트 | | `RefCell` | 단일 | 가능(런타임 검사) | 불가 | 런타임 빌림 플래그 | 실용적인 결정 트리는 짧습니다. 힙에 소유자가 하나 필요하면 `Box`. 한 스레드에서 여러 소유자가 필요하면 `Rc`. 스레드를 넘나들며 여러 소유자가 필요하면 `Arc`. 공유된 값을 변경해야 하면 안쪽 타입을 `RefCell`(단일 스레드)이나 `Mutex`(멀티스레드)로 감쌉니다. [참조와 포인터 타입](https://www.lurklurk.org/effective-rust/)에 관한 Effective Rust 지침도 같은 계층 구조에 도달합니다. 가장 권한이 작은 선택지에서 시작하고, 컴파일러가 강제할 때만 기능을 더합니다. ## Rust 스마트 포인터 면접 질문 스마트 포인터는 믿을 만한 면접 주제입니다. 지원자가 문법이 아니라 소유권을 이해하는지 시험하기 때문입니다. 아래 질문은 자주 등장하며, 더 많은 질문은 [Rust 스마트 포인터 면접 모듈](/technologies/rust/interview-questions/smart-pointers)에 모여 있습니다. **`Rc`와 `Arc`의 차이는 무엇인가요?** 둘 다 참조 카운트를 통해 공유 소유권을 제공합니다. `Rc`는 비원자적 카운터를 사용하며 단일 스레드에 국한됩니다. `Arc`는 원자적 연산을 사용하며 약간의 런타임 비용을 치르고 스레드 간에 공유할 수 있습니다. 컴파일러가 이 구분을 강제합니다. `Rc`는 `Send`도 `Sync`도 아니므로 스레드 경계를 넘을 수 없습니다. **왜 `Rc`를 `RefCell`과 결합하나요?** `Rc`는 공유 소유권을 주지만 접근은 불변에 국한됩니다. `RefCell`은 내부 가변성을 더해, 런타임 빌림 검사와 함께 소유자가 공유 참조를 통해 값을 변경할 수 있게 합니다. 둘을 합친 `Rc>`가 단일 스레드의 공유 가변 관용 구성 요소입니다. **Rust에서 참조 카운트가 어떻게 메모리를 누수시킬 수 있나요?** 서로를 참조하는 두 `Rc`(또는 `Arc`) 값은 순환을 형성하며, 그 강한 참조 카운트가 결코 0에 도달하지 않으므로 할당이 해제되지 않습니다. 비소유 참조를 지니는 `Weak`로 순환을 끊으면 올바른 정리가 회복됩니다. **언제 `Box`가 `Rc`보다 나은가요?** 단일 소유자로 충분할 때는 언제나 그렇습니다. `Box`는 참조 카운트 오버헤드가 없으므로 힙 할당, 재귀 타입, 트레이트 객체의 기본값입니다. 진정한 공유 소유권이 필요할 때만 `Rc`에 손을 뻗습니다. ## 결론 Rust의 스마트 포인터는 소유권을 제약에서 조합 가능한 구성 요소의 집합으로 바꿉니다. 그중 무엇을 선택할지는 문제의 형태에서 곧바로 따라 나옵니다. - 값이 힙에 단일 소유자를 필요로 하거나, 재귀 타입이 고정된 크기를 필요로 하거나, 함수가 트레이트 객체를 반환할 때는 `Box`를 택합니다. - 한 스레드에서 여러 소유자에는 `Rc`를 사용하고, 소유권이 스레드 경계를 넘는 순간 `Arc`로 전환합니다. - 단일 스레드 코드의 내부 가변성에는 `RefCell`를, 그 동시성 대응물에는 `Mutex`를 더하며, 빌림 가드와 잠금 가드는 짧게 유지합니다. - 의도적으로 조합합니다. 한 스레드의 공유 가변 상태에는 `Rc>`, 스레드를 넘나들 때는 `Arc>`입니다. - 카운트 포인터 사이의 참조 순환에 주의하고, 누수를 피하기 위해 `Weak`로 끊습니다. - 가장 권한이 작은 타입을 기본으로 삼고, 더 많은 기능이 필요할 때 컴파일러가 알려 주도록 합니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/rust/rust-smart-pointers-box-rc-arc-refcell