# Rust의 소유권과 빌림: 완벽 가이드 > Rust의 소유권과 빌림 시스템을 마스터합니다. 소유권 규칙, 참조, 라이프타임, 고급 메모리 관리 패턴을 다룹니다. - Published: 2026-02-20 - Updated: 2026-04-29 - Author: SharpSkill - Tags: rust, ownership, borrowing, memory management, systems programming - Reading time: 18 min --- 소유권 시스템은 Rust를 다른 모든 프로그래밍 언어와 구별짓는 특징입니다. 이 독특한 접근 방식은 가비지 컬렉터 없이 메모리 안전성을 보장하며, 런타임이 아닌 컴파일 타임에 버그를 잡아냅니다. 이 심층 가이드는 소유권과 빌림의 메커니즘을 기초부터 고급 프로덕션 패턴까지 다룹니다. > **Rust의 철학** > > Rust 컴파일러는 까다로운 프로그래밍 보조자처럼 작동합니다. 컴파일 타임에 차단된 모든 소유권 오류는 프로덕션에서 방지된 잠재적 버그를 의미합니다. ## 소유권의 세 가지 기본 규칙 소유권 시스템은 단순하지만 엄격한 세 가지 규칙에 기반합니다. 이 규칙들이 체화되면 Rust의 멘탈 모델은 자연스럽고 예측 가능해집니다. ```rust // ownership_rules.rs // Demonstration of the three fundamental rules fn main() { // Rule 1: Each value has exactly ONE owner let s1 = String::from("hello"); // s1 is the sole owner // Rule 2: There can only be one owner at a time let s2 = s1; // Ownership transferred (moved) from s1 to s2 // println!("{}", s1); // Compile ERROR: s1 no longer exists println!("s2 = {}", s2); // Only s2 is valid now // Rule 3: When the owner goes out of scope, the value is dropped { let s3 = String::from("temporary"); println!("s3 inside block = {}", s3); } // s3 is automatically freed here (drop is called) // println!("{}", s3); // ERROR: s3 no longer exists } ``` 이 세 가지 규칙은 use-after-free, double-free, 메모리 누수와 같은 버그 카테고리 전체를 제거합니다. 컴파일러는 이 규칙들이 준수되는지 정적으로 검증합니다. ## Move 대 Copy: 전송 의미론 이해하기 할당의 동작은 데이터 타입에 따라 달라집니다. Copy 트레이트를 구현하는 타입은 복제되고, 그 외의 타입은 move를 통해 전송됩니다. ```rust // move_vs_copy.rs // Distinction between Copy types and Move types fn main() { // Copy types: values stored on the stack, known size let x: i32 = 42; let y = x; // x is COPIED, not moved println!("x = {}, y = {}", x, y); // Both are valid // Other Copy types: f64, bool, char, tuples of Copy types let point = (3.0, 4.0); let point_copy = point; // Tuple copy println!("Original: {:?}, Copy: {:?}", point, point_copy); // Move types: values on the heap, dynamic size let s1 = String::from("owned"); let s2 = s1; // s1 is MOVED to s2 // println!("{}", s1); // ERROR: value moved println!("s2 = {}", s2); // Vec, HashMap, Box are also Move types let vec1 = vec![1, 2, 3]; let vec2 = vec1; // Move, not copy // println!("{:?}", vec1); // ERROR println!("vec2 = {:?}", vec2); } // Explicit clone to duplicate Move types fn explicit_clone() { let original = String::from("important data"); let clone = original.clone(); // Explicit duplication (memory cost) println!("Original: {}", original); // Still valid println!("Clone: {}", clone); // Independent copy } ``` Move/Copy의 구분은 기본적입니다. 할당이 소유권을 전송하는지, 아니면 독립적인 복사본을 생성하는지를 결정합니다. > **Clone을 사용해야 할 때** > > `.clone()` 호출은 의도적이어야 합니다. clone으로 가득 찬 코드는 설계상의 문제를 시사할 수 있습니다. 빌림이 종종 더 나은 해결책입니다. ## 빌림: 불변 참조와 가변 참조 빌림은 소유권을 가져가지 않고 값에 접근할 수 있게 해줍니다. 이 메커니즘은 Rust 코드를 안전하면서도 고성능으로 만듭니다. ```rust // borrowing_basics.rs // Immutable and mutable references fn main() { let s = String::from("hello"); // Immutable reference: read-only, multiple allowed let len = calculate_length(&s); // Immutable borrow println!("'{}' has {} characters", s, len); // s still valid // Multiple simultaneous immutable references: OK let r1 = &s; let r2 = &s; let r3 = &s; println!("r1={}, r2={}, r3={}", r1, r2, r3); } fn calculate_length(s: &String) -> usize { // s is a reference, not the owner s.len() } // s goes out of scope but doesn't drop anything (not owner) // Mutable references: modification allowed fn mutable_borrowing() { let mut s = String::from("hello"); change(&mut s); // Mutable borrow println!("After modification: {}", s); } fn change(s: &mut String) { s.push_str(", world!"); // Modification via mutable reference } ``` 빌림의 황금 규칙: 여러 개의 불변 참조 또는 단 하나의 가변 참조이며, 결코 두 가지가 동시에 존재할 수 없습니다. ## Borrow Checker의 규칙 Borrow checker는 빌림 규칙을 검증하는 컴파일러 컴포넌트입니다. 그 오류를 이해하면 문제를 빠르게 해결할 수 있습니다. ```rust // borrow_checker_rules.rs // Strict borrow checker rules fn main() { // RULE 1: No mutable reference with immutable references let mut s = String::from("hello"); let r1 = &s; // Immutable reference: OK let r2 = &s; // Another immutable reference: OK // let r3 = &mut s; // ERROR: cannot borrow as mutable println!("{} and {}", r1, r2); // AFTER using r1 and r2, they are "dead" let r3 = &mut s; // Now OK: r1 and r2 no longer used r3.push_str(" world"); println!("{}", r3); // RULE 2: Only one mutable reference at a time let mut data = String::from("exclusive"); let ref1 = &mut data; // let ref2 = &mut data; // ERROR: already borrowed mutably ref1.push_str("!"); println!("{}", ref1); } // RULE 3: References cannot outlive the data fn dangling_reference_prevented() { let reference; { let s = String::from("short-lived"); // reference = &s; // ERROR: s doesn't live long enough } // s is dropped here, reference would be invalid // Solution: move the value out of the scope let owned_outside; { let s = String::from("moved out"); owned_outside = s; // Move, not reference } println!("{}", owned_outside); // OK: owned_outside is the owner } ``` Borrow checker는 Non-Lexical Lifetimes(NLL)를 사용합니다. 참조는 마지막 사용까지만 활성화된 것으로 간주되며, 스코프 끝까지 유지되지 않습니다. ## 라이프타임: 참조의 지속 시간 표기하기 라이프타임은 참조가 유효하게 유지되는지 컴파일러가 검증할 수 있도록 돕는 표기입니다. 대부분의 경우 자동으로 추론됩니다. ```rust // lifetimes_explained.rs // Explicit lifetime annotations // Without annotation: compiler infers lifetimes fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, } } // With explicit annotation: same function fn first_word_explicit<'a>(s: &'a str) -> &'a str { // 'a means: returned reference lives as long as the input match s.find(' ') { Some(i) => &s[..i], None => s, } } // When annotations are necessary: multiple references fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // Compiler cannot guess which reference is returned if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("long string"); let result; { let string2 = String::from("xyz"); result = longest(&string1, &string2); println!("Longest: {}", result); // OK here } // println!("{}", result); // ERROR if uncommented: string2 dropped } ``` 라이프타임은 데이터가 살아있는 기간을 변경하지 않습니다. 서로 다른 참조의 라이프타임 간의 관계를 기술할 뿐입니다. ## 구조체에서의 라이프타임 구조체가 참조를 포함할 때, 구조체가 참조된 데이터보다 오래 살지 않도록 보장하기 위해 라이프타임을 표기해야 합니다. ```rust // struct_lifetimes.rs // Structs containing references // Struct with reference: lifetime required struct ImportantExcerpt<'a> { part: &'a str, // This reference must live at least as long as the struct } impl<'a> ImportantExcerpt<'a> { // Method returning a reference with the same lifetime fn level(&self) -> i32 { 3 } // Elision rule: &self implies the output lifetime fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention: {}", announcement); self.part // Returns with 'a lifetime from self } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let excerpt = ImportantExcerpt { part: first_sentence, // OK: novel outlives excerpt }; println!("Excerpt: {}", excerpt.part); println!("Level: {}", excerpt.level()); } // Static lifetime: reference valid for the entire program duration fn static_lifetime_example() { let s: &'static str = "This string is in the binary"; // String literals always have 'static lifetime println!("{}", s); } ``` 라이프타임 생략 규칙은 일반적인 경우에 표기를 생략할 수 있게 해주어 코드를 더 읽기 쉽게 만듭니다. ## 고급 패턴: 내부 가변성 때로는 가변성이 컴파일 타임이 아닌 런타임에 검증되어야 합니다. Rust는 이 패턴을 위한 타입을 제공합니다: RefCell과 Cell입니다. ```rust // interior_mutability.rs // Interior mutability with RefCell and Cell use std::cell::{Cell, RefCell}; // Cell: for Copy types, replaces the entire value struct Counter { count: Cell, // Mutable despite &self } impl Counter { fn new() -> Counter { Counter { count: Cell::new(0) } } fn increment(&self) { // Modification via immutable reference! self.count.set(self.count.get() + 1); } fn get(&self) -> u32 { self.count.get() } } // RefCell: for non-Copy types, checks at runtime struct CachedValue { value: RefCell>, } impl CachedValue { fn new() -> CachedValue { CachedValue { value: RefCell::new(None) } } fn get_or_compute(&self, compute: impl FnOnce() -> String) -> String { // borrow() for reading, borrow_mut() for writing if self.value.borrow().is_none() { *self.value.borrow_mut() = Some(compute()); } self.value.borrow().as_ref().unwrap().clone() } } fn main() { let counter = Counter::new(); counter.increment(); counter.increment(); println!("Counter: {}", counter.get()); // 2 let cache = CachedValue::new(); let result = cache.get_or_compute(|| { println!("Expensive computation..."); String::from("result") }); println!("Value: {}", result); // Second call: no recomputation let result2 = cache.get_or_compute(|| String::from("never executed")); println!("Cache hit: {}", result2); } ``` RefCell과 Cell은 빌림 검증을 런타임으로 옮깁니다. 규칙 위반은 컴파일 오류 대신 panic을 발생시킵니다. > **Panic에 주의** > > `RefCell::borrow_mut()`은 값이 이미 빌려진 경우 panic을 발생시킵니다. 명시적인 오류 처리를 위해서는 `try_borrow_mut()`을 사용하는 것이 좋습니다. ## 스마트 포인터와 소유권 Box, Rc, Arc와 같은 스마트 포인터는 특정 사용 사례에 대해 서로 다른 소유권 전략을 제공합니다. ```rust // smart_pointers.rs // Box, Rc, and Arc for different ownership patterns use std::rc::Rc; use std::sync::Arc; use std::thread; // Box: single owner, data on the heap fn box_example() { let boxed = Box::new(vec![1, 2, 3, 4, 5]); println!("Boxed vec: {:?}", boxed); // Useful for: recursive types, large objects, trait objects } // Rc: reference counting, multiple owners (single-thread) fn rc_example() { let data = Rc::new(String::from("shared data")); let clone1 = Rc::clone(&data); // Increments the counter let clone2 = Rc::clone(&data); println!("Count: {}", Rc::strong_count(&data)); // 3 println!("All share: {}, {}, {}", data, clone1, clone2); } // Freed when counter reaches 0 // Arc: thread-safe Rc (Atomic Reference Counting) fn arc_example() { let data = Arc::new(vec![1, 2, 3]); let handles: Vec<_> = (0..3).map(|i| { let data_clone = Arc::clone(&data); thread::spawn(move || { println!("Thread {}: {:?}", i, data_clone); }) }).collect(); for handle in handles { handle.join().unwrap(); } } fn main() { box_example(); rc_example(); arc_example(); } ``` 스마트 포인터의 선택은 소유권 패턴에 따라 달라집니다: 단일(Box), 단일 스레드 공유(Rc), 또는 멀티 스레드 공유(Arc)입니다. ## 실용적인 소유권 패턴 다음은 소유권 시스템을 중심으로 코드를 구조화하기 위한 일반적인 패턴들입니다. ```rust // ownership_patterns.rs // Practical patterns for ownership management // Pattern 1: Builder pattern with chained ownership struct RequestBuilder { url: String, headers: Vec<(String, String)>, timeout: Option, } impl RequestBuilder { fn new(url: &str) -> Self { RequestBuilder { url: url.to_string(), headers: Vec::new(), timeout: None, } } // Consumes self and returns the new self fn header(mut self, key: &str, value: &str) -> Self { self.headers.push((key.to_string(), value.to_string())); self // Returns ownership } fn timeout(mut self, seconds: u64) -> Self { self.timeout = Some(seconds); self } fn build(self) -> Request { Request { url: self.url, headers: self.headers, timeout: self.timeout.unwrap_or(30), } } } struct Request { url: String, headers: Vec<(String, String)>, timeout: u64, } // Pattern 2: Cow (Copy-on-Write) to avoid allocations use std::borrow::Cow; fn process_text(input: &str) -> Cow { if input.contains("REPLACE") { // Allocation only if modification needed Cow::Owned(input.replace("REPLACE", "NEW")) } else { // No allocation, returns a reference Cow::Borrowed(input) } } // Pattern 3: Take to extract from an Option fn extract_value(data: &mut Option) -> String { data.take().unwrap_or_else(|| String::from("default")) // take() replaces with None and returns ownership of the value } fn main() { // Builder pattern let request = RequestBuilder::new("https://api.example.com") .header("Authorization", "Bearer token") .header("Content-Type", "application/json") .timeout(60) .build(); println!("URL: {}, Timeout: {}s", request.url, request.timeout); // Cow pattern let text1 = process_text("hello world"); // No allocation let text2 = process_text("hello REPLACE"); // Allocation println!("{} | {}", text1, text2); // Take pattern let mut optional = Some(String::from("extracted")); let value = extract_value(&mut optional); println!("Value: {}, Option: {:?}", value, optional); // None } ``` 이 패턴들은 소유권 시스템을 활용하여 사용하기 편리하고 고성능인 API를 만듭니다. ## 결론 Rust의 소유권과 빌림 시스템은 메모리 관리에서 패러다임의 전환을 나타냅니다. 한번 마스터하면 고성능이면서 안전한 코드를 작성하기 위한 강력한 동맹이 됩니다. **핵심 포인트:** ✅ 세 가지 소유권 규칙: 단일 소유자, 소유권 전송, 자동 drop ✅ 빌림: 여러 개의 불변 참조 또는 단 하나의 배타적 가변 참조 ✅ 라이프타임: 참조 라이프타임 간의 관계 표기 ✅ 내부 가변성: 런타임에 검증되는 가변성을 위한 RefCell과 Cell ✅ 스마트 포인터: Box(단일), Rc(공유), Arc(thread-safe) ✅ 실용적인 패턴: 관용적인 API를 위한 Builder, Cow, Take Borrow checker는 처음에는 엄격해 보일 수 있지만, 보고하는 모든 오류는 회피된 잠재적 버그를 의미합니다. 연습을 통해 소유권 관점에서 사고하는 것이 자연스러워지고, 모든 언어에서 코드 품질이 향상됩니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/rust/ownership-borrowing-rust-complete-guide