# Rust 소유권과 빌림: 메모리 안전성의 핵심 원리 완벽 해설 > Rust의 소유권(Ownership), 빌림(Borrowing), 라이프타임(Lifetime) 개념을 체계적으로 분석합니다. 빌림 검사기의 동작 원리와 실무 패턴, 기술 면접 대비 핵심 포인트를 다룹니다. - Published: 2026-03-31 - Updated: 2026-03-31 - Author: Anthony Fillion-Maillet - Tags: rust, ownership, borrowing, memory-management, systems-programming - Reading time: 10 min --- Rust의 소유권(Ownership)과 빌림(Borrowing)은 메모리 안전성 보장의 근간을 이루는 핵심 개념입니다. 가비지 컬렉터를 사용하는 다른 언어와 달리, Rust는 컴파일 타임에 빌림 검사기(Borrow Checker)를 통해 엄격한 규칙을 적용합니다. 이를 통해 널 포인터 역참조, 데이터 경쟁(Data Race), Use-After-Free 오류 등 광범위한 메모리 버그를 런타임 오버헤드 없이 원천적으로 차단합니다. > **소유권의 세 가지 규칙** > > Rust의 모든 값은 정확히 하나의 소유자(Owner)를 가집니다. 소유자가 스코프를 벗어나면 해당 값은 해제(Drop)됩니다. 소유권은 이전(Move)되거나 일시적으로 빌려줄(Borrow) 수 있습니다. 이 세 가지 규칙만으로 가비지 컬렉션을 완전히 대체합니다. ## 이동 시맨틱(Move Semantics)이 가비지 컬렉션을 대체하는 방식 대부분의 프로그래밍 언어에서는 여러 변수가 동일한 힙 할당 데이터를 가리킬 수 있습니다. Rust는 근본적으로 다른 접근 방식을 택합니다. 힙에 할당된 값을 다른 변수에 대입하면 소유권이 *이동(Move)* 되며, 원래 바인딩은 무효화됩니다. 컴파일러가 이 규칙을 제로 코스트로 강제합니다. ```rust // move_semantics.rs fn main() { let original = String::from("interview prep"); let moved = original; // ownership transfers here // println!("{}", original); // compile error: value moved println!("{}", moved); // works fine } ``` 위 코드는 이중 해제(Double-Free) 버그를 방지하는 대표적인 예시입니다. `String` 타입은 힙에 메모리를 할당하므로, Rust는 항상 하나의 변수만 해당 할당을 소유하도록 보장합니다. 반면 `i32`나 `bool`처럼 스택에만 존재하는 타입은 `Copy` 트레이트를 구현하며, 이동 대신 복사가 이루어집니다. ```rust // copy_vs_move.rs fn main() { let x: i32 = 42; let y = x; // copy, not move -- i32 is Copy println!("x = {}, y = {}", x, y); // both valid let s1 = String::from("hello"); let s2 = s1.clone(); // explicit deep copy println!("s1 = {}, s2 = {}", s1, s2); // both valid after clone } ``` `Copy`와 `Clone`의 차이는 기술 면접에서 빈번하게 출제되는 주제입니다. `Copy`는 암묵적이며 비용이 낮은(비트 단위 복사) 반면, `Clone`은 명시적으로 호출해야 하며 힙 할당 등 비용이 높을 수 있습니다. 이 구분을 명확히 설명할 수 있어야 합니다. ## 불변 참조를 통한 빌림(Borrowing) 모든 곳에서 소유권을 이전하면 코드 작성이 비현실적으로 어려워집니다. Rust의 빌림(Borrowing)은 소유권을 넘기지 않고 값에 대한 접근 권한을 빌려주는 방식으로 이 문제를 해결합니다. 불변 참조(`&T`)는 읽기 전용 접근을 허용하며, 동시에 여러 개가 존재할 수 있습니다. ```rust // immutable_borrowing.rs fn calculate_length(s: &String) -> usize { s.len() // read access through the reference } // s goes out of scope, but doesn't drop the String (not the owner) fn main() { let greeting = String::from("hello, Rust"); let len = calculate_length(&greeting); // borrow, don't move println!("'{}' has {} characters", greeting, len); // greeting still valid } ``` `&` 기호는 값을 빌리는 참조를 생성합니다. 함수 시그니처의 `&String`은 `calculate_length` 함수가 소유권을 가져가지 않고 빌리기만 한다는 것을 선언합니다. 함수가 반환된 후에도 호출자는 완전한 소유권을 유지합니다. > **빌림 규칙 한눈에 보기** > > 특정 시점에서 하나의 값은 다음 중 하나만 가질 수 있습니다: 여러 개의 불변 참조(`&T`), 또는 정확히 하나의 가변 참조(`&mut T`). 두 가지가 동시에 존재할 수 없습니다. 이 규칙은 데이터 경쟁을 컴파일 타임에 방지합니다. ## 가변 참조와 배타적 접근 규칙 가변 참조(`&mut T`)는 쓰기 접근 권한을 부여하지만, 배타성(Exclusivity)을 강제합니다. 특정 스코프 내에서 하나의 값에 대한 가변 참조는 오직 하나만 존재할 수 있습니다. 이를 통해 두 개의 코드 경로가 동일한 데이터를 동시에 수정하는 상황을 원천적으로 차단합니다. ```rust // mutable_borrowing.rs fn append_greeting(s: &mut String) { s.push_str(", welcome to Rust!"); // modify through mutable ref } fn main() { let mut message = String::from("Hello"); append_greeting(&mut message); println!("{}", message); // "Hello, welcome to Rust!" } ``` `mut` 키워드는 세 곳에 명시되어야 합니다: 변수 바인딩(`let mut`), 참조 타입(`&mut`), 그리고 함수 매개변수입니다. 세 가지 모두 필수입니다. 동일한 스코프에서 두 번째 가변 참조를 생성하려 하면 컴파일 오류가 발생합니다. ```rust // exclusivity_rule.rs fn main() { let mut data = String::from("shared state"); let r1 = &mut data; // let r2 = &mut data; // compile error: second mutable borrow println!("{}", r1); // After r1's last usage, a new mutable borrow is allowed let r3 = &mut data; // this works -- non-lexical lifetimes r3.push_str(" updated"); println!("{}", r3); } ``` Rust 2021 에디션은 비어휘적 라이프타임(Non-Lexical Lifetimes, NLL)을 사용합니다. 빌림은 스코프 블록의 끝이 아니라 마지막 사용 지점에서 종료됩니다. 이로 인해 배타적 접근 규칙이 안전성을 유지하면서도 훨씬 편리하게 적용됩니다. ## 라이프타임: 참조의 유효 기간을 컴파일러에게 알리는 방법 라이프타임(Lifetime)은 참조가 가리키는 데이터보다 오래 존재하지 않도록 보장하는 Rust의 장치입니다. 대부분의 경우 컴파일러는 *라이프타임 생략 규칙(Lifetime Elision Rules)* 을 통해 자동으로 라이프타임을 추론합니다. 여러 참조가 상호작용할 때 명시적 어노테이션이 필요해집니다. ```rust // lifetime_annotation.rs fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let result; let string1 = String::from("Rust ownership"); { let string2 = String::from("borrowing"); result = longest(string1.as_str(), string2.as_str()); println!("Longest: {}", result); } } ``` `'a` 구문은 라이프타임 매개변수이며, 새로운 개념이 아니라 이미 존재하는 관계를 명시적으로 표기하는 것입니다. 위 함수 시그니처는 "반환되는 참조는 두 입력 참조 중 어느 것보다도 오래 살 수 없다"는 것을 선언합니다. 컴파일러는 이 정보를 활용하여 댕글링 참조(Dangling Reference)를 방지합니다. ## 구조체에서의 빌림과 라이프타임 바운드 참조를 보유하는 구조체는 반드시 라이프타임 매개변수를 선언해야 합니다. 이를 통해 구조체가 참조하는 데이터보다 오래 존재할 수 없도록 보장합니다. C/C++에서 흔히 발생하는 댕글링 포인터 문제를 원천 차단하는 메커니즘입니다. ```rust // struct_lifetime.rs struct Excerpt<'a> { text: &'a str, } impl<'a> Excerpt<'a> { fn summary(&self) -> &str { let end = self.text.len().min(20); &self.text[..end] } } fn main() { let article = String::from("Rust ownership model eliminates memory bugs"); let excerpt = Excerpt { text: article.as_str(), }; println!("Summary: {}", excerpt.summary()); } ``` `Excerpt<'a>`의 라이프타임 `'a`는 구조체의 유효성을 기저 문자열에 연결합니다. `excerpt`보다 먼저 `article`을 해제하면 컴파일 오류가 발생합니다. 이처럼 Rust는 타입 시스템 수준에서 참조의 유효성을 강제합니다. > **면접에서 자주 나오는 함정** > > 댕글링 참조 관련 질문은 [Rust 면접](/technologies/rust/interview-questions/smart-pointers)에서 매우 빈번하게 출제됩니다. 정답은 항상 동일합니다: Rust는 라이프타임 분석을 통해 컴파일 타임에 댕글링 참조를 방지합니다. 런타임 검사도 없고, 널 포인터도 없습니다. ## 실무 Rust에서의 소유권 패턴 프로덕션 Rust 코드에서는 몇 가지 반복적으로 등장하는 소유권 패턴이 있습니다. 이러한 패턴을 인지하면 개발 속도와 면접 대응력 모두 크게 향상됩니다. ```rust // ownership_patterns.rs fn process_and_return(mut input: String) -> String { input.push_str(" -- processed"); input } fn contains_keyword(text: &str, keyword: &str) -> bool { text.to_lowercase().contains(&keyword.to_lowercase()) } fn sanitize(input: &mut String) { *input = input.trim().to_string(); } fn main() { let raw = String::from("user input"); let processed = process_and_return(raw); let found = contains_keyword(&processed, "input"); println!("Contains 'input': {}", found); let mut padded = String::from(" spaces everywhere "); sanitize(&mut padded); println!("Sanitized: '{}'", padded); } ``` 이 패턴들 사이의 선택은 명확한 휴리스틱을 따릅니다. 기본적으로 불변 빌림을 사용하고, 수정이 필요할 때 가변 빌림을 사용하며, 호출자가 더 이상 값을 필요로 하지 않을 때만 소유권을 이전합니다. 이 접근 방식은 [Rust 기초 가이드](/blog/rust/rust-basics-experienced-developers)에서 더 자세히 다루고 있습니다. ## 빌림 검사기 오류와 해결 방법 빌림 검사기는 구체적인 오류 코드를 생성합니다. 가장 흔한 오류 유형을 이해하면, 당혹스러운 컴파일 오류를 간단한 수정으로 전환할 수 있습니다. ```rust // common_fixes.rs fn main() { let mut scores = vec![90, 85, 78]; let first = scores[0]; scores.push(95); println!("First: {}, All: {:?}", first, scores); let name = String::from("Alice"); let greeting = format!("Hello, {}", name); println!("{} says {}", name, greeting); let outer; { let inner = String::from("temporary"); outer = inner; } println!("{}", outer); } ``` 각 수정은 동일한 원칙을 따릅니다. 빌림과 소유권이 Rust의 규칙에 맞도록 코드 구조를 재배치하는 것입니다. 빌림 검사기와 싸우는 것은 보통 다른 언어에서 버그를 유발했을 설계 문제가 있다는 신호입니다. [동시성과 빌림](/technologies/rust/interview-questions/concurrency-basics)을 다루는 고급 패턴에서는 `Arc`와 `Mutex` 같은 공유 소유권 타입이 필수적입니다. ## 결론 - Rust의 모든 값은 하나의 소유자를 가지며, 대입 시 소유권이 이전(Move)됩니다. 단, `Copy` 트레이트를 구현한 타입은 예외입니다. - 불변 참조(`&T`)는 공유 읽기 접근을 허용하고, 가변 참조(`&mut T`)는 배타적 쓰기 접근을 강제합니다. - 빌림 검사기는 데이터 경쟁과 댕글링 참조를 런타임 비용 없이 컴파일 타임에 방지합니다. - 라이프타임은 참조 간의 관계를 명시적으로 표기하는 것이며, 새로운 제약을 추가하는 것이 아닙니다. - 빌림 검사기가 코드를 거부할 때는 `unsafe`에 의존하기보다 소유권 흐름을 재구성해야 합니다. - [Rust 면접 질문](/blog/rust/rust-interview-questions)을 통해 이러한 패턴을 연습하면 기술 면접 전에 충분한 숙련도를 확보할 수 있습니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/rust/rust-ownership-borrowing-demystified