안녕하세요.
Rust의 소유권과 참조에 대해서 알아보도록 하겠습니다.
서론
Rust의 가장 중요한 특징은 메모리 안정성을 보장하면서도 GC(가비지 컬렉터) 없이 동작합니다.
이를 가능하게 하는 핵심 개념이 소유권, 참조와 빌림, 슬라이스 입니다.
💿 소유권 (Ownership)
🔹 개념
Rust에서는 heap에 저장되는 값은 특정한 변수 하나만이 소유권을 가집니다.
소유권을 가진 변수는 해당 값이 유효한 범위를 결정하며, 소유권이 사라지면 값도 메모리에서 해제됩니다.
🔹 소유권 규칙
- 한 번에 딱 하나의 owner만 존재할 수 있습니다.
- owner가 scope 밖으로 벗어나는 때, 값은 버려집니다. (자동 반납 지점)
- 값을 참조하는 경우, 원본 값을 변경할 수 없습니다. (기본적으로 불변 참조)
fn main() { // s는 유효하지 않음 (선언 전이므로)
let s = "Hello world!"; // s는 이 지점부터 유효함
println!("{}", s); // s를 가지고 무언가를 함
} // 이 스코프는 이제 끝이므로, s는 더이상 유효하지 않음
ex) 소유권 이동
소유권이 s1에서 s2로 이동했기에 더 이상 s1을 사용할 수 없습니다. (컴파일 에러 발생)
fn main() {
let s1 = String::from("hello world");
// s2로 소유권을 이동
let s2 = s1;
// s1은 소유권을 상실했기 때문에 s1에 접근하는 순간 컴파일 에러 발생
println!("{}", s1);
}
Rust의 String은 힙 메모리를 사용합니다.
기본적으로 복사가 아니라, 소유권이 새로운 변수로 이동하여 중복 해제 오류를 방지합니다.
ex) 값 복사
Clone을 사용하면 소유권 이동 없이, 값 복사를 할 수 있습니다.
fn main() {
let s1 = String::from("hello world");
let s2 = s1.clone(); // 깊은 복사 (독립적인 인스턴스)
println!("s1 = {}, s2 = {}", s1, s2);
}
ex) 스택에 저장되는 타입들
이러한 타입들은 자동으로 copy 가능하여 소유권이 이동하지 않습니다.
- u32와 같은 정수형 타입
- bool 타입
- f64와 같은 부동 소수점 타입
- copy가 가능한 타입만으로 구성된 튜플
- (i32, i32)는 가능
- (i32, String)은 불가능
fn main() {
let x = 5;
let y = x; // copy가 일어남
println!("{}", x); // 컴파일 에러가 안남
println!("{}", y);
}
소유권과 함수
함수에 parameter로 넘겨주는 것도 소유권을 잃습니다.
fn main() {
let s = String::from("hello world"); // s가 스코프 안으로 들어옴
takes_ownership(s); // s의 값이 함수 안으로 이동
println!("{}", s); // 더 이상 유효하지 않음 -> 컴파일 에러
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string이 스코프 밖으로 벗어남 -> 메모리 해제됨
이럴 때 참조를 넘겨서 소유권을 빌려주면 main에서 다시 사용가능합니다.
💿 참조와 빌림 (Reference & Borrowing)
🔹 개념
소유권을 넘기지 않고도 값을 사용할 수 있는 방법
🔹 참조의 두 가지 종류
1) 불변 참조 (&T) → 원본 데이터를 변경할 수 없음 (여러 개 가능)
2) 가변 참조 (&mut T) → 원본 데이터를 변경 가능 (단, 동시에 하나만 존재 가능)
1) 불변 참조
s1의 소유권은 이동하지 않기 때문에 main 함수에서 계속 사용할 수 있습니다.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len); // 사용 가능
}
fn calculate_length(s: &String) -> usize {
s.len()
}
2) 가변 참조
참조로 넘긴 변수를 수정하고 싶을 때 사용합니다.
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world"); // 변경 가능
}
가변 참조의 제약
Rust에서는 데이터 레이스를 방지하기 위해 다음 규칙이 적용됩니다.
- 하나의 스코프 안에서 여러 개의 가변 참조(&mut T)를 만들 수 없음
- 하나의 스코프 안에서 불변 참조(&T)와 가변 참조(&mut T)를 동시에 사용할 수 없음
아래 코드는 동시에 두 개의 가변 참조를 가지므로 컴파일 오류가 납니다.
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 컴파일 에러
println!("{} and {}", r1, r2);
}
아래 코드는 불변, 가변 참조자를 동시에 가지므로 컴파일 오류가 납니다.
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // 컴파일 에러
println!("{} and {} and {}", r1, r2, r3);
}
💿 슬라이스 (Slice)
🔹 개념
컬렉션의 일부를 참조하는 데이터 타입입니다.
소유권을 가지지 않으면서 데이터의 일부분을 안전하게 사용할 수 있도록 합니다.
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 부분 문자열 참조
let world = &s[6..11];
println!("{}, {}", hello, world);
println!("{}", s); // 사용 가능
}
슬라이스는 원본 데이터의 일부를 참조하지만, 소유권을 가지지 않습니다.
🔹 정리
개념 | 설명 | 특징 |
소유권 (Ownership) | 모든 값은 하나의 소유자가 있음 | 소유권이 이동하면 원래 변수는 사용할 수 없음 |
참조 & 빌림 (Borrowing) | 값을 소유권 이동 없이 사용할 수 있음 | 불변 참조(&T): 여러 개 가능, 가변 참조(&mut T): 동시에 하나만 가능 |
슬라이스 (Slice) | 데이터의 일부를 참조하는 타입 | &str, &[T] 등, 원본을 변경하지 않음 |
'Rust' 카테고리의 다른 글
[Rust] 에러 처리 (0) | 2025.03.30 |
---|---|
[Rust] 스마트 포인터 (2) - RefCell<T>, Arc<T>, Mutex<T> (0) | 2025.03.24 |
[Rust] 스마트 포인터 (1) - Box<T>, Rc<T> (2) | 2025.03.24 |
[Rust] 제네릭(Generic), 트레잇(Trait), 동적 디스패처(dynamic dispatch) (2) | 2025.03.11 |
[Rust] 기초 문법 (2) | 2025.02.28 |