Rust는 C/C++의 성능을 유지하면서도 메모리 안전성을 컴파일 타임에 보장하는 시스템 프로그래밍 언어입니다. 가비지 컬렉터 없이 어떻게 이것이 가능한지, 그 핵심에 있는 소유권(Ownership) 시스템을 이해해봅니다.
왜 Rust인가?
C/C++ 프로그램의 70% 이상의 보안 취약점이 메모리 관련 버그(use-after-free, buffer overflow, null pointer dereference)에서 발생한다는 Microsoft와 Google의 연구 결과가 있습니다. Rust는 이 문제를 런타임이 아닌 컴파일 타임에 원천 차단합니다.
- C/C++ 수준의 성능: 제로코스트 추상화(Zero-cost Abstraction)
- 메모리 안전성: 가비지 컬렉터 없이도 메모리 누수·댕글링 포인터 방지
- 스레드 안전성: 컴파일러가 데이터 레이스를 사전 차단
- 현대적 생태계: Cargo 패키지 매니저, 풍부한 표준 라이브러리
핵심 개념: 소유권(Ownership)
Rust의 모든 메모리 안전성은 세 가지 소유권 규칙에서 출발합니다.
- 모든 값은 하나의 소유자(owner)가 있다
- 소유자는 동시에 하나뿐이다
- 소유자가 스코프를 벗어나면 값은 자동으로 해제(drop)된다
fn main() {
let s1 = String::from("hello"); // s1이 소유자
let s2 = s1; // 소유권이 s2로 이동(move)
// println!("{}", s1); // 컴파일 에러! s1은 더 이상 유효하지 않음
println!("{}", s2); // OK
} // s2가 스코프를 벗어나면 메모리 자동 해제
빌림(Borrowing)과 참조(Reference)
소유권을 이전하지 않고 값을 사용하려면 참조(&)를 사용합니다. 이를 빌림이라 합니다.
fn calculate_length(s: &String) -> usize { // s는 참조(빌림)
s.len()
} // s가 스코프를 벗어나도 원본은 해제되지 않음
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 참조를 전달
println!("'{}' 길이는 {}입니다", s, len); // s는 여전히 유효
}
뮤터블 참조와 빌림 검사기
Rust의 빌림 검사기(Borrow Checker)는 두 가지 규칙을 강제합니다.
- 동시에 여러 개의 불변 참조(&T)는 허용
- 동시에 단 하나의 가변 참조(&mut T)만 허용 (불변 참조와 공존 불가)
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 불변 참조 1
let r2 = &s; // 불변 참조 2 - OK
// let r3 = &mut s; // 에러! 불변 참조가 살아있는 동안 가변 참조 불가
println!("{} {}", r1, r2);
// r1, r2 사용 종료 이후
let r3 = &mut s; // 이제는 OK
r3.push_str(" world");
}
라이프타임(Lifetime)
라이프타임은 참조가 유효한 범위를 명시적으로 표현하는 문법입니다. 컴파일러가 대부분 추론하지만, 여러 참조가 관계될 때는 명시가 필요합니다.
// 'a는 라이프타임 파라미터: x와 y 중 짧은 쪽의 수명으로 반환값 수명 결정
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
실전 활용: Rust를 배워야 할 시점
Rust는 모든 상황에 최적은 아닙니다. 아래 기준으로 도입을 검토하세요.
- 적합: 시스템 프로그래밍, 임베디드, CLI 도구, WebAssembly, 고성능 API 서버
- 신중: 빠른 프로토타이핑, 소규모 스크립트, 팀의 Rust 경험 부재