메모리 안전성 심층 분석: 참조와 수명
학습 목표: Rust의 참조(
&)가 C#의 포인터나 참조 타입과 어떻게 다른지 심층적으로 분석합니다. 특히 **수명(Lifetime)**의 기초 개념을 익히고, 왜 컴파일 타임의 안전성 증명이 C#의 런타임 체크(경계 검사, Null 가드 등)보다 강력하고 효율적인지 이해합니다.
1. 참조 vs 포인터
C#의 unsafe 블록에서 쓰던 포인터는 위험하지만, Rust의 참조는 항상 안전함이 보장됩니다.
- C# 포인터: 메모리 주소를 직접 가리키며, 유효성 검사는 개발자의 몫입니다.
- Rust 참조: 빌림 검사기가 유효성을 추적하며, 잘못된 메모리 접근은 컴파일조차 되지 않습니다.
#![allow(unused)] fn main() { // [Rust는 안전이 기본입니다] fn safe_swap(a: &mut i32, b: &mut i32) { let temp = *a; *a = *b; *b = temp; } // ❌ Null 체크나 경계 검사 코드가 없어도 컴파일러가 안전을 보증합니다. }
2. 수명(Lifetime): 데이터가 얼마나 살아야 하는가?
C# 개발자에게 가장 낯선 개념 중 하나입니다. 수명이란 **"어떤 참조가 가리키는 실제 데이터가 메모리에 살아있는 기간"**을 뜻합니다.
- C#: GC가 알아서 데이터를 살려둡니다. 하지만 이로 인해 메모리 사용량이 늘고 성능이 희생됩니다.
- Rust: 데이터가 사라진 뒤에 그 자리를 가리키는 '허공에 뜬 참조(Dangling Reference)'를 컴파일 타임에 차단합니다.
#![allow(unused)] fn main() { fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // 💡 'a는 "반환되는 참조는 입력된 x, y 중 짧은 수명만큼만 유효하다"는 명시적 약속입니다. }
3. 런타임 체크 vs 컴파일 타임 증명
| 특징 | C# (Runtime Checks) | Rust (Compile-time Proof) |
|---|---|---|
| 경계 검사 | 배열 접근 시마다 매번 체크 (IndexOutOfRange) | 안전함이 증명되면 체크 코드 자체를 제거 |
| Null 관리 | 실행 중 Null 체크 필수 (NullReferenceException) | Null 자체가 존재하지 않음 (Option으로 대체) |
| 데이터 경합 | 락(Lock) 없이 접근 시 런타임 오류 가능 | 빌림 규칙으로 원천 차단 |
| 성능 결과 | 체크 오버헤드 발생 | 네이티브 수준의 속도 |
💡 실무 팁: 반복 중 수정 버그 (Concurrent Modification)
C#에서 리스트를 foreach로 돌리다가 요소를 삭제하면 런타임 에러(InvalidOperationException)가 납니다. Rust는 이를 컴파일 타임에 잡아냅니다. 반복자가 데이터를 읽는 중(&)에는 누구도 데이터를 수정(&mut)할 수 없다는 빌림 규칙 덕분입니다.