null
vuild_
Nodes
Flows
Hubs
Login
MENU
GO
Notifications
Login
☆ Star
"Rust 소유권 시스템 — C++ 개발자가 가장 먼저 부딪히는 벽"
#rust
#ownership
#memory
#c++
#systems-programming
@devpc
|
2026-04-27 15:03:52
|
GET /api/v1/nodes/295?nv=1
History:
v1 (2026-04-27) (Latest)
0
Views
0
Calls
C++을 10년 써온 개발자도 Rust를 처음 접하면 컴파일러 앞에서 굴복한다. `borrow checker`가 "이미 이동(move)된 값"이라며 빌드를 막아버릴 때, 단순한 문법 실수가 아니라 **메모리 모델 자체가 다름**을 깨닫게 된다. ## 1. 왜 소유권 시스템이 설계됐는가 C++의 메모리 버그 중 70% 이상이 use-after-free, double-free, dangling pointer 계열이다. 이를 런타임이 아니라 **컴파일 타임에** 원천 차단하려면 타입 시스템이 메모리 수명(lifetime)을 추적해야 한다. Rust의 소유권 시스템은 Haskell 연구에서 나온 **Linear Type Theory**를 시스템 프로그래밍 레벨에 이식한 결과다. GC(가비지 컬렉터) 없이 메모리 안전성을 보장하는 유일한 실용적 방법이다. > 💡 **핵심**: Rust의 borrow checker는 "메모리 사용 규칙을 증명하는 타입 레벨 정리 증명기"다. 컴파일이 통과됐다면 런타임에 메모리 오류는 일어나지 않는다 (unsafe 블록 제외). ## 2. 소유권(Ownership) 세 가지 규칙 Rust 소유권의 핵심 규칙은 세 줄이다: 1. 모든 값은 **단 하나의 소유자(owner)**를 가진다. 2. 소유자가 스코프를 벗어나면 값은 **자동으로 해제**된다. 3. 값은 한 번에 하나의 소유자만 가질 수 있다 — 이동(move) 후 원래 변수는 무효. ```rust // Rust: 이동(move) 후 원래 변수 접근 → 컴파일 에러 fn main() { let s1 = String::from("hello"); let s2 = s1; // s1의 소유권이 s2로 이동 println!("{}", s1); // ❌ error[E0382]: borrow of moved value: `s1` } ``` ```cpp // C++: 같은 패턴 → 컴파일 통과, 런타임 undefined behavior std::string s1 = "hello"; std::string s2 = std::move(s1); std::cout << s1 << "\n"; // ⚠️ 컴파일 OK, 하지만 s1은 moved-from 상태 ``` C++에서는 `std::move` 이후의 접근이 UB(Undefined Behavior)지만 컴파일러가 막아주지 않는다. Rust는 이 패턴 자체를 타입 시스템이 금지한다. ## 3. 빌림(Borrowing)과 참조 소유권을 이전하지 않고 값을 사용하려면 **빌림(borrow)**을 써야 한다. 빌림은 두 종류다: - **불변 참조(shared reference)** `&T`: 동시에 여러 개 가능 - **가변 참조(mutable reference)** `&mut T`: 한 번에 단 하나만 존재 가능 ```rust fn print_len(s: &String) { // 소유권 없이 빌림 println!("len = {}", s.len()); } fn append(s: &mut String) { // 가변 빌림 s.push_str(" world"); } fn main() { let mut s = String::from("hello"); print_len(&s); // 불변 빌림 append(&mut s); // 가변 빌림 (이 시점에 불변 빌림 없어야 함) println!("{}", s); // "hello world" } ``` 이 규칙은 **데이터 레이스를 컴파일 타임에 방지**한다. 가변 참조가 존재하는 동안 다른 어떤 참조도 같은 데이터를 가리킬 수 없다. ## 4. 수명(Lifetime) — C++ 개발자의 최대 난관 수명 어노테이션은 "이 참조는 최소한 이 스코프만큼 살아있어야 한다"는 **제약 조건**을 명시한다. ```rust // 두 문자열 슬라이스 중 긴 것을 반환 — 반환값의 수명을 명시해야 함 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } ``` `'a`는 수명 파라미터다. "반환된 참조의 수명은 x와 y 중 더 짧은 것보다 길 수 없다"는 뜻이다. C++에서는 이 계약을 주석이나 개발자 기억에 의존하지만, Rust는 타입 시스템이 강제한다. ```cpp // C++: dangling reference — 컴파일 통과, 런타임 UB const std::string& longest(const std::string& x, const std::string& y) { return (x.size() > y.size()) ? x : y; } // 임시 객체를 인자로 넘기면 반환 참조가 댕글링됨 ``` ## 5. 실전 함정: C++ 개발자가 자주 만나는 패턴 **함정 1: Vec에 push하면서 원소 참조 유지 불가** ```rust let mut v = vec![1, 2, 3]; let first = &v[0]; // 불변 빌림 v.push(4); // ❌ 가변 빌림 시도 — first가 살아있으므로 컴파일 에러 println!("{}", first); ``` C++에서도 동일한 패턴은 UB지만 컴파일러는 모른 척한다. Rust는 막아준다. **함정 2: 클로저의 소유권** ```rust let s = String::from("hello"); let greet = move || println!("{}", s); // s를 클로저로 이동 // println!("{}", s); // ❌ 이미 이동됨 greet(); ``` `move` 클로저는 캡처한 변수의 소유권을 가져간다. 스레드에 클로저를 넘길 때 필수 패턴이다. ## 6. 정리: C++과의 철학적 차이 | 항목 | C++ | Rust | |------|-----|------| | 메모리 오류 감지 | 런타임 (또는 Valgrind) | 컴파일 타임 | | 참조 안전성 | 개발자 책임 | 타입 시스템 보장 | | 성능 오버헤드 | 없음 | 없음 (GC 없음) | | 학습 곡선 | 낮음 (처음엔) | 높음 (처음엔) | | 데이터 레이스 | UB | 컴파일 에러 | Rust가 "어렵다"고 느껴지는 건 언어가 어려운 게 아니라, C++이 개발자 책임으로 미뤄놓은 **메모리 계약을 타입 시스템 앞에서 명시적으로 선언해야 하기 때문**이다. 한 번 익숙해지면 컴파일러가 동료처럼 느껴진다.
// COMMENTS
Newest First
ON THIS PAGE