Rust Safety & Ownership
Rust Safety 및 Ownership의 정의, 범위, 선행 지식, 학습 주제, 참고 근거를 정리한 CS&E 학습 노드입니다.
Article
M
Me
hyunyoun's Blog
programming-languages-compilersprogramming-languagescompilerslanguage-platformsecosystemsrust-safetyownershiplanguages-compilers9 min read
1. Overview
Rust의 안전성과 소유권(Rust Safety & Ownership)은 가비지 컬렉터(GC) 없이 C/C++ 수준의 제로 코스트(Zero-cost) 성능을 지향하면서도, 컴파일 타임에 메모리 누수, 댕글링 포인터, 데이터 경쟁(Data Race) 위험을 줄이는 시스템 프로그래밍 언어의 설계를 다룹니다.
학습자는 Rust의 메모리 관리 모델인 **소유권(Ownership)**과 데이터 이동(Move)의 의미를 살펴봅니다. 나아가 **빌림(Borrowing)**과 불변/가변 참조자(&T, &mut T)의 배타성 규칙을 빌림 검사기(Borrow Checker)가 어떻게 검증하는지 정리합니다. 마지막으로 다중 스레드 간 상태 공유 시 컴파일러가 보장하는 **Send / Sync 트레이트(Traits)**와 동시성 안전성, 그리고 컴파일 타임 다형성인 제네릭 트레이트 메커니즘까지 익혀 현대 시스템 프로그래밍의 핵심 역량을 확보합니다.
2. Scope & Boundaries
In-Scope
- 소유권 시스템 (Ownership System): 3가지 소유권 규칙, Move(이동) 의미론, Drop(소멸) 타이밍, 얕은/깊은 복사(
Clone). - 빌림과 참조 (Borrowing & References): 불변 참조(
&T), 가변 참조(&mut T), 다중 불변 vs 단일 가변(Aliasing vs Mutability) 배타성 규칙. - 수명 (Lifetimes): 댕글링 참조 방지, 라이프타임 어노테이션(
'a), 라이프타임 생략 규칙(Elision Rules). - Fearless Concurrency (동시성): 데이터 경쟁 차단,
Send/Sync마커 트레이트,Arc<Mutex<T>>기반 안전한 상태 공유.
Out-of-Scope
- Rust 비동기 생태계 (Async Rust):
tokio,async/await심화 내부(Pin, Future) → 전문 Rust 백엔드 프레임워크 영역. - 매크로 시스템 (Macros):
macro_rules!, 절차적 매크로(Proc-macro) 작성 → 메타프로그래밍 심화 영역.
Boundaries
- C++ 스마트 포인터 vs Rust 소유권: C++의
std::unique_ptr은 런타임 오버헤드가 없지만std::move후 접근(Use-after-move)하면 런타임에 크래시될 수 있습니다. Rust의 소유권은move발생 후 해당 변수 접근 자체를 **컴파일러가 에러 처리(Borrow Checker)**하여 런타임 문제의 가능성을 컴파일 타임에 줄입니다. 이 엄격함이 Rust의 높은 학습 곡선의 원인이자 안전성의 기반입니다.
3. Counterexample
- Use-After-Move와 컴파일러 방어 (Rust Move Semantics):
let s1 = String::from("hello"); let s2 = s1; println!("{}", s1);. 힙에 할당된 문자열s1의 소유권이s2로 전이(Move)되었습니다. C++이나 Python에서는s1이 얕은 복사되어 여전히 접근 가능하거나 런타임에 문제를 일으킬 수 있지만, Rust 컴파일러는 세 번째 줄에서error[E0382]: borrow of moved value: s1컴파일 에러를 내고 빌드를 거부합니다. 메모리 이중 해제(Double Free)를 언어 설계 레벨에서 막는 방식입니다. - 멀티스레드 데이터 경쟁 차단 (Data Race Prevention):
let mut counter = 0; thread::spawn(move || { counter += 1; }); counter += 1;. 두 스레드가 가변 상태counter를 동시에 수정하려는 시도. C++나 Java에서는 런타임에 값이 깨지는 경쟁 조건(Race Condition)이 발생하지만, Rust에서는 다수의 소유자나 가변 참조자를 허용하지 않는 규칙 덕분에 컴파일 타임에error[E0382]: use of moved value에러로 차단됩니다. 이를 해결하려면 명시적으로Arc<Mutex<T>>로 감싸 안전함을 증명해야 합니다.
4. Prerequisites
- 포인터와 힙 메모리 (Basic): 값 타입과 참조 타입, 스택과 힙 메모리 차이. (03-01 Memory & CPU Architecture)
- C/C++ 메모리 버그 (Recommended): C/C++의 메모리 누수와 데이터 경쟁 버그 경험이 있어야 Rust의 필요성을 통감할 수 있습니다. (05-04-01 C & Modern C++)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 값의 주인은 오직 하나, 소유권과 이동 (Ownership & Move Rules)
- Why to Learn: "왜 다른 언어에선 잘 되던 코드가 Rust에선 컴파일 에러가 날까?"의 근본 원인인 소유권 철학을 체득해야, 컴파일러(Borrow Checker)의 규칙을 이해하고 메모리 안전성을 확보할 수 있기 위함입니다.
- What to Learn:
- Concepts: 소유권(Ownership) 3규칙 (1값 1소유자, 소유자 스코프 종료 시 값 폐기), 이동(Move Semantics), 복사 트레이트(
Copy), 깊은 복사(Clone). - Skills: 스코프에 따른 Drop 자동 호출 확인, Move 후 변수 사용 에러 해결.
- Concepts: 소유권(Ownership) 3규칙 (1값 1소유자, 소유자 스코프 종료 시 값 폐기), 이동(Move Semantics), 복사 트레이트(
- How to Learn:
- 1단계: 소유권 규칙:
let s = String::from("hello"); { let s2 = s; } println!("{}", s);. 블록 안에서s의 소유권이s2로 전이되고 블록 종료 시s2가 Drop(메모리 해제). 따라서 블록 밖에서s사용 시 컴파일 에러. GC 없이 스코프 기반 메모리 관리를 달성하는 규칙을 살펴봅니다. - 2단계: Copy 타입: 정수, 부울 등 스택에만 존재하는 크기 고정 타입은 Move 대신 비트 복사(Copy)가 일어납니다.
let x = 5; let y = x; println!("{}", x);는 에러가 나지 않는 힙/스택 타입 간의 차이를 살펴봅니다.
- 1단계: 소유권 규칙:
- Implement: 파이썬 의사 코드(Pseudo Code)로 Rust 소유권 시뮬레이터 구현.
Variable클래스 생성.move(from_var, to_var)호출 시from_var의 내부 포인터를 None으로 비우고. 접근 시UseAfterMoveException발생 로직 구현. "컴파일 타임 댕글링 방지" 메커니즘 가시화.
Recommended
Core Topic 02: 읽거나 쓰거나, 빌림과 라이프타임 (Borrowing & Lifetimes)
- Why to Learn: 소유권을 매번 넘겨주는 것은 비효율적이므로, 포인터(참조)를 안전하게 빌려 쓰는 빌림 규칙(Aliasing vs Mutability)과, 빌려준 주소가 원본 해제 후 참조(Dangling)되지 않음을 보장하는 라이프타임 검증을 익히기 위함입니다.
- What to Learn:
- Concepts: 불변 참조(
&T), 가변 참조(&mut T), 빌림 배타성(다수&TOR 단일&mut T), 댕글링 참조 차단, 라이프타임 어노테이션('a). - Skills: 빌림 검사(Borrow Checker) 에러 해석(
cannot borrow as mutable), 생략된 라이프타임 유추.
- Concepts: 불변 참조(
- How to Learn:
- 1단계: 빌림 배타성: 독서실 책(데이터). 여러 명이 동시에 읽기(
&T) 가능. 누군가 연필로 고치기(&mut T) 시작하면 다른 누구도 읽거나 고칠 수 없음(단독 접근 보장). 이 규칙이 Data Race와 Iterator 무효화 버그를 차단하는 방식을 살펴봅니다. - 2단계: 라이프타임: 함수
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str. 반환된 참조자의 수명은 인자로 들어온 x, y 중 짧은 수명('a)에 종속됨을 명시. x가 해제된 후 반환값을 쓰려 하면 컴파일러가 라이프타임 불일치로 차단하는 댕글링 방지 방식을 살펴봅니다.
- 1단계: 빌림 배타성: 독서실 책(데이터). 여러 명이 동시에 읽기(
- Implement: Rust 코드 스니펫 디버깅 실습 (파이썬 시뮬레이션). 불변 참조
ref1,ref2생성 후 가변 참조ref3생성 시도 시BorrowError("Cannot borrow as mutable because it is also borrowed as immutable")발생기 작성. 빌림 상태 머신 추적 로직 데모.
Practical
Core Topic 03: 두려움 없는 동시성, 스레드 안전성과 Send/Sync (Fearless Concurrency)
- Why to Learn: C/C++ 멀티스레딩에서 자주 발생하는 데이터 경쟁(Data Race)을 Rust가 컴파일 타임에
Send와Sync타입 마커로 제한하는 방식을 이해하고, 병렬 프로그래밍 안전성을 높이기 위함입니다. - What to Learn:
- Concepts: 스레드 간 상태 공유(Shared State),
std::thread::spawn, 마커 트레이트(Send,Sync),Mutex<T>(상호 배제 락 내장),Arc<T>(원자적 참조 계수 공유). - Skills:
Arc<Mutex<T>>패턴으로 스레드 간 가변 상태 안전한 공유 구현.
- Concepts: 스레드 간 상태 공유(Shared State),
- How to Learn:
- 1단계:
Send와Sync:Rc<T>(단일 스레드 참조 계수)는Send마커가 없어 스레드 경계를 넘으려 하면 컴파일 에러. 스레드 안전한Arc<T>(Atomic RC)를 써야만 컴파일 통과. 런타임 스레드 안전성을 타입 시스템으로 보장하는 방식을 살펴봅니다. - 2단계:
Mutex<T>의 소유권 결합: C++에서는 락(Lock)과 보호할 데이터가 분리되어 락 없이 데이터에 접근할 위험이 있습니다. Rust의Mutex<T>는 데이터를 감싸는 박스. 데이터에 접근하려면 반드시lock().unwrap()을 호출해 가변 참조(&mut T)를 얻어야만 하는 강제 설계를 살펴봅니다.
- 1단계:
- Implement: Rust 동시성 멀티스레딩 패턴 파이썬 모사. 10개 스레드에서 공용 카운터 증가시키기. 단순 전역 변수 공유 시 (GIL 해제 가정) Data Race 결과 관찰.
ArcMutex(initial_val)객체를 만들어with arc_mutex.lock() as val: val += 1패턴 강제하는 API 작성 후 안전성 보장 데모.
Advanced
Core Topic 04: 비용 없는 다형성, 트레이트와 단형성화 (Traits & Zero-cost Abstraction)
- Why to Learn: Java의 인터페이스(Interface)와 달리 런타임 가상 메서드 테이블(vtable) 비용 없이, 컴파일 타임 템플릿 확장을 통해 기계어 레벨 성능을 확보하는 제네릭(Generic)과 트레이트(Trait)의 최적화 구조를 이해하기 위해서입니다.
- What to Learn:
- Concepts: 트레이트(Trait, 행위 정의), 트레이트 바운드(
T: Display), 정적 디스패치(Static Dispatch)와 단형성화(Monomorphization), 동적 디스패치(Dynamic Dispatch,dyn Trait). - Skills: 커스텀 Trait 구현, 제네릭 함수 작성, 정적 vs 동적 디스패치 선택 트레이트오프.
- Concepts: 트레이트(Trait, 행위 정의), 트레이트 바운드(
- How to Learn:
- 1단계: Trait: 공통 동작 정의.
trait Summary { fn summarize(&self) -> String; }. 구조체Article과Tweet에 각각 구현.fn notify<T: Summary>(item: &T)는 Summary가 구현된 아무 타입이나 받는 제네릭 다형성을 살펴봅니다. - 2단계: 단형성화(Monomorphization): 컴파일러는
notify(article)과notify(tweet)호출을 보고, 런타임 확인 대신notify_Article(..)과notify_Tweet(..)이라는 두 개의 구체적 기계어 함수를 자동 복제 생성(Static Dispatch). 런타임 다형성 오버헤드가 제로가 되는 원리를 살펴봅니다.
- 1단계: Trait: 공통 동작 정의.
- Implement: C++ Template 메커니즘 비교 파이썬 코드.
fn_generic(T obj)호출. 실행 전(컴파일 타임 시뮬레이션) 사용된 타입 목록([Article, Tweet])을 수집하여 각 타입 전용 함수 문자열을 생성(Code Generation)하는 스크립트 작성. 단형성화로 인한 바이너리 크기 증가 vs 런타임 속도 트레이드오프 수치 로그.
7. Terminology
8. References
Primary
- [P1] CS2023 - Programming Languages (PL) - Type Systems (Ownership and Linear Types)
- [P5] SFIA - Software Development (PROG) - Systems Programming
Secondary
- [The Rust Programming Language] Steve Klabnik, Carol Nichols - Ownership, Borrowing, and Lifetimes
- [Programming Rust] Jim Blandy - Memory Safety and Concurrency
Industry
- [Rust Foundation] - Rustonomicon (The Dark Arts of Unsafe Rust)
- [Microsoft Security Response Center (MSRC)] - Why Rust for Safe Systems Programming
9. Final Checklist
Primary
- 변수를 다른 변수에 대입할 때 발생하는 '이동(Move)' 시맨틱이 어떻게 얕은 복사(Shallow Copy)로 인한 이중 해제(Double Free) 버그를 차단하는지 증명할 수 있는가?
- 참조자(Borrow) 규칙 중 "여러 개의 불변 참조(
&T)는 가능하지만, 가변 참조(&mut T)는 오직 하나만 존재해야 한다"는 규칙이 의미하는 바를 설명할 수 있는가?
Secondary
- 멀티스레드 환경에서 가변 참조(
&mut T)가 하나만 존재한다는 Rust의 컴파일러 규칙이, 어떻게 뮤텍스 락(Mutex Lock) 없이도 데이터 레이스(Data Race)를 차단하는지 설명할 수 있는가? - 댕글링 포인터(Dangling Pointer)가 발생하는 C++의 시나리오를 제시하고, Rust의 라이프타임(Lifetime) 추론이 이를 컴파일 타임에 어떻게 거부(Reject)하는지 묘사할 수 있는가?
Industry
- 가비지 컬렉션(GC)으로 인한 런타임 STW(Stop-The-World) 지연 시간을 허용하기 어려운 임베디드, HFT(고빈도 거래), OS 커널 분야에서 Rust가 대안이 되는 아키텍처적 이유를 논증할 수 있는가?
- Rust의 무결성을 잠시 포기하고 C 라이브러리(FFI)와 통신하기 위해
unsafe블록을 열었을 때, 이곳에서 터질 수 있는 보안 취약점을 어떻게 격리하고 검증할지 설계할 수 있는가?